MySQL事务机制详解


一、事务介绍

在实际应用场景中,许多操作我们并无法保证一定会正确执行,当一个完整的操作链路在某处因特殊原因断开导致后续业务失败,通常即需要实现业务的回滚操作,而这个过程即事务的过程。

1. 数据问题

(1) 脏读

脏读即一个事务读取到另一个的尚未提交的数据。

假设表中存在一条记录 id=1, age=20,事务二开始后通过 update 语句将记录改为 age=25 但在随后又执行了回滚操作,若此时在回滚前事务一执行了查询将得到记录 id=1, age=25,但实际经过事务回滚后的表中记录为 id=1, age=20,即事务一读取到了事务二尚未提交的数据。

(2) 不可重复读

不可重复读即在一个事务过程中多次读取的同一数据存在不一致情况。

同样以记录 id=1, age=20 为例,事务一开始之后查询 id=1 的数据得到 id=1, age=20,此时事务二通过 update 语句将记录改为 age=25,若此时事务一再次执行查询得到的结果则为 id=1, age=25,即同一个事务内两次查询的记录结果不一致。

(3) 幻读

幻读即在一个事务过程中多次读取的同一数据集存在不一致情况。

同样以记录 id=1, age=20 为例,事务一开始之后执行 select * 查询得到一条数据,此时事务二新增了记录 id=2,若此时事务一再次执行全量查询得到的记录将有两条,即第二次查询凭空多出一条数据,两次查询的结果集不一致。

2. 事务隔离

(1) READ_UNCOMMITTED

读未提交,一个事务可以读取另一个事务未提交的数据,可能会造成脏读的情况。

(2) READ_COMMITTED

读已提交,一个事务只能读取到其他事务已经提交的数据,避免了脏读,但可能造成不可重复。

(3) REPEATABLE_READ

可重复读,默认的 MYSQL 隔离级别,当前线程在读取记录时不允许其他线程修改变更,避免了脏读、不可重复读,但可能导致幻读。

(4) SERIALIZABLE

序列化,隔离的最高级别,事务将串行执行,事务的读操作会加读锁,写操作会加写锁,虽不会造成上述提到的几类情况但性能较低。

二、事务管理

1. 事务创建

MySQL 中可以使用 beginstart transaction 显式的开启事务,通过 commit 提交事务。若没有显式开启默认则会提交隐式事务,在语句执行结束之后会自动提交,无法实现回滚。

如最常用的增删改查等语句都是默认开始事务并在语句执行结束时自动提交。

insert into tb_user values(123, 'alex', 'male');

上述的插入命令默认是开始事务并自动提交的,但是通常我们感知不强,其等价于下述操作。

begin;

insert into tb_user values(123, 'alex', 'male');

commit;

2. 事务回滚

当任务执行异常时需要执行回滚通过 rollback 即可。

例如下述事务中若在第一个插入完成后执行第二个失败,此时通过 rollback 即可撤销第一个插入语句的效果。

begin;

insert into tb_user values(123, 'alex', 'male');

insert into tb_user values(456, 'beth', 'female');

-- rollback transaction
rollback;

commit;

3. 保存点

在一个事务中可以创建多个 savepoint 标记点,并可通过 rollback to 回滚至指定保存点,通过 release savepoint 关键字删除创建的保存点。

-- create 
savepoint sp1;

-- rollback
rollback to sp1;

-- delete
release savepoint sp1;

三、MVCC介绍

多版本并发控制 MVCC 是一种无锁读取的优化策略,它的无锁是特指读取时不需要加锁。

基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。

1. 实现机制

MVCC 用来解决读写冲突的无锁并发控制,就是为事务分配单向增长的时间戳,为每个数据修改保存一个版本,版本与事务时间戳相关联。

MVCC 的实现原理主要是基于版本链、undo 日志与 Read View 实现,InnoDB 在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记录删除的版本号。

  • 创建版本号:insert 操作时事务的 id
  • 删除版本号:insert 时为 null,删除时为当前事务的 id

当读操作时,读取的是删除版本号为 null 或创建版本号最大的数据,保证我们读取的是最新的数据。

2. 版本链

数据库中的每行数据除了肉眼看见的数据,还有几个隐藏字段,分别是 db_trx_iddb_roll_pointerdb_row_id

字段 描述
db_row_id 隐含一个大小为 6byte 的自增 ID,如果数据表没有主键,则自动以 db_row_id 产生一个聚簇索引。
db_trx_id 记录创建这条记录/最后一次修改该记录的事务 ID,大小为 6byte。
db_roll_pointer 回滚指针(版本链关键,大小为 7byte),指向这条记录的上一个版本(存储于 rollback segment 里)。

实际还有一个删除 flag 隐藏字段,记录被更新或删除并不代表真的删除,而是删除 flag 变了。当通过 delete 关键字执行删除命令时,系统并不是真正意义上的进行删除,而是将对应记录的占用的空间标记为可用。

四、表锁

1. 基本介绍

表锁分为两大类:读锁写锁 ,当一个会话对某张表加锁后(无论是加 读锁 还是 写锁),其都无法对其它表执行任何操作。

  • 当前会话给表加 读锁,当前会话可对加锁表执行 查询 操作,其它会话能 读取 加锁表的数据,但 增改 操作需要等待加锁会话释放表锁。
  • 当前会话给表加 写锁,当前会话可对加锁表执行任意操作,其它会话对加锁表执行 增删改查 需要等待加锁会话释放表锁。
-- lock all table
FLUSH TABLES with <type> lock;

-- lock single table, default lock 50s(innodb_lock_wait_timeout).
lock TABLES <table_name> <type>; 

-- unlock all table
unlock TABLES;

-- view lock table
show open TABLES where In_Use > 0;

2. 元数据锁

Metadata lock 是表级锁,行锁中的读写操作对应在 Metadata lock 中都属于读锁。

  • 所有的 DML 操作都会在表上加一个 Metadata 读锁;
  • 所有的 DDL 操作都会在表上加一个 Metadata 写锁;

通过查看 performance_schema.metadata_locks 元数据表可查看当前激活的元数据锁。

select
   *
from
   performance_schema.metadata_locks;

五、行锁

表锁虽然能够保证数据的原子性但其带来的性能损耗也是显而易见的,因此 MYSQL 中同时引入了行锁实现更细粒度的操作,即锁的作用域是表中的单条记录,对于表中其它数据并不影响。

InnoDB 行锁是通过给记录的 索引项加锁 来实现的,只有通过索引条件检索数据才会生效,否则 InnoDB 将使用表锁。

1. 分类介绍

MySQL 中的行锁分为两类:共享锁与排他锁,二者的作用类似于表锁中的读锁与写锁。

  • 共享锁:共享锁允许一个事务读数据,不允许修改数据,如果其他事务要再对该行加锁,只能加共享锁。
  • 排他锁:排他锁是修改数据时加的锁,可以读取和修改数据,一旦一个事务对该行数据加锁,其他事务将不能再对该数据加任务锁。
-- 共享锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

-- 排他锁
SELECT * FROM table_name WHERE ... FOR UPDATE

2. 实现算法

MySQLInnoDB 存储引擎支持三种行锁的算法,默认行锁模式为 Next-Key Lock,各自的作用效果如下:

(Ⅰ) Record Lock

  • 记录锁,仅针对单个记录上的锁。
  • 如一个索引有 2、4、5 则锁的范围即为:{2, 4, 5}

(Ⅱ) Gap Lock

  • 间隙锁,锁定一个范围,但不包含记录本身。
  • 如一个索引有 2、4、5 ,那该索引可能被间隙锁锁的范围为 (-∞, 2), (2, 4), (4, 5), (5, +∞)

(Ⅲ) Next-Key Lock

  • 作用效果等价于 Record Lock + Gap Lock,锁定范围以及记录本身。
  • 如一个索引有 2、4、5 ,那该索引可能被邻键锁锁的范围为 (-∞, 2], (2, 4], (4, 5], (5, +∞)

六、意向锁

1. 介绍描述

MySQL 数据库中的意向锁是一种辅助锁定机制,用于协助 InnoDB 引擎在表级别和行级别之间进行切换,可以帮助提高事务的并发性能和效率,特别是在处理表级锁时,避免了不必要的等待和锁冲突。

意向锁并不真正锁定任何数据,它们只是向其他事务提供了一个有关当前事务在将表或行锁定时的意向。如果事务已经持有了一个适当的意向锁,它可以请求一个与其兼容的锁,否则它必须等待直到适当类型的意向锁可用。

2. 基本分类

意向锁分为 意向共享锁(IS)意向排它锁(IX) ,它们的作用是向其他事务表明当前事务在将表或行锁定时的意向,当一个事务请求表或行级锁时,它必须先获得适当类型的意向锁。

当一个事务需要在某个表中的某些行上设置 排它锁(X锁)共享锁(S锁) 时,它必须首先请求 表级意向锁 ,以便其他事务知道该表已被锁定,如果意向锁已被其他事务申请,那么当前事务会阻塞等待,直到获取到该锁。

  • 若一个事务请求对表中的某些行进行 共享锁 时,它会请求 意向共享锁(IS)
  • 若一个事务请求对表中的某些行进行 排它锁 时,它会请求 意向排它锁(IX)

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录