mysql 开发进阶篇系列 14 锁问题(避免死锁,死锁查看分析)
一. 概述
通常来说,死锁都是应用设计问题,通过调整业务流程,数据库对象设计,事务大小,以及访问数据库的sql语句,绝大部分死锁都可以避免,下面介绍几种避免死锁的常用方法:
1. 在应用中,如果不同的程序并发操作多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。按顺序对表进行操作,是很常用的一种避免死锁的操作。 比如:有二个不一样的存储过程,同时在对一个表进行复杂的删改操作。这种情况可以考虑先让一个执行完成,再让另一个在执行。
2. 在程序中以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。比如常见的就是多线程下在程序中lock锁住(单进程下),在进程下保持串行处理。
3. 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排它锁,而不是先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其它事务可能又已经获得了相同记录的共享锁,从而造成锁冲突。 我理解是在事务中首先将要更新的记录,以select .. for update方式获得排它锁, 在事务里处理完逻辑后就可以直接更新而不用考虑锁冲突。 代码如下:
SET autocommit=0 -- 将要更新的数据先获得排它锁 SELECT * FROM city WHERE city_id=103 FOR UPDATE; -- 逻辑处理 .... -- 最后更新可以避免锁冲突 UPDATE city SET cityname='杭州' WHERE city_id=103; COMMIT;
4. 在默认级别Repeatable read下, 如果两个线程同时对相同条件记录用 select .. for update 加排它锁,在没有符合该条件记录情况下,两个线程都会加锁成功。当一个程序发现记录不存在,就试图插入一条新数据,如果两个线程都这么做,就会出现死锁。这是因为在Repeatable read下产生了间隙锁。这种情况下,将隔离级别改成Read commited,就可避免问题。 如下图表格 贴出了二个隔离级别下产生锁的差异。
5. 当在Repeatable read下,如果两个线程都先执行select .. for update。 在判断是否存在符合条件的记录,如果没有,就插入记录,此时,只有一个线程能插入成功,另一个线程会出现锁等待, 当第1个线程提交后,第2个线程如因为主键值重复,会出现异常。但却获得了一个排它锁, 需要执行rollback释放排它锁。避免影响其它事务。
总结:尽管通过上面介绍和sql 优化等措施,可以大大减少死锁,但死锁很难完全避免。因此。 在程序设计中总是捕获并处理死锁异常是一个很好的编程习惯。在程序异常里或commit或rollback。
二. 检查死锁产生的原因
如果出现死锁,可以用SHOW ENGINE INNODB STATUS 命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的sql语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等,以此分析死锁产生的原因和改进措施。
-- 查看最后一个死锁 SHOW ENGINE INNODB STATUS;
LATEST DETECTED DEADLOCK ------------------------ 2018-08-02 18:07:45 0x7f3a12209700 *** (1) TRANSACTION: TRANSACTION 35489574, ACTIVE 114 sec STARTING INDEX READ mysql TABLES IN USE 1, locked 1 LOCK WAIT 4 LOCK struct(s), HEAP size 1136, 2 ROW LOCK(s) MySQL thread id 2634494, OS thread handle 139887387092736, QUERY id 109768880 172.168.18.202 root Sending DATA -- 因为会话2 已获得排他锁, 些语句 等待 SELECT * FROM cityNew WHERE city_id=103 FOR UPDATE *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS SPACE id 479 page NO 3 n bits 72 INDEX GEN_CLUST_INDEX of TABLE `test`.`cityNew` trx id 35489574 lock_mode X waiting *** (2) TRANSACTION: TRANSACTION 35489577, ACTIVE 8 sec STARTING INDEX READ, thread declared inside INNODB 5000 mysql TABLES IN USE 1, locked 1 4 LOCK struct(s), HEAP size 1136, 3 ROW LOCK(s) MySQL thread id 2634624, OS thread handle 139887388956416, QUERY id 109768953 172.168.18.202 root statistics -- 死锁 SELECT * FROM city WHERE city_id=103 FOR UPDATE *** (2) HOLDS THE LOCK(S): RECORD LOCKS SPACE id 479 page NO 3 n bits 72 INDEX GEN_CLUST_INDEX of TABLE `test`.`cityNew` trx id 35489577 lock_mode X *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS SPACE id 477 page NO 3 n bits 80 INDEX PRIMARY of TABLE `test`.`city` trx id 35489577 lock_mode X LOCKS rec but NOT gap waiting *** WE ROLL BACK TRANSACTION (2) ------------