数据库事务--隔离性与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)

隔离性最高。
读请求加读锁,写请求加写锁。
读读不互斥,读写互斥,写写互斥。

如何选择隔离级别

这是需要综合考虑的。

  1. 数据一致性要求,这里的数据一致性,其实更多是指利用数据库来处理线程安全问题。除了最后一个串行化,其他的都或多或少存在问题。对于平时说的数据一致性,指的是业务一致性,利用消息队列,分布式事务,重试等手段来保障最后业务一致性即可。
  2. 并发程度,如果业务场景并不是并发的,隔离级别就不重要了
  3. 性能要求,串行化的隔离级别性能最低,其他隔离级别性能都还好。
  4. 最佳实践:对于 MySQL 来说,采取默认的可重复读隔离级别就足以应付大部分场景了,性能也足够。

MVCC

目的

这个方案在很多数据库都有相关实现,使用它主要是为了处理读-写冲突。

原理

主要实现方案是维护数据的历史版本链。

版本链

数据表中的每行数据,都含有几个隐藏字段,分别是:

  • db_trx_id:最近操作这行记录的事务 ID
  • db_roll_pointer:指向这行记录的上一个版本
  • db_row_id:隐含的自增 ID,如果数据表没有主键,InnoDB 会使用它来生成聚簇索引
undo log

可见数据库事务–原子性和持久性原理(2)

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)


数据库事务--隔离性与MVCC(3)
http://ttoobbyyy.github.io/2023/12/26/数据库事务--隔离性与MVCC(3)/
作者
jianren.xiao
发布于
2023年12月26日
许可协议