死锁和死锁检测
在上一部分的例子里,影院余额这行的行锁在一个事务里不会停留很长时间,但是如果这个影院做活动,可以低价预售一年内所有的电影票,而且这个活动只持续一天。当活动时间开始的时候,MySQL就挂了,cpu消耗接近100%,但是整个数据库每秒执行不到100个事务。
当并发系统里不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
这时候,事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁。 事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。
当出现死锁以后,有两种策略:
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数
innodb_lock_wait_timeout
来设置。另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数
innodb_deadlock_detect
设置为on
,表示开启这个逻辑。
在InnoDB里,innodb_lock_wait_timeout
的默认值是50s,这意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
但是又不能直接把这个时间设置为很小的值,比如1秒。这样当出现死锁的时候,确实很快可以解开;但是如果不是死锁,只是简单的锁等待,超时时间设置的太短,就会容易误伤。
正常还是采用第二种策略,即主动死锁检测,而且innodb_deadlock_detect
的默认值本来就是on
。主动死锁检测是能够快速发现死锁并处理的,但是他也会有额外的负担。比如如果所有的事务都要更新同一行的场景,每个新来的被堵住的线程,都会判断会不会由于自己的加入导致死锁,这是一个复杂度O(n)的操作。假设有1000个并发线程要同时更新同一行,死锁的操作检车就是1000*1000=100w的数量级,在这个期间要消耗大量的cpu资源。
那么怎么解决这种由热点行更新导致的性能问题?
如果确定这个业务一定不会出现死锁,可以临时把死锁检测关掉,会出现大量的超时,对业务有损。
控制并发度:比如同一行最多只有10个线程在更新,那么死锁检测的成本就会很低。
在客户端做并发控制,这种方法并不可行,因为客户端很多
因此要做在数据库服务端,或是中间件里。如果考虑修改MySQL源码或是自研数据库的话,对于相同行的更新,在进入引擎之前就排队。
还可以考虑将一行改成逻辑上的多行来减少锁冲突。 比如影院账户可以考虑放在多条记录上,比如10个记录,影院的账户总额等于这10个记录的值的总和,每次随机选择一条记录来加金额。这样每次冲突的概率变为1/10,可以减少锁等待个数,也就减少了死锁检测的CPU消耗。