**提到事务,我们都了解事务具有4个基本属性ACID,而通常我们不知道四个属性的实现,这里我们尝试对数据库的事务进行分析,尝试理解事务的实现。**
**事务是并发控制的基本单位**。
*****
ACID四大特性是事务实现的基础,了解了ACID的实现,我们就清楚了事务的实现。
# 原子性
原子性保证了一组操作由多个多个子操作组成,这些子操作要么全部执行,要么全部不执行。
![](https://box.kancloud.cn/e63344a17ea18c66ed3c10c73392a1b4_733x213.png)
## 日志回滚
为了实现原子性,在写入操作发生异常的时候,对前面已执行的更新操作进行**回滚**。
在MySQL中通过 **回滚日志(undo log)** 来实现。事务中所有的修改操作会先记录到这个回滚日志中,然后写入数据库中。
比如插入数据时,回滚日志中记录了insert into user (id) values (1);
回滚数据时需要执行 delete from user where id = 1;
回滚日志时实现了持久性。
## 事务的状态
事务的状态只有3中,分别是:Active,Commited,Failed
![](https://box.kancloud.cn/e571893e360e390b1d26a1f144779a56_567x197.png)
# 持久性
事务的持久性体现在:只要事务被提交,数据一定保存到数据库中(磁盘)。
## 重做日志
MySQL通过 **重做日志(redo log)** 来实现事务的持久性。重做日志由两部分组成,分别是内存中的重做日志缓冲区,磁盘中的重做日志文件。
修改数据时的写入顺序如下
1. 从磁盘读取行记录写入内存
2. 内存中的行记录被更新
3. 更新日志写入重做日志缓冲区
4. 重做日志缓冲区的内容写入到重做日志文件
5. 内存中的行记录更新到磁盘
> 第4,5步在事务 commit 时执行。
重做日志以512字节的块形式保存,跟磁盘扇区大小一致,保证了重做日志写入磁盘的原子性。
![](https://box.kancloud.cn/363bd1eea3c32aeedc28ff662c58e4d9_760x302.png)
## 原子性和一致性
回滚日志保证了发生错误或者需要回滚的事务能够被成功回滚。
重做日志保证了对于已提交事务的修改能够写入数据库,发生意外宕机时,从重写日志恢复磁盘数据。
# 隔离性
> 用来描述多个事务之间的关系。
**事务并行**产生了脏读,不可重复读,幻读问题。
隔离性是以上问题的解决方案。隔离性决定了一个事务里的修改,哪些内容在其他事务里是可见的。
隔离级别是隔离性的具体实现策略,分别是:
1. read uncommited (脏读)
2. read commited (解决了“脏读“,存在“不可重复读“)
3. repeatable read (解决了“不可重复读“,产生了“幻读“)
4. serializable
![](https://box.kancloud.cn/0291e9930b631cdf9b766a57e10e77d6_851x164.png)
## 脏读
> 在一个事务中,读取了其他事务未提交的数据
当隔离级别是read uncommited时,session 2 中插入的未提交数据,在session1中可以被访问
![](https://box.kancloud.cn/5723f3efb5d965b3a0d61a8bee2b0eeb_845x372.png)
## 不可重复读
> 在同一个事务中,一行记录的两次查询结果不一致
当隔离级别是read commited的情况下,session1中前后两次查询的结果不一致。
不可重复读发生的原因是,存储引擎不会在查询数据时添加行锁,锁定id=3这条记录。
![](https://box.kancloud.cn/d6ae1e17204374502dee74dfe8c36df8_844x427.png)
## 幻读
> 在同一个事务中,读取了指定范围的数据集之后,另一个事务往该范围内插入了新数据
由于repeatable read的原因,session1中的两次查询得到了同样的结果,但是插入数据时返回错误
![](https://box.kancloud.cn/7966fdfc447dbf5908bcea0a90ad7389_857x482.png)
## 隔离性(隔离级别)的实现
隔离级别是对隔离性的实现,限制同一时刻对共享资源的操作。
隔离级别的实现方式有
1. 共享锁 & 互斥锁
2. 多版本控制,支持数据被事务更新时对旧版本数据的访问,显著提高了读性能。
## Next-Key解决幻读问题
![](https://box.kancloud.cn/8cc4daa290d181a7a4b68d365f6a8f49_876x418.png)
当我们更新索引列age时,比如 `SELECT * FROM users WHERE age = 25 FOR UPDATE;`,InnoDB不仅锁定了 (21,25] 区间,还锁定了 (25, 30] 区间。确保其他事务无法插入age=25的数据。
## 多版本并发控制
为了提升了“重复读“的读性能。
InnoDB维护了一个版本号,每启动一个事务,版本号就加一。表中添加两列,表示“创建时间“和“过期时间“,用来保存版本号。
CURD操作对这两列的维护策略如下
* insert:写入创建时间
* update:更新创建时间,写入过期时间
* select:创建时间小于等于当前事务的版本号
* delete:写入过期时间
## 小结
隔离级别“提交读“解决了“脏读“问题
隔离级别“重复读“解决了“不可重复读“问题
隔离级别“串行化“解决了“幻读“问题
但由于“串行化“的性能问题,innodb通过Next-key来解决幻读问题。
多版本并发控制提升了“重复读“读性能
## 参考资料
[『浅入深出』MySQL 中事务的实现](https://draveness.me/mysql-transaction)
[浅谈数据库并发控制 - 锁和 MVCC](https://draveness.me/database-concurrency-control)