请支持正版:MySQL实战45讲
MySQL的行锁是在引擎层由各个引擎实现的,但并不是所有的引擎都支持行锁,比如MyISAM引擎不支持行锁。不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响并发
InnoDB是支持行锁的,这也是MyISAM被InnoDB替代的原因之一
所以,接下来咱们聊的就是MySQL的行锁,以及减少行锁对并发的影响
行锁:针对数据表种行记录的锁,也就是说事务A更新了一行,而这时候事务B也要更新同一行,则必须等到事务A的操作完成后才能进行更新
下面有一个例子,事务B的update语句执行时会是什么现象?假设字段id是表t的主键
事务A | 事务B |
---|---|
begin; update t set k = k + 1 where id = 1; update t set k = k + 1 where id = 2; |
|
begin; update t set k = k + 2 where id = 1; |
|
commit; |
这个问题的结论取决于事务A在执行完两条update语句后,持有哪些锁,以及在什么时候释放
事实上:事务B的update会被阻塞,直到事务A执行commit之后,事务B才能继续执行
也就是说事务A拥有两个记录的锁,都是在commit的时候释放的,那么结论就是:在InnoDB种,行锁是在需要的时候才加上的,但是并不是不需要的时候就立刻释放,而是要等到事务结束的时候才释放。这个就是两阶段锁
知道了这个有什么用呢,其实:如果你的事务需要锁住多个行,要把最可能造成冲突的行尽可能的放在最后
当并发系统种不同线程出现循环依赖资源,涉及的线程都在等待别的线程释放资源的时候,就会导致这几个线程都进入无限等待的状态,这个过程就称之为死锁
例如:
事务A | 事务B |
---|---|
begin; update t set k = k + 1 where id = 1; |
begin; |
update t set k = k + 1 where id = 2; | |
update t set k = k + 1 where id = 2; | |
update t set k = k + 1 where id = 1; |
这时候,事务A在等待事务B释放id = 2的行锁,而事务B在等待事务A释放id = 1的行锁,事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁的时候,有两种策略:
在Innodb中innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要经过50s才会超时退出,然后其他线程才有可能继续执行。对于大型系统,这个时间是完全无法接受的
但是我们又不能把时间设置一个成一个很小的值,比如1s,这样当遇到死锁的时候,确实是很快就解开,但如果不是死锁,而是普通的锁等待呢?所以,如果超时时间设置的太短,那么会出现很多误伤
所以,正常情况下,我们还是采用第二种策略,即:主动死锁检测,而且innodb_deadlock_detect的默认值就是on,主动死锁检测在发生死锁的时候,是能够快速的发现并进行处理的,但是也有一些额外负担
想象一下:当一个事务锁被锁的时候,就要看看它依赖的线程有没有被被人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁
那如果是我们所有事务都要更新同一行呢?
每个新来的被堵住的线程,都要判断会不会由于自己的加入是否导致了死锁,这是一个时间复杂度是O(n)的操作
假设有1000个线程并发更新同一行,那么死锁检测就是100万的量级。虽然最终的结果是没有死锁,但是这期间要消耗大量的cpu资源,因此你会看到cpu资源利用率很高,但是每秒却执行不了几个事务
根据上面的分析,我们接着讨论:怎么解决这种热点行更新导致的性能问题
基本思路是:对于相同的行的更新,在进入引擎之前进行排队,这样InnoDB就不会有大量的死锁检测
如何实现这个方案呢?
可以考虑通过把一行改成逻辑上的多行来避免锁冲突,比如说,要更新某账户余额,可以把余额放在多条记录上,比如10条记录,总额度等于这10个记录的值的总和。每次要加金额的时候,随机加其中一条即可,这样冲突概率就大大减少
但是这样做还有一些细节需要处理,比如说余额为0的时候