文章目录
事务
What?Why?
什么是事务?为什么要有事务?
事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
假如A转账给B 100 元,先从A的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完A的100元后,还没来得及给B加上,银行系统异常了,最后导致A的余额减少了,B的余额却没有增加,这肯定是不行的。所以就需要事务,将A的钱回滚回去。
为什么要有事务呢? 就是为了保证数据的最终一致性。
事务的四大特性
事务四大特性,即ACID,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。
- 一致性:指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
- 隔离性:多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。
- 持久性:表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。
好像歪果仁都有拼凑单词的习惯…其实事务主要是为了实现 C ,也就是一致性,具体是通过AID,即原子性、隔离性和持久性来达到一致性的目的,所以这四个不应该相提并论,但是他们就想拼成单词,就把它们排好序搞在一起来念了…
事务并发存在的问题
- 脏读:一个事务读取到了另一个未提交事务修改过的数据
- 重复读:同一个事务内,前后多次读取一条记录,读取到的内容不一致(主要是针对update)
- 幻读:同一个事务内,前后多次读取一个结果集,读取到的内容不一致(主要是针对insert、delete)
事务的隔离级别
- Read Uncommitted:读未提交,一个事务可以读取其他事务没有提交的数据。存在脏读、重复读、幻读问题。
- Read Committed:读已提交,一个事务只能读取其他事务已经提交的数据。解决了脏读问题,但是无法解决重复读和幻读问题。(Oracle的默认隔离级别)
- Repeatable Read:可重复读,一个事务在读取数据的时候,其他事务不可以修改。解决了重复读问题,但是无法解决幻读问题。(MySQL的默认隔离级别)
- Serializable:串行化,事务最高的隔离级别,在该级别下,所有事务都是进行串行化顺序执行的。可以避免脏读、不可重复读与幻读等所有并发问题。但是这种事务隔离级别下,事务执行很耗性能。
MVCC
多版本并发控制(Multi-Version Concurrency Control,MVCC):指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undolog保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC 在 Read Committed 和 Repeatable Read 两个隔离级别下工作。
MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过“行级锁+MVCC”一起实现的,正常读的时候不加锁,写的时候加锁。而 MVCC 的实现依赖:隐藏字段、undolog、版本链、Read View。
隐藏字段
InnoDB存储引擎在每行数据的后面添加了三个隐藏字段:
- DB_TRX_ID(6字节):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除
- DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息
- DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。即当表没有主键或唯一非空索引时,InnoDB就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了(这个DB_ROW_ID跟MVCC关系不大…)
undo log
undo log,回滚日志,用于记录数据被修改前的信息。在表记录修改之前,会先把数据拷贝到undo log里,如果事务回滚,即可以通过undolog来还原数据。
可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。
版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:
- 假设现在有一张user表,表里面有一条数据,id为1,名字为张三:
- 现在开启一个事务,对user表执行
update user set name = '李四' where id = 1
,会进行如下流程操作:
- 首先获得事务ID=100
- 把user表修改前的数据拷贝到undo log
- 修改user表中,id为1的数据,名字改为李四
- 把修改后的数据事务Id=101改成当前事务版本号,并把roll_pointer指向undo log数据地址
快照读和当前读
- 快照读:读取的是记录数据的可见版本(有旧的版本)。不加锁的普通的select语句都是快照读。
- 当前读:读取的是记录数据的最新版本,显式加锁的都是当前读(
for update
、lock in share mode
)。
Read View
- Read View是什么呢? 它就是事务执行SQL语句时,产生的读视图。实际上在InnoDB中,每个SQL语句执行前都会得到一个Read View。
- Read View有什么用呢? 它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据。
Read view 的几个重要属性:
- m_ids:当前系统中那些活跃(未提交)的事务ID,它数据结构为一个List。
- min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。
- max_limit_id:表示生成Read View时,系统中应该分配给下一个事务的id值。
- creator_trx_id:创建当前Read View的事务ID。
对于可见版本的判断是从最新版本开始沿着版本链逐渐寻找老的版本,如果遇到符合条件的版本就返回。
判断条件如下:
- 如果当前数据版本的
trx_id < min_limit_id
,说明修改这条数据的事务在当前事务生成 Read View 的时候已提交,所以可见。 - 如果当前数据版本的
trx_id >= max_limit_id
,说明修改这条数据的事务在当前事务生成 Read View 的时候还未启动,所以不可见(事务ID的生成是递增的)。 - 如果
min_limit_id <= trx_id < max_limit_id
:- 如果
m_ids包含trx_id
,则表示在Read View生成的时刻,这个事务还没有提交,但是如果trx_id == creator_trx_id
,说明这条数据是自己生成的,因此是可见的,否则(trx_id != creator_trx_id
)就是不可见的。 - 如果
m_ids不包含trx_id
,说明这个事务在Read View生成之前就已经提交了,因此是可见的。
- 如果
Read Committed 下的 MVCC
事务A在user表中插入一条数据后再多次执行查询,期间事务B有执行更新操作并提交。
事务A | 事务B |
---|---|
1. begin | |
2. insert into user(id, name) value(1, ‘张三’) | |
3. begin | |
4. select * from user where id = 1; // 得到 张三 | |
5. update user set name = ‘李四’ where id = 1; | |
6. commit | |
7. select * from user where id = 1; // 得到 李四 | |
… |
最后事务A查询到的结果是name=李四的记录,我们基于MVCC,来分析一下执行流程:
-
A开启事务,trx_id为100,插入“张三”后:
-
B开启事务,trx_id为101
-
事务A生成一个Read View
然后回到版本链,开始从版本链中挑选可见的记录:变量 值 m_ids [100, 101] max_limit_id 102 min_limit_id 100 creator_trx_id 100
最新版本的列name的内容是张三,该版本的trx_id值为100。(creator_trx_id == trx_id == 100
) -
事务B进行修改操作然后提交
-
事务A再次执行查询操作,新生成一个Read View
变量 值 m_ids [100] max_limit_id 102 min_limit_id 100 creator_trx_id 100 然后去上图的版本链中挑选可见的记录,因为
trx_id == 101
不在 m_ids 集合中(说明该记录已提交)并且min_limit_id == 100 < trx_id == 101 < max_limit_id == 102
,说明该记录对当前事务是可见的,因此就会返回 name = “李四” 的记录。
如此就可以看出,在读已提交(RC)隔离级别下,同一个事务里,两个相同的查询,读取同一条记录(id=1),却返回了不同的数据(第一次查出来是张三,第二次查出来是李四),因此RC隔离级别,存在不可重复读并发问题。
Repeatable Read 下的 MVCC
还是上面的例子,在RR的隔离级别下,最终查询出来的结果是name=张三的记录。
各种事务隔离级别下的Read view工作方式是不一样的,RR可以解决不可重复读问题,就是跟Read View工作方式有关:
-
在RC的隔离级别下,同一个事务里面,每一次查询都会产生一个新的Read View副本,这样就可能造成同一个事务里前后读取数据可能不一致的问题:
变量 值 m_ids [100, 101] max_limit_id 102 min_limit_id 100 creator_trx_id 100 变量 值 m_ids [100] max_limit_id 102 min_limit_id 100 creator_trx_id 100 两次查询基于两个不同的Read View,因此第一次返回
trx_id == 100
的记录,而第二次查询却返回trx_id == 101
的新的提交记录。 -
而在RR的隔离级别下,一个事务里只会获取一次Read View,都是副本共用的,从而保证每次查询的数据都是一样的:
变量 值 m_ids [100, 101] max_limit_id 102 min_limit_id 100 creator_trx_id 100 第一次查询时生成一个Read View后,之后的查询都会使用这个Read View,因此查询出来的都是
trx_id == 100
的记录(creator_trx_id == trx_id == 100
)。