使用select for share,for update的场景及死锁陷阱

SELECT ... FOR SHARE 和 SELECT ... FOR UPDATE语句是innodb事务中的经常使用语句
for share会给表增长一个is锁,给记录行增长一个s锁,for update会给表增长一个ix锁,给记录行增长一个x锁。html

SELECT ... FOR SHARE使用场景

他们的意思就如语法表示的同样,SELECT ... FOR SHARE,我选择一些记录,这些记录能够share,其余事务也能够读,可是若是你要修改,很差意思,我加了一个s锁,你是不能够修改的。这个语句的应用场景之一是用来读取到最新的数据。
例如,由于innodb中mvcc机制的存在,在可重复读隔离级别下,A事务修改某一行的数据,B事务在A事务提交前是看不到A事务对该行的修改的,可是利用SELECT ... FOR SHARE,B事务会等待A事务释放该行的锁才能查看到该行数据。
建立一个测试表:mysql

-- ----------------------------
-- Table structure for test_tab -- ---------------------------- DROP TABLE IF EXISTS `test_tab`; CREATE TABLE `test_tab` ( `f1` int(11) NOT NULL AUTO_INCREMENT, `f2` varchar(11) NOT NULL DEFAULT '1', PRIMARY KEY (`f1`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ----------------------------
-- Records of test_tab -- ---------------------------- INSERT INTO `test_tab` VALUES ('1', '1');

 

 

SELECT ... FOR UPDATE使用场景

下面再说SELECT ... FOR UPDATE,我选择一些记录,这些select的记录是我下一步要update的,你要读或者修改这些记录,很差意思,我加的是x锁,你读不了也改不了。只有我当前事务提交了,这些记录你才能够读到或者修改。这个语句的应用场景之一是为了防止更新丢失。
例如,A事务和B事务同时读取银行帐户余额,是2元钱,A事务看到2元,消费了1元,将余额更新为1元,B事务看到2元,消费了1元,也将余额更新为1元,那么帐户变为1元,可是实际应该扣费2元。使用SELECT ... FOR UPDATE读取记录,能够避免这种丢失更新的现象
丢失更新现象:
sql

 

防止丢失更新
数据库

 

可能有人看到这里会有疑问:为何innodb采用MVCC这种多版本并发控制,每次看到的不是最新的数据,而是之前的一个快照呢?
这是由于一个事务的操做有可能成功,也有可能失败rollback,在一个事务commit以前,被其余事务读到还没提交的变动记录,会产生数据不同的现象(脏读),这种状况就是innodb最低的隔离级别READ UNCOMMITTED,能够读到没有commit的数据。
那么若是想要不产生脏读,容易想到的是采用锁的方式,当一个事务更改某行记录,就加上锁,其余事务等待该事务执行完毕才能读取到该行记录,可是这样作的话会产生大量的锁占用与等待,效率是很是低下的,所以innoDB采用了MVCC的方式。简单的说,A事务变动某行记录,innodb会产生对应的redo log,若是接下来A事务进行回滚,innodb能够根据redo log将记录回滚到事务开始以前的状态。在A事务没有结束时,若是B事务来查询该行记录,B事务会根据A事务变动后的记录值(在内存中)加上redo log“计算”出A事务开始前的该行记录值,从而读取到该行记录的一个快照,其中并不会产生锁与等待。
若是是可重复读REPEATABLE READ的隔离级别(默认隔离级别),B事务进行过程当中看到的始终会是B事务开始前的记录行快照信息,无论B事务进行过程当中A事务有没有完成;若是是提交读READ COMMITTED级别,B事务进行过程当中,能够看到A事务提交对记录行修改值(即若是A事务没有完成,B查询到的是A事务开始前的记录值,若是A事务完成了,B事务查询到的是A事务完成后的记录值),在这种状况下会产生不可重复读的现象,即同一次事务中屡次查询看到的结果会不同。

并发

使用select for share,for update的陷阱

再说使用select for share,for update的陷阱,for share会给记录行增长一个s锁,for update会给记录行增长一个x锁。若是此时有另外一个事务B也想给这些记录行加s锁或者x锁,此时就会产生等待,即事务B等待事务A,此时,若是事务A对这些记录行想加上另外一个类型的锁,就会产生死锁,用等待图来表示就是,事务B在等待事务A释放资源,接下来,事务A又必须等待事务B释放资源,如此造成了一个有向的环。让咱们举例说明,为了方便观察,咱们将锁等待超时时间设置长一点,首先,来看一个互相占用资源的例子:mvc

-- ----------------------------
-- Table structure for test_tab -- ---------------------------- DROP TABLE IF EXISTS `test_tab`; CREATE TABLE `test_tab` ( `f1` int(11) NOT NULL AUTO_INCREMENT, `f2` varchar(11) NOT NULL DEFAULT '1', PRIMARY KEY (`f1`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ----------------------------
-- Records of test_tab -- ---------------------------- INSERT INTO `test_tab` VALUES ('1', '1'); INSERT INTO `test_tab` VALUES ('2', '1');

 

死锁示例1:

 

上面的示例中,A等B,只要B释放资源,A就能够进行下去,可是B接下来的操做是去等待A,造成了一个环,产生死锁。
这种互相占有不一样资源的例子等待对方释放应该是最多见的死锁场景了,下面,咱们来看一下不常见的函数

 

死锁示例2:

 

-- ----------------------------
-- Table structure for test_tab -- ---------------------------- DROP TABLE IF EXISTS `test_tab`; CREATE TABLE `test_tab` ( `f1` int(11) NOT NULL AUTO_INCREMENT, `f2` varchar(11) NOT NULL DEFAULT '1', PRIMARY KEY (`f1`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ----------------------------
-- Records of test_tab -- ---------------------------- INSERT INTO `test_tab` VALUES ('1', '1');

 

上述两个事务并无互相占有不一样资源,B事务甚至没有实际占有资源,可是也产生了死锁,缘由是在第二步中B事务等待A事务释放资源,而且B事务要求分配一个x锁,接下来A事务须要一个f1=1的x锁,可是此时B事务已经在等待x锁,A事务只有一个s锁,并不能升级成x锁,所以A事务须要等待B。最终造成B等A,A又等B的环状图,产生死锁。
若是第一步中A事务使用的是for update呢?那么这种死锁状况就不会发生,由于for update语句已经申请到一个x锁,A事务此时持有x锁就能够直接在第3步执行删除操做,并不须要等待B事务的任何资源。测试

 

死锁示例3:

下面是一个因插入致使产生的死锁,数据库建立及数据同上
url

 

上面这个例子能够看作innoDB中“幻行”的解决方案,使用for share或者for update语句将锁定记录及记录之间的空白区间,阻止任何其余事务在该区间中插入数据(若是其余事务容许插入,这将致使同一个事务中屡次读取到不同的数据,如A事务select,B事务insert提交,A事务select for share,能够读取到B事务刚刚提交的记录)spa

此外,根据测试,在mysql8.0中若是next key锁区间重合,那么只能第一个事务拥有该区间的锁,其余事务不是等待该区间的锁,而是等待该区间第一个数据的锁,这方面的缘由不明。若是再配合max等函数的话,又会出现一些神奇的死锁现象,例如插入意向锁的冲突。这些方面估计只有查看innodb的源码才能知道缘由了,这里不深刻探究了。

总之,明白死锁的缘由是因为事务之间互相等待对方占有的资源,在等待图中造成了环便可,分析死锁有如下方式:
查看当前事务
SELECT * FROM information_schema.INNODB_TRX;
查看当前锁
SELECT * FROM `performance_schema`.data_locks;
查看当前锁等待
SELECT * FROM `performance_schema`.data_lock_waits;
分析死锁日志:
show ENGINE INNODB STATUS;
在日志中搜索“LATEST DETECTED DEADLOCK”

咱们看到,使用for update或者for share时有可能发生死锁状况,虽然死锁并不可怕,mysql拥有死锁检测的机制打破死锁而且咱们能够从新选择执行该事物,当时当死锁频繁出现时,仍是应当注意并加以排查的。最好的状况是不出现死锁,所以若是快照数据知足要求时,少用for share或者for update语句,虽然有时你看起来只是在一行记录上加锁,可是因为间隙锁和下一个键锁的存在,锁住的可能不止是一行记录。

参考资料:mysql8.0官方文档

相关文章
相关标签/搜索