深入理解MySQL事务(万字详)
文章目录
- 什么是事务
- 为什么会出现事务
- 事务的版本支持
- 事务的提交方式
- 事务常见操作方式
- 正常演示 - 证明事务的开始与回滚
- 非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
- 非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
- 非正常演示3 - 对比试验。begin会自动更改提交方式
- 非正常演示4 - 证明单条 SQL 与事务的关系
- 事务的隔离级别
- 查看与设置隔离性
- 读未提交(Read Uncommitted)
- 读提交(Read Committed)
- 可重复读(Repeatable Read)
- 串行化(Serializable)
- 一致性
- 多版本并发控制MVCC
- 3个记录隐藏列字段
- undo日志
- 快照
- Read View
什么是事务
首先,我们知道MySQL是一定会在同时被多个用户访问的,那么就会发送下面这种情况
上述情况就会导致,同一张票被出售两次,显然不合理;
那么我们应该如何做,才可以避免上述情况呢?当满足下列条件时,即可避免
- 买票的过程是原子的
我买票的时候别人不能影响我
- 买票的过程不能互相影响
我正在买的时候,你来买了
- 买完票应该是永久的
我买完票,交完钱了,你不能不给我票,直接扣我的钱,而什么也不给我,即不给票也不还钱
- 买前,买后状态是确定的
买前、买后、买成功、买失败,不能模棱两可,一定是确定的,成不成功并不重要
那么到底什么是事务呢???
事务是一组逻辑上相关的数据库操作(DML语句),这些操作要么全部成功,要么全部失败。事务的主要目的是确保数据的完整性和一致性。例如,在一个银行转账系统中,转账操作需要同时更新两个账户的余额,这两个操作必须作为一个整体执行,否则会导致数据不一致。
- 我对我的账号
+100
; - 我对你的账号
-100
;
单独的一条SQL并没有实际意义
一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包
装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,最多很多 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题
所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性,通常称为ACID属性:
-
原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。如果事务在执行过程中发生错误,系统会回滚到事务开始前的状态
-
一致性(Consistency):事务执行前后,数据库必须保持一致状态。这意味着事务执行后,数据库必须满足所有的完整性约束
-
隔离性(Isolation):多个事务并发执行时,每个事务的操作应该与其他事务隔离,防止数据不一致。MySQL提供了不同的隔离级别来控制事务的隔离程度,包括读未提交( Readuncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
-
持久性(Durability):一旦事务提交,对数据的修改就是永久的,即使系统发生故障也不会丢失
综上,事务就是在ACID
四大属性的加持下,由一条、多条SQL构成的;
事务的本质(白话)是站在MySQL使用者角度,我要完成一个功能(转账),这个功能由多条SQL语句组成;
为什么会出现事务
首先,事务被 MySQL 编写者设计出来;
本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,
不需要我们去考虑各种各样的潜在错误和并发问题。
因此事务本质上是为了应用层服务的
,而不是伴随着数据库系统天生就有的。
事务的版本支持
首先,在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
我们可以通过以下SQL来查询
- Engine: 表示存储引擎的名称
- Support: 表示服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎,DISABLED表示支持引擎但已将其禁用
- Comment: 表示存储引擎的简要说明
- Transactions: 表示存储引擎是否支持事务,可以看到InnoDB存储引擎支持事务,而MyISAM存储引擎不支持事务
- XA: 表示存储引擎是否支持XA事务
- Savepoints: 表示存储引擎是否支持保存点
事务的提交方式
事务的提交方式常见的有两种:
- 自动提交
- 手动提交
我们可以用下面SQL来查询当前的提交方式
此时Value的值为ON表示自动提交
用 SET 来改变 MySQL 的自动提交模式:
将autocommit的值设置为1表示打开自动提交,设置为0表示关闭自动提交
事务常见操作方式
正常演示 - 证明事务的开始与回滚
提前准备
为了便于演示,我们将MySQL的隔离级别设置的比较低,即成读未提交
现象如下
创建测试表,简单银行用户表
启动一个事务,一旦启动后,之后的所有SQL都属于同一个事务
两种启动事务的方式
设置savapoint s1
并插入一条数据
设置savapoint s2
并插入一条数据
设置savapoint s3
并插入一条数据
回滚事务
回滚到s3时,s3后面的SQL就没有了;
回滚到s1时,s1后面的SQL就没有了;
提交事务
提交事务后就不能回滚了
丢弃整个SQL,直接
rollback;
非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
事务在提交之前因为某些原因与MySQL断开连接,那么MySQL会自动让事务回滚到最开始,此时事务内容无效;
这也就是ACID中的A属性,原子性
非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
非正常演示3 - 对比试验。begin会自动更改提交方式
查看autocommit的值为ON,表示事务的提交方式是自动提交
事务在提交之前与MySQL断开连接,那么MySQL依旧会自动让事务回滚到最开始;
证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
非正常演示4 - 证明单条 SQL 与事务的关系
将autocommit设置为ON,表示事务执行后自动提交
- 查看autocommit的值为ON,表示事务的提交方式是自动提交
- 左终端中直接向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中肯定能够查询到新插入的这条记录
- 执行单条SQL后不使用commit进行提交,MySQL异常退出,这时右终端仍然可以看到之前新插入的记录了,因为单条SQL在执行后被自动提交持久化了
将autocommit设置为OFF,表示事务执行后需要手动提交
- 设置autocommit的值为OFF,表示事务执行后需要手动提交
- 左终端中直接向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中肯定能够查询到新插入的这条记录
- 执行单条SQL后不使用commit进行提交,MySQL异常退出,这时右终端看不到之前新插入的记录了,因为单条SQL在执行后异常退出,MySQL断开连接则会自动进行回滚操作
结论:我们之前一直都在使用单SQL事务,只不过autocommit默认是打开的,因此单SQL事务执行后自动就被提交了
事务的隔离级别
举个例子:
你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
- 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
- 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
-
读未提交(Read Uncommitted):事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读。
-
读已提交(Read Committed):事务只能读取其他事务已提交的数据,避免了脏读,但可能会导致不可重复读和幻读。
-
可重复读(Repeatable Read):MySQL默认的隔离级别,确保同一事务中多次读取同一数据时,结果一致。避免了脏读和不可重复读,但可能会有幻读。
-
串行化(Serializable):最高的隔离级别,强制事务串行执行,避免了脏读、不可重复读和幻读,但性能最差。
查看与设置隔离性
查看隔离级别
- 查看全局的隔离级别
SELECT @@global.transaction_isolation;
- 查看会话隔离级别
SELECT @@session.transaction_isolation;
也可以省略掉session
SELECT @@transaction_isolation;
设置隔离级别
语法:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED
| READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
- 设置会话隔离级别
只会影响当前会话的隔离级别,不会影响全局的,即使新起会话也不会影响
- 设置全局隔离级别
设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话
读未提交(Read Uncommitted)
事务A所作的修改在没有提交之前,事务B就已经能够看到了
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读
读提交(Read Committed)
事务A所作的修改在没有提交之前,事务B不能看到
只有当事务A提交后,事务B才能看到修改后的数据
在B事务没有commit之前,执行过程中,两个相同的select查询得到了不同的数据,这种现象叫做不可重复读
不可重复读是个问题吗??
可重复读(Repeatable Read)
A事务修改数据,B事务并不能查到
只有A事务commit后,B事务也查不到
只有当双方都commit后,B事务才可以查到
- 在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是可重复读
- 一个事务在执行过程中,相同的select查询得到了新的数据,如同出现了幻觉,这种现象叫做幻读
串行化(Serializable)
将隔离级别都设置为串行化,双方查询互不影响
此时事务A要删除一个数据,但是阻塞在了这里
但是事务B的查询并不受影响
当事务Bcommit后,离开事务A就恢复,立马执行SQL对表进行修改
- 串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用
结论:
一致性
多版本并发控制MVCC
数据库的并发场景
- 读-读并发:不存在任何问题,也不需要并发控
- 读-写并发:有线程安全问题,可能会存在事务隔离性问题,可能遇到脏读、幻读、不可重复读
- 写-写并发:有线程安全问题,可能会存在两类更新丢失问题
- 每个事务都有自己的事务ID,ID的大小决定着,事务到来的顺序;
- mysqld可能会在一个时间范围中处理多个事务,事务也有自己的声明周期,mysqld要对多个事务进行管理;
3个记录隐藏列字段
当我们创建下面一个表
该记录不仅包含name和age字段,还包含上述三个隐藏字段
undo日志
MySQL 中的一段内存缓冲区,用来保存日志数据的
- redo log:重做日志,用于MySQL崩溃后进行数据恢复,保证数据的持久性
- bin log:逻辑日志,用于主从数据备份时进行数据同步,保证数据的一致性
- undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性
快照
现在有一个事务ID为10的事务,要将刚才插入学生表中的记录的学生姓名“张三”改为“李四”
- 事务10,因为要修改,所以要先给该记录加行锁
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
- 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 ‘李四’。并且修改原始记录的隐藏字段
DB_TRX_ID
为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针DB_ROLL_PTR
列,里面写入undo log
中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它 - 事务10提交,释放锁
又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)
- 先给该记录加行锁
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log
- 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段
DB_TRX_ID
为当前 事务11 的ID。而原始记录的回滚指针DB_ROLL_PTR
列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它 - 事务11提交,释放锁
这样就形成了一个基于历史版本的链表
上面的每个版本,我们可以称之为快照
当前读 、快照读
当前读:读取最新的记录,就叫做当前读。
快照读:读取历史版本,就叫做快照读。
事务在进行增删查改的时候,并不是都需要进行加锁保护:
-
事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需要进行加锁保护
-
事务在进行select查询的时候,既可能是当前读也可能是快照读,如果是当前读,那也需要进行加锁保护,但如果是快照读,那就不需要加锁,因为历史版本不会被修改,也就是可以并发执行,提高了效率,这也就是MVCC的意义
Read View
Read View
就是事务进行 快照读 操作的时候生产的读视图
(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID
其源码
class ReadView {// 省略...
private:/** 高水位:大于等于这个ID的事务均不可见*/trx_id_t m_low_limit_id;/** 低水位:小于这个ID的事务均可见 */trx_id_t m_up_limit_id;/** 创建该 Read View 的事务ID*/trx_id_t m_creator_trx_id;/** 创建视图时的活跃事务id列表*/ids_t m_ids;/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/** 标记视图是否被关闭*/bool m_closed;// 省略...
};
也就是
-
id < m_up_limit_id || id == m_creator_trx_id
事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见 -
id >= m_low_limit_id
事务id大于等于m_low_limit_id,则不可见 -
m_ids.empty()
事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见
相关文章:

深入理解MySQL事务(万字详)
文章目录 什么是事务为什么会出现事务事务的版本支持事务的提交方式事务常见操作方式正常演示 - 证明事务的开始与回滚非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)非正常演示2 - 证明commit了…...

微信小程序使用picker根据接口给的省市区的数据实现省市区三级联动或者省市区街道等多级联动
接口数据如上图 省市区多级联动,都是使用的一个接口通过传参父类的code。返回我们想要的数据 比如获取省就直接不要参数。市就把省得code传给接口,区就把市的code作为参数。 <picker mode"multiSelector" :range"mulSelect1" …...

Go Fx 框架使用指南:深入理解 Provide 和 Invoke 的区别
1. 什么是 Fx 框架? Fx 是一个基于 Go 语言的依赖注入框架,专注于简化应用程序的生命周期管理和依赖的构建。在复杂的应用程序中,Fx 通过模块化的设计方式将组件连接起来,使开发者能够更高效地管理依赖关系。 Fx 的核心理念是&a…...

VSCode+Continue实现AI辅助编程
Continue是一款功能强大的AI辅助编程插件,可连接多种大模型,支持代码设计优化、错误修正、自动补全、注释编写等功能,助力开发人员提高工作效率与代码质量。以下是其安装和使用方法: 一、安装VSCode 参见: vscode安…...

阿里云服务器在Ubuntu上安装redis并使用
1、redis安装 sudo apt install lsb-release curl gpgcurl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpgecho "deb [signed-by/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.…...

Blazor-Blazor呈现概念
静态和交互式呈现概念 在Blazor开发中,Razor 组件具备两种重要的呈现方式,分别是静态呈现和交互式呈现。 静态呈现 也被称为静态渲染,是一种典型的服务器端方案。在这种模式下,组件呈现时,用户与.NET/C# 代码之间缺…...

14-6-2C++的list
(一)list对象的带参数构造 1.list(elem);//构造函数将n个elem拷贝给本身 #include <iostream> #include <list> using namespace std; int main() { list<int> lst(3,7); list<int>::iterator it; for(itlst.begi…...

StarRocks常用命令
目录 1、StarRocks 集群管理&配置命令 2、StarRocks 常用操作命令 3、StarRocks 数据导入和导出 1、StarRocks 集群管理&配置命令 查询 FE 节点信息 SHOW frontends; SHOW PROC /frontends; mysql -h192.168.1.250 -P9030 -uroot -p -e "SHOW PROC /dbs;"…...

激光雷达和相机早期融合
通过外参和内参的标定将激光雷达的点云投影到图像上。 • 传感器标定 首先需要对激光雷达和相机(用于获取 2D 图像)进行外参和内参标定。这是为了确定激光雷达坐标系和相机坐标系之间的转换关系,包括旋转和平移。通常采用棋盘格等标定工具&…...

PMP–一、二、三模–分类–12.采购管理
文章目录 技巧十二、采购管理 一模12.采购管理--3.控制采购--输出--风险登记册--每个被选中的卖方都会带来特殊的风险。随着早期风险的过时以及新风险的出现,在项目执行期间对风险登记册进行变更。 供应商还未开始做,是一个风险,当做风险进行…...

C++ 标准模板库 (STL, Standard Template Library)
声明:大佬们~这是Tubishu在追寻stl过程中偶然得到了“颢天”大佬的笔记,shushu感觉非常有帮助🔥又颢天佬未曾来过CSDN,索性在此传达颢天大佬的功德🧎 传送门在此➡️颢天笔记✨✨ C 标准模板库 (STL, Standard Templa…...

从spec到iso的koji使用
了解一下Linux发行版流程::从spec到iso的koji使用 for Fedora 41。 Fedora 41有24235个包,我们选择 minimal 的几十个源码包,百多个rpm包构建。 配3台服务器 40C64G 48C64G 80C128G,有点大材小用,一台就够了 …...

【记录自开发的SQL工具】工具字符拼接、Excel转sql、生成编码、生成测试数据
记录自己开发的一个SQL聚合工具 功能介绍: 文本加引号 给多行文本前后添加引号,并用逗号连接,直接复制到 sql 中的 in 条件中 Excel转SQL 适用于将Excel表格的数据,批量导入到数据库的场景 此工具能快速将excel表格转换为i…...

Cesium特效——城市白模的科技动效的各种效果
最终效果图如下: 实现方法: 步骤一:使用cesiumlib生产白模,格式为3dtiles 注意事项:采用其他方式可能导致白模贴地,从而导致不能实现该效果,例如把步骤二的服务地址改为Cesium Sandcastle 里的…...

VS Code i18n国际化组件代码code显示中文配置 i18n ally
VUE项目做i18n国际化之后,代码中的中文都变成了code这时的代码就会显得非常难读,如果有一个插件能把code转换成中文显示就好了 vscode插件搜索“i18n ally” 在项目根文件夹下创建文件:.vscode/settings.json settings.json 内容如下 {"…...

C++ —— 智能指针 unique_ptr (上)
C —— 智能指针 unique_ptr (上) 普通指针的不足普通指针的释放智能指针智能指针 unique_ptr智能指针初始化错误用法get()方法返回裸指针智能指针不支持指针的运算(、-、、- -) 普通指针的不足 new和new [] 的内存需要用delete和…...

技术 · 创作 · 生活 | 我的 2024 全面复盘
目录 🌟2024年度总结:回顾、成长与突破🌟🚀 一、技术成长与突破 🚀🔗 1. 深入区块链与智能合约🔍 2. 探索新兴技术 ✍️ 二、创作与博客历程 ✍️📖 1. 内容创作的演变🏆…...

表的增删改查(MySQL)
1. 表的增删改查 CRUD : Create(创建), Retrieve(读取),Update(更新),Delete(删除) 1.1 Create 语法: INSERT [INTO] table_name [(column [, column] ...)] VALUES (value_list) [, (value_list)] ...value_list:…...

【设计模式】JAVA 策略 工厂 模式 彻底告别switch if 等
【设计模式】JAVA 策略 工厂 模式 彻底告别switch if 等 目录 【设计模式】JAVA 策略 工厂 模式 彻底告别switch if 等 优势 适用场景 项目结构 关键代码 优势 消除 switch:将分支逻辑分散到独立的策略类中。 开闭原则:新增类型只需添加新的 TypeHa…...

基于Springboot用axiospost请求接收字符串参数为null的解决方案
问题 今天在用前端 post 请求后端时发现,由于是以 Json对象的形式传输的,后端用两个字符串形参无法获取到对应的参数值 前端代码如下: axios.post(http://localhost:8083/test/postParams,{a: 1, b:2} ,{Content-Type: application/jso…...

最长递增——蓝桥杯
1.题目描述 在数列 a1,a2,⋯,an 中,如果ai<ai1<ai2<⋯<aj,则称 ai 至 aj 为一段递增序列,长度为 j−i1。 定一个数列,请问数列中最长的递增序列有多长。 输入描述 输入的第一行包含一个整数 n。…...

【MFC】C++所有控件随窗口大小全自动等比例缩放源码(控件内字体、列宽等未调整) 20250124
MFC界面全自动等比例缩放 1.在初始化里 枚举每个控件记录所有控件rect 2.在OnSize里,根据当前窗口和之前保存的窗口的宽高求比例x、y 3.枚举每个控件,根据比例x、y调整控件上下左右,并移动到新rect struct ControlInfo {CWnd* pControl;CRect original…...

C#标准Mes接口框架(持续更新)
前言 由于近期我做了好几个客户的接入工厂Mes系统的需求。但是每个客户的Mes都有不同程度的定制需求,原有的代码复用难度其实很大。所以打算将整个接入Mes系统的框架单独拿出来作为一个项目使用,同时因为不同的设备接入同一个Mes系统,所以代…...

【Uniapp-Vue3】动态设置页面导航条的样式
1. 动态修改导航条标题 uni.setNavigationBarTitle({ title:"标题名称" }) 点击修改以后顶部导航栏的标题会从“主页”变为“动态标题” 2. 动态修改导航条颜色 uni.setNavigationBarColor({ backgroundColor:"颜色" }) 3. 动态添加导航加载动画 // 添加加…...

SQL 递归 ---- WITH RECURSIVE 的用法
SQL 递归 ---- WITH RECURSIVE 的用法 开发中遇到了一个需求,传递一个父类id,获取父类的信息,同时获取其所有子类的信息。 首先想到的是通过程序中去递归查,但这种方法着实孬了一点,于是想,sql能不能递归查…...

期权帮|如何利用股指期货进行对冲套利?
锦鲤三三每日分享期权知识,帮助期权新手及时有效地掌握即市趋势与新资讯! 如何利用股指期货进行对冲套利? 对冲就是通过股指期货来平衡投资组合的风险。它分为正向与反向两种策略: (1)正向对冲ÿ…...

INCOSE需求编写指南-第1部分:介绍
第1部分:介绍Section 1: Introduction 1.1 目的和范围 Purpose and Scope 本指南专门介绍如何在系统工程背景下以文本形式表达需求和要求陈述。其目的是将现有标准(如 ISO/IEC/IEEE 29148)中的建议以及作者、主要贡献者和审稿员的最佳实践结…...

FFPlay命令全集合
FFPlay是以FFmpeg框架为基础,外加渲染音视频的库libSDL构建的媒体文件播放器。 ffplay工具下载并播放视频,可以辅助卡看流信息。 官网下载地址:http://ffmpeg.org/download.html#build-windows 下载build好的exe程序: 此处下载…...

Mono里运行C#脚本34—内部函数调用的过程
本文来分析Mono运行脚本时,会调用一些C实现的函数代码。 而这个过程又是怎么样实现的呢? 比如前面分析的脚本: IL_0000: call string class MonoEmbed::gimme() 在这里会调用C函数实现的MonoEmbed::gimme()函数。 而这个函数是在C程序内部实现,通过下面的代码来注册到运行…...

rust feature h和 workspace相关知识 (十一)
feature 相关作用和描述 在 Rust 中,features(特性) 是一种控制可选功能和依赖的机制。它允许你在编译时根据不同的需求启用或禁用某些功能,优化构建,甚至改变代码的行为。Rust 的特性使得你可以轻松地为库提供不同的…...