事务特性
- 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成
- 一致性(Consistency):事务操作前后,数据的完整性约束,数据库应该保持一致性。举例来说就是A转账给B的事务前后,AB资产之和不变。
- 隔离性(Isolation):数据库支持并发事务,隔离性用来描述不同事务运行期间,他们数据视图之间不受彼此影响。
- 持久性(Durability):事务处理结束后,对数据的修改是永久的,系统故障后也不能丢失。
InnoDB引擎实现四个特性的技术
- 原子性:undo log
- 持久性:redo log
- 隔离性:锁 + MVCC
- 一致性:原子性+持久性+隔离性
并发事务的隔离性问题
- 脏读:A事务运行期间,读到了B事务尚未提交的修改。
- 不可重复读:A事务运行期间,对于同一数据两次查询结果不同。
- 幻读:事务运行期间,对于符合某个条件的记录集,两次查询结果中「记录数量」发生了变化。
InnoDB的隔离等级
是否解决 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 RU | ❌ | ❌ | ❌ |
读已提交 RC | ✅ | ❌ | ❌ |
可重复读 RR | ✅ | ✅ | ✅(InnoDB部分解决) |
串行化 Serializable | ✅ | ✅ | ✅ |
MVCC
我不知道有没有人和我一样,多线程学多了之后脑子里就全是线程安全问题,任何东西都要扯上线程安全性。如果你没有,那么恭喜你,你至少没有强迫症,不至于想到头昏脑胀。
反正,在初次看MVCC相关的知识的时候,隐藏字段、ReadView真是把我头搞昏了,本身并不难,但是我喜欢扯上线程安全问题,搞得头昏。
总之,MySQL对于事务创建、事务提交、维护活跃事务这一块,有互斥保护就完事了。
接下来正式进入MVCC。
快照读 vs 当前读
数据操作方式 | 说明 | 对应SQL |
---|---|---|
快照读 | 基于多版本并发控制,本质上是基于记录的版本链,实际读取的是版本链上记录,也就可能是历史记录 | 普通select |
当前读 | 这个名字取的莫名其妙的,而且实际上不止是读,还可能写,反正只要记住他会对当前数据集加锁就完事 | select … lock in share mode🍎select … for update; 🍎 update;🍎 insert; 🍎delete |
数据库并发场景已经解决策略
- 读-读:这个不用管,全读没有并发问题
- 读-写:我们在隔离级别里面举的例子,基本都是读写场景,这个需要处理,解决方法是锁+MVCC。
- 写-写
3.1 第一类丢失更新-回滚丢失(AB同时开启事务,B提交了,但是A却后面回滚了,导致B的更新没了,这个所有隔离级别中都不会出现,因为回滚还要看undo log的操作事务ID)
3.2 第二类丢失更新-覆盖丢失 (AB同时开启事务,AB读出一样的记录,B先更新,接着A又更新了,这下B更新又丢了,这个需要通过加锁解决【悲观 & 乐观】)
针对上面的读写情况,我有几句想说的:如果不谈数据库,我们普通写个读写并发的场景,就比如Java中的读写锁这种,显然读和写是互斥的,但是MySQL中,读不一定要和写互斥,因为MySQL中的隔离性规定了读的特殊性,你看那几个隔离级别,几乎都是和读这个字有关系的。在实际的MySQL读中,读的不一定是目前记录的最新版本,正是因为这个特殊的规定,MySQL普通读可以不加锁,通过MVCC和版本链来读取。
undo log 和版本链
undo log是用来保证事务原子性的一份日志文件,保存了记录的历史版本。实际上,事务内部对于记录的修改,是直接反映到聚簇索引上的记录的,只不过这些新记录会有隐藏字段指到undo log中的历史记录,便于回滚。
每行记录都有几个隐藏字段:
- db_trx_id:最近修改/插入这条记录的事务ID
- db_roll_pointer:回滚指针,指向上一个版本(在rollback segment中)
- db_row_id:隐式主键,没有指定主键的时候才生成。
Read View
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
Read View主要是用来做版本链中的记录可见性
判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View实际字段名字不重要,反正也记不住。。。
只要记住有这几部分:
当前所有活跃事务ID
(活跃指的是开启后尚未提交/回滚)下一次将会分配的事务ID
(事务ID分配是自增的,当前时刻的下一时刻如果要创建一个事务,就对应这个ID)min(当前所有活跃事务ID)
【min是最小值的意思】创建这个ReadView的事务ID
上图中,当前活跃id为102和103,最小的是102,下一次会分配的事务ID是104,创建这个ReadView的事务ID是103。
判断记录可见性的算法
我们知道每行记录都有隐藏字段db_trx_id标记最近修改/插入他的事务ID,现在就是拿这个db_trx_id来和Read View里面的字段做对比,依此来判断创建这个ReadView的事务能否看到这个对应的记录。
注意下面的算法是依次进行判断的(if-else格式的
):
db_trx_id
<min(当前活跃的事务ID)
||db_trx_id
==创建这个ReadView的事务ID
注意上述两个表达式的右边都是ReadView字段,里面所提及的事务ID所在时间节点应该是ReadView被创建的时候。
这样的话,第一个表达式,当前记录操作ID小于创建ReadView时候的时候存在的所有ID,说明这玩意早就被提交掉了,当然可见;第二个表是,当前记录操作ID等于创建ReadView的事务ID,一个事务内部那肯定也可以看见。db_trx_id
>=下一次将会分配的事务ID
说明这个记录的操作事务ID的产生是后于我们创建ReadView的时刻的,那么这个是不可见的。- 剩余情况,只剩下
db_trx_id
>=min(当前活跃的事务ID)
&&db_trx_id
<下一次将会分配的事务ID
&&db_trx_id
!=创建这个ReadView的事务ID
:此时要看db_trx_id
在不在活跃事务id集合中,如果在的话,由于事务隔离性,当前活跃的其他事务我们看不见,如果不在的话说明已提交,可以看见。