菜鸟笔记
提升您的技术认知

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

1.行锁的定义

MySQL的行锁是在引擎层由各个引擎自己实现的。但是并不是所有引擎都支持行锁。比如MyISAM引擎就不支持行锁,不支持行锁意味着并发控制只能用表锁,也就是同一张表在任何时刻只能有一个更新在执行。而InnoDB是支持行锁的,这是InnoDB取代MyISAM的重要原因。

行锁,顾名思义就是针对数据表中行记录的锁。比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等待事务A的操作完成后才进行更新

2. 两阶段锁

下面给个例子,会出现什么现象?假设字段id是表t的主键。

答案是,事务B的的update语句会被阻塞,直到事务A执行commit之后,事务B才能继续执行。原因是,事务A持有两个记录的行锁,都是在commit的时候才释放

由此我们就引出来了两阶段锁的定义:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放

根据两阶段锁的过程,对我们的事务处理就有个启发:如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放

下面举个例子

假设你负责实现一个电影票在线交易业务,顾客A要在电影院B购买电影票。简化下流程,会涉及到下面几个操作:
1.从顾客A的账户余额中扣除电影票价;
2.从影院B的账户余额中增加这张电影票价;
3.记录一条交易日志

如果按照正常的1、2、3执行顺序,那当有另一个顾客C也要在B电影院购买电影票,那就会出现第二个操作的冲突,因为需要修改同一行数据。

根据两阶段锁的定义,如果按照3、1、2的执行顺序,那么影响账户余额这一行的锁时间就最少,最大程度地减少了事务之间地锁等待,提升并发度。

但是虽然这样设计,但是还是有可能会出现并发造成地死锁情况,

3. 死锁和死锁检测

当并发线程中都在等待别的线程释放资源时,进入循环等待的状态,称为死锁

例如下面这个例子:

上面的例子,会出现事务A在等待事务B释放id=2的行锁,事务B则在等待事务A释放id=1的行锁。

一般有两种解决策略:

  1. 直接进入等待,直到超时,超时时间可以通过参数innodb_lock_wait_timeout来设置。
  2. 发起死锁检测,发现死锁后,主动回滚死锁链条中的某个事务,让其它事务可以继续执行。将参数innodb_deacklock_detect开启即可

对于第一种策略,innodb_lock_wait_timeout默认值时50s,意味着等待超过50s才会退出,这肯定是不能接受的。但是这个时间又不能设置太短,否则可能线程只是在正常等待,就被退出了。

因此,在正常情况下采用的时第二种策略,且本身innodb_lockdead_detect的默认值就是on。但是这种策略是有负担的,因为每个新来的线程,都要检测是否时因为我的加入而导致死锁,需要O(n)的时间复杂度进行检查。

那么如何解决热点更新导致的性能问题

  1. 把死锁检测关掉,代码确保业务一定不会出现死锁,但是这是有风险的。
  2. 控制并发度,例如使得每一行的更新最多十个线程,这样死锁检测不会太耗费时间。
  3. 将一行改成逻辑上的多行来减少锁冲突。例如影院的总余额等于表中的10行相加,那么事务在更新的时候,可以对这十行中的任意一行更新即可,减少了锁冲突的概率。