文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-04-02 19:55

关注

【51CTO.com原创稿件】说到数据库事务,想到的就是要么都做修改,要么都不做,或者是 ACID 的概念。其实事务的本质就是锁、并发和重做日志的结合体。一文快速搞懂MySQL InnoDB事务ACID实现原理

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

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 实现的。
画张图你就明白了:![](https://s1.51cto.com/images/blog/201904/03/ea21d2246fdb9b22d050882a0d885d2a.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

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 加锁的顺序不一致。
不懂死锁概念模型的可以先看一幅图:![](https://s1.51cto.com/images/blog/201904/03/e18abb279d6c1c7494a70460f0ba8c52.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

左鸟线程获取了左肉的锁,想要获取右肉的锁,右鸟的线程获取了右肉的锁。
右鸟想要获取左肉的锁。左鸟没有释放左肉的锁,右鸟也没有释放右肉的锁,那么这就是死锁。
接下来还用刚才的那张 M 表来分析一下数据库死锁,比较好理解:![](https://s1.51cto.com/images/blog/201904/03/b9ba3e051a8796966437328ac5cd9bd5.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

**四种隔离级别**
那么按照最严格到最松的顺序来讲一下四种隔离级别:
**①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 隔离级别下,对于快照数据,总是读最新的一份快照数据见红标:
![](https://s1.51cto.com/images/blog/201904/03/abed48da5c98aad28c72bb229d277d43.jpg?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
undo log 会产生 redo log,因为 undo log 需要持久性保护 。
最后,你会发现姜承尧的 MySQL InnoDB 书上的很多内容都是官方手册的翻译,无论是看源码还是学习新框架,最好看原汁原味的。
只要你坚持,一步一步来,总归会成功的。切忌,学技术急不来,快就是稳,稳就是快。
来源:https://www.cnblogs.com/GrimMjx/p/10575147.html

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】
阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯