文章目录
- Mysql3——事务原理
- 1. 事务概念
- 2. 隔离级别
- 3. MVCC多版本并发控制
- 3.1 read view
- 3.2 事务可见性
- 3.3 undo log
- 4. InnoDB锁
- 4.1 全局锁
- 4.2 表级锁
- 4.3 行级锁
- 4.4 不同DML的加锁情况
- 4.5 锁的对象
- 4.6 锁的兼容性
- 5. 死锁问题
- 5.1 查看锁信息
- 5.2 死锁原因
- 5.3 死锁的解决
- 学习参考
Mysql3——事务原理
本文讲述了mysql中事务的概念、原理,重点讲述了四种隔离级别、mvcc、锁以及死锁问题。
1. 事务概念
事务的定义:在数据库存在多个连接并发执行sql语句的场景下,由用户定义的一系列操作,这些操作要么都做,要么都不做,是一个不可分割的整体,其它线程不能访问中间状态。
事务语句:
开启事务:
begin/start transaction;
回滚事务:
rollback;
提交事务:
commit;
如果不写begin和commit,那么每一条sql语句都会被作为一个事务执行。
事务特性:
- 原子性,由undolog保证,保证一系列操作要么全部执行,要么回滚到执行前的状态。
- 一致性,保证操作前后数据库满足完整性约束以及用户逻辑上定义的约束条件。例如对于银行转账业务,事务发生前后银行的总余额应该不变。
- 隔离性,保证多个连接并发执行的事务互不干扰,防止交叉执行导致数据不一致。通过锁和MVCC来实现。为了提高并发性能,适当破坏逻辑上的一致性,设置了四种隔离级别:对同一块区域的写操作需要串行执行,对于读操作进行优化。
- 持久性,由redolog保证,在事务提交时会将所作的修改操作写入到redolog并刷入磁盘。
2. 隔离级别
-
read uncommitted(读未提交)
读:不做任何处理
写:自动加x锁
-
read committed(读已提交)
读:mvcc,读取最新版本的行数据
写:自动加x锁
-
repeatable read(可重复读)默认隔离级别
读:mvcc,保证多次读取的同一行数据一致
写:自动加x锁,额外加锁
-
serializable(可串行化)
读:自动加S锁(next-key lock)
写:自动加x锁
不同隔离级别的并发异常
-
脏读:一个事务读到另一个事务未提交修改的数据,及一个事务能看到另一个事务的中间状态。
原因:读取的数据被另一个事务修改了,但是还没提交。
解决方式:将隔离级别提升未可重复读。
-
不可重复读:一个事务内同一行记录读到两次不一样的数据。
原因:在两次读取之间,另一个事务修改并且提交了同一行数据。
解决方式:将隔离级别提升到可重复读。
-
幻读:同一事务中,同一查询条件下两次读取到的结果集不一致。
原因:两次查询之间另一事务的插入、删除或者修改了数据记录。当前读和快照读的不一致。写操作使用当前读而读操作使用快照读。
解决方式:提升至可串行化,或者使用FOR SHARE或FOR UPDATE为读操作加锁。
-
丢失更新:在任何隔离级别都可能发生,除非完全串行化。
相关DCL
-- 设置隔离级别
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE];
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 查看当前会话隔离级别
SELECT @@session.transaction_isolation;
-- 手动给读操作加S锁
SELECT ... [LOCK IN SHARE MODE | FOR SHARE];
-- 手动给读操作加X锁
SELECT ... FOR UPDATE;
-- 查看当前锁信息
SELECT * FROM information_schema.innodb_locks;
如何在可重复读的隔离级别下解决幻读问题?
幻读是指在同一事务中,针对同一查询条件执行两次查询时,其结果集不一致,这些变化是由于其他事务在两次查询之间插入、删除或修改了数据导致的。
可以手动为查询操作加锁,从而避免在两次查询之间,其它事务修改数据。
SELECT * FORM `employees` WHERE `salary` > 1000 [FOR UPDATE|FOR SHARE];
不可重复读与幻读的区别
- 不可重复读:同一行数据被修改导致读取结果不一致。
- 幻读:表中符合条件的行数或内容发生变化,而不是单一行数据的值变化。
3. MVCC多版本并发控制
MVCC(Multi-Version Concurrency Control,多版本并发控制)它通过为每个事务创建一致性视图和undo log中保存的多版本的记录,来实现一致性非锁定读。非锁定读是指不需要等待访问的行上X锁的释放;
MVCC主要是通过read view事务视图实现的。
3.1 read view
mvcc实现原理:MVCC使用read view确定哪个版本的数据对当前事务可见,生成”快照“,实现一致性非锁定读。
在两个隔离级别中的区别在于创建read view的时机不同:
- read committed下MVCC会为事务的每个select都生成一个read view,这意味着同一事务多次读取同一条数据可能出现不一致。
- repeatable read下MVCC只会在第一次查询时创建一个read view,在整个事务期间都使用这个read view,
read view的构成:
-
**m_ids:**当前活跃事务 ID 列表:当前数据库中所有未提交事务的事务 ID 列表。用于确定哪些事务是活跃的,哪些事务对当前事务不可见。
-
**min_trx_id:**最小活跃事务 ID:活跃事务 ID 列表中最小的事务 ID。用于标识最早的未提交事务。
-
**max_trx_id:**最大已分配事务 ID:在生成 Read View 时分配的最大事务 ID。所有事务 ID 小于这个值的事务已被创建。
-
**creator_trx_id:**当前事务 ID:创建当前 Read View 的事务 ID。
3.2 事务可见性
read view的主要功能就是确定哪个版本的数据记录对于事务可见。
聚集索引数据记录的隐藏列
- **trx_id:**当某个事务对某条聚集索引记录进行修改时,将会把当前事务的 id 赋值给 trx_id ;
- **roll_pointer:**当某个事务对某条聚集索引记录进行修改时,会将上一个版本的记录写到 undo log,然后通过 roll_pointer 指向旧版本记录,通过它可以找到修改前的记录;
事务的可见性
trx_id < min_trx_id
:说明该记录在创建 read_view 之前已经提交,所以对当前事务可见;trx_id >= max_trx_id
:说明该记录是在创建 read_view 之后启动事务生成的,所以对当前事务 不可见;min_trx_id <= trx_id < max_trx_id
:此时需要判断是否在 m_ids 列表中;在列表中
:生成该版本记录的事务仍处于活跃状态,该版本记录对当前事务不可见;不在列表中
:生成该版本记录的事务已经提交,该版本记录对当前事务可见;
3.3 undo log
undo log保存旧版本数据实现MVCC。
undo 日志用来帮助事务回滚以及 MVCC 的功能;存储在共享表空间中;undo 是逻辑日志,回滚时 将数据库逻辑地恢复到原来的样子,根据 undo log 的记录,做之前的逆运算;比如事务中有 insert 操作,那么执行 delete 操作;对于 update 操作执行相反的 update 操作;
同时 undo 日志记录行的版本信息,用于处理 MVCC 功能;
4. InnoDB锁
MySQL当中事务采用三种粒度的锁:针对表(B+树)、页(B+树叶子节点)、行(叶子节点中某个记录)三种。
根据加锁的范围可分为全局锁、表级锁、行级锁,innodb主要使用行级锁保证事务的隔离性。其中行级锁又可分为记录锁、间隙锁、意向插入锁和临键锁。
根据锁的功能可分为共享锁(S锁)、排他锁(X锁)、意向共享锁、意向排他锁、auto_incre锁。
4.1 全局锁
主要用于数据库备份
flush tables with read lock; # 使整个数据库处于只读状态
unlock tables; # 解锁
4.2 表级锁
MyIsam采用表级锁。
InnoDB中的表级锁类型:
-
表锁
lock tables `table_name` [read | write]; unlock tables;
-
元数据锁
crud, alter
-
意向锁:目的是快速判断是否有记录已被加锁,除了全表扫描操作外,不会阻塞其它行级锁。
-
auto_inc自增锁:特殊锁,主要是为了实现自增约束,语句结束后释放锁
4.3 行级锁
- 记录锁(record lock):针对一条数据记录加锁,分为S锁和X锁;RC级别下面使用。InnoDB引擎通过索引来实现记录锁。
- 间隙锁(gap lock):锁定一个范围,不包含记录,解决幻读问题。RR级别下面使用。即使查询没有匹配到任何记录,InnoDB 仍会锁住查询范围的间隙,以防止其他事务插入新数据。全开区间。
- 临键锁(next-key lock): 记录锁和间隙锁的结合,既锁住当前记录,也锁住其查询范围的间隙。也是主要在RR隔离界别下使用。左开右闭区间。
- 意向插入锁(insert intention lock):只会在插入操作中临时加锁,并且不会阻止其他事务对间隙内非冲突位置的插入操作。
4.4 不同DML的加锁情况
查询操作
- 如果隔离级别时RC或者RR,并且没有手动加锁,则使用mvcc进行读快照数据。
- 使用
lock in share mode
或者for share
,加S锁 - 使用
for update
,加X锁 - 在RU隔离级别下,查询操作不加任何锁
删除、更新
- 自动加X锁
插入
- 自动加x锁。
- 插入意向锁,一种特殊的间隙锁,表示事务打算在某个间隙中插入一条记录。
插入意向锁(Insert Intention Lock)插入意向锁是一种 临时的间隙锁。只会在插入操作中临时加锁,并且不会阻止其他事务对间隙内非冲突位置的插入操作。插入意向锁是对间隙锁机制的一种优化,用于处理多事务并发插入的情况。它通过降低锁冲突来提高并发性能,同时保持一致性。
- auto_inc lock自增锁,特殊的表级锁。
4.5 锁的对象
讨论一下不同隔离级别下执行update操作回对聚集索引和辅助索引分别加什么锁。
update `table_name` set `filed` = val where `field2` = val2;
-
聚集索引
- 如果查询命中,RC和RR是一样的,都对数据加记录锁
- 如果查询未命中,RC不加锁,RR会在查询范围加间隙锁
-
辅助唯一索引
- 如果查询命中,RR和RC一样,对辅助索引和对应聚集索引的行加记录锁。(RR进行了锁降级Next-key lock --> Record lock)
- 如果查询未命中,RC不加锁,RR对辅助索引的查询范围加间隙锁,不对聚集索引加锁,防止幻读。
-
辅助非唯一索引
- 如果命中,RC对命中的辅助索引和聚集索引加记录锁,RR对辅助索引加间隙锁,对聚集索引加记录锁。
- 如果未命中,RC不加锁,RR对辅助索引加间隙锁,不对聚集索引加锁。
-
无索引
不使用索引时,innoDB无法快速定位记录的范围,因此会进行更加严格的加锁,整张表可能会被大量间隙锁和记录锁覆盖,实际效果接近表锁。
- 没有命中时,RC对每个记录加记录锁,而RR不仅对每个记录加锁,而且对每个间隙加间隙锁。
4.6 锁的兼容性
锁类型 | S锁 | X锁 | IS锁 | IX锁 | AI锁 |
---|---|---|---|---|---|
S锁 | ✔️ | ❌ | ✔️ | ❌ | ✔️ |
X锁 | ❌ | ❌ | ❌ | ❌ | ❌ |
IS锁 | ✔️ | ❌ | ✔️ | ✔️ | ✔️ |
IX锁 | ❌ | ❌ | ✔️ | ✔️ | ✔️ |
AI锁 | ✔️ | ❌ | ✔️ | ✔️ | ❌ |
- 意向锁不会阻塞行级锁,只会阻塞表级读锁和表级写锁,以及全表扫描。
- 意向锁之间相互兼容
- IS锁只对X锁不兼容
- 当事务试图读或者写某一条记录时,会现在表上加上意向锁,然后才在要操作的记录上加上读锁或者写锁。
意向锁是表级锁,不会阻塞行级锁,行级锁只会与行级锁冲突。
5. 死锁问题
5.1 查看锁信息
# 查看系统中事务的状态
select * from information_schema.innodb_trx\G
# 查看系统中锁的信息
select * from performance_schema.data_locks\G
# 查看系统中正在等待的锁的信息
select * from performance_shcema.data_lock_waits\G
5.2 死锁原因
死锁:两个或两个以上的事务在执行语句的过程中因为争夺锁资源而相互等待的情况。Mysql采用wait-for graph(采用非递归深度优先的图算法实现)方式来进行死锁检测。
死锁原因
- 相反的加锁顺序,解决方式:有序加锁,每个连接加锁的顺序一致
- 锁不兼容,当前事务持有了待插入记录的下一个记录 的X锁,但是在等待队列中存在一个S锁的请求,则可能会发生死锁。
InnoDB在检测到死锁后会马上回滚一个事务。
5.3 死锁的解决
- 尽可能以相同顺序来访问索引记录和表;
- 如果能确定幻读和不可重复读对应用影响不大,考虑将隔离级别降低为 RC;
- 添加合理的索引,不走索引将会为每一行记录加锁,死锁概率非常大;
- 尽量在一个事务中只锁定所需要的资源,减小死锁概率;
- 避免大事务,将大事务分拆成多个小事务;
- 大事务占用资源多,耗时长,冲突概率变高;
- 避免同一时间点运行多个对同一表进行读写的语句
学习参考
学习更多相关知识请参考零声 github。