死锁
死锁
死锁是指两个或两个以上事务因争夺资源而互相等待的情况,MyISAM 表锁是 deadlock free 的,这是因为 MyISAM 总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,这就决定了在 InnoDB 中发生死锁是可能的。
--- session 1 获取 table_1 中排他锁
mysql> set autocommit = 0;
mysql> select * from table_1 where where id=1 for update;
--- session 2 获取 table_2 中排他锁
mysql> select * from table_2 where id=1 for update;
--- session 1 申请 table_2 锁,等待 session_2 完毕
mysql> select * from table_2 where id =1 for update;
--- session 2 申请 table_1 锁,等待 session_1 完毕
mysql> select * from table_1 where where id=1 for update;
出现死锁以后,有两种策略:
-
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。在 InnoDB 中,默认值是 50s。这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。此外,虽然发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务;但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁,这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决。
-
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。默认值本身就是 on。正常情况下选择该策略,但是它也是有额外负担的,如果瞬间有大量线程请求会消耗消耗大量的 CPU 资源,但是每秒却执行不了几个事务,因为每次都要检测。
在实际情况下,死锁大部分是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的 SQL 语句,绝大部分死锁都可以避免。
- 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。例如将热点账户拆分成若个个同样功能的账户,万一发生高并发,建议在应用层做限流或者排队,当然也可以在数据库层做排队,这个需要修改数据库源码。
- 在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
- 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。