mysql的行锁是在引擎层由各个引擎自己实现的。但是并不是所有引擎都支持行锁。比如myisam引擎就不支持行锁,不支持行锁意味着并发控制只能用表锁,也就是同一张表在任何时刻只能有一个更新在执行。而innodb是支持行锁的,这是innodb取代myisam的重要原因。
行锁,顾名思义就是针对数据表中行记录的锁。比如事务a更新了一行,而这时候事务b也要更新同一行,则必须等待事务a的操作完成后才进行更新。
下面给个例子,会出现什么现象?假设字段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的执行顺序,那么影响账户余额这一行的锁时间就最少,最大程度地减少了事务之间地锁等待,提升并发度。
但是虽然这样设计,但是还是有可能会出现并发造成地死锁情况,
当并发线程中都在等待别的线程释放资源时,进入循环等待的状态,称为死锁。
例如下面这个例子:
上面的例子,会出现事务a在等待事务b释放id=2的行锁,事务b则在等待事务a释放id=1的行锁。
一般有两种解决策略:
- 直接进入等待,直到超时,超时时间可以通过参数innodb_lock_wait_timeout来设置。
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某个事务,让其它事务可以继续执行。将参数innodb_deacklock_detect开启即可
对于第一种策略,innodb_lock_wait_timeout默认值时50s,意味着等待超过50s才会退出,这肯定是不能接受的。但是这个时间又不能设置太短,否则可能线程只是在正常等待,就被退出了。
因此,在正常情况下采用的时第二种策略,且本身innodb_lockdead_detect的默认值就是on。但是这种策略是有负担的,因为每个新来的线程,都要检测是否时因为我的加入而导致死锁,需要o(n)的时间复杂度进行检查。
那么如何解决热点更新导致的性能问题?
- 把死锁检测关掉,代码确保业务一定不会出现死锁,但是这是有风险的。
- 控制并发度,例如使得每一行的更新最多十个线程,这样死锁检测不会太耗费时间。
- 将一行改成逻辑上的多行来减少锁冲突。例如影院的总余额等于表中的10行相加,那么事务在更新的时候,可以对这十行中的任意一行更新即可,减少了锁冲突的概率。