一文快速搞懂MySQL InnoDB事务ACID实现原理

作者:微信小助手

发布时间:2019-04-02T18:41:03

说到数据库事务,想到的就是要么都做修改,要么都不做,或者是 ACID 的概念。其实事务的本质就是锁、并发和重做日志的结合体。


这一篇主要讲一下 InnoDB 中的事务到底是如何实现 ACID 的:

  • 原子性(atomicity)

  • 一致性(consistency)

  • 隔离性(isolation)

  • 持久性(durability)


隔离性


隔离性的实现原理就是锁,因而隔离性也可以称为并发控制、锁等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能互相分离。


再者,比如操作缓冲池中的 LRU 列表,删除,添加、移动 LRU 列表中的元素,为了保证一致性那么就要锁的介入。


InnoDB 使用锁为了支持对共享资源进行并发访问,提供数据的完整性和一致性。


那么到底 InnoDB 支持什么样的锁呢?我们先来看下 InnoDB 的锁的介绍:


InnoDB 中的锁


你可能听过各种各样的 InnoDB 的数据库锁,Gap 锁,共享锁,排它锁,读锁,写锁等等。但是 InnoDB 的标准实现的锁只有 2 类,一种是行级锁,一种是意向锁。


InnoDB 实现了如下两种标准的行级锁:

  • 共享锁(读锁 S Lock),允许事务读一行数据。

  • 排它锁(写锁 X Lock),允许事务删除一行数据或者更新一行数据。


行级锁中,除了 S 和 S 兼容,其他都不兼容。


InnoDB 支持两种意向锁(即为表级别的锁):

  • 意向共享锁(读锁 IS Lock),事务想要获取一张表的几行数据的共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。

  • 意向排他锁(写锁 IX Lock),事务想要获取一张表中几行数据的排它锁,事务在给一个数据行加排它锁前必须先取得该表的 IX 锁。


首先解释一下意向锁,以下为意向锁的意图解释:

The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.


大致意思是加意向锁为了表明某个事务正在锁定一行或者将要锁定一行数据。


首先申请意向锁的动作是 InnoDB 完成的,怎么理解意向锁呢?例如:事务 A 要对一行记录 R 进行上 X 锁,那么 InnoDB 会先申请表的 IX 锁,再锁定记录 R 的 X 锁。


在事务 A 完成之前,事务 B 想要来个全表操作,此时直接在表级别的 IX 就告诉事务 B 需要等待而不需要在表上判断每一行是否有锁。


意向排它锁存在的价值在于节约 InnoDB 对于锁的定位和处理性能。另外注意了,除了全表扫描以外意向锁都不会阻塞。


锁的算法


InnoDB 有 3 种行锁的算法:

  • Record Lock:单个行记录上的锁。

  • Gap Lock:间隙锁,锁定一个范围,而非记录本身。

  • Next-Key Lock:结合 Gap Lock 和 Record Lock,锁定一个范围,并且锁定记录本身。主要解决的问题是 RR 隔离级别下的幻读。


这里主要讲一下 Next-Key Lock。MySQL 默认隔离级别 RR 下,这时默认采用 Next-Key locks。


这种间隙锁的目的就是为了阻止多个事务将记录插入到同一范围内从而导致幻读。注意了,如果走唯一索引,那么 Next-Key Lock 会降级为 Record Lock。


前置条件为事务隔离级别为 RR 且 SQL 走的非唯一索引、主键索引。如果不是则根本不会有 Gap 锁!先举个例子来讲一下 Next-Key Lock。


首先建立一张表:

mysql> show create table m_test_db.M;
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                     |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| M     | CREATE TABLE `M` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `user_id` varchar(45DEFAULT NULL,
  `name` varchar(45DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `IDX_USER_ID` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)


首先 Session A 去拿到 user_id 为 26 的 X 锁,用 force index,强制走这个非唯一辅助索引,因为这张表里的数据很少。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from m_test_db.M force index(IDX_USER_ID) where user_id = '26' for update;
+----+---------+-------+
| id | user_id | name  |
+----+---------+-------+
|  5 | 26      | jerry |
|  6 | 26      | ketty |
+----+---------+-------+
2 rows in set (0.00 sec)


然后 Session B 插入数据:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into m_test_db.M values (8,25,'GrimMjx')
;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction


明明插入的数据和锁住的数据没有毛线关系,为什么还会阻塞等锁最后超时呢?这就是 Next-Key Lock 实现的。


画张图你就明白了:

Gap 锁锁住的位置,不是记录本身,而是两条记录之间的间隔 Gap,其实就是防止幻读(同一事务下,连续执行两句同样的 SQL 得到不同的结果)。


为了保证图上 3 个小箭头中间不会插入满足条件的新记录,所以用到了 Gap 锁防止幻读。


简单的 Insert 会在 Insert 的行对应的索引记录上加一个 Record Lock 锁,并没有 Gap 锁,所以并不会阻塞其他 Session 在 Gap 间隙里插入记录。


不过在 Insert 操作之前,还会加一种锁,官方文档称它为 Intention Gap Lock,也就是意向的 Gap 锁。


这个意向 Gap 锁的作用就是预示着当多事务并发插入相同的 Gap 空隙时,只要插入的记录不是 Gap 间隙中的相同位置,则无需等待其他 Session 就可完成,这样就使得 Insert 操作无须加真正的 Gap Lock。


Session A 插入数据:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into m_test_db.M values (10,25,'GrimMjx')
;
Query OK, 1 row affected (0.00 sec)


Session B 插入数据,完全没有问题,没有阻塞:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into m_test_db.M values (11,27,'Mjx')
;
Query OK, 1 row affected (0.00 sec)


死锁


了解了 InnoDB 是如何加锁的,现在可以去尝试分析死锁。死锁的本质就是两个事务相互等待对方释放持有的锁导致的,关键在于不同 Session 加锁的顺序不一致。


不懂死锁概念模型的可以先看一幅图:

左鸟线程获取了左肉的锁,想要获取右肉的锁,右鸟的线程获取了右肉的锁。


右鸟想要获取左肉的锁。左鸟没有释放左肉的锁,右鸟也没有释放右肉的锁,那么这就是死锁。


接下来还用刚才的那张 M 表来分析一下数据库死锁,比较好理解:

四种隔离级别


那么按照最严格到最松的顺序来讲一下四种隔离级别:


①Serializable(可序列化)