一文看懂MySQL的行锁

时间:2022-12-04 15:56:36

MySQL的全局锁和表锁可以看这篇文章:MySQL的全局锁和表锁
进入正文

行锁

行锁是由各个存储引擎自己实现的,并不是所有的引擎都支持行锁。

MyISAM引擎就不支持行锁,同一时刻一张表只能有一个更新在执行。

现在说InnoDB的行锁,行锁,顾名思义,就是事务A更新一行,同时事务B也要更新一行,那么事务B只好等事务A更新完成再去更新。

两阶段锁

在InnoDB中,行锁是在需要的时候才加上的,并不是事务开始就加上了,当然了,它也不是不需要了就释放锁,锁是在事务提交之后才进行释放的。这就是两阶段锁协议。

知道了这个设定,如果事务中需要锁多个行,可以把最可能造成冲突,最可能造成并发的尽量放在后面。

使用行锁可能产生死锁问题:

死锁产生的条件:互斥条件,请求和保持条件,不可抢占条件,不可剥夺条件。

一文看懂MySQL的行锁

事务A正在等待事务B释放id=2行的锁,事务B正在等待事务A释放id=1行的锁。可见,形成了死锁。

当出现死锁的额时候有两种解决方案:

  • 一种策略是,直接进入等待,知道超时,这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
  • 第二个策略是,可以发起死锁检测,当死锁发生时,主动回滚死锁发生时的某一条事务,让其他食物得以回滚。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

在InnoDB中,innodb_lock_wait_timeout参数的默认超时时间是50s,(出现等待有可能不是死锁,可能是简单的等待锁资源)意思是当出现死锁后,被锁住的线程要超过50s才能退出。然后其他任务才能执行,这对于在线业务来说,肯定是不能接受的。

把参数调小,比如说是1s,那么可能会把没有发生死锁,而是简单等待锁资源的线程给释放了。

所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。

比如一个事务被锁了, 就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。

每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。

问题怎么解决由这种热点行更新导致的性能问题呢?

1. 尽量想办法不产生死锁。 出现死锁了就回滚,然后业务重试应该就没有问题了,这是业务无损的。

如果死锁检测关闭了,发生死锁等待之后超时,这是业务有损的。

2. 另一个思路是控制并发度。根据上面的分析,你会发现如果并发能够控制住,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。一个直接的想法就是,在客户端做并发控制。但是,你会很快发现这个方法不太可行,因为客户端很多。我见过一个应用,有 600 个客户端,这样即使每个客户端控制到只有 5 个并发线程,汇总到数据库服务端以后,峰值并发数也可能要达到 3000。因此,这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。可你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。

3. 还是以影院账户为例,可以考虑放在多条记录上,比如 10 个记录,影院的账户总额等于这 10 个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10,可以减少锁等待个数,也就减少了死锁检测的 CPU 消耗。