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