数据库事务--隔离性与MVCC(3)
前言
并发是程序最为复杂的场景之一,在数据库领域,做好隔离就是为了解决并发的问题。然而不同于我们在业务场景中处理并发问题,直接加锁就好了。对于数据库,还得考虑整体吞吐量,不能用锁“一棍子打死”。于是就存在诸如读锁,写锁,意向锁,范围锁等一系列锁的类型。而且锁的范围也得考虑,锁表,还是只锁对应的行。这些都是需要考虑的,同时,除了锁,还会存在并发程度高的解决方案,如MVCC(多版本并发控制协议) 等
锁+MVCC 就是数据库解决并发问题的方案。
一句话术语解释
读锁(共享锁)
允许多个线程同时进入临界区。一般用于读取场景,不修改数据。
写锁(排他锁)
一次只允许一个线程进入临界区,一般用于修改场景。
读写场景分开,是因为对于数据库来说,一般是读场景远大于写场景。如果两者频率差不多,这么区分的意义不大。
意向锁
在获取数据表的共享/排他锁之前,需要先获取数据表对应的共享/排他意向锁,表明要对数据表进行加锁了。这里的锁是表级锁,不是行级锁。
解决的问题是:在对数据表加表级共享/排他锁之前,需要确保数据表中无行级锁。如果要遍历那速度太慢了,就有了意向锁。意向锁会和表级共享锁和表级排他锁互斥,和行级锁都兼容。
意向锁的设计还是很不错的,可以借鉴。
范围锁(gap 锁,next-key 锁)
范围锁指的是在查找或者更新一定范围的数据时,为了避免幻读现象,加上了锁,使得这个范围中的数据不可以被新增,修改,删除。
MVCC
多版本控制协议,本质上是保留了一行数据的多个历史版本。不同的事务根据一定的可见性规则,会看到不同的历史版本数据。从而将数据隔离开来。
本质是为了处理读写冲突。
快照读 VS 当前读
- 快照读:指的是利用 MVCC 读取的历史数据
- 当前读:指的是通过加锁的方式,读取数据库的最新数据
隔离级别
以下四种隔离级别,隔离性越来越高,并行度越来越低。
在理解下面的隔离级别时,可以自行代入有两个事务在同时间执行。
读未提交(Read uncommitted)
隔离性最低。
读到另一个事务未提交的数据。也就是另一个事务没有提交的数据都对本事务可见。相当于没有任何隔离措施。这种现象也叫做脏读。
读已提交(Read committed)
读取到另一个事务提交的数据。
会产生不可重复读问题,指的是同一个事务,两次读取同一行记录,可能读取到的结果不一样。
解决了脏读问题。解决方式是使用了 MVCC,在事务执行期间,每次 select 查询数据,都会根据可见性规则查看可见的历史数据,对于那种未提交的数据是看不到的。
可重复读(Repeatable read)
多次读取同一行数据,数据都是一致的。
也就是解决了不可重复读问题,解决方式也是使用了 MVCC,在事务执行期间,事务第一次 select 查询数据,会根据可见性规则查看一份可见数据,后面再次查询还是使用同一份数据。于是每次查询数据都是一致的。
会产生幻读问题。幻读问题一般理解是查找一定范围数据,两次查找的结果不一致。举两个例子。
案例一:
查找 id 1-50 的数据,第一次读取到 30 条数据,而第二次读取到 31 条数据。
这种 case,分为两种情况。
快照读:通过 MVCC 处理,利用 MVCC 的可见性规则,可以让读取的数据只有 30 条。
当前读:MySQL 通过gap 锁解决了这个问题,给这个范围内的数据加上行锁和间隙锁。
案例二:
查找 id 为 1 的数据,第一次没有查到,第二次也没有查到,但是想要插入的时候,报错了。被其他事务抢先插入了。
这种 case 也属于幻读。而且在可重复读的隔离级别下是处理不了的
串行化(Serializable)
隔离性最高。
读请求加读锁,写请求加写锁。
读读不互斥,读写互斥,写写互斥。
如何选择隔离级别
这是需要综合考虑的。
- 数据一致性要求,这里的数据一致性,其实更多是指利用数据库来处理线程安全问题。除了最后一个串行化,其他的都或多或少存在问题。对于平时说的数据一致性,指的是业务一致性,利用消息队列,分布式事务,重试等手段来保障最后业务一致性即可。
- 并发程度,如果业务场景并不是并发的,隔离级别就不重要了
- 性能要求,串行化的隔离级别性能最低,其他隔离级别性能都还好。
- 最佳实践:对于 MySQL 来说,采取默认的可重复读隔离级别就足以应付大部分场景了,性能也足够。
MVCC
目的
这个方案在很多数据库都有相关实现,使用它主要是为了处理读-写冲突。
原理
主要实现方案是维护数据的历史版本链。
版本链
数据表中的每行数据,都含有几个隐藏字段,分别是:
- db_trx_id:最近操作这行记录的事务 ID
- db_roll_pointer:指向这行记录的上一个版本
- db_row_id:隐含的自增 ID,如果数据表没有主键,InnoDB 会使用它来生成聚簇索引
undo log
Read View(可见性规则)
每次判断某行记录是否可以展示的时候,都会生成一个 readView。它有 4 个主要属性:
trx_ids: 当前系统活跃(未提交)事务版本号集合。low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”creator_trx_id: 创建当前read view的事务版本号;
而判断数据是否可见,则有以下判断条件:db_trx_id<up_limit_id||db_trx_id==creator_trx_id(显示)
如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。
或者数据的事务ID等于creator_trx_id,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。db_trx_id>=low_limit_id(不显示)
如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断db_trx_id是否在活跃事务(trx_ids)中不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示。已存在:则代表Read View生成时刻,事务还在活跃,还没有Commit,修改的数据,当前事务也是看不见的。
总结
数据库的隔离性在平时业务开发中不怎么会涉及,但读写锁,MVCC 等并发解决方案还是值得学习的
参考资料
详解 MySql InnoDB 中意向锁的作用 - 掘金 (juejin.cn)
全网最全一篇数据库MVCC详解,不全你打我 - 掘金 (juejin.cn)