当前位置: 首页 > 科技观察

本文快速理解MySQLInnoDB事务的ACID实现原理

时间:2023-03-17 12:38:57 科技观察

[.com原稿]说到数据库事务,想到的要么全部修改,要么不做,或者ACID的概念.事实上,事务的本质是锁、并发和重做日志的结合。本文主要讲一下InnoDB中事务是如何实现ACID的:AtomicityConsistencyIsolationIsolationDurability隔离的原理是锁,所以隔离也可以叫并发控制,锁等,事务的隔离要求每次读的对象-写事务可以和其他事务的操作对象分开。再者,比如操作缓冲池中的LRU链表,删除、添加、移动LRU链表中的元素,为了保证一致性,都需要加锁。InnoDB使用锁来支持对共享资源的并发访问,提供数据完整性和一致性。那么InnoDB支持什么样的锁呢?先来看看InnoDB锁的介绍:InnoDB锁你可能听说过各种InnoDB数据库锁,Gap锁,共享锁,排他锁,读锁,写锁等等,但是InnoDB对锁的标准实现只有两种,一种是行级锁,一种是意向锁。InnoDB实现了以下两种标准的行级锁:共享锁(readlockSLock),允许事务读取一行数据。排它锁(writelockXLock),允许一个事务删除一行数据或者更新一行数据。行级锁中,除了S和S是兼容的,其他都是不兼容的。InnoDB支持两种意向锁(即表级锁):意向共享锁(读锁ISLock),一个事务想要为表中的几行数据获取共享锁,事务在添加共享锁之前锁定数据行必须首先获取表的IS锁。意向排他锁(writelockIXLock),事务想要获得一个表中几行数据的排他锁,事务必须先获得该表的IX锁,然后再给一个数据行加排他锁。先解释一下意向锁。以下是意向锁的用途:IX和IS锁的主要目的是表明有人正在锁定一行,或将要锁定表中的一行。大致意思是加意向锁,表示一个事务正在锁定一行或者即将锁定一行数据。首先,申请意向锁的动作是由InnoDB完成的。如何理解意向锁?例如事务A要对R记录的一行加X锁,InnoDB会先在表上申请IX锁,然后再给R记录加X锁。在事务A完成之前,事务B要执行全表操作。这时候IX直接在表级别告诉事务B等待,不判断每一行是否对表加锁。意向排他锁的价值在于节省InnoDB对锁的定位和处理性能。另请注意,意向锁不会阻塞,除非是全表扫描。锁算法InnoDB有3种行锁算法:记录锁:对单行记录的锁。GapLock:间隙锁,锁定一个范围,而不是记录本身。Next-KeyLock:组合GapLock和RecordLock以锁定范围并锁定记录本身。主要解决RR隔离级别下的幻读问题。这里主要说下Next-KeyLock。在MySQL默认隔离级别RR下,默认使用Next-Key锁。这个间隙锁的目的是防止多个事务向同一个范围插入记录,造成幻读。请注意,如果您使用唯一索引,Next-KeyLock将降级为RecordLock。前置条件是事务隔离级别为RR的非唯一索引和主键索引,SQL运行。否则,根本就没有Gaplock!让我举一个Next-KeyLock的例子。先建表:mysql>showcreatetablem_test_db.M;+--------+-------------------------------------------------------+|Table|CreateTable|+--------+------+|表|创建表|+--------+----------------------------------------------------------+|M|CREATETABLE`M`(`id`int(11)NOTNULLAUTO_INCREMENT,`user_id`varchar(45)DEFAULTNULL,`name`varchar(45)DEFAULTNULL,PRIMARYKEY(`id`),KEY`IDX_USER_ID`(`user_id`))ENGINE=InnoDBAUTO_INCREMENT=15DEFAULTCHARSET=utf8|+-------+----------------------------------------------------------+1rowinset(0.00sec)首先,SessionA拿到user_id为26的X锁,使用forceindex,强制这个非唯一的辅助索引,因为这个表的数据很少。mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>select*fromm_test_db.Mforceindex(IDX_USER_ID)whereuser_id='26'forupdate;+----+--------+-------+|id|user_id|name|+----+--------+------+|5|26|jerry||6|26|ketty|+----+----------+--------+2rowsinset(0.00sec)然后SessionB插入数据:mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>insertintom_test_db.Mvalues(8,25,'GrimMjx');ERROR1205(HY000):Lockwaittimeoutexceeded;tryrestartingtransaction明明插入的数据和加锁的数据没有关系,为什么还是阻塞等待加锁超时?这就是Next-KeyLock所实现的。画个图你就明白了:Gap锁锁定的位置不是记录本身,而是两条记录之间的gap。其实就是为了防止幻读(同一个事务下,连续执行同一条SQL的两句得到不同的结果)。为了保证图上三个小箭头之间不会插入满足条件的新记录,使用了Gap锁来防止幻读。简单的Insert会对Insert行对应的索引记录加一个RecordLock锁,没有Gap锁,所以不会阻塞其他Session在Gap间隙插入记录。但是在Insert操作之前,会加一把锁。官方文档称之为IntentionGapLock,即意向Gap锁。这个意向间隙锁的目的是表示当多个事务并发插入同一个间隙时,只要插入的记录不在间隙中的同一个位置,就可以完成而无需等待其他会话,这样Insert操作不需要添加。缝隙锁。SessionA插入数据:mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>insertintom_test_db.Mvalues(10,25,'GrimMjx');QueryOK,1rowaffected(0.00sec)SessionB插入数据,一点问题都没有,没有阻塞:mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>insertintom_test_db.Mvalues(11,27,'Mjx');QueryOK,1rowaffected(0.00sec)Deadlock了解了InnoDB如何锁定,现在您可以尝试分析死锁。死锁的本质是两个事务互相等待对方释放对方持有的锁。关键是不同session加锁的顺序不一致。如果不理解死锁的概念模型,可以先看一张图:左鸟线程已经获取了左肉的锁,想获取右肉的锁,而右肉的线程鸟已经获得了正确的肉。右边的鸟想获得左边肉的锁。左鸟没有释放左肉的锁,右鸟也没有释放右肉的锁,那么这就是死锁。接下来用刚才的M表来分析数据库死锁,比较好理解:四种隔离级别那么我们从最严格到最松的顺序来说说四种隔离级别:①Serializable(序列化)***Transaction隔离级别。主要用于InnoDB存储引擎的分布式事务。执行事务排序并串行执行事务。无需冲突控制,但设备速度较慢。按照JimGray在《Transaction Processing》一书中的说法,ReadCommitted和Serializable的开销几乎是一样的,甚至Serializable更好。SessionA设置隔离级别为Serializable,启动一个事务执行一句SQL:mysql>select@@tx_isolation;+----------------+|@@tx_isolation|+--------------+|SERIALIZABLE|+----------------+1rowinset,1warning(0.00sec)mysql>starttransaction;QueryOK,0rowsaffected(0.00sec)mysql>select*fromm_test_db.M;+----+--------+--------+|id|user_id|name|+----+---------+--------+|1|20|mjx||2|21|ben||3|23|may||4|24|tom||5|26|杰瑞||6|26|ketty||7|28|kris|+----+--------+--------+7rowsinset(0.00sec)SessionBinsert一条数据,timeout:mysql>starttransaction;QueryOK,0rowsaffected(0.00sec)mysql>insertintom_test_db.Mvalues(9,30,'test');ERROR1205(HY000):Lockwaittimeoutexceeded;tryrestartingtransaction②RepeatableRead(可重复读取)一个事务根据相同的查询条件对于检索到的数据,其他事务插入满足其查询条件的新数据,导致幻读。在RR隔离级别下,InnoDB存储引擎已经使用了Next-KeyLock算法来避免幻读,理解一下概念即可。InnoDB使用MVCC读取数据。在RR隔离级别下,总是读取事务开始时的行数据版本。SessionA检查id=1的数据:mysql>settx_isolation='repeatable-read';QueryOK,0rowsaffected,1warning(0.00sec)mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>select*fromm_test_db.Mwhereid=1;+----+--------+--------+|id|user_id|name|+----+--------+--------+|1|20|GrimMjx|+----+--------+--------+1rowinset(0.01sec)SessionB修改id=1数据:mysql>settx_isolation='repeatable-read';QueryOK,0rowsaffected,1warning(0.00sec)mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>updatem_test_db.Msetname='Mjx';QueryOK,7rowsaffected(0.00sec)Rowsmatched:7Changed:7Warnings:0现在SessionA再次检查id=1的数据,数据还是事务开始时的数据。mysql>select*fromm_test_db.Mwhereid=1;+----+--------+--------+|id|user_id|name|+----+-------+--------+|1|20|GrimMjx|+----+--------+---------+1rowinset(0.00sec)③ReadCommitted(读已提交)事务从开始直到提交,任何修改对其他事务都是不可见的。InnoDB使用MVCC读取数据。RC隔离级别下,始终读取锁定行***的快照数据。SessionA检查id=1的数据:mysql>settx_isolation='read-committed';QueryOK,0rowsaffected,1warning(0.00sec)mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>select*fromm_test_db.Mwhereid=1;+----+--------+------+|id|user_id|name|+----+--------+------+|1|20|Mjx|+----+--------+------+1rowinset(0.00sec)SessionB修改Namewithid=1并Commit:mysql>settx_isolation='repeatable-read';QueryOK,0rowsaffected,1warning(0.00sec)mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>updatem_test_db.Msetname='testM'whereid=1;QueryOK,1rowsaffected(0.00sec)Rowsmatched:1Changed:1Warnings:0//注意,在这里提交!mysql>commit;QueryOK,0rowsaffected(0.00sec)SessionA重新查询id=1的记录,发现数据为***数据:mysql>select*fromm_test_db.Mwhereid=1;+----+---------+--------+|id|user_id|name|+----+--------+------+|1|20|testM|+----+--------+--------+1rowinset(0.00sec)④事务中ReadUncommitted(读取未提交)修改,即使是notcommitted,其他Transaction也都是可见的。SessionA查看id=3的数据,没有Commit:mysql>settx_isolation='read-uncommitted';QueryOK,0rowsaffected,1warning(0.00sec)mysql>select@@tx_isolation;+------------------+|@@tx_isolation|+----------------+|READ-UNCOMMITTED|+------------------+1rowinset,1warning(0.00sec)mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>select*fromm_test_db.Mwhereid=3;+----+---------+------+|id|user_id|name|+----+--------+------+|3|23|may|+----+--------+------+1rowinset(0.00sec)SessionB修改了id=3的数据,但是没有Commit:mysql>settx_isolation='read-未提交';QueryOK,0rowsaffected,1warning(0.00sec)mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>updatem_test_db.Msetname='GRIMMJX'whereid=3;QueryOK,1rowaffected(0.00sec)Rowsmatched:1Changed:1Warnings:0Session再次查看得到新的结果:mysql>select*fromm_test_db.Mwhereid=3;+----+--------+--------+|id|user_id|name|+----+--------+--------+|3|23|GRIMMJX|+----+--------+---------+1rowinset(0.00sec)也是k大量笔墨介绍这里的隔离,这个比较重要,需要静态用心学习的特点,也是把它放在第一位的原因。原子性、一致性和持久事务隔离是通过锁实现的,原子性、一致性和持久性是通过数据库的redolog和undolog来实现的。重做日志称为重做日志,用于保证事务的原子性和持久性,恢复事务修改的页面操作。undolog用于保证事务的一致性,undorollbackline记录到某个特性版本和MVCC函数。两者的内容不同。redo记录的是物理日志,undo是逻辑日志。重做重做日志由重做日志缓冲区(redologbuffer)和重做日志文件(redologfile)组成,前者是volatile的,后者是persistent的。InnoDB通过ForceLogatCommit机制实现持久化。Committing时,首先要将事务的所有日志写入redolog文件进行持久化,直到Commit操作完成才算完成Commit操作。当事务提交时,日志并没有写入重做日志文件,而是等待一个事件周期,然后再执行Fsync操作。由于在事务提交时不强制执行Fsync操作,显然这可以提高数据库性能。记住3点:重做日志是在InnoDB层产生的。redolog是物理格式的日志,记录了每一页的修改。在事务处理过程中不断写入重做日志。undo事务回滚和MVCC,需要undo。undo是逻辑日志,只是将数据库逻辑恢复到原来的状态,但回滚后数据结构和页面本身可能不一样了。例如:用户执行插入10w条数据的事务,表空间随之增加。用户执行ROLLBACK后,插入的数据会回滚,但表空间的大小不会相应缩小。实际的做法是和之前的思路做同样的操作。Insert对应Delete,Update对应反向Update,实现原子性。InnoDB中MVCC的实现依赖于undo。举一个经典的例子:Bob转100元给Smith,那么就有以下三个版本。RR隔离级别下,对于快照数据,总是读取事务开始时的行数据版本。见黄色标记。RC隔离级别下,对于快照数据,总是读取最新的快照数据,见红色标记:undolog会产生redolog,因为undolog需要持久化保护。***,你会发现蒋承尧的MySQLInnoDB这本书很多内容都是官方手册的翻译。不管是看源码还是学习新的框架,最好还是看原味。只要坚持,一步一个脚印,最终就会成功。学技术不要着急,快就是稳,稳就是快。来源:https://www.cnblogs.com/GrimMjx/p/10575147.html【原创稿件,合作网站转载请注明原作者及出处.com】