07行锁功过:怎么减少行锁对性能的影响

分享:  

行锁

行锁,顾名思义就是对表的行进行加锁,是存储引擎层来设计实现的:

  • MyISAM没有行锁,对表执行更新时只能加表锁,并发度就比较低
  • InnoDB支持行锁,并发度就比MyISAM高,所以一般用InnoDB代替MyISAM

ps: 减少行锁的冲突,有助于进一步提高并发处理能力。

两阶段封锁协议

在一个事务中:

  • 加锁,是在需要的时候按需加锁;
  • 释放锁,是在事务提交的时候释放锁;

知道这个后,我们可以在编码时进行一点优化。

如果事务涉及到锁定多个行的情况,尽量将可能导致锁冲突的行操作往后放,这样减少了锁持有时间,从而降低锁冲突。

举个例子,现在有个顾客A从电影院B买电影票,需要执行:

  • 1:扣账户A余额的操作;

  • 2:需要给电影院B增加余额的操作;

  • 3:记录一条交易日志;

那这几个操作该如何排序呢?因为会有很多人买电影票,所以操作2的冲突概率是比较大的,所以将2排在最后,而操作3是在额外的表中追加记录,基本不存在行冲突,所以不如放在最前面,A可能除了买电影票还可能买其他,冲突概率次之。

所以排序为3、1、2比较合理。

死锁和死锁检测

调整上面的操作顺序,只能尽量减少锁冲突,提高并发度,但是不能完全保证避免死锁。

死锁原因

造成死锁的原因,就是多个事务中加锁顺序不一致,造成了循环依赖:比如事务t1已经持有了锁a,现在申请锁b,但是锁b呢已经被事务t2持有,事务t2还在申请锁a,但是a已经被t持有。这样事务t1、t2相互等待对方,都拿不到锁,就造成了死锁。

死锁检测

解决死锁问题,有这么几种方法,一种是死锁避免,一种是死锁检测。

  • 死锁避免,可以让获取锁的操作有一个最大超时时间,超过这个时间就返回获取锁失败,让事务退出,事务退出的时候释放掉已经持有的锁,这样就避免了死锁。

    mysql中可以通过设置变量innodb_lock_wait_timeout的值来设定这个超时时间,默认值是50s,这个时间还是很长的,一旦真的发生了死锁,对业务不可用时间也比较长,50s啊!

    如果把这个变量设为1s呢,也不行,可能会有很多的锁获取失败的情况,但是可能是正常获取锁操作,非死锁,会造成很多误伤,也不好!

  • 死锁检测,通过死锁检测算法来检测是否会出现死锁操作,比如获取一个锁之前,先检查这个锁被哪个线程持有,没有也就正常拿到锁了,如果被线程t2持有,继续检查这个线程t2有没有要申请的锁被当前线程持有,如果有,那么当前线程发起的加锁请求将会导致一个循环依赖,会发生死锁。

    这个时候,可以直接让当前事务失败,释放锁,或者干掉另一个事务t2让它释放锁,也就避免了死锁。

    死锁检测默认是开启的,innodb_deadlock_detect,通过这个变量来设置。

相关开销

死锁避免虽然效果不怎么令人满意,一般还是会开启死锁检测的,但是死锁检测的过程前面也简单描述了,实际上这个死锁检测的过程会更复杂,假如有1000个线程,当前线程t1可能希望获得线程t2上的锁,t2可能希望获得t3上的锁,….,t1000可能希望获得当前线程t1的锁……就是要分析做很多分析才能判断出会不会导致死锁。

有的时候线程数多了之后,死锁检测开销也会比较高,表现就是CPU占用率很高,比如100%,但是每秒并没有执行几个事务。

热点记录

对于某些热点记录,更新频繁的记录,这样的锁冲突的情况会比较多,而且线程数也比较多的情况下,问题更明显,CPU占用很高,但是执行不了几个事务,尽管没有真的发生死锁。

对于热点记录如何解决呢?

  • 方法一:将对一条记录的操作拆分成对多个记录,每次更新时随机选一条,降低锁冲突的概率,比如改为随机更新10条记录中的一条,冲突概率就下降为原来的1/10;
  • 方法二:改成用写增量流水日志的方式,定期地取合并日志中的操作更新到原来的那一条记录;

这里的思想,很分布式缓存热key的处理方式也是类似的,要么就是通过写多个key来解决,要么就是记录流水异步更新来解决。