07 | 行锁功过:怎么减少行锁对性能的影响?
Mysql的行锁是在引擎层由各个引擎自己实现的,但是不是所有引擎支持行锁。比如MyISAM不支持,但是InnoDB支持。
行锁即针对数据表中行记录的锁。比如事务A更新了一行,此时事务B也要更新同一行,那么必须等待事务A操作完成后才能更新。
两阶段锁
举个例子,下列操作中,视id是表t的主键。事务B的update执行时会是什么现象呢?
这取决于事务A执行完两条update语句后,持有哪些锁,以及会在什么时候释放。实际上事务B的update语句会被阻塞,直到事务A执行commit后,事务B才能继续执行。
所谓两阶段锁协议,即在事务中,行锁是在需要的时候才加上,等到事务结束才释放。
这给我们的启示在于,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。安排语句顺序,可以最大程度减少事务之间的锁等待,提升并发度。
有时会发生这种现象:CPU消耗接近100%,但整个数据库每秒就执行不到100个事务。这是什么原因呢?这就要说到死锁和死锁检测了。
死锁和死锁检测
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程进入无限等待的状态,这就是死锁。
此时,事务A等待事务B释放id=2的行锁,事务B在等待事务A释放id=1的行锁。事务A和B都在等待对方的资源释放,即进入死锁状态。有两种策略:
- 直接进入等待,直到超时。超时时间通过innodb_lock_wait_timeout来设置。
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务继续执行。
在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是设置成很小的值也不太合适,会出现很多误伤。
因此,第二种策略是建议使用的。主动死锁检测:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
假如有1000个并发线程要同时更新同一行,那么死锁检测会消耗大量的CPU资源,这样CPU利用率很高,但是执行的事务却很少。
怎么解决由这种热点行更新导致的性能问题呢?问题的症结在于,死锁检测要耗费大量的CPU资源。
- 一是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。
- 另一个思路是控制并发度。现如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。
在客户端做并发控制?不太可行,因为客户端很多,即使每个客户端控制到5个并发线程,汇总到数据库服务端后其峰值也是很高的。
所以建议在数据库服务端控制并发,基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。
此外,还可以通过将一行改成逻辑上的多行来减少锁冲突。这样每次冲突概率变成原来的几分之一,可以减少锁等待个数,也就减少了死锁检测的CPU消耗。