- 事务的ACID性质
- 事务分类
- 事务提交和回滚
- 事务的隔离性和隔离级别
- 脏读、幻读、不可重复读
- 事务实现原理
- ————————数据库隔离级别——————-
- 查数据库的隔离级别
- 创建 bank表
- 创建 finance
- 查询当前数据库中的所有表
- 在back表中插入数据
- finance 插入数据
- 查询表数据
- undo log
- mysql锁
- MVCC多版本并发控制
- 如insert、update、delete操作时,删除操作用1个bit表示。
- DB_TRX_ID是最重要的一个,可以通过语句“show engine innodb status”来查找
- DB_TRX_ID记录了行的创建的时间删除的时间在每个事件发生的时候,每行存储版本号,而不是存储事件实际发生的时间。每次事物的开始这个版本号都会增加。自记录时间开始,每个事物都会保存记录的系统版本号。
- 依照事物的版本来检查每行的版本号。在insert操作时 “创建时间”=DB_TRX_ID,这时,“删除时间”是未定义的;在update时,复制新增行的“创建时间”=DB_TRX_ID,删除时间未定义,旧数据行“创建时间”不变,
- 删除时间=该事务DB_TRX_ID;delete操作,相应数据行的“创建时间”不变,删除时间=该事务的DB_ROW_ID;select操作对两者都不修改,只读相应的数据
事务定义:
数据中的事务是指,对数据库执行一批操作,该操作是一个原子操作,是一个最小执行单位,可以有多个sql语句组成,在整个操作中,所有sql语句必须成功,有一个sql执行失败,则整个操作都失败,数据回滚。这样的操作就是一组事务。
例如:
张三向王五转账,过程分为两大步:
- 张三账户余额扣除100元
- 王五账户加100元
那么步骤一和步骤二,就是一组事务,当其中一步执行失败,那么数据就需要回滚,张三和王五的账户余额都不能改变,只有当两步都执行成功,这才是具有原子性的事务操作。
事务的ACID性质
一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。(比如:A向B转账,不可能A扣了钱,B却没有收到)
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。(比u人:A正在从一张银行卡里面取钱,在A取钱的过程中,B不能向这张银行卡打钱)
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务分类
mysql中事务默认是隐式事务,执行insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。
是否开启隐式事务是由变量autocommit控制的
事务分为隐式事务和显式事务
隐式事务
事务自动开启、提交或回滚,比如insert、update、delete语句,事务的开启、提交或回滚由mysql内部自动控制的。
查看变量autocommit是否开启了自动提交【autocommit为ON表示开启了自动提交】
show variables like 'autocommit';#默认开启
显式事务
也就是通过sql控制事务
语法:
start transaction;# 开启事务
..... # 组成事务的DML语句
# 执行事务操作
commit|rollback;## 提交/回滚
事务提交和回滚
mysql> start transaction;#手动开启事务
mysql> insert into t_user(name) values('pp');
mysql> commit;#commit之后即可改变底层数据库数据
mysql> select * from t_user;
+----+------+
| id | name |
+----+------+
| 1 | jay |
| 2 | man |
| 3 | pp |
+----+------+
3 rows in set (0.00 sec)
mysql> start transaction;
mysql> insert into t_user(name) values('yy');
mysql> rollback; # 事务回滚
mysql> select * from t_user;
+----+------+
| id | name |
+----+------+
| 1 | jay |
| 2 | man |
| 3 | pp |
+----+------+
3 rows in set (0.00 sec)
savepoint关键字
除了通过rollback
回滚整个事务,我们还可以通过关键字savepoint
指定回滚部分数据。
演示示例:
# 检查表中不存在数据
select * from testDigData;
#开启事务
start transaction;
# 向表中添加数据
insert into testDigData (name, age, phone, createDate, lastUpdateTime,user_uuid) VALUES ('张三',15,18739473139,now(),now(),uuid());
# 设置保存点
savepoint part1;
# 再次新增一条数据
insert into testDigData (name, age, phone, createDate, lastUpdateTime,user_uuid) VALUES ('王五',18,18739473139,now(),now(),uuid());
这个时候我们发现第二条数据,是无效的不需要添加入库的,那么我们通过保存点就可以回滚第二条数据
# 回滚 保存点到当前rollback之间所有操作
rollback to part1;
# 提交事务
commit ;
# 再次查询表数据
select * from testDigData;
执行了2次插入操作,最后只插入了1条数据。
注意:
隔离性有隔离级别(4个):
- 读未提交:read uncommitted
- 读已提交:read committed
- 可重复读:repeatable read
- 串行化:serializable | | 脏读 | 不可重复读 | 幻读 | | —- | —- | —- | —- | | Read uncommitted | √ | √ | √ | | Read committed | × | √ | √ | | Repeatable read | × | × | √【对InnoDB不可能】 | | serializable | × | × | × |
读未提交
- 事物A和事物B,事物A未提交的数据,事物B可以读取到
- 这里读取到的数据叫做“脏数据”
这种隔离级别最低,这种级别一般是在理论上存在,数据库隔离级别一般都高于该级别
读已提交
事物A和事物B,事物A提交的数据,事物B才能读取到,这种隔离级别高于读未提交,换句话说,对方事物提交之后的数据,我当前事物才能读取到
- Oracle默认隔离级别
- 这种级别可以避免“脏数据”
-
可重复读
事务A和事务B,事务A提交之后的数据,事务B读取不到
- 事务B是可重复读取数据
- 这种隔离级别高于读已提交
- 换句话说,对方提交之后的数据,我还是读取不到
- 这种隔离级别可以避免“不可重复读取”,达到可重复读取
- 比如1点和2点读到数据是同一个
- MySQL默认级别
-
串行化
事务A和事务B,事务A在操作数据库时,事务B只能排队等待
- 这种隔离级别很少使用,吞吐量太低,用户体验差
- 这种级别可以避免“幻像读”,每一次读取的都是数据库中真实存在数据,事务A与事务B串行,而不并发
注意: 隔离级别从小到大,安全性越来越高,但是效率越来越低。但是一般情况下不会修改数据库默认的隔离级别,只有在极特殊情况下才会做出修改已解决一些特殊问题。
查数据库的隔离级别:
#mysql5.7及之后版本
show variables like 'transaction_isolation';或者select @@transaction_isolation;
#mysql5.7之前版本
show variables like 'tx_isolation';或者select @@tx_isolation;
#注意mysql5.7之后才是transaction_isolation,之前都是tx_isolation,但是mysql8.0.3之后tx_isolation就被去掉了
修改数据库的隔离级别:
set global transaction isolation level 级别字符串;
脏读、幻读、不可重复读
脏读
- 读未提交产生脏读问题
- 脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。【简单说,事务A读取了事务B为提交的数据,可能使得最后提交的数据不正确】 | 时间顺序 | 转账事务 | 取款事务 | | —- | —- | —- | | 1 | | start transaction | | 2 | start transaction | | | 3 | | 查询账户余额为2000元 | | 4 | | 取款1000元,余额被更改为1000元(未提交) | | 5 | 查询账户余额为1000元(产生脏数据) | | | 6 | | 取款操作发生未知错误,事务回滚,余额变更为2000元 | | 7 | 转入2000元,余额被更改为3000元(脏读1000+2000) | | | 8 | 提交事务 | | | 备注 | 按照正常逻辑此时账户应该为4000元 | |
不可重复读
- 在同一事务中,多次读取同一数据返回的结果有所不同,换句话说,后续读取可以读到另一事务已提交的更新数据 | 时间顺序 | 事务A | 事务B | | —- | —- | —- | | 1 | start transaction | | | 2 | select年龄为20岁 | | | 3 | | start transaction | | 4 | 其他操作 | | | 5 | | update年龄30岁 | | 6 | | | | 7 | select 年龄30岁 | commit | | 备注 | 按照正常逻辑,事务A的前后两次读取到的数据应该一致 | |
通俗解释:
有一个比较长的事务正在执行,这个时候,另外一个事务读取了相同的数据,进行修改,并且提交持久化到数据库,这个时候比较长的事务读取到的数据是新的数据,和第一次读取的不一样。
幻读
- 幻读在可重复读的模式下才会出现
- 一个事务操作(DML)数据表中所有的记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改;
幻读示例1
| 时间顺序 | 事务A | 事务B | | —- | —- | —- | | 1 | start transaction | | | 2 | | start transaction | | 3 | | | | 4 | 其他操作 | | | 5 | | insert 100条数据 | | 6 | | commit | | 7 | select count(*) 200条 | | | 备注 | 按照正常逻辑,事务A前后两次读取到的数据总量应该一致 | |
幻读示例2
在事务1中,查询User表id为1的是用户否存在,如果不存在则插入一条id为1的数据。
select * from User where id = 1;
在事务1查询结束后,事务2往User表中插入了一条id为1的数据。
insert into `User`(`id`, `name`) values (1, 'Joonwhee');
此时,由于事务1查询到id为1的用户不存在,因此插入1条id为1的数据。
insert into ` User`(`id`, `name`) values (1, 'Chillax');
但是由于事务2已经插入了1条id为1的数据,因此此时会报主键冲突,对于事务1 的业务来说是执行失败的,这里事务1 就是发生了幻读,因为事务1读取的数据状态并不能支持他的下一步的业务,见鬼了一样。这里要灵活的理解读取的意思,第一次select是读取,第二次的insert其实也属于隐式的读取,只不过是在mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
不可重复读和幻读区别
问题1:不可重复读是读取了其他事务更改的数据,针对update操作 解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
问题2:幻读是读取了其他事务新增的数据,针对insert与delete操作 解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
小结:
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改
事务实现原理
上面介绍了,事务的四大特性,一致性,原子性,隔离性以及持久性,这些特性无非不是在保证数据库的可靠性以及并发处理
可靠性:
数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,想要做到这个,我需要知道我修改之前和修改之后的状态,所以就有了undo log和redo log。
并发处理:
也就是说当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到啥程度得看业务系统的场景了,实现这个就得用MySQL 的隔离级别。
实现事务功能的三个技术:
- 日志文件(redo log 和 undo log)
- 锁技术
- MVCC
准备
准备工作,创建表& 数据 ```sql————————数据库隔离级别——————-
查数据库的隔离级别
select @@transaction_isolation;
创建 bank表
create table bank( id int primary key auto_increment, name varchar(255) not null default ‘’, balance decimal not null default 0 )ENGINE = InnoDB DEFAULT CHARSET = utf8;
创建 finance
create table finance( id int primary key auto_increment, name varchar(255) not null default ‘’, amount decimal not null default 0 )ENGINE = InnoDB DEFAULT CHARSET = utf8;
查询当前数据库中的所有表
show table status;
在back表中插入数据
insert into bank ( name, balance) VALUES (‘张三’,1000);
finance 插入数据
insert into finance(name, amount) VALUES (‘张三’,500);
查询表数据
select from bank; select from finance;
<a name="e6975af3"></a>
### redo log
<a name="157f048d"></a>
#### 什么是redo log?
redo log叫做**重做日志**,是用来**实现事务的持久性**。该日志文件由两部分组成:**重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中**。当事务提交之后会把所有修改信息都会存到该日志中。上面我们创建了两个表,
```sql
# 查询表数据
# bank表
select * from bank;
# finance表
select * from finance;
修改表中的数据,将银行bank表中的余额转到理财中
# 开启事务
start transaction ;
# 查询张三账户余额
select balance from bank where name ='张三';
# 银行账户余额减少400 生成 重做日志 balance=600 缓存中记录
update bank set balance = balance - 400;
# 理财账户加400 生成 重做日志 balance=900 缓存中记录
update finance set amount = amount + 400;
# redo log 记录 持久化日志磁盘
commit;
redo log作用是什么?
mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。
那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?这样会导致丢部分已提交事务的修改信息!
所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。
- 总结:redo log是用来恢复数据的,用于保障,已提交事务的持久化特性(记录了已经提交的操作)
undo log
什么是 undo log?
undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log
结论:
- 每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上
- 所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等
undo log 进行回滚
为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚
回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:
- 如果在回滚日志里有新增数据记录,则生成删除该条的语句
- 如果在回滚日志里有删除数据记录,则生成生成该条的语句
- 如果在回滚日志里有修改数据记录,则生成修改到原先数据的语句
undo log 作用
undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。
- 总结:undo log是用来回滚数据的用于保障,未提交事务的原子性
mysql锁
为什么使用锁?
当有大量请求读数据表时,仅仅是读并不需要使用锁,但是当有多个请求修改或者是新增数据时,为了防止数据的正确性,就需要使用锁控制并发。
读写锁
共享锁(shared lock),又叫做”读锁”
- 读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。
排他锁(exclusive lock),又叫做”写锁”
- 写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。
- 总结:通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行
MVCC多版本并发控制
MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。一般情况下,事务性储存引擎不是只使用表锁,行加锁的处理数据,而是结合了MVCC机制,以处理更多的并发问题。Mvcc处理高并发能力最强,但系统开销 比最大(较表锁、行级锁),这是最求高并发付出的代价。
InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间, 当然存储的并不是实际的时间值,而是系统版本号。
以上片段摘自《高性能Mysql》这本书对MVCC的定义。他的主要实现思想是通过数据多版本来做到读写分离。从而实现不加锁读进而做到读写并行。MVCC在mysql中的实现依赖的是undo log与read view;
- undo log :undo log 中记录某行数据的多个版本的数据。
- read view :用来判断当前版本数据的可见性
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。
mysql 下MVCC具体实现
InnoDB实现MVCC的方法是,它存储了每一行的三个额外的隐藏字段
- DB_TRX_ID:一个
6byte
的标识,每处理一个事务,其值自动+1【“创建时间”和“删除时间”记录的就是这个DB_TRX_ID的值】 ```sql如insert、update、delete操作时,删除操作用1个bit表示。
DB_TRX_ID是最重要的一个,可以通过语句“show engine innodb status”来查找
show engine innodb status
DB_TRX_ID记录了行的创建的时间删除的时间在每个事件发生的时候,每行存储版本号,而不是存储事件实际发生的时间。每次事物的开始这个版本号都会增加。自记录时间开始,每个事物都会保存记录的系统版本号。
依照事物的版本来检查每行的版本号。在insert操作时 “创建时间”=DB_TRX_ID,这时,“删除时间”是未定义的;在update时,复制新增行的“创建时间”=DB_TRX_ID,删除时间未定义,旧数据行“创建时间”不变,
删除时间=该事务DB_TRX_ID;delete操作,相应数据行的“创建时间”不变,删除时间=该事务的DB_ROW_ID;select操作对两者都不修改,只读相应的数据
2. **DB_ROLL_PTR:** 大小是`7byte`,指向写到`rollback segment`(回滚段)的一条`undo log`记录【 update操作的话,记录update前的ROW值】
2. **DB_ROW_ID:** 大小是`6byte`,该值随新行插入**单调增加**。
> 当由innodb自动产生聚集索引时聚集索引(即没有主键时,因为MYSQL默认聚簇表,会自动生成一个ROWID)
> 包括这个DB_ROW_ID的值,不然的话聚集索引中不包括这个值,这个用于索引当中。
<a name="1a63ac23"></a>
##### 示例
bank表插入三条数据
```sql
# 开启事务
start transaction ;
# 在back表中插入数据
insert into bank ( name, balance) VALUES ('张三',1000);
insert into bank ( name, balance) VALUES ('王五',500);
insert into bank ( name, balance) VALUES ('李四',250);
commit ;
假设系统的版本号从1开始。对应在数据中的表如下(后面两列是隐藏列,我们通过查询语句并看不到)
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
- SELECT(查询数据)
InnoDB会根据以下两个条件检查每行记录:
(1)InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
(2)行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果. - DELETE(删除数据)
InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.
看下面的具体例子分析:
第二个事务,ID为2;
假设1start transaction;
select * from bank; //(1)
select * from bank; //(2)
commit;
假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据; 第三个事务ID为3;
新增后表数据:start transaction;
insert into bank ( name, balance) VALUES ('麻子',500);
commit;
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
4 | 麻子 | 500 | 3 | undefined |
然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
假设2
假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后,接着又执行了事务4; 第四个事务:
start transaction;
delete from yang where id=1;
commit;
此时数据库中的表如下:
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
4 | 麻子 | 500 | 3 | undefined |
接着执行事务ID为2的事务(2),根据SELECT
检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from bank
也会把id=1的数据检索出来.所以,事务2中的两条select
语句检索出来的数据都如下:
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
- UPDATE
InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.
假设3
假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个用户对这张表执行了UPDATE操作;第5个事务:
start transaction;
update bank set name='李武' where id=2;
commit;
根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
4 | 麻子 | 500 | 3 | undefined |
5 | 李武 | 500 | 5 | undefined |
继续执行事务2的(2),根据select 语句的检索条件,得到下表:
id | name | balance | 创建时间(事务ID) | 删除时间(事务ID) |
---|---|---|---|---|
1 | 张三 | 1000 | 1 | undefined |
2 | 王五 | 500 | 1 | undefined |
3 | 李四 | 250 | 1 | undefined |
MVCC结合隔离级别
- READ UNCOMMITTED :不适用MVCC读,可以读到其他事务修改甚至未提交的
- READ COMMITTED :其他事务对数据库的修改,只要已经提交,其修改的结果就是可见的, 与这两个事务开始的先后顺序无关,不完全适用于MVCC读,
- REPEATABLE READ:可重复读,完全适用MVCC,只能读取在它开始之前已经提交的事务对数据库的修改, 在它开始以后,所有其他事务对数据库的修改对它来说均不可见
- SERIALIZABLE :完全不适合适用MVCC,这样所有的query都会加锁,再它之后的事务都要等待
MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下
可重复读下的MVCC
SELECT
Innodb检查每行数据,确保他们符合两个标准则返回查询结果:
- InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务 读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行
- 行的删除操作的版本一定是未定义的或者大于当前事务的版本号。确定了当前事务开始之前,行没有被删除
INSERT
InnoDB为每个新增行记录当前系统版本号作为创建ID。
DELETE
InnoDB为每个删除行的记录当前系统版本号作为行的删除ID。
UPDATE
InnoDB复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。
活跃事务列表
如果根据事务DB_TRX_ID
去比较获取事务的话,按道理在一个事务B(在事务A后,但A还没commit
)select
的话B.DB_TRX_ID>A.DB_TRX_ID
则应该能返回A事务对数据的操作以及修改。那不是和前面矛盾?其实不然。InnoDB
每个事务在开始的时候,会将当前系统中的活跃事务列表(trx_sys->trx_list
)创建一个副本(read view
),然后一致性读去比较记录的tx id
的时候,并不是根据当前事务的tx id
,而是根据read view
最早一个事务的tx id
(read view->up_limit_id
)来做比较的,这样就能确保在事务B之前没有提交的所有事务的变更,B事务都是看不到的。当然,这里还有个小问题要处理一下,就是当前事务自身的变更还是需要看到的。