MySQL事务必知必会
MySQL 中并发事务的控制方式无非就两种:锁 和 MVCC。锁可以看作是悲观控制的模式,多版本并发控制 (MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。
锁
根据业务逻辑划分
悲观锁:对数据并发操作持保守态度,通过锁机制来实现事务间的排他性。
乐观锁:认为对同一记录的并发操作小概率发生,所以不用每次操作都上锁,一般通过给记录增加版本号或者时间戳字段来实现。适用于写少读多的情况下。
根据兼容性划分
共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
S 锁 | X 锁 | |
---|---|---|
S 锁 | 不冲突 | 冲突 |
X 锁 | 冲突 | 冲突 |
根据颗粒度划分
行锁:针对索引字段加的锁,仅对相关的一行或者多行记录上锁,InnoDB默认所级别。加锁开销较大,加锁比较慢,容易出现死锁,但是并发性能更高。行级锁和存储引擎有关,是在存储引擎层面实现的。
表锁: 针对非索引字段加的锁,对当前操作的整张表加锁,MyISAM默认所级别。实现简单,资源消耗也比较少,加锁快,不会出现死锁,但是并发效率差。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
InnoDB中的行锁
InnoDB 行锁是通过对索引数据页上的记录加锁实现的。
- 记录锁(Record Lock):属于单个行记录上的锁。
- 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
- 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
在 InnoDB 默认的隔离级别REPEATABLE-READ下,行锁默认使用的是Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化:
- 查询记录存在,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
- 查询记录不存在,降级为间隙锁。
意向锁
意向锁是表级锁,为了快速判断表中的记录有没有行锁,从而知道是否可以对某个表使用表锁。
意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InnoDB 会先获取该数据行所在在数据表的对应意向锁。 意向锁之间是互相兼容的。
意向锁和表级的共享锁和表级的排它锁互斥,但是不会与行级的共享锁和行级的排他锁互斥。
MVCC
用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
1、读操作(SELECT):
当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下:
- 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。
- 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。
- 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。
2、写操作(INSERT、UPDATE、DELETE):
当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下:
- 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。
- 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。
- 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。
3、事务提交和回滚:
- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。
- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。
4、版本的回收:
为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。
MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。
快照读和当前读
快照读(一次性非锁定读):单纯的SELECT
语句,不声明加锁的情况。由 MVCC 机制来保证不出现幻读。
快照即记录的历史版本,每行记录可能存在多个历史版本(多版本技术)。
快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。
只有在事务隔离级别RC(读取已提交)和RR(可重读)下,InnoDB 才会使用一致性非锁定读:
- 在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
- 在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。
快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。
当前读(一致性锁定读):就是给行记录加 X 锁或 S 锁,即SELECT
语句声明加锁和修改记录的时候。使用 Next-Key Lock 进行加锁来保证不出现幻读。
InnoDB实现MVCC
MVCC
的实现依赖于:隐藏字段、Read View、undo log。 在内部实现中,InnoDB
通过数据行的DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR
找到undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View
之前已经提交的修改和该事务本身做的修改。
隐藏字段
DB_TRX_ID(6字节)
:表示最后一次插入或更新该行的事务 id。此外,delete
操作在内部被视为更新,只不过会在记录头 Record header
中的 deleted_flag
字段将其标记为已删除。
DB_ROLL_PTR(7字节)
回滚指针,指向该行的 undo log
。如果该行未被更新,则为空。
DB_ROW_ID(6字节)
:如果没有设置主键且该表没有唯一非空索引,InnoDB
会使用该 id 来生成聚簇索引。
Read View
Read View
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”。
主要有以下字段:
m_low_limit_id
:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见。m_up_limit_id
:活跃事务列表m_ids
中最小的事务 ID,如果m_ids
为空,则m_up_limit_id
为m_low_limit_id
。小于这个 ID 的数据版本均可见。m_ids
:Read View
创建时其他未提交的活跃事务 ID 列表。创建Read View
时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids
不包括当前事务自己和已提交的事务(正在内存中)。m_creator_trx_id
:创建该Read View
的事务 ID。
PS: 在 RC 隔离级别下的 每次select
查询前都生成一个Read View
, 在 RR 隔离级别下只在事务开始后 第一次select
数据前生成一个Read View
。
undo-log
undo log
主要有两个作用:
- 当事务回滚时用于将数据恢复到修改前的样子。
- 另一个作用是
MVCC
,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过update undo log
读取之前的版本数据,以此实现非锁定读。
数据可见性算法
在 InnoDB
存储引擎中,创建一个新事务后,执行每个 select
语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号。 即 保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids)。
当用户在这个事务中要读取某个记录行的时候,InnoDB
会将该记录行的 DB_TRX_ID
与 Read View
中的一些变量及当前事务ID进行比较,判断是否满足可见性条件。
事务
隔离级别
READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读)(InnoDB引擎默认) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化) :最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |