上一篇文章介绍了全局锁的特性,并且与其他功能相同的方法做了对比;讲解了表锁的两种类型,今天主要来说一下MySQL的行锁。
行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,MyISAM引擎就不支持行锁。不支持意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB是支持行锁的,这也是MyISAM被InnoDB替代的重要原因之一。
因此今天主要来围绕行锁,以及如何通过减少锁冲突来提升业务并发度。
行锁
行锁就是针对数据表中行记录的锁。这很好理解,比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成之后才能进行更新。
当然,数据库中还有一些没那么一目了然的概念和设计,这些概念如果理解和使用不当,容易导致程序出现非预期行为,比如两阶段锁。
两阶段锁
假设事务B的update语句执行会是什么现象呢(id是表t的主键)
结论取决于事务A在执行完两条update语句后,持有哪些锁,以及在什么时候释放。实际上事务B的update语句会被阻塞,直到事务A执行commit之后,事务B才能继续执行。
因此事务A持有的两个记录的行锁,都是在commit的时候才释放的。
也就是说,在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
所以,如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
经典的事务问题举例:
假设要实现一个电影票在线交易业务,顾客A要在影院B购买电影票,这个业务需要涉及到以下操作:
- 从顾客A账户余额中扣除电影票价
- 给影院B的账户余额增加这张电影票价
- 记录一条交易日志
就是说要完成这个交易,需要update两条记录,并insert一条记录。为了 保证交易的原子性要把这三个操作放在一个事务中。那么如何安排这三个语句在事务中的顺序呢?
试想如果有一个顾客C也要在影院买票,那么两个事务冲突的部分即使语句2了,因为它们要更新同一行数据。
根据两阶段锁协议,不论如何安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以要把语句2安排在最后,那么影院账户余额这一行的锁时间就最少。最大程度减少了事务之间的锁等待,提升了并发度。
如果影院做活动:可以低价预售一年内所有的电影票,并且这个活动只做一天。那么服务器肯定会宕机,CPU消耗接近100%但是整个数据库每秒执行不到100个事务,这是什么原因呢?
因此这里要说一下死锁和死锁检测了。
死锁和死锁检测
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待状态,称为死锁。用数据库中的行锁举个例子:
事务A在等待事务B释放id=2的行锁,事务B在等待事务A释放id=1的行锁,就会进入死锁状态。出现死锁之后又两种策略:
- 第一、直接进入等待,直到超时。超时时间由参数
innodb_lock_wait_timeout
来设置。 - 第二、发起死锁检测,发现死锁后,回滚死锁链条中的某一个事务,让其他事务继续执行。将参数
innodb_deadlock_detect
设置为on,表示开启这个逻辑。
在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,出现死锁之后要等待50s后超时退出,对于在线服务器来说这个等待时间是无法接受的。
有人也许会说,将超时时间设置成一个很小的值,可以解决死锁;但是如果是简单的锁等待,就会出现“误伤队友”的情况。
因此正常情况下我们还是要采用第二种策略。
主动死锁检测
每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是O(n)的操作。假设1000个并发线程要同时更新同一行,死锁检测操作就是100万这个量级的。虽然最终检测结果没有死锁,但是这期间要消耗大量的CPU资源。就会看到CPU利用率很高但是每秒却执行不了几个事务。
上面讲述的死锁检测要耗费大量的CPU资源,那么怎么解决由这种热点行更新导致的性能 问题呢?
- 第一、头痛医头方法
如果确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。虽然有一定的风险,但是减少了一定的CPU使用率。
因为业务设计的时候出现死锁一般就会回滚然后重试就可以了,这是业务无损的。但是关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。
- 第二、控制并发度
根据上面的分析,发现如果并发能够控制住就能将死锁检测成本降低很多。而控制并发不可以做在客户端,因为即使每个客户端控制很少的并发线程,汇总到数据库服务端以后,峰值并发数也是成倍增长的。
因此并发控制要做在服务端。如果有中间件,可以考虑在中间件实现:如果你的团队有能修改MySQL源码的人, 也可以做在MySQL里面。 基本思路是:对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。
如何从设计上优化这个问题?
可以考虑通过将一行改成逻辑上的多行来减少锁冲突。在多用户对数据库某一行进行update的时候按照随机的方式去排队这要减少锁的个数,也就减少了CPU消耗,但是逻辑复杂。