第九节:事务
事务的介绍
在事物进行过程中,未结束之前,DML语句是不会更改底层数据,只是将历史操作记录一下,在内存中完成记录,只有在事物结束的时候,而且是成功的结束的时候,才会修改底层硬盘文件中的数据
事务定义
- 事务:是用户一系列的数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位(换句话说就是事务是用户写的具有完整逻辑的sql组,这个组里包含多个sql操作。这些sql要么操作都执行成功,这个逻辑才算完成。要么都失败,就回滚(rollback)到事务执行之前的数据)
- 通常一个事务对应一个完整的业务(一个完整的业务由一条或者多条的DML(insert、update、delete)语句联合完成))
- 事务用来管理 insert,update,delete 语句,或者说DML语句才有事务
- 事务中DML语句的条数和业务逻辑有关,业务逻辑不同,DML语句的个数不同(例如:在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序)
事务使用场景
- 某个业务需要执行一个SQL组,包含多个SQL语句,其中的SQL语句要么都执行,要么都不执行,才能够保证数据的完整性
- 例如银行转账:不可再分最小的单元,包含用户的一系列操作。账户转账是一个完整的业务,对应mysql中的一个事务
- 用户a给用户b转账5000元主要步骤可以概括为如下两步:
- 第一,账户a账户减去100元
- 第二,账户b账户增加100元
- 解释:a用户给b用户转账,需经过a用户扣款,b用户到账。如果在a用户扣款之后,环节发生了错误,需要a用户扣款取消,否则数据会出现a用户扣款,b用户未到账,金额的总数出现了错误。此时就需要在转账这个操作中加入事务,要么同时执行(a扣款,b到账)成功,否则回滚(a扣款,异常错误,b账号未到账,取消a扣款)
- 用户a给用户b转账5000元主要步骤可以概括为如下两步:
事务处理相关命令
- begin 或 start transaction :显式地开启一个事务
- savepoint identifier:允许在事务中创建一个保存点,一个事务中可以有多个 savepoint
- release savepoint identifier: 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常
- commit :提交事务,并使已对数据库进行的所有修改成为永久性的
- rollback :回滚会结束用户的事务,并撤销正在进行的所有未提交的修改
- rollback to identifier :把事务回滚到标记点
- 查看存储引擎:show create table 表名
- 更改引擎 :alter table 表名 engine=新引擎名
- 查询自动提交功能状态: select @@autocommit
- 查看自动提交是否开启:show variables like 'autocommit'; ( value列的on表示当前为自动提交状态。)
- 设置自动提交功能: set autocommit=0或1
- 设置分离水平 :set session transaction isolation level 分离水平
事务四大特征
- 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做
- 事务其实和一个操作没有什么太大的区别,它是一系列的数据库操作的集合,如果事务不具备原子性,就无法保证事务中的所有操作都被执行或者都未被执行
- 回滚日志:事务的原子性是通过undo日志实现的
- 事务的状态:因为事务具有原子性,所以事务有多种状态
- Partially Commited:在最后一条语句执行之后
- Failed:发现事务无法正常执行之后
- Aborted:事务被回滚并且数据库恢复到了事务进行之前的状态之后
- Commited:成功执行整个事务
- 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的
- 官方定义:如果事务从一致的数据库开始以原子方式独立运行,则在事务结束时数据库必须再次保持一致
- 一致性概念的两层含义:
- 第一层意思:是对数据完整性的约束(包括主键约束、引用约束以及一些约束检查等等),就是指事务执行前后以及过程中不会破坏数据的完整性
- 第二层意思:是对于开发者的要求,要求开发者写出逻辑正确并且合理的事务(比如银行转账,事务中的逻辑不可能只扣钱或者只加钱)
- 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
- 大部分数据库都使用READ COMMITED作为默认的事务隔离级别
- MySQL 使用了 REPEATABLE READ作为默认的事务隔离级别
- 持久性(durability):指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响
- 重做日志:事务的持久性是通过redo log来实现的
事务处理
- 事务的自动提交:在默认情况下MySQL开启的是autocommit模式,也就是隐含的将每条语句当做一个事务处理,每条SQL都会被自动提交。如果想把SQL组作为一个事务,可以把自动提交挂起,调用commit提交事务
- 显式地开启事务:会挂起自动提交来禁止当前会话的事务自动提交。显示开启事务一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0用来禁止使用当前会话的自动提交,只有当执行了commit操作才会提交事务
- 事务的开启标志:在默认情况下mysql是自动提交模式,只要执行一条DML(insert、update、delete)语句就开启了事物并且提交了事务,显示的开启事务是使用begin(或者 start transaction)标志着事务的开启
- 事务的结束标志(提交或者回滚):
- 提交:成功的结束,将内存中所有的DML语句操作历史记录和底层硬盘数据来一次同步
- 回滚:失败的结束,将内存中所有的DML语句操作历史记录全部清空
- 注意:部分回滚,仅仅是清除回滚点之后的DML语句操作,内存还存在DML语句历史操作,事务并没有结束,需要提交或者回滚才标志中事务的结束
- 手动开启事务处理的两种方法:
- 用 begin来实现:
- 使用begin或者start transaction 来挂起自动提交,声明事务的开始
- 编写业务需要的 DML语句(insert、update、delete)
- 提交事务或者回滚事务
- 实例:
begin; select * from commoditytype; delete from commoditytype where ct_id = 6; #没有提交事务,该操作没有写入磁盘 commit # 提交事务,上步的操作写入磁盘数据删除。 如果不进行commit提交事务,退出窗口重新登录,发现数据并没有删除 select * from commoditytype; #
- 使用set 设置自动改变autocommit 的值:
- set autocommit = 0 表示关闭自动提交模式(就是SQL语句不提交commit将无法改变表 中 的数据)
- set autocommit = 1 开启自动提交的模式。mysql系统自动的默认的模式(就是运行DML命令会自动进行开始提交或者回滚)
- 实例:
select @@autocommit; #查看是否开启自动提交模式,默认为1 。 set autocommit = 0; # 1表示开启自动提交模式;0表示关闭自动提交模式 delete from commoditytype where ct_id = 6; #重新开启一个mysql窗口查看数据还在 commit # 重新开启一个窗口查看数据已经删除
- 用 begin来实现:
-
事务的回滚:直接rollback是回滚到begin之前的地方,部分回滚可以回滚到任何想回滚的地方
- 直接回滚
- 将自动提交关闭,编写DML语句,然后收到回滚,内存中的DML语句全部被清除,数据表不变
- 实例:
begin; #挂起自动开启事务,声明显示开启事务 select * from commoditytype; delete from commoditytype where ct_id = 7; #该操作在内存中执行,数据没有写入到磁盘中 select * from commoditytype; #查看的是内存中表的数据,显示内存中数据已经删除 rollback; # 回滚事务,内存中所有的操作都被清除,恢复到事务开始(begin)之前的地方 select * from commoditytype; # 查看数据还是和开始的数据相同,并没有删除
- 部分回滚
- 通过savepoint设置一个回滚点,然后通过rollback to savepoint 回滚到回滚点的位置
- 注意点:rollback和commit都会将数据写入到磁盘,标志着该事务的结束;部分回滚还是在内存中操作,并没有把数据写入磁盘,事务还没有结束
- 实例:
begin; delete from commoditytype where ct_id = 6; delete from commoditytype where ct_id = 7; savepoint sp_name; delete from commoditytype where ct_id = 8; select * from commoditytype; # 此时在内存中操作,6、7、8都被删除了 rollback to savepoint sp_name; # 返回到保存点的位置,保存点前面的的DML操作都被执行,后面的操作都没有执行(该步骤还是在内存中操作的) select * from commoditytype; # 该操作还是在内存中进行的,只有6和7 被删除了 commit; # DML操作才会将数据写入到磁盘中,另一个mysql窗口中的数据才会改变;不提交,另外一个mysql窗口中的数据并没有变化
- 直接回滚
redo和undo日志
- 数据库的数据是有缓存的,如果没有缓存,每次都从读写物理disk中的数据,性能太低下了
- 存储引擎会定期将data buffer的内容刷新到data file中
- 当遇到内存不足或者data buffer已满等情况时,存储引擎也会将data buffer中的内容或者部分内容转储到data file中
- 缓存可能会导致事务失去持久性:数据库数据有了缓存,就很难保证缓存中的数据和磁盘中的数据一致,用户操作的数据都是缓存中的数据,等待存储引擎将更新的数据数据刷新到date file中,如果数据库此时宕机,就会导致数据的丢失
- 预写日志:先持久化日志的策略叫做Write Ahead Log
- 通过undo保证事务的原子性,redo保证持久性
- undo日志文件是如何保证事务的原子性?
- undo记录了数据在事务开始之前的值,当事务执行失败或者ROLLBACK时可以通过undo记录的值来恢复数据,保证事务中操作要么全部成要么全部失败
- mysql是怎么实现持久性的,为什么要使用redo实现持久性 ?
- Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失,无法实现持久性
- 如果在事务提交前直接把数据写入磁盘就行,解决事务的持久性问题,可是会造成磁盘IO的浪费和影响速度。如:
- 只是修改一个页面的一个字节,就将整个页面(一个页面16k大小)刷入磁盘,比较浪费资源
- 事务里面的SQL涉及多贵数据页面的修改,这些数据可能不是相邻的,也就属于随记IO,速度会比较慢
- 如果采用redo 日志,当做数据修改的时候,不仅会在data buffer操作,还会将修改的值记录到redo buffer中,在事务提交之前会写入到redo日志中,当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
- redo 的优点:
- redo log记录了哪一页修改的数据,体积小,刷盘快
- redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快
- 总结:redo log进行刷盘比对数据页刷盘效率高
- 数据库相关文件和缓存的介绍:
- data file:数据库存放数据的文件
- data buffer:存储引擎为date file开辟的内存缓存空间
- undo log:用于记录事务开始前的状态,用于事务失败时的回滚操作
- log buffer:存储引擎为undo日志开辟的内存缓存空间
- redo log :记录事务执行后的状态,用来恢复未写入data file的已成功事务更新的数据
- redo buffer:存储引擎为redo日志开辟的内存缓存空间
- redo和undo日子文件实现持久性和原子性的过程如下:
A 事务开始 B 记录AA=3到undo_buf (写undo日志到log buffer;) C 修改AA=1 记录redo_buf D 记录BB=5到undo_buf E 修改BB=7 记录redo_buf F 将redo_buffer写到redo log(磁盘)中 G 事务提交 H 存储引擎会在合适的时间将data buffer中的数据刷新到磁盘中
F之前系统崩溃由于所有数据都在内存,恢复后重新从磁盘载入之前的数据即可 FG之间的崩溃可以使用redo来恢复更新的数据 G之前的回滚都可以使用undo来完成,避免部分成功部分失败导致事务,保证事务的原子性
事务的隔离级别
事务的并发问题
- 脏读:事务还没提交,他的修改已经被其他事务看到
- 如:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
- 不可重复读:同一事务中两个相同SQL读取的内容可能不同。两次读取之间其他事务提交了修改可能会造成读取数据不一致
- 如:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据的结果不一致
- 幻读:同一事务中两个相同SQL读取的内容可能不同(发现他以前没发现的数据)。和不可重复读很类似,不过修改数据改成增加数据
- 如:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读
- 小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
设置事务隔离级别
- mysql默认的事务隔离级别为repeatable-read
- 查看mysql的默认的事务的隔离级别:select @@tx_isolation;
- 方式一:可以在/etc/mysql/mysql.conf.d/mysqld.cnf文件中使用transaction-isolation来设置服务器的事务隔离级别。
- transaction-isolation的值可以是:
- read-uncommitted
- read-committed
- repeatable-read
- serializable
- 例如:
- vim /etc/mysql/mysql.conf.d/mysqld.cnf
- [mysqld]下面添加:transaction-isolation = read-committed
- sudo service mysql restart
- transaction-isolation的值可以是:
- 方式二:通过命令动态设置隔离级别
- 语法:set [global | session] transaction isolation level isolation-level
- 语法解释 :
- global:对所有的会话有效
- session:只对当前的会话有效
- isolation-level的值为:
- read uncommitted
- read committed
- repeatable read
- serializable
- 实例:
- 设置会话级隔离级别为read committed : set transaction isolation level read committed;或:mysql> set session transaction isolation level read committed;
- 设置全局级隔离级别为read committed:set global transaction isolation level read committed;
事务隔离级别
- 实现事务隔离的方式基本可以分为以下两种:
- 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改
- 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(简称MVCC或MCC)
- MySQL事务隔离级别:
- 读未提交:read uncommitted
- 读已提交:read committed
- 可重复读:repeatable read
- 串行化:serializable
- 四种隔离级别对应的问题:
事务隔离级别 脏读 不可重复读 幻读 READ UNCOMMITED 是 是 是 READ COMMITED 否 是 是 REPEATABLE READ 否 否 是 SERIALIZABLE 否 否 否
读未提交
- 对于事务A和事务B,A事务没有提交的数据,事务B 也可以读到,这里读取到的数据叫做“脏数据”
- 该离级别最低,这种级别一般是在理论上存在,数据库隔离级别一般都高于该级别
- 实例:
- 打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表test
set session transaction isolation level read uncommitted; begin; select * from test;
- 在客户端A的事务提交之前,打开另一个客户端B,更新表commoditytype:
set session transaction isolation level read uncommitted; begin; update test set price = 1000 where id = 1; select * from commoditytype;
- 这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据
- B执行回滚(rollback),A 再次查询又看到以前的数据
- 可以看到,当B还没提交,A就可以看到更新的数据,这个时候很可能出现问题,比如B后来执行ROLLBACK,那客户端A查询到的数据其实就是脏数据
- 打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表test
读已提交
- 事物A和事物B,事物A提交的数据,事物B才能读取到,这种级别可以避免“脏数据”,这种隔离级别会导致“不可重复读取”,Oracle默认隔离级别
- 解释:
- 事务事务A没有提交之前,事务B都是读取到是事务开始之前的数据(解决脏读问题,事务B读取的数据不会随着事务A中DML语句的修改而修改)
- 但是事务A提交之后,事务B读取的是事务A提交之后的问题,导致事务B读取两次的数据不一致
- 实例:
- 打开一个客户端A,并设置当前事务模式为read committed,查询表test的所有记录:
set session transaction isolation level read committed; begin; select * from test;
- 打开另一个客户端B,更新test中的数据:
set session transaction isolation level read committed; begin; update test set price = 1000 where id = 1;
- 客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
- 客户端B的提交事务
- 客户端A查询查询到客户端B更新的数据,客户端A中的事务A两次查询的结果不一致,即产生不可重复读的问题
- 打开一个客户端A,并设置当前事务模式为read committed,查询表test的所有记录:
可重复读
- 在事物A处理数据过程中对数据多次读取,由于事务B的插入/删除操作而导致事务A在多次读取过程中读取到不存在或者消失的数据
- 解决了不可重复读的问题但是没有解决幻读的问题
- 注意: mysql中 可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本),解决了幻读问题;insert、update和delete会更新版本号,是当前读(当前版本),没有解决幻读问题
- 实例:
- 打开一个客户端A,并设置当前事务模式为repeatable read,查询表test的所有记录
set session transaction isolation level repeatable read; begin; select * from test;
- 打开另一个客户端B,更新表test并提交
set session transaction isolation level repeatable read; begin; update test set price = 2000 where id = 2; insert into test values(4,400); commit;
- 在客户端A查询表test的所有记录,查询结果一样,没有出现不可重复读和幻读的问题(select操作不会更新版本号,是快照读(历史版本)
- 在客户端A,接着执行 (客户端A 读取的到数据不含有id=4 这行,id= 2 对应 price= 200)
update test set price = price + 100 where id = 2, # insert、update和delete会更新版本号,其他事务提交之后的版本 update test set price = price + 100 where id = 4,
-
客户端A :id= 2 对应的price= 2100 ;id = 4 对应的price= 500
- 打开一个客户端A,并设置当前事务模式为repeatable read,查询表test的所有记录
串行化
- 事务操作(insert、update和delete)数据库时,事务B只能排队等待事务A结束,事务A和事务B串行而不并发
- 这种隔离级别吞吐量太低,很少使用,可以避免幻读
- 实例:
- 打开客户端A,并设置当前事务模式为serializable,查询表test
set session transaction isolation level serializable; begin; select * from test;
- 打开客户端B,并设置当前事务模式为serializable
set session transaction isolation level serializable; begin; select * from test; insert into test values (5,500);# 该语句提交后不会立即执行而是会进入排队等待的状态,等待事务A结束才会执行
- A事务正在进行中,B事务的delete、update和insert操作无法立即执行,而是一直等待;直到事务A提交,事务B的操作才可以执行
- 打开客户端A,并设置当前事务模式为serializable,查询表test
事务隔离级别的实现
- 上面的都是数据库中的理论概念,在mysql和oracle这样的数据库中为了性能并不是完全按照上面的理论来实现的
- mysql就是利用的是锁和MVCC机制来实现事务的隔离的(mysql使用锁和mvcc在可重复读阶段解决了幻读(查询幻读)问题)
- 那种隔离级别中用到MVCC机制?
- MVCC只适用于Msyql隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read)
- Read uncimmitted由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC.(MVCC的创建版本和删除版本只要在事务提交后才会产生)
- 串行化由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题
- 综上所述:MVCC主要作用于事务性的,由行锁控制的数据库模型
- 数据库对于隔离级别的实现:就是使用并发控制机制对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新
- 介绍三个重要的并发控制机制:锁、时间戳和MVCC
锁
- 锁是一种常见的并发控制机制
- mysql中锁分为两种:
- 共享锁(Shared),又称读锁。
- 排它锁(Exclusive),又称为写锁
- 读锁保证了读操作可以并发执行,相互不会影响,而写锁保证了在更新数据库数据时不会有其他的事务访问或者更改同一条记录造成不可预知的问题
- 读写锁之间的关系:
- 一个事务对数据对象O加了 读锁,可以对 O进行读取操作,但是不能进行更新操作。加锁期间其它事务能对O 加 读锁,但是不能加 写锁
- 一个事务对数据对象 O 加了写锁,就可以对 O 进行读取和更新。加锁期间其它事务不能对 O 加任何锁
- 总结:多读单写(数据对象可以被多个事务添加读锁,如果一个事务添加了写锁,其他事务则不可以加锁)
时间戳
- 时间戳也是一种实现事务隔离性的并发控制器。利用该方式的实现事务隔离性的的如PostgreSQL
- 数据库会为每一条记录保留两个字段;读时间戳和写时间戳
- 读时间戳:记录了所有的访问该记录的事务中最大的时间戳(包括了所有访问该记录的事务中的最大时间戳)
- 写时间戳:保存了将记录改到当前值的事务的时间戳
- 使用时间戳实现事务的隔离性时,往往都会使用乐观锁,先对数据进行修改,在写回时再去判断当前值,也就是时间戳是否改变过,如果没有改变过,就写入,否则,生成一个新的时间戳并再次更新数据
MVCC
- mysql通过维护多个版本的数据,数据库可以允许事务在数据被其他事务更新时对旧版本的数据进行读取;因为所有的读操作不再需要等待写锁的释放,所以能够显著地提升读的性能
- 快照读:不会更新版本号,读取历史数据的方式叫做快照读(select操作会执行快照读)
- 当前读 :会更新版本号,读取数据库最新版本数据的方式当前读(update、insert、delete操作都会执行当前读)
- 特点:
- MVCC其实广泛应用于数据库技术,适用范围广
- MVCC并没有简单的使用数据库的行锁,而是使用了行级锁
- InnoDB存储引擎MVCC的实现策略:
- 会在表的每一行数据中额外添加两个隐藏的列:分别保存当前行创建时的系统版本号的值和当前行删除时的系统版本号的值
- 一个行记录数据含有的多个版本的快照数据保存在在undo log中
- 事务提交之后才会覆盖以前的版本,失败或者回滚恢复以前的版本
- 系统版本号:系统版本号是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增
- 事务版本号:事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较
- 每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的
- 基本原理
- MVCC的实现,通过保存数据在某个时间点的快照来实现的。同一事务的版本号是相同的,所以读到的行的数据版本时相同的,所以在同一个事务内无论何时查询到的数据一致;不同事务的版本号是不同的,同一个时刻不同事务查询相同表里的数据可能是不同的
- 注意:
- select操作会是快照读,不会更新版本号
- insert、update、delete操作是当前读,会更新版本号
- 具体实例解释如下:
- 事务A:插入数据,就把事务版本号作为创建版本号
- 插入一行数据:insert into Student values(1,"Tom"); # insert是当前读,会更新版本号
- 假设事务A版本号为100,那么插入后的数据行如下:
id name create version delete version 1 Tom 100
- 事务B :在更新操作的时候,采用的是先标记旧的那行记录为已删除,然后插入一行新的记录的方式
- select * from Student where id =1; # 返回的结果是Tom
- 要把name字段更新:update Student set name= 'Weiking' where id=1; # update是当前读,会更新版本号
- select * from Student where id =1; # 返回的结果是Weiking
- 假设事务B版本号为101,那么更新后的数据行如下:
id name create version delete version 1 Tom 100 101 1 Weiking 101
- 事务C:查询操作不会更新版本号
- 查询了该行的数据:select * from Student where id =1;
- 假设事务C版本号为102,那么更新后的数据行如下:
id name create version delete version 1 Tom 100 101 1 Weiking 101
- 事务D:删除操作的时候,就把事务版本号作为删除版本号
- 删除了该行数据:delete from table where id=1; # delete 是当前读,会更新版本号
- 假设事务D版本号为103,那么更新后的数据行如下:
id name create version delete version 1 Tom 100 101 1 Weiking 101 103
- 结论:当查询事务版本号小于当前版本的删除版本号(或者删除版本号为空),且大于或等于当前版本的创建版本号,即可查看该版本的数据
- 注意:事务B满足上面两个版本,所以Tom和Weiking的数据都可以查询出来(更新之前查询是Tom,更新之后查询是Weiking)
- 事务A:插入数据,就把事务版本号作为创建版本号

浙公网安备 33010602011771号