初次使用ESC的感想
一、背景介绍 我是浙江大学宁波理工学院电子信息工程专业的一名大三学生。由于导师的要求,我接触到了飞天加速计划。起初是被飞天加速计划优秀的开发环境及齐全的课程储备所吸引。但,随着我的深入学习,我发现飞天加速计划是学生党的福音,它提供的平台基本满足了我开发所学要的一切需求。最后,飞天计划还提供了足够多的时间供我熟悉ECS,方便我进行更深入的学习。二、使用体验 正如前文所提到过的,我是第一次接触到ECS,对它的基本概念一无所知。但是,在开发者社区我找到了许多资源来简单的了解我的目标,例如《云计算的前世今生》、《七天玩转云服务器》等课程。这些课程在我进行实例开发时提供了巨大的帮助。对于实验过程中遇到的问题,我记忆最深的是在web界面操作时,我很困惑为什么输入的密码不被显示,是由于什么原因?经过我的了解和查询,我发现最多的回答是:基于安全的考虑。如果您有跟详尽的答案,也请发表出来。三、感悟总结 经过这些天的学习,以及对云服务器的使用,我对ECS有了初步的认识,并且对于课程《基于ECS搭建云上博客》有了更深的理解。接下去我会学习更多的专业知识,观看更多的课程视频,将飞天加速计划平台充分利用起来,使它在我的大学学习过程中发挥更多作用。同时,我也会把它介绍给更多的朋友,大家一起进步。
手把手带你实战 AGP 7.x ASM 字节码插桩
一、前言字节码插桩技术在 Android 领域应用广泛,甚至在不少中高级面试中,是必问的技术面之一。它的应用场景包括但不限于:性能优化:监控函数耗时,优化线程数量。无痕埋点:不侵入业务源码,实现全量埋点。隐私合规:监控敏感方法调用,防止 App 因安全风险等原因而被下架。……字节码插桩的本质是对字节码文件(.class)的修改。从原理上讲,利用文本编辑器手动编辑也是能修改的(笑,但实际上一般是通过各种框架来做。实现字节码插桩的框架有很多,ASM:https://asm.ow2.io/AspectJ:https://www.eclipse.org/aspectj/ReDex:https://fbredex.com/在 AGP 7.0 以前,通过 AGP 的 Transform API 来实现字节码插桩;从 7.0 开始,Transform API 被声明为 Deprecated,并计划在 AGP 的 8.0 版本中移除。但这并不表示无法再使用字节码插桩了,相反,有一套新的 API —— TransformAction,供我们实现这一需求。二、目的一句话,本文将会带大家一步一步使用 AGP 7.0 开始推荐使用的 TransformAction API,来实现 ASM 插桩。可以学到什么如何编写一个 Gradle Plugin如何使用 TransformAction API 进行字节码插桩了解其中的一些坑,避免重蹈覆辙需要了解什么Kotlin 语法Gradle 的一些知识:包括不限于文件组成结构、Composite Build三、实战有时我们想知道一个函数究竟有没有被执行,常见的手段有断点、手动加 Log。今天我们以 ASM 插桩的方式,在进入函数时打印一条 Log 和时间点。假设已经有一个 Android 项目(可以新建一个),并且确保 AGP 版本大于 7.0.0。下面所用到的 Android 项目(主工程)名称为 AsmTourism。1. 添加自定义插件工程目录Gradle 7 引入了 Composite Build,可以让一个 Gradle 项目参与到另一个 Gradle 项目的构建当中。这里采用这种方式来实现自定义插件,而不是使用保留目录 buildSrc(会有坑)或者发布插件的方式。1.1 新建 build-logic 目录在项目根目录下新建 build-logic 文件夹(名字其实可以随意),并在该目录内创建 settings.gradle.kts 文件。再把主工程的 gradle 文件夹、gradle.properties 文件拷贝到该目录。此时的目录结构如下:1.2 新建 convention 目录在 build-logic 目录下,新建 convention 目录,作为插件源码的模块目录。同时,还需要创建 Gradle 项目必备的文件 convention/build.gradle.kts 和文件夹 convention/src/main/kotlin/。此时的目录结构如下:1.3 连接主工程和 build-logic 工程在主工程的 settings.gradle 文件中,使用 includeBuild 将主工程 AsmTourism 和插件工程 build-logic 连接起来。diff --git a/settings.gradle b/settings.gradle
index a777782..2b49529 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,5 @@
pluginManagement {
+ includeBuild("build-logic")
repositories {
gradlePluginPortal()
google()1.4 小结熟悉 Gradle 的朋友会发现,这里其实相当于是在主工程目录中,创建了一个 Gradle 工程,它有它自己的 settings.gradle.kts 文件和一个名为 convention 的模块。Tips:如果一个目录下存在 settings.gradle.kts 文件,Gradle 会把它当作一个 Gradle 工程,而不是模块。2. 编写 Gradle 插件2.1 配置 build-logic/settings.gradle.kts// 配置项目的依赖源
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
// 将 convention 模块加入编译
include(":convention")2.2 配置 convention/build.gradle.ktsplugins {
// 使用 kotlin dsl 作为 gradle 构建脚本语言
`kotlin-dsl`
}
// 配置字节码的兼容性
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
val agpVersion = "7.2.2"
val kotlinVersion = "1.7.10"
val asmVersion = "9.3"
// AGP 依赖
implementation("com.android.tools.build:gradle:$agpVersion") {
exclude(group = "org.ow2.asm")
}
// Kotlin 依赖 —— 插件使用 Kotlin 实现
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") {
exclude(group = "org.ow2.asm")
}
// ASM 依赖库
implementation("org.ow2.asm:asm:$asmVersion")
implementation("org.ow2.asm:asm-commons:$asmVersion")
implementation("org.ow2.asm:asm-util:$asmVersion")
}
gradlePlugin {
plugins {
// 注册插件,这样可以在其他地方 apply
register("LogPlugin") {
// 注册插件的 id,需要应用该插件的模块可以通过 apply 这个 id
id = "me.hjhl.gradle.plugin.log"
implementationClass = "LogPlugin"
}
}
}2.3 创建插件 LogPlugin在 build-logic/convention/src/main/kotlin/ 目录下新建 LogPlugin.kt 文件,内容如下:import org.gradle.api.Plugin
import org.gradle.api.Project
class LogPlugin : Plugin<Project> {
companion object {
private const val TAG = "LogPlugin"
}
override fun apply(target: Project) {
log("======== start apply ========")
log("apply target: ${target.displayName}")
log("======== end apply ========")
}
private fun log(msg: String) {
println("[$TAG]: $msg")
}
}此时的目录结构如下:2.4 app 模块中应用插件回到 app/build.gradle 文件,在 plugins 语句块中应用该插件,如下:diff --git a/app/build.gradle b/app/build.gradle
index 7ace7ff..aa2d937 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
+ id 'me.hjhl.gradle.plugin.log'
}
android {sync 一下工程,如果可以在 AS 的 Build 窗口中如下输出:则表示插件应用成功了。2.5 小节这一步需要重点注意插件在项目中的注册和使用。其次,需要熟悉下 Gradle 插件的编写方式 —— 从继承 Plugin<Project> 开始。3. 实现 ASM 插桩3.1 编写 Transform 类,实现 ClassVisitor不需要繁琐的手段,AGP 提供了一个抽象接口 AsmClassVisitorFactory 简化了 Transform 的编写流程,我们只需要这样使用:定义一个抽象类,实现该接口。实现 createClassVisitor 和 isInstrumentable 两个方法。如下:package me.hjhl.gradle.plugin.log
import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
abstract class LogTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
// 返回一个 ClassVisitor 对象,其内部实现了我们修改 class 文件的逻辑
return object : ClassVisitor(Opcodes.ASM5, nextClassVisitor) {
val className = classContext.currentClassData.className
// 这里,由于只需要修改方法,故而只重载了 visitMethod 找个方法
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val oldMethodVisitor =
super.visitMethod(access, name, descriptor, signature, exceptions)
// 返回一个 MethodVisitor 对象,其内部实现了我们修改方法的逻辑
return LogMethodVisitor(className, oldMethodVisitor, access, name, descriptor)
}
}
}
override fun isInstrumentable(classData: ClassData): Boolean {
return true
}
}3.2 实现 MethodVisitorpackage me.hjhl.gradle.plugin.log
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.AdviceAdapter
class LogMethodVisitor(
private val className: String,
nextMethodVisitor: MethodVisitor,
access: Int,
name: String?,
descriptor: String?,
) : AdviceAdapter(Opcodes.ASM5, nextMethodVisitor, access, name, descriptor) {
override fun onMethodEnter() {
// 往栈上加载两个变量,用于后面的函数调用
mv.visitLdcInsn("LogMethodVisitor")
mv.visitLdcInsn("enter: $className.$name")
// 调用 android.util.Log 函数
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"android/util/Log",
"i",
"(Ljava/lang/String;Ljava/lang/String;)I",
false
)
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
}
}注意到,我们并没有直接继承并实现抽象类 MethodVisitor,而是继承 AdviceAdapter —— 它是继承自 MethodVisitor 的,这样的好处是简化了代码,只需要添加我们需要的逻辑即可 —— 这里我们打印了所调用方法的类及名字。3.3 注册 Transform原来 Transform API 是通过 AppExtension 注册的,现在 AGP 中是通过 AndroidComponentsExtension 注册 Transform。用法如下:diff --git a/build-logic/convention/src/main/kotlin/LogPlugin.kt b/build-logic/convention/src/main/kotlin/LogPlugin.kt
index 0d71d92..dc62f26 100644
--- a/build-logic/convention/src/main/kotlin/LogPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/LogPlugin.kt
@@ -1,3 +1,7 @@
+import com.android.build.api.instrumentation.FramesComputationMode
+import com.android.build.api.instrumentation.InstrumentationScope
+import com.android.build.api.variant.AndroidComponentsExtension
+import me.hjhl.gradle.plugin.log.LogTransform
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -9,6 +13,15 @@ class LogPlugin : Plugin<Project> {
override fun apply(target: Project) {
log("======== start apply ========")
log("apply target: ${target.displayName}")
+ val androidComponentsExtension =
+ target.extensions.getByType(AndroidComponentsExtension::class.java)
+ androidComponentsExtension.onVariants { variant ->
+ log("variant: ${variant.name}")
+ variant.instrumentation.apply {
+ transformClassesWith(LogTransform::class.java, InstrumentationScope.PROJECT) {}
+ setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
+ }
+ }
log("======== end apply ========")
}3.4 小结注意新 Transform API 的注册方式 AndroidComponentsExtension 和关键类 AsmClassVisitorFactory。至于它们的用法,可以查看本文提供的参考资料或搜索 ASM 相关资料了解。4. 结果安装并运行 App,看到如下 Log 表示插桩成功。Tips:如果对插桩结果有疑问,或者想从字节码角度分析,可以使用 Bytecode Viewer 查看字节码。例如:https://github.com/Konloch/bytecode-viewer5. Q & AQ:为什么说使用 buildSrc 写插件会有坑?A:插件工程(本文中的 build-logic/convention)中,需要依赖 AGP 以访问注册 Transform 的 API,但如果宿主模块中使用了 plugins 语句块的方式引入 AGP 插件,并且使用 buildSrc 来编写插件,则会导致 sync/编译报错。Q:为什么在 build-logic/convention/build.gradle.kts 中,要 exclude org.ow2.asm?A:在 AGP 和 Kotlin 中,也使用到了 ASM。如果不屏蔽掉,使用时选错,会导致编译出现奇怪的报错。Tips:使用时尤为注意,ASM 相关的类是否来自手动依赖的 org.ow2.asm 包中。四、总结如果使用过旧版 Transform API,会发现 TransformAction 的方式节省了非常多的模版代码,比如处理增量编译问题。这使得插件开发者能更专注于核心逻辑实现,提高效率。源码仓库Github:https://github.com/HJHL/AsmTourism五、参考资料AGP 7.0 release note:https://developer.android.com/studio/releases/gradle-plugin#7-0-0AGP API 指南:https://developer.android.com/reference/tools/gradle-api/7.2/classes旧版 Transform API:https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/transform/TransformGradle 项目的目录文件结构说明:https://docs.gradle.org/current/userguide/organizing_gradle_projects.htmlGradle Plugin 开发教程:https://docs.gradle.org/current/userguide/custom_plugins.htmlGradle 官方的 Transform 教程 —— 基于 TransformAction API:https://docs.gradle.org/current/userguide/artifact_transforms.htmlASM 官网:https://asm.ow2.io/Java 方法签名:https://docs.oracle.com/en/java/javase/18/docs/specs/jni/types.html#type-signatures
MySQL 05 进阶之存储引擎
1.1 连接层:最上层是一些客户端和链接服务,包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。1.2 服务层:第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如 过程、函数等。在该层,服务器会解 析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询的顺序,是否利用索引等, 最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存,如果缓存空间足够大, 这样在解决大量读操作的环境中能够很好的提升系统的性能。1.3 引擎层:存储引擎层, 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。数据库中的索引是在存储引擎层实现的。1.4 存储层数据存储层, 主要是将数据(如: redolog、undolog、数据、索引、二进制日志、错误日志、查询日志、慢查询日志等)存储在文件系统之上,并完成与存储引擎的交互。和其他数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。2、存储引擎介绍存储引擎是MySQL的核心,是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。我们可以在创建表的时候,来指定选择的存储引擎,如果没有指定将自动选择默认的存储引擎。2.1 建表时指定存储引擎:CREATE TABLE 表名(
字段1 字段1类型 [ COMMENT 字段1注释 ] ,
......
字段n 字段n类型 [COMMENT 字段n注释 ]
) ENGINE = INNODB [ COMMENT 表注释 ] ;2.2 查询当前数据库支持的存储引擎show engines;演示:show create table emp;我们可以看到,创建表时,即使我们没有指定存储疫情,数据库也会自动选择默认的存储引擎。3、存储引擎特点接下来我们就来介绍重点常见的三种存储引擎 InnoDB、MyISAM、Memory的特点。3.1 InnoDBInnoDB是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB是默认的MySQL 存储引擎。它的特点有如下几点:DML操作遵循ACID模型,支持事务;行级锁,提高并发访问性能;支持外键FOREIGN KEY约束,保证数据的完整性和正确性;文件名:xxx.ibd,xxx代表的是表名,innoDB引擎的每张表都会对应这样一个表空间文件,存储该表的表结构(frm-早期的 、sdi-新版的)、数据和索引。show variables like 'innodb_file_per_table';如果该参数开启,代表对于InnoDB引擎的表,每一张表都对应一个ibd文件。 我们直接打开MySQL的数据存放目录: C:\ProgramData\MySQL\MySQL Server 8.0\Data , 这个目录下有很多文件夹,不同的文件夹代表不同的数据库,我们直接打开test(因为我有一个数据库就叫test)文件夹。 可以看到里面有很多的ibd文件,每一个ibd文件就对应一张表,比如:我们有一张表 emp,就有这样的一个emp.ibd文件,而在这个ibd文件中不仅存放表结构、数据,还会存放该表对应的索引信息。 而该文件是基于二进制存储的,不能直接基于记事本打开,我们可以使用mysql提供的一 个指令 ibd2sdi ,通过该指令就可以从ibd文件中提取sdi信息,而sdi数据字典信息中就包含该表的表结构。 逻辑存储结构:表空间 : InnoDB存储引擎逻辑结构的最高层,ibd文件其实就是表空间文件,在表空间中可以包含多个Segment段。段 : 表空间是由各个段组成的, 常见的段有数据段、索引段、回滚段等。InnoDB中对于段的管 理,都是引擎自身完成,不需要人为对其控制,一个段中包含多个区。区 : 区是表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。页 : 页是组成区的最小单元,页也是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。行 : InnoDB 存储引擎是面向行的,也就是说数据是按行进行存放的,在每一行中除了定义表时所指定的字段以外,还包含两个隐藏字段。3.2 MyISAMMyISAM是MySQL早期的默认存储引擎。它的特点如下:不支持事务,不支持外键支持表锁,不支持行锁访问速度快文件名格式:xxx.sdi:存储表结构信息xxx.MYD: 存储数据xxx.MYI: 存储索引3.3 MemoryMemory引擎的表数据时存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为临时表或缓存使用。它的特点如下:内存存放hash索引(默认)文明名格式:xxx.sdi:存储表结构信息3.4 区别及特点常见面试题:InnoDB引擎与MyISAM引擎的区别 ?InnoDB引擎, 支持事务, 而MyISAM不支持。InnoDB引擎, 支持行锁和表锁, 而MyISAM仅支持表锁, 不支持行锁。InnoDB引擎, 支持外键, 而MyISAM是不支持的。主要是上述三点区别,当然也可以从索引结构、存储限制等方面,更加深入的回答,具体参 考如下官方文档:MySQL :: MySQL 8.0 Reference Manual :: 15.1 Introduction to InnoDBMySQL :: MySQL 8.0 Reference Manual :: 16.2 The MyISAM Storage Engine4、存储引擎选择在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操作,那么InnoDB存储引擎是比较合适的选择。MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。
MySQL 04 基础之事务
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。就比如: 张三给李四转账1000块钱,张三银行账户的钱减少1000,而李四银行账户的钱要增加1000。 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败。正常情况: 转账这个操作, 需要分为以下这么三步来完成 , 三步完成之后, 张三减少1000, 而李四 增加1000, 转账成功 : 异常情况: 转账这个操作, 也是分为以下这么三步来完成 , 在执行第三步是报错了, 这样就导致张三减少1000块钱, 而李四的金额没变, 这样就造成了数据的不一致, 就出现问题了。 为了解决上述的问题,就需要通过数据的事务来完成,我们只需要在业务逻辑执行之前开启事务,执行完毕后提交事务。如果执行过程中报错,则回滚事务,把数据恢复到事务开始之前的状态。注意: 默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务1、事务操作数据库脚本:drop table if exists account;
create table account
(
id int primary key AUTO_INCREMENT comment 'ID',
name varchar(10) comment '姓名',
money double(10, 2) comment '余额'
) comment '账户表';
insert into account(name, money)
VALUES ('张三', 2000),
('李四', 2000);1.1 未控制事务1、测试正常情况-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';测试完毕之后检查数据的状态, 可以看到数据操作前后是一致的2、 测试异常情况-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三'; 出错了....
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四'我们把数据都恢复到2000, 然后再次一次性执行上述的SQL语句(出错了.... 这句话不符合SQL语法,执行就会报错),检查最终的数据情况, 发现数据在操作前后不一致了.1.2 控制事务一1、查看/设置事务提交方式SELECT @@autocommit ;
SET @@autocommit = 0 ;2、 提交事务COMMIT;3、回滚事务ROLLBACK;注意:上述的这种方式,我们是修改了事务的自动提交行为, 把默认的自动提交修改为了手动提交, 此时我们执行的DML语句都不会提交, 需要手动的执行commit进行提交。1.3 控制事务二1、开启事务START TRANSACTION 或 BEGIN ;2、提交事务COMMIT;3、回滚事务ROLLBACK;转账案例:-- 开启事务
start transaction
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三的余额减少1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四的余额增加1000
update account set money = money + 1000 where name = '李四';
-- 如果正常执行完毕, 则提交事务
commit;
-- 如果执行过程中报错, 则回滚事务
-- rollback;2、事务的四大特性原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。上述就是事务的四大特性,简称ACID。3、并发事务问题1、赃读:一个事务读到另外一个事务还没有提交的数据。2、不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。3、幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了 "幻影“。4、事务隔离级别为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别。主要有以下几种:1、查看事务隔离级别SELECT @@TRANSACTION_ISOLATION; 12、设置事务隔离级别SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }注意:事务隔离级别越高,数据越安全,但是性能越低。
ECS使用攻略与心得
自我介绍我是来自华南理工大学的一名研二在读学生,目前在阿里云的云原生部门实习。因为实习的原因接触到了阿里云的一些产品,感受到了国内乃至最顶尖的服务质量。处于兴趣自己想要在服务器上搭建几个环境,同学告诉我你们阿里云现在就有一个飞天加速计划·高校学生在家实践活动诶。说来也惭愧自己居然不知道。ECS使用攻略网上已经有很多各种环境搭建的攻略了,在这里我分享一下在ECS搭建中遇到的一些问题吧,有的解决了有的目前还没有解决。拿到ECS一开始我安装了jenkins,jenkins是一种类似于流程管理的工具软件,可以实现各种服务的自动流程转接。我们可以通过网页端通过ECS公网地址加上端口号直接进行访问。但是我在本地通过浏览器访问却一直连不通,明明通过ping也能ping通云服务器。在确认了远程环境搭建无误后我百思不得其解,后来通过调研资料,发现ECS默认并没有开放浏览器80端口的对外开放。最后通过实例中的安全规则配置完美解决。 有的时候大家刚上手一个东西,可能由于不熟悉会踩很多坑。在成功开放ECS访问端口后,为了学习一下现在主流的K8S容器编排服务,我选择在ECS上安装minikube(毕竟线上环境只有2核2G)。但是在安装的过程中一直遇到一个错误,最后也没能解决,查了很多资料有的人说是DNS无法解析,有一些log文件没能成功下载,但是我修改了DNS地址也没有解决。问题如下:希望有大神能帮忙解决一下。收获总结目前虽然只使用了短短两天,但是跟着阿里云社区中的一些教程走下来,感受到了阿里云服务器的稳定和方便,并且开放了很多对外的监控平台,让我们能够对程序运行乃至服务部署中发生的事情一清二楚。十分感谢阿里云社区给我们在校生提供的这么好的一次学习与探索机会。
高校学生参加飞天加速计划
我是一名来自四川的大四学生,用阿里云服务器已经一年多了,平时能够在阿里云服务器上部署一些自己的小项目,不仅能够学习,还能满足自己的一些成就感。阿里云的很多活动对学生非常友好,最开始看到阿里云的学生服务器仅需9元一月,就直接下手了一年,服务器配置能够满足我的基本需求,平时学习Linux和部署一些项目在上面拿去参赛等。 阿里云非常的人性,在开通服务器时会介绍很多知识,能够让小白快速上手。开通服务器后,就安装了宝塔面板,这是一个非常好的服务器运维面板,提供了很多便捷的工具。阿里云服务器ECS可以搭建个人网站、小程序等,安全性也很高,不需要担心安全问题。 飞天加速计划是个非常好的活动,它为我在家中学习的大学生提供了一个稳定性强、容易使用、安全度高的云服务器,可以通过学习,做出属于自己的网页,可以和同学一起开发小程序、微信公众号等等。还搭建了小程序服务端,在比赛中还获了奖。阿里云将其在、大数据、AI、低代码、数据智能等方面,提供给开发者便利。宣布升级到2.0的"飞天加速计划",助力开发者应用创新。其中包括:面向高校师生,提供30亿小时免费资源,提供教育基金,用于学生教育与实践;面向开发者,提供专属特价产品及免费学习资源、认证服务和比赛基金,给我们很多优惠以及很多方便。 最后,对计算机感兴趣的,特别是喜欢开发的,一定要拥有一台自己的云服务器,刚好阿里云的活动就能够满足大部分的学生用户,刚开始还有免费的ECS领取,也希望更多的大学生加入到这一队列来,为自己增加一个新的技能!我希望阿里云能够不断创新,给广大应用人员提供更多的方便。
47 张图带你 MySQL 进阶!!!(一)
MySQL 存储引擎存储引擎概述数据库最核心的一点就是用来存储数据,数据存储就避免不了和磁盘打交道。那么数据以哪种方式进行存储,如何存储是存储的关键所在。所以存储引擎就相当于是数据存储的发动机,来驱动数据在磁盘层面进行存储。MySQL 的架构可以按照三层模式来理解存储引擎也是 MySQL 的组建,它是一种软件,它所能做的和支持的功能主要有并发支持事务完整性约束物理存储支持索引性能帮助MySQL 默认支持多种存储引擎,来适用不同数据库应用,用户可以根据需要选择合适的存储引擎,下面是 MySQL 支持的存储引擎MyISAMInnoDBBDBMEMORYMERGEEXAMPLENDB ClusterARCHIVECSVBLACKHOLEFEDERATED默认情况下,如果创建表不指定存储引擎,会使用默认的存储引擎,如果要修改默认的存储引擎,那么就可以在参数文件中设置 default-table-type,能够查看当前的存储引擎show variables like 'table_type';奇怪,为什么没有了呢?网上求证一下,在 5.5.3 取消了这个参数可以通过下面两种方法查询当前数据库支持的存储引擎show engines \g在创建新表的时候,可以通过增加 ENGINE 关键字设置新建表的存储引擎。create table cxuan002(id int(10),name varchar(20)) engine = MyISAM;上图我们指定了 MyISAM 的存储引擎。如果你不知道表的存储引擎怎么办?你可以通过 show create table 来查看如果不指定存储引擎的话,从MySQL 5.1 版本之后,MySQL 的默认内置存储引擎已经是 InnoDB了。建一张表看一下如上图所示,我们没有指定默认的存储引擎,下面查看一下表可以看到,默认的存储引擎是 InnoDB。如果你的存储引擎想要更换,可以使用alter table cxuan003 engine = myisam;来更换,更换完成后回显示 「0 rows affected」 ,但其实已经操作成功我们使用 show create table 查看一下表的 sql 就知道存储引擎特性下面会介绍几个常用的存储引擎以及它的基本特性,这些存储引擎是 **MyISAM、InnoDB、MEMORY 和 MERGE **MyISAM在 5.1 版本之前,MyISAM 是 MySQL 的默认存储引擎,MyISAM 并发性比较差,使用的场景比较少,主要特点是不支持事务操作,ACID 的特性也就不存在了,这一设计是为了性能和效率考虑的。不支持外键操作,如果强行增加外键,MySQL 不会报错,只不过外键不起作用。MyISAM 默认的锁粒度是表级锁,所以并发性能比较差,加锁比较快,锁冲突比较少,不太容易发生死锁的情况。MyISAM 会在磁盘上存储三个文件,文件名和表名相同,扩展名分别是 .frm(存储表定义)、.MYD(MYData,存储数据)、MYI(MyIndex,存储索引)。这里需要特别注意的是 MyISAM 只缓存索引文件,并不缓存数据文件。MyISAM 支持的索引类型有 全局索引(Full-Text)、B-Tree 索引、R-Tree 索引Full-Text 索引:它的出现是为了解决针对文本的模糊查询效率较低的问题。B-Tree 索引:所有的索引节点都按照平衡树的数据结构来存储,所有的索引数据节点都在叶节点R-Tree索引:它的存储方式和 B-Tree 索引有一些区别,主要设计用于存储空间和多维数据的字段做索引,目前的 MySQL 版本仅支持 geometry 类型的字段作索引,相对于 BTREE,RTREE 的优势在于范围查找。数据库所在主机如果宕机,MyISAM 的数据文件容易损坏,而且难以恢复。增删改查性能方面:SELECT 性能较高,适用于查询较多的情况InnoDB自从 MySQL 5.1 之后,默认的存储引擎变成了 InnoDB 存储引擎,相对于 MyISAM,InnoDB 存储引擎有了较大的改变,它的主要特点是支持事务操作,具有事务 ACID 隔离特性,默认的隔离级别是可重复读(repetable-read)、通过MVCC(并发版本控制)来实现的。能够解决脏读和不可重复读的问题。InnoDB 支持外键操作。InnoDB 默认的锁粒度行级锁,并发性能比较好,会发生死锁的情况。和 MyISAM 一样的是,InnoDB 存储引擎也有 .frm文件存储表结构 定义,但是不同的是,InnoDB 的表数据与索引数据是存储在一起的,都位于 B+ 数的叶子节点上,而 MyISAM 的表数据和索引数据是分开的。InnoDB 有安全的日志文件,这个日志文件用于恢复因数据库崩溃或其他情况导致的数据丢失问题,保证数据的一致性。InnoDB 和 MyISAM 支持的索引类型相同,但具体实现因为文件结构的不同有很大差异。增删改查性能方面,果执行大量的增删改操作,推荐使用 InnoDB 存储引擎,它在删除操作时是对行删除,不会重建表。 </div>
我向面试官讲解了单例模式,他对我竖起了大拇指(三)
优势 1 :一目了然的代码代码对比饿汉式与懒汉式来说,更加地简洁。最少只需要3行代码,就可以完成一个单例模式:public enum Test {
INSTANCE;
}我们从最直观的地方入手,第一眼看到这3行代码,就会感觉到少,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。优势 2:天然的线程安全与单一实例它不需要做任何额外的操作,就可以保证对象单一性与线程安全性。我写了一段测试代码放在下面,这一段代码可以证明程序启动时仅会创建一个 Singleton 对象,且是线程安全的。我们可以简单地理解枚举创建实例的过程:在程序启动时,会调用 Singleton 的空参构造器,实例化好一个Singleton 对象赋给 INSTANCE,之后再也不会实例化public enum Singleton { INSTANCE; Singleton() { System.out.println("枚举创建对象了"); } public static void main(String[] args) { / test(); / } public void test() { Singleton t1 = Singleton.INSTANCE; Singleton t2 = Singleton.INSTANCE; System.out.print("t1和t2的地址是否相同:" + t1 == t2); }}// 枚举创建对象了// t1和t2的地址是否相同:true除了优势1和优势2,还有最后一个优势是 保护单例模式,它使得枚举在当前的单例模式领域已经是 无懈可击 了优势 3:枚举保护单例模式不被破坏使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。防反射枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。防止反序列化创建多个枚举对象在读入 Singleton 对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,使用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。小结:(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。总结(1)单例模式常见的写法有两种:懒汉式、饿汉式(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加 volatile 关键字防止指令重排序(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。 </div>
我向面试官讲解了单例模式,他对我竖起了大拇指(二)
这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。接下来要做的就是优化性能:目标是如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}上面的代码已经完美地解决了并发安全 + 性能低效问题:第 2 行代码,如果 singleton 不为空,则直接返回对象,不需要获取锁;而如果多个线程发现 singleton 为空,则进入分支;第 3 行代码,多个线程尝试争抢同一个锁,只有一个线程争抢成功,第一个获取到锁的线程会再次判断singleton 是否为空,因为 singleton 有可能已经被之前的线程实例化其它之后获取到锁的线程在执行到第 4 行校验代码,发现 singleton 已经不为空了,则不会再 new 一个对象,直接返回对象即可之后所有进入该方法的线程都不会去获取锁,在第一次判断 singleton 对象时已经不为空了因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)完整的代码如下所示:public class Singleton { private static Singleton singleton; private Singleton(){} public static Singleton getInstance() { if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化 if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支 singleton = new Singleton(); } } } return singleton; } }上面这段代码已经近似完美了,但是还存在最后一个问题:指令重排使用 volatile 防止指令重排创建一个对象,在 JVM 中会经过三步:(1)为 singleton 分配内存空间(2)初始化 singleton 对象(3)将 singleton 指向分配好的内存空间指令重排序是指:JVM 在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能在这三步中,第 2、3 步有可能会发生指令重排现象,创建对象的顺序变为 1-3-2,会导致多个线程获取对象时,有可能线程 A 创建对象的过程中,执行了 1、3 步骤,线程 B 判断 singleton 已经不为空,获取到未初始化的singleton 对象,就会报 NPE 异常。文字较为晦涩,可以看流程图:使用 volatile 关键字可以防止指令重排序,其原理较为复杂,这篇文章不打算展开,可以这样理解:使用 volatile 关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生 NPE 异常了。volatile 还有第二个作用:使用 volatile 关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。最终的代码如下所示:public class Singleton { private static volatile Singleton singleton; private Singleton(){} public static Singleton getInstance() { if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化 if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支 singleton = new Singleton(); } } } return singleton; } }破坏懒汉式单例与饿汉式单例无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。利用反射破坏单例模式下面是一段使用反射破坏单例模式的例子public static void main(String[] args) { // 获取类的显式构造器 Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor(); // 可访问私有构造器 construct.setAccessible(true); // 利用反射构造新对象 Singleton obj1 = construct.newInstance(); // 通过正常方式获取单例对象 Singleton obj2 = Singleton.getInstance(); System.out.println(obj1 == obj2); // false}上述的代码一针见血了:利用反射,强制访问类的私有构造器,去创建另一个对象利用序列化与反序列化破坏单例模式下面是一种使用序列化和反序列化破坏单例模式的例子public static void main(String[] args) { // 创建输出流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file")); // 将单例对象写到文件中 oos.writeObject(Singleton.getInstance()); // 从文件中读取单例对象 File file = new File("Singleton.file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton newInstance = (Singleton) ois.readObject(); // 判断是否是同一个对象 System.out.println(newInstance == Singleton.getInstance()); // false}两个对象地址不相等的原因是:readObject() 方法读入对象时它必定会返回一个新的对象实例,必然指向新的内存地址。让面试官鼓掌的枚举实现我们已经掌握了懒汉式与饿汉式的常见写法了,通常情况下到这里已经足够了。但是,追求极致的我们,怎么能够止步于此,在《Effective Java》书中,给出了终极解决方法,话不多说,学完下面,真的不虚面试官考你了。在 JDK 1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举枚举实现单例模式完整代码如下:public enum Singleton { INSTANCE; public void doSomething() { System.out.println("这是枚举类型的单例模式!"); }}使用枚举实现单例模式较其它两种实现方式的优势有 3 点,让我们来细品。 </div>
计算机网络基础知识总结(二)
TCP/IP 协议簇TCP/IP 协议是我们程序员接触最多的协议,实际上,TCP/IP 又被称为 TCP/IP 协议簇,它并不特指单纯的 TCP 和 IP 协议,而是容纳了许许多多的网络协议。OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、运输层、会话层、表示层和应用层。但是这显然是有些复杂的,所以在TCP/IP协议中,它们被简化为了四个层次和 OSI 七层网络协议的主要区别如下应用层、表示层、会话层三个层次提供的服务相差不是很大,所以在 TCP/IP 协议中,它们被合并为应用层一个层次。由于数据链路层和物理层的内容很相似,所以在 TCP/IP 协议中它们被归并在网络接口层一个层次里。“我们的主要研究对象就是 TCP/IP 的四层协议。下面 cxuan 和你聊一聊 TCP/IP 协议簇中都有哪些具体的协议IP 协议IP 是 互联网协议(Internet Protocol) ,位于网络层。IP是整个 TCP/IP 协议族的核心,也是构成互联网的基础。IP 能够为运输层提供数据分发,同时也能够组装数据供运输层使用。它将多个单个网络连接成为一个互联网,这样能够提高网络的可扩展性,实现大规模的网络互联。二是分割顶层网络和底层网络之间的耦合关系。ICMP 协议ICMP 协议是 Internet Control Message Protocol, ICMP 协议主要用于在 IP 主机、路由器之间传递控制消息。ICMP 属于网络层的协议,当遇到 IP 无法访问目标、IP 路由器无法按照当前传输速率转发数据包时,会自动发送 ICMP 消息,从这个角度来说,ICMP 协议可以看作是 错误侦测与回报机制,让我们检查网络状况、也能够确保连线的准确性。ARP 协议ARP 协议是 地址解析协议,即 Address Resolution Protocol,它能够根据 IP 地址获取物理地址。主机发送信息时会将包含目标 IP 的 ARP 请求广播到局域网络上的所有主机,并接受返回消息,以此来确定物理地址。收到消息后的物理地址和 IP 地址会在 ARP 中缓存一段时间,下次查询的时候直接从 ARP 中查询即可。TCP 协议TCP 就是 传输控制协议,也就是 Transmission Control Protocol,它是一种面向连接的、可靠的、基于字节流的传输协议,TCP 协议位于传输层,TCP 协议是 TCP/IP 协议簇中的核心协议,它最大的特点就是提供可靠的数据交付。TCP 的主要特点有 慢启动、拥塞控制、快速重传、可恢复。UDP 协议UDP 协议就是 用户数据报协议,也就是 User Datagram Protocol,UDP 也是一种传输层的协议,与 TCP 相比,UDP 提供一种不可靠的数据交付,也就是说,UDP 协议不保证数据是否到达目标节点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP 是一种无连接的协议,传输数据之前源端和终端无需建立连接,不对数据报进行检查与修改,无须等待对方的应答,会出现分组丢失、重复、乱序等现象。但是 UDP 具有较好的实时性,工作效率较 TCP 协议高。FTP 协议FTP 协议是 文件传输协议,英文全称是 File Transfer Protocol,应用层协议之一,是 TCP/IP 协议的重要组成之一,FTP 协议分为服务器和客户端两部分,FTP 服务器用来存储文件,FTP 客户端用来访问 FTP 服务器上的文件,FTP 的传输效率比较高,所以一般使用 FTP 来传输大文件。DNS 协议DNS 协议是 域名系统协议,英文全称是 Domain Name System,它也是应用层的协议之一,DNS 协议是一个将域名和 IP 相互映射的分布式数据库系统。DNS 缓存能够加快网络资源的访问。SMTP 协议SMTP 协议是 简单邮件传输协议,英文全称是 Simple Mail Transfer Protocol,应用层协议之一,SMTP 主要是用作邮件收发协议,SMTP 服务器是遵循 SMTP 协议的发送邮件服务器,用来发送或中转用户发出的电子邮件SLIP 协议SLIP 协议是指串行线路网际协议(Serial Line Internet Protocol),是在串行通信线路上支持 TCP/IP 协议的一种点对点(Point-to-Point)式的链路层通信协议。PPP 协议PPP 协议是 Point to Point Protocol,即点对点协议,是一种链路层协议,是在为同等单元之间传输数据包而设计的。设计目的主要是用来通过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种共通的解决方案。网络核心概念传输方式网络根据传输方式可以进行分类,一般分成两种 面向连接型和面向无连接型。面向连接型中,在发送数据之前,需要在主机之间建立一条通信线路。面向无连接型则不要求建立和断开连接,发送方可用于任何时候发送数据。接收端也不知道自己何时从哪里接收到数据。分组交换在互联网应用中,每个终端系统都可以彼此交换信息,这种信息也被称为 报文(Message),报文是一个集大成者,它可以包括你想要的任何东西,比如文字、数据、电子邮件、音频、视频等。为了从源目的地向端系统发送报文,需要把长报文切分为一个个小的数据块,这种数据块称为分组(Packets),也就是说,报文是由一个个小块的分组组成。在端系统和目的地之间,每个分组都要经过通信链路(communication links) 和分组交换机(switch packets) ,分组要在端系统之间交互需要经过一定的时间,如果两个端系统之间需要交互的分组为 L 比特,链路的传输速率为 R 比特/秒,那么传输时间就是 L / R秒。一个端系统需要经过交换机给其他端系统发送分组,当分组到达交换机时,交换机就能够直接进行转发吗?不是的,交换机可没有这么无私,你想让我帮你转发分组?好,首先你需要先把整个分组数据都给我,我再考虑给你发送的问题,这就是存储转发传输存储转发传输存储转发传输指的就是交换机在转发分组的第一个比特前,必须要接受到整个分组,下面是一个存储转发传输的示意图,可以从图中窥出端倪由图可以看出,分组 1、2、3 向交换器进行分组传输,并且交换机已经收到了分组1 发送的比特,此时交换机会直接进行转发吗?答案是不会的,交换机会把你的分组先缓存在本地。这就和考试作弊一样,一个学霸要经过学渣 A 给学渣 B 传答案,学渣 A 说,学渣 A 在收到答案后,它可能直接把卷子传过去吗?学渣A 说,等我先把答案抄完(保存功能)后再把卷子给你。排队时延和分组丢失什么?你认为交换机只能和一条通信链路进行相连?那你就大错特错了,这可是交换机啊,怎么可能只有一条通信链路呢?所以我相信你一定能想到这个问题,多个端系统同时给交换器发送分组,一定存在顺序到达和排队的问题。事实上,对于每条相连的链路,该分组交换机会有一个输出缓存(output buffer) 和 输出队列(output queue) 与之对应,它用于存储路由器准备发往每条链路的分组。如果到达的分组发现路由器正在接收其他分组,那么新到达的分组就会在输出队列中进行排队,这种等待分组转发所耗费的时间也被称为 排队时延,上面提到分组交换器在转发分组时会进行等待,这种等待被称为 存储转发时延,所以我们现在了解到的有两种时延,但是其实是有四种时延。这些时延不是一成不变的,其变化程序取决于网络的拥塞程度。因为队列是有容量限制的,当多条链路同时发送分组导致输出缓存无法接受超额的分组后,这些分组会丢失,这种情况被称为 丢包(packet loss),到达的分组或者已排队的分组将会被丢弃。下图说明了一个简单的分组交换网络在上图中,分组由三位数据平板展示,平板的宽度表示着分组数据的大小。所有的分组都有相同的宽度,因此也就有相同的数据包大小。下面来一个情景模拟: 假定主机 A 和 主机 B 要向主机 E 发送分组,主机 A 和 B 首先通过100 Mbps以太网链路将其数据包发送到第一台路由器,然后路由器将这些数据包定向到15 Mbps 的链路。如果在较短的时间间隔内,数据包到达路由器的速率(转换为每秒比特数)超过15 Mbps,则在数据包在链路输出缓冲区中排队之前,路由器上会发生拥塞,然后再传输到链路上。例如,如果主机 A 和主机 B 背靠背同时发了5包数据,那么这些数据包中的大多数将花费一些时间在队列中等待。实际上,这种情况与许多普通情况完全相似,例如,当我们排队等候银行出纳员或在收费站前等候时。转发表和路由器选择协议我们刚刚讲过,路由器和多个通信线路进行相连,如果每条通信链路同时发送分组的话,可能会造成排队和丢包的情况,然后分组在队列中等待发送,现在我就有一个问题问你,队列中的分组发向哪里?这是由什么机制决定的?换个角度想问题,路由的作用是什么?把不同端系统中的数据包进行存储和转发 。在因特网中,每个端系统都会有一个 IP 地址,当原主机发送一个分组时,在分组的首部都会加上原主机的 IP 地址。每一台路由器都会有一个 转发表(forwarding table),当一个分组到达路由器后,路由器会检查分组的目的地址的一部分,并用目的地址搜索转发表,以找出适当的传送链路,然后映射成为输出链路进行转发。那么问题来了,路由器内部是怎样设置转发表的呢?详细的我们后面会讲到,这里只是说个大概,路由器内部也是具有路由选择协议的,用于自动设置转发表。电路交换在计算机网络中,另一种通过网络链路和路由进行数据传输的另外一种方式就是 电路交换(circuit switching)。电路交换在资源预留上与分组交换不同,什么意思呢?就是分组交换不会预留每次端系统之间交互分组的缓存和链路传输速率,所以每次都会进行排队传输;而电路交换会预留这些信息。一个简单的例子帮助你理解:这就好比有两家餐馆,餐馆 A 需要预定而餐馆 B 不需要预定,对于可以预定的餐馆 A,我们必须先提前与其进行联系,但是当我们到达目的地时,我们能够立刻入座并选菜。而对于不需要预定的那家餐馆来说,你可能不需要提前联系,但是你必须承受到达目的地后需要排队的风险。下面显示了一个电路交换网络在这个网络中,4条链路用于4台电路交换机。这些链路中的每一条都有4条电路,因此每条链路能支持4条并行的链接。每台主机都与一台交换机直接相连,当两台主机需要通信时,该网络在两台主机之间创建一条专用的 端到端的链接(end-to-end connection)。分组交换和电路交换的对比分组交换的支持者经常说分组交换不适合实时服务,因为它的端到端时延时不可预测的。而分组交换的支持者却认为分组交换提供了比电路交换更好的带宽共享;它比电路交换更加简单、更有效,实现成本更低。但是现在的趋势更多的是朝着分组交换的方向发展。分组交换网的时延、丢包和吞吐量因特网可以看成是一种基础设施,该基础设施为运行在端系统上的分布式应用提供服务。我们希望在计算机网络中任意两个端系统之间传递数据都不会造成数据丢失,然而这是一个极高的目标,实践中难以达到。所以,在实践中必须要限制端系统之间的 吞吐量 用来控制数据丢失。如果在端系统之间引入时延,也不能保证不会丢失分组问题。所以我们从时延、丢包和吞吐量三个层面来看一下计算机网络分组交换中的时延计算机网络中的分组从一台主机(源)出发,经过一系列路由器传输,在另一个端系统中结束它的历程。在这整个传输历程中,分组会涉及到四种最主要的时延:节点处理时延(nodal processing delay)、排队时延(queuing delay)、传输时延(total nodal delay)和传播时延(propagation delay)。这四种时延加起来就是 节点总时延(total nodal delay)。如果用 dproc dqueue dtrans dpop 分别表示处理时延、排队时延、传输时延和传播时延,则节点的总时延由以下公式决定: dnodal = dproc + dqueue + dtrans + dpop。时延的类型下面是一副典型的时延分布图,让我们从图中进行分析一下不同的时延类型分组由端系统经过通信链路传输到路由器 A,路由器A 检查分组头部以映射出适当的传输链路,并将分组送入该链路。仅当该链路没有其他分组正在传输并且没有其他分组排在该该分组前面时,才能在这条链路上自由的传输该分组。如果该链路当前繁忙或者已经有其他分组排在该分组前面时,新到达的分组将会加入排队。下面我们分开讨论一下这四种时延节点处理时延节点处理时延分为两部分,第一部分是路由器会检查分组的首部信息;第二部分是决定将分组传输到哪条通信链路所需要的时间。一般高速网络的节点处理时延都在微妙级和更低的数量级。在这种处理时延完成后,分组会发往路由器的转发队列中排队时延在队列排队转发过程中,分组需要在队列中等待发送,分组在等待发送过程中消耗的时间被称为排队时延。排队时延的长短取决于先于该分组到达正在队列中排队的分组数量。如果该队列是空的,并且当前没有正在传输的分组,那么该分组的排队时延就是 0。如果处于网络高发时段,那么链路中传输的分组比较多,那么分组的排队时延将延长。实际的排队时延也可以到达微秒级。传输时延队列 是路由器所用的主要的数据结构。队列的特征就是先进先出,先到达食堂的先打饭。传输时延是理论情况下单位时间内的传输比特所消耗的时间。比如分组的长度是 L 比特,R 表示从路由器 A 到路由器 B 的传输速率。那么传输时延就是 L / R 。这是将所有分组推向该链路所需要的时间。真实情况下传输时延通常也在毫秒到微妙级传播时延从链路的起点到路由器 B 传播所需要的时间就是 传播时延。该比特以该链路的传播速率传播。该传播速率取决于链路的物理介质(双绞线、同轴电缆、光纤)。如果用公式来计算一下的话,该传播时延等于两台路由器之间的距离 / 传播速率。即传播速率是 d/s ,其中 d 是路由器 A 和 路由器 B 之间的距离,s 是该链路的传播速率。 </div>