【51CTO.com原创稿件】说到数据库事务,想到的就是要么都作修改,要么都不作,或者是 ACID 的概念。其实事务的本质就是锁、并发和重作日志的结合体。html
这一篇主要讲一下 InnoDB 中的事务究竟是如何实现 ACID 的:mysql
mysql> show create table m_test_db.M; +-------+----------------------------------------------------------+ | Table | Create Table | +-------+----------------------------------------------------------+ | M | CREATE TABLE `M` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(45) DEFAULT NULL, `name` varchar(45) DEFAULT 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 |
+----+---------+-------+ sql
`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(可序列化)** 最高事务隔离级别。主要用在 InnoDB 存储引擎的分布式事务。强制事务排序,串行化执行事务。 不须要冲突控制,可是慢速设备。根据 Jim Gray 在《Transaction Processing》一书中指出,Read Committed 和 Serializable 的开销几乎是同样的,甚至 Serializable 更优。 Session A 设置隔离级别为 Serializable,并开始事务执行一句 SQL: mysql> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | SERIALIZABLE | +----------------+ 1 row in set, 1 warning (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M; +----+---------+-------+ | id | user_id | name | +----+---------+-------+ | 1 | 20 | mjx | | 2 | 21 | ben | | 3 | 23 | may | | 4 | 24 | tom | | 5 | 26 | jerry | | 6 | 26 | ketty | | 7 | 28 | kris | +----+---------+-------+ 7 rows in set (0.00 sec) Session Binsert 一条数据,超时: mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into m_test_db.M values (9,30,'test'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction **②Repeatable Read(可重复读)** 一个事务按相同的查询条件读取之前检索过的数据,其余事务插入了知足其查询条件的新数据,产生幻读。 InnoDB 存储引擎在 RR 隔离级别下,已经使用 Next-Key Lock 算法避免了幻读,了解概念便可。 InnoDB 使用 MVCC 来读取数据,RR 隔离级别下,老是读取事务开始时的行数据版本。 Session A 查看 id=1 的数据: mysql> set tx_isolation='repeatable-read'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M where id =1; +----+---------+---------+ | id | user_id | name | +----+---------+---------+ | 1 | 20 | GrimMjx | +----+---------+---------+ 1 row in set (0.01 sec) Session B 修改 id=1 的数据: mysql> set tx_isolation='repeatable-read'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update m_test_db.M set name = 'Mjx'; Query OK, 7 rows affected (0.00 sec) Rows matched: 7 Changed: 7 Warnings: 0 而后如今 Session A 再查看一下 id=1 的数据,数据仍是事务开始时候的数据。 mysql> select * from m_test_db.M where id =1; +----+---------+---------+ | id | user_id | name | +----+---------+---------+ | 1 | 20 | GrimMjx | +----+---------+---------+ 1 row in set (0.00 sec) **③Read Committed(读已提交)** 事务从开始直到提交以前,所作的任何修改对其余事务都是不可见的。 InnoDB 使用 MVCC 来读取数据,RC 隔离级别下,老是读取被锁定行最新的快照数据。 Session A 查看 id=1 的数据: mysql> set tx_isolation='read-committed'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M where id =1; +----+---------+------+ | id | user_id | name | +----+---------+------+ | 1 | 20 | Mjx | +----+---------+------+ 1 row in set (0.00 sec) Session B 修改 id=1 的 Name 而且 Commit: mysql> set tx_isolation='repeatable-read'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update m_test_db.M set name = 'testM' where id =1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 // 注意,这里commit了! mysql> commit; Query OK, 0 rows affected (0.00 sec) Session A 再查询 id=1 的记录,发现数据已是最新的数据: mysql> select * from m_test_db.M where id =1; +----+---------+-------+ | id | user_id | name | +----+---------+-------+ | 1 | 20 | testM | +----+---------+-------+ 1 row in set (0.00 sec) **④Read Uncommitted(读未提交)** 事务中的修改,即便没有提交,对其余事务也都是可见的。 Session A 查看一下 id=3 的数据,没有 Commit: mysql> set tx_isolation='read-uncommitted'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> select @@tx_isolation; +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ 1 row in set, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from m_test_db.M where id =3; +----+---------+------+ | id | user_id | name | +----+---------+------+ | 3 | 23 | may | +----+---------+------+ 1 row in set (0.00 sec) Session B 修改 id=3 的数据,可是没有 Commit: mysql> set tx_isolation='read-uncommitted'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update m_test_db.M set name = 'GRIMMJX' where id = 3; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Session A 再次查看则看到了新的结果: mysql> select * from m_test_db.M where id =3; +----+---------+---------+ | id | user_id | name | +----+---------+---------+ | 3 | 23 | GRIMMJX | +----+---------+---------+ 1 row in set (0.00 sec) 这里花了不少笔墨来介绍隔离性,这是比较重要,须要静下心来学习的特性。因此也是放在第一个的缘由。 **原子性、一致性、持久性** 事务隔离性由锁实现,原子性、一致性和持久性由数据库的 redo log 和 undo log 实现。 redo log 称为重作日志,用来保证事务的原子性和持久性,恢复提交事务修改的页操做。 undo log 来保证事务的一致性,undo 回滚行记录到某个特性版本及 MVCC 功能。二者内容不一样。redo 记录物理日志,undo 是逻辑日志。 **redo** 重作日志由重作日志缓冲(redo log buffer)和重作日志文件(redo log file)组成,前者是易失的,后者是持久的。 InnoDB 经过 Force Log at Commit 机制来实现持久性,当 Commit 时,必须先将事务的全部日志写到重作日志文件进行持久化,待 Commit 操做完成才算完成。 当事务提交时,日志不写入重作日志文件,而是等待一个事件周期后再执行 Fsync 操做,因为并不是强制在事务提交时进行一次 Fsync 操做,显然这能够提升数据库性能。 请记住 3 点: 重作日志是在 InnoDB 层产生的。 重作日志是物理格式日志,记录的是对每一个页的修改。 重作日志在事务进行中不断被写入。 **undo** 事务回滚和 MVCC,这就须要 undo。undo 是逻辑日志,只是将数据库逻辑恢复到原来的样子,可是数据结构和页自己在回滚以后可能不一样。 例如:用户执行 insert 10w 条数据的事务,表空间于是增大。用户执行 ROLLBACK 以后,会对插入的数据回滚,可是表空间大小不会所以收缩。 实际的作法就是作与以前想法的操做,Insert 对应 Delete,Update 对应反向 Update 来实现原子性。 InnoDB 中 MVCC 的实现就是靠 undo,举个经典的例子:Bob 给 Smith 转 100 元,那么就存在如下 3 个版本,RR 隔离级别下,对于快照数据,老是读事务开始的行数据版本见黄标。 RC 隔离级别下,对于快照数据,老是读最新的一份快照数据见红标:  undo log 会产生 redo log,由于 undo log 须要持久性保护 。 最后,你会发现姜承尧的 MySQL InnoDB 书上的不少内容都是官方手册的翻译,不管是看源码仍是学习新框架,最好看原汁原味的。 只要你坚持,一步一步来,总归会成功的。切忌,学技术急不来,快就是稳,稳就是快。 来源:https://www.cnblogs.com/GrimMjx/p/10575147.html 【51CTO原创稿件,合做站点转载请注明原文做者和出处为51CTO.com】