事务
概述
事务是访问并可能操作各种数据项的一个数据库操作序列。例如,在人员管理系统中,你要删除一个人员,则你不仅要删除人员的基本资料,还要删除和该人员相关的信息,如信箱,文章等等。而这些数据库操作语句就构成了一个事务
- 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
- 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
事务必须要满足四个条件(ACID),即原子性、一致性、隔离性、持久性(Durability)。
- 原子性:一个事务中的所有操作,要么全部完成,要么都不执行,不会存在中间某个环节。若事务在执行过程中发生错误,则会回滚到事务开始前的状态。
- 一致性:一个事务开始前和结束以后,数据库的完整性没有被破坏,即写入的资料必须完全符合所有预设规则。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。
- 隔离性:一个事务所作的修改在最终提交以前,对其他事务是不可见的。
- 持久性:一个事务一旦提交,则其所做的修改会永远保存至数据库。即使系统发生崩溃,事务执行的结果也不能丢失。
自动提交模式:MYSQL默认采用自动提交模式,即:若不显示使用 START TRANSACTION 语句来开始一个事务,则只要执行DML(数据库操作语言,如insert,delete,update等)操作的语句,MySQL会立即隐式提交事务。
事务语法
开启事务
我们可用下面两种语句之一来开启一个事务。
BEGIN;
START TRANSACTION;
若我们不显示的使用上述两句之一开启一个事务,则每一条语句都算是一个独立的事务。这种特性称为事务的自动提交。
执行begin实际并没有开启一个事务,对数据进行增删改查等操作后才开启了一个事务。
提交事务
COMMIT
⼿动中⽌事务
若写了几条语句之后发现有一条写错了,我们可以中止事务来将数据库恢复到事务执行前的样子。
ROLLBACK
保存点
我们可以在事务对应的数据库语句中打点,在调用ROLLBACK语句时可以指定回滚到哪个点,而不是最初的远点。
SAVEPOINT 保存点名称;
回滚到某一点则为:
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;
并发一致性问题
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。
丢失修改
一个事务修改了另一个未提交事务修改过的数据,则称为丢失修改。
如下图所示,A和B各开启一个事务,事务B修改name列值,然后事务B接着修改同样的列值,如果事务B进行回滚,则事务A的更新也不会存在。
读脏数据
一个事务读到了另一个未提交事务修改过的数据,则称为脏读。
如下图所示,A和B各开启一个事务,事务B修改name列值为‘关羽’,然后事务A再去查询这条记录,如果读到name值为‘关羽’,而事务B稍后进行了回滚,则事务A相当于读取到一个不存在的数据。
不可重复读
如果一个事务可以读到另一个已提交事务修改过的数据, 且每当其他事务对该数据进行1次修改并提交后,该事务都能查询得到最新值,这样每次查询的结果都不一致。则称为不可重复读。
如下图所示,我们在事务B中提交两个隐式事务(语句结束事务就提交),这2个事务都修改了number列为1的name列值。每次事务提交后,如果事务A中的事务都能看到最新值,则称这种现象为不可重复读。
幻影读
若一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入符合那些条件的记录,原先的事务再次按照该条件查询时,若能把另一个事务插入的记录也读出来,则发生了幻读。
如下图所示,事务A根据条件number > 0 得到name列值为''刘备'的记录;之后事务B提交一个插入一条新记录的隐式事务;之后事务A再根据相同条件查询表,得到的结果中包含事务B插入的新记录。
若此例中事务B是删除了同样符合条件的记录,而不是添加新记录,则这种现象不称为幻读。幻读强调的是一个事务按某个相同条件多次读取记录时,后读取时读取到了之前没有读取到的数据。
总结
这四种问题的轻重程度为:脏写 > 脏读 > 不可重复读 > 幻读。
产生并发不一致性问题的主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,不过封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
隔离级别
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离定义了数据库系统中一个事务中操作的结果在何时以何种方式对其他并发事务操作可见。
事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。这四种隔离级别都可以阻止丢失修改。
未提交读
事务中的修改,即使没有提交,对其他事务也是可见的。可能会导致脏读、幻读或不可重复读。
提交读
一个事务所做的修改在提交之前对其它事务是不可见的。
可以阻止脏读,但是幻读或不可重复读仍有可能发生。
可重复读(read Repeatable)
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改。可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读),MySQL通过MVCC来实现可重复读。
下面我们演示一个可能产生的幻读示例:
测试前的数据
sql语句
事务1 | 事务2 |
begin | begin |
select * from goods | |
insert into goods value(null, 'ff', 33); | |
commit | |
update goods set name = 'cc'; | |
commit |
期望的结果:
id name
1 gg
2 mouse
实际的结果:
- 本例中初始记录的name列值为ll,事务A查到了这一条记录;
- 然后事务B添加了一条新记录,然后提交事务B;
- 事务A本意是想更改原先的那唯一一条数据,但却把事务B添加的数据页修改了。
让我们再看看幻读的定义:
若一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入符合那些条件的记录,原先的事务再次按照该条件查询时,若能把另一个事务插入的记录也读出来,则发生了幻读。
此例我们将事务A的第二次查询操作 更换为了修改操作,同样是''读''出到了新插入的数据。
可以看出,可重复读的隔离级别解决了 读数据情况下的幻读,对于修改操作依旧存在幻读问题。
快照读和当前读
快照读
通过MVCC机制,虽然使得数据变得可重复读,但读取到的数据可能是历史数据,而非最新的。这种读取历史数据的方式,称之为快照读。
在Innodb引擎,快照的生成是在第一次执行select的时候,记录下这次select的结果,之后的select,则会返回这次快照的数据,即便其他事务提交了也不会影响select的数据,这就实现了可重复读。
当前读
对数据的修改操作(insert,update,delete)都是采用当前读模式。在执行这几个操作都会读取最新的记录,即使是别的事务提交的数据也可以查询到。
假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。这就是可重复读中的例子出现问题的原因:事务B添加一个新记录后,事务A知道了这个新的记录。
如果想要彻底解决幻读,有两个办法:
- 使用串行化读的隔离级别
- MVCC+next-key locks
但实际很多项目不会使用到上面2中方法,很多时候幻读是我们完全可以接受的。
可串行化(Serializable)
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
设置事务的隔离级别
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
其中的level可选值有4个:REPEATABLE READ,READ COMMITTED,READ UNCOMMITTED,SERIALIZABLE。
SET关键字后面可以放置GLOBAL关键字、SESSION关键字或者什么都不放,这样会对不同范围的事务产⽣不同的影响,如下所示:
(1)只在全局范围内影响:只对执行完该语句之后的产生的会话起作用;对当前存在的会话无效。
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
(2)在会话范围内影响:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- 对当前会话的所有后续的事务有效;
- 该语句可以在已经开启的事务中间执⾏,但不会影响当前正在执⾏的事务;
- 如果在事务之间执⾏,则对后续的事务有效。
(3)只对执⾏语句后的下⼀个事务产⽣影响:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- 只对当前会话中下⼀个即将开启的事务有效;
- 下⼀个事务执⾏完后,后续事务将恢复到之前的隔离级别。
- 该语句不能在已经开启的事务中间执⾏,会报错
多版本并发控制MVCC
概述
当有人从数据库读数据的同时,另外一个人在写数据,这会造成 读数据的人可能看到“半写”或不一致的数据。加锁是解决问题的方法之一,但效率很差。而MVCC也是一种解决方法。多版本控制MVCC是一种并发控制的方法,一般在数据库管理系统中实现对数据库的并发访问。
每当一个事务对一条记录进行修改时,都会生成旧版本记录,这些旧版本记录都一个专门记录修改该记录的事务id的隐藏列,这些旧版本记录构成一个链表。
每次执行一次SELETCT语句时都会生成一个ReadView。它记录了生成时还未提交的事务列表m_ids,以及创建ReadView的事务id creator_trx_id。我们通过ReadView里记录的各个值来与某一记录的各个版本来比较,比较标准是修改该版本的事务id是否是未提交的(活跃状态)。
MVCC只在可提交读和可重复读两个隔离级别下工作。
具体实现
版本链
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中包含两个必要的隐藏列(row_id并不是必要的:当我们创建的表中有主键或者⾮ NULL的UNIQUE键时都不会包含row_id列):
- trx_id:每次⼀个事务对某条聚簇索引记录进⾏改动时,都会把该事务的事务id赋值给trx_id隐藏列。我们可以看成是修改版本号。
- roll_pointer:每次对某条聚簇索引记录进⾏改动时,都会把旧的版本写⼊到undo⽇志中,然后这个隐藏列就相当于⼀个指针,可以通过它来找到该记录 修改前的信息。
每次对记录进行改动,都会记录一条undo日志,每次对该记录更新后,都会把旧值放到一条undo日志。我们把旧值看成是该记录的旧版本。另外每个版本中还包含生成该版本时对应的事务id。
每条undo日志有roll_pointer属性(INSERT操作对应的undo⽇志没有该属性,因为该记录并没有更早 的版本),我们将这些undo日志连起来构成一个链表。随着更新次数的增多,所有版本都会被roll_pointer属性连接成一个链表,我们把这个链表叫做版本链,版本链的头结点就是当前记录最新值。
ReadView
对于未提交读隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本即可;对于使用可串行化隔离级别的事务而言,我们通过使用加锁的方式来访问记录;
对于使用可重复读和可提交读隔离级别的事务而言,必须保证读到已提交了的事务修改过的记录(若另一个事务已修改但还未提交,是不会直接读取最新版本的记录),因此需要判断版本链中哪个版本是当前事务可见的。因此便有了ReadView的概念。
ReadView包含4个内容:
- m_ids:表示在⽣成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在⽣成ReadView时当前系统中最⼩且未提交的事务id,也就是m_ids中的最⼩值。
- max_trx_id:表示⽣成ReadView时系统中应该分配给下⼀个事务的id值。
- creator_trx_id:表示⽣成该ReadView的事务的事务id。
(1)只有在对表中的记录做改动时(执⾏INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否 则在⼀个只读事务中的事务id值都默认为0。
(2)max_trx_id并不是m_ids中的最⼤值,事务id是递增分配的。⽐⽅说现在有id为1,2,3这三个事务,之后id为3的事务提 交了。那么⼀个新的读事务在⽣成ReadView时,m_ids就包括1和2:min_trx_id的值就是1,max_trx_id的值就是4。
我们可以通过事务生成的ReadView与每个版本的修改版本号trx_id进行比较,以此判断记录的某个版本是否可见:
- 如果修改该版本的事务id == ReadView的creator_trx_id,说明该版本是由当前版本修改的,可以访问;
- 如果被访问版本的trx_id < ReadView中的min_trx_id,表明修改该版本的事务在 当前事务⽣成ReadView前已经提交了,该版本可以被当前事务访问。
- 如果被访问版本的trx_id > ReadView中的max_trx_id值,表明当前事务⽣成ReadView后,某个事务a才开始修改该版本,即事务a还未提交,所以该版本不可被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断⼀下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时,修改该版本的事务是未提交的,该版本不可以被访问;如果不在,说明创建ReadView时⽣成该版本的事务已经被提交,该版本可以被访问。
若某个版本的数据对当前事务不可见,则顺着版本链找到下一个版本的数据,继续按照上面的步骤判断可见性,直至版本链中最后一个版本,如果仍旧不可见,意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
在MySQL中,可重复读和可提交读隔离级别的一个非常大区别就是生成的ReadView时机不同。
可重复读和可提交读隔离级别的区别示例
假设现在只有一条事务id为80的事务插入一条记录
READ COMMITTED —— 每次读取数据前都⽣成⼀个ReadView
假设系统里有两个id100,id200事务在进行:事务1添加两行记录。
此时表hero中number为1的记录得到的版本链表为:
假设有一个使用可提交读隔离级别的事务开始执行:
这个个SELECT1的执⾏过程如下:
- 执⾏SELECT语句时会先⽣成⼀个ReadView,ReadView的m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id 为0。
- 然后从从版本链中挑选可⻅的记录,从图中可以看出,最新版本的列name的内容是'张⻜',该版本的trx_id值为100,在m_ids列表内,所以不符合可⻅性 要求,根据roll_pointer跳到下⼀个版本。
- 下⼀个版本的列name的内容是'关⽻',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下⼀个版本。
- 下⼀个版本的列name的内容是'刘备',该版本的trx_id值为80,⼩于ReadView中的min_trx_id值100,说明修改该版本的事务已提交,符合要求,返回这个版本的记录。
之后我们提交事务id为100的事务:
然后再到事务id为200的事务中更新表hero中number为1的记录:
此时表hero中number为1的记录的版本链为:
然后在使用可提交读隔离级别的事务中继续查找number为1的记录:
这个SELECT2的执行过程为:
- 在执⾏SELECT语句时会⼜会单独⽣成⼀个ReadView,该ReadView的m_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以再次⽣成快照 时就没有它了),min_trx_id为200,max_trx_id为201,creator_trx_id为0。
- 然后从版本链中挑选可⻅的记录,从图中可以看出,最新版本的列name的内容是'诸葛亮',该版本的trx_id值为200,在m_ids列表内,所以不符合可⻅性 要求,根据roll_pointer跳到下⼀个版本。
- 下⼀个版本的列name的内容是'赵云',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下⼀个版本。
- 下⼀个版本的列name的内容是'张⻜',该版本的trx_id值为100,⼩于ReadView中的min_trx_id值200,所以这个版本是符合要求的,最后返回给⽤户的 版本就是这条列name为'张⻜'的记录。
REPEATABLE READ —— 在第⼀次读取数据时⽣成⼀个ReadView
假设现在有⼀个使⽤REPEATABLE READ隔离级别的事务开始执⾏:
SELECT1的执⾏过程为:
- 执⾏SELECT语句时会先⽣成⼀个ReadView,ReadView的m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id 为0。
- 然后从版本链中挑选可⻅的记录,从图中可以看出,最新版本的列name的内容是'张⻜',该版本的trx_id值为100,在m_ids列表内,所以不符合可⻅性 要求,根据roll_pointer跳到下⼀个版本。
- 下⼀个版本的列name的内容是'关⽻',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下⼀个版本。
- 下⼀个版本的列name的内容是'刘备',该版本的trx_id值为80,⼩于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给⽤户的版本就是这条列name为'刘备'的记录。
之后把事务id为100的事务提交
再到事务id为200的事务中更新表hero中number为1的记录
此刻,表hero中number为1的记录的版本链为:
然后再到刚才使⽤REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录
这个SELECT2的执⾏过程为:
- 直接复⽤之前的ReadView,之前的ReadView的 m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
- 然后从版本链中挑选可⻅的记录,从图中可以看出,最新版本的列name的内容是'诸葛亮',该版本的trx_id值为200,在m_ids列表内,所以不符合可⻅性 要求,根据roll_pointer跳到下⼀个版本。
- 下⼀个版本的列name的内容是'赵云',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下⼀个版本。同理后两个页不符合,继续跳到后第三个版本
- 版本的列name的内容是'刘备',该版本的trx_id值为80,⼩于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给⽤户的版 本就是这条列c为'刘备'的记录。
两次SELECT查询得到的结果是重复的,记录的列c值都是'刘备',这就是可重复读的含义。如果我们之后再把事务id为200的记录提交了,然后再到刚 才使⽤REPEATABLE READ隔离级别的事务中继续查找这个number为1的记录,得到的结果还是'刘备'
执行DELETE语句和UPDATE语句不会立即把对应的记录完全从页面中删除,而是对记录打上一个删除标志位。我们将该过程称为delete mark操作。这主要是为MVCC服务。随着系统运行,在确定系统中包含最早产⽣的那个ReadView的事务不会再访问某些update undo⽇志以及被打了删除标记的记录后,有⼀个后台运⾏的 purge线程会把它们真正的删除掉
Next-Key Locks
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现,MVCC 不能解决幻影读问题,而MVCC + Next-Key Locks 可以解决幻读问题。
Record Locks
锁定一个记录上的索引,而非记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
Gap Locks
锁定索引之间的间隙,但不包含索引本身。
Next-Key Locks
它是Record Locks 和Gap Locks的结合,它锁定了一个记录上的索引和索引之间的间隙。当查询的索引含义唯一属性时,Next-Key Lock 会进行优化,降级为Record Lock。
现在我们新建一张表test。设置xid列为普通索引后,向xid列添加记录:
CREATE TABLE `test` (
`id` int(11) primary key auto_increment,
`xid` int, KEY `xid` (`xid`) )
ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into test(xid) values (1), (3), (5), (8), (11);
此时该索引可能被锁住的范围为:(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)
Session A执行后会锁住的范围是:(5, 8], (8, 11]。因此Session B执行到第六步会阻塞,跳过第六步,第七步也会阻塞,但是并不阻塞第八步,第九步也不阻塞。
封锁
概述
并发事务访问相同记录带来的问题大致分为:
(1)写-写情况:即并发事务相继对相同的记录做出改动。这种情况下会发生脏写的问题,任何⼀种隔离级别都不允许这种问题的发⽣。所以在多个未提交事务相继对⼀条记录做改动时,需要让 它们排队执⾏,这个排队的过程其实是通过锁来实现的。所谓的锁其实是⼀个内存中的结构。
锁结构里有很多信息,为了简化理解,只把两个较重要的数据标识出来:
- trx信息:代表这个锁结构是哪个事务⽣成的。
- is_waiting:代表当前事务是否在等待。如果该值为true,表示当前事务需要等待。
(2)读-写或写-读情况:⼀个事务进⾏读取操作,另⼀个进⾏改动操作。
这种情况下可能发⽣脏读、不可重复读、幻读的问题,有两种方法解决:
- 读操作利⽤多版本并发控制(MVCC),写操作进⾏加锁。
- 读、写操作都采⽤加锁的⽅式
采⽤MVCC⽅式的话,读-写操作彼此并不冲突,性能更⾼,采⽤加锁⽅式的话,读-写操作彼此需要排队执⾏,影响性能。但有些特殊业务会要求采用加锁的方式执行。
事务利⽤MVCC进⾏的读取操作称之为⼀致性读或者快照读。所有普通的SELECT语句在可提交读、 可重复读隔离级别下都算是⼀致性读。⼀致性读并不会对表中的任何记录做加锁操作,其他事务可以⾃由的对表中的记录做改动。
共享锁和独占锁
并发事务的读-读情况不会引起问题,过对于写-写、读-写或写-读的情况可能会出现问题。在使用加锁的方式解决问题时,于既要允许读-读情况不受影响,⼜要使写-写、读-写或写-读情况中的操作相互阻塞,因此锁分为两类:
- 共享锁:简称S锁。在事务要读取⼀条记录时,需要先获取该记录的S锁。
- 排他锁:简称X锁,在事务要改动⼀条记录时,需要先获取该记录的X锁。
若事务A首先获取一个记录的X锁后,无论另一个事务B想获取该记录的S锁还是X锁都会被阻塞,直到事务T1提交。因此我们称S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的。
锁定读的语句
有时如果我们在读取记录时不希望别的事务读写该记录,我们可以对读取的记录加S锁:
SELECT ... LOCK IN SHARE MODE;
或者加X锁:
SELECT ... FOR UPDATE;
写操作
(1)DELETE:对⼀条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取⼀下这条记录的X锁,然后再执⾏delete mark操作。我们也可以把 这个定位待删除记录在B+树中位置的过程看成是⼀个获取X锁的锁定读。
(2)UPDATE:
1)若未修改该记录的键值并且 被更新的列占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录的位置,然后再获取⼀下记录的X 锁,最后在原记录的位置进⾏修改操作。
2)若未修改该记录的键值并且 ⾄少有⼀个被更新的列占⽤的存储空间在修改前后发⽣变化,则先在B+树中定位到这条记录的位置,然后获取⼀下记录 的X锁,将该记录彻底删除掉(就是把记录彻底移⼊垃圾链表),最后再插⼊⼀条新记录。
3)如果修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来⼀次INSERT操作
3)INSERT:
⼀般情况下,新插⼊⼀条记录的操作并不加锁,不过会有隐式锁来保护新插⼊的记录在本事务提交前不被别的事务访问。
多粒度锁
上面提到的锁都是针对记录而言,这种锁称为行级锁或行锁。对⼀条记录加锁只影响这条记录。
一个事务也可以在表级别进行加锁,称为表级锁或表锁。对⼀个表加锁影响整个表中的记录。给表加的锁可分为S锁和X锁。
InnoDB存储引擎 支持行级锁(row-level locking)和表级锁,默认为行级锁。
MyISAM 存储引擎 和 Memory存储引擎 采用表级锁(table-level locking)
当我们对一个表加锁时,我们如何知道是否上了行锁,这时就需要意向锁:
- 意向共享锁:简称IS锁,当事务准备在某条记录上加S锁时,需要先在表级别加⼀个IS锁或更强的锁。
- 意向独占锁:简称IX锁,当事务准备在某条记录上加X锁时,需要先在表级别加⼀个IX锁。
当我们添加IS锁时,不需要关心是否有IX锁,反之亦然,因此我们说IX锁和IX锁是兼容的。IS和IX锁只是为了快速判断当前表里是否有记录被锁上,以避免用遍历的方式来查看表中有没有上锁的记录。
封锁协议
三级封锁协议
一级封锁协议
事务 T 要修改数据 A 时必须加 X 锁,结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
二级封锁协议
在一级的基础上,要求读取数据时必须加 S 锁,读取完马上释放 S 锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,另一个事务就不能再加 S 锁了,也就不会读入数据。
三级封锁协议
在二级的基础上,要求读取数据 时必须加 S 锁,直到事务结束了才能释放 S 锁。
可以解决不可重复读的问题,因为当一个事务读取一个数据A时,其它事务不能对A 加 X 锁,从而避免了在读的期间数据发生改变。
两段锁协议
事务调度一般有串行调度和并行调度。
- 串行调度:多个事务依次串行执行,且只有当一个事务的所有操作都执行完后才执行另一个事务的所有操作。只要是串行调度,执行的结果都是正确的。
- 并行调度:利用分时的方法同时处理多个事务。但是并行调度的调度结果可能是错误的
为了结合两种调度的特点,有一种具有串行调度效果的并行调度方法:两段锁协议。
两段锁协议 指所有的事务必须分两个阶段对数据项加锁和解锁:
- 第一个阶段是获得封锁。事务可以获得任何数据项上的任何类型的锁,但是不能释放。若加锁不成功,则事务进入等待状态,直到加锁成功才继续执行;
- 第二阶段是释放封锁,事务可以释放任何数据项上的任何类型的锁,但不能再进行加锁操作。
MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
参考资料
- https://juejin.im/post/5c9040e95188252d92095a9e#comment
- https://cyc2018.github.io/CS-Notes/#/README
- 《MySQL是怎样运行的:从根儿上理解MySQL》