行锁、优化行锁性能
1.行锁
行锁相较于表锁,行锁的的粒度更细,并发度更高,但也意味着更大的锁开销。对于没有行锁的引擎,例如MyISAM来说,同一时间对一个表的更新只能有一个执行。显而易见,处于对业务高并发度的要求,行锁的出现是必要的。
顾名思义,行锁就是针对表中行记录的锁。在一个事务A中,执行对某一行的更新语句,此时这行就被加上了行锁,另一个事务如果也想更新同一行,就只能等在事务A提交之后才能执行。
图1-2 不同事务同时更新同一行被阻塞
2.两段锁协议
知道了行锁的基本概念和作用,那么行锁是被什么时候上锁,又是什么时候被释放的呢?通过上面的例子,我们大致可以知道,在事务A提交后,和事务A更新同一行的的语句就能解除阻塞被执行。即锁在事务提交后才释放。
两段锁协议即在语句需要加锁时进行加锁,等到事务结束后再统一释放。
3.基于两段锁协议优化并发度
两段锁协议在语句需要加锁时进行加锁,等到事务结束后再统一释放。这就能是我们通过合理安排一个事务中的语句执行次序来提高并发读的一个条件。
假设两个人同时点一家外卖。我们要实现这个业务就是扣除这两个人的余额,增加商家的余额。即:
#对于用户1
update sale_account set money = newmoney where sale_id = 1;#获得商家数据的锁
update user_account set money = newmoney where user_id = 1;
#对于用户2
update sale_account set money = newmoney where sale_id = 1;#阻塞
update user_account set money = newmoney where user_id = 2;#阻塞
我们发现两个事务都要对同一个表中,同一条商家余额记录进行更改。如果对商家表的修改放在事务的第一句,那么在某一个事务执行时,首先获得了商家表中商家的余额数据的锁,那么另一个事务也会直接阻塞在第一句。这就导致由于两段锁协议而使得并发度变差。
但是我们可以通过调整这个业务的语句执行顺序来提高并发度。先执行对用户表余额的修改,再去执行对商家余额的修改。让影响并发度的锁尽量往后放。这就能提升整个业务的并发度。
#对于用户1
update user_account set money = newmoney where user_id = 1;#正常执行
update sale_account set money = newmoney where sale_id = 1;#获得商家数据的锁
#对于用户2
update user_account set money = newmoney where user_id = 2;#正常执行
update sale_account set money = newmoney where sale_id = 1;
这样最大程度的减少了锁等待的时间,提高了业务的并发度。如果一个事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
4.数据库的死锁控制
刚刚谈到了数据库的行锁以及加锁时的两段锁协议和机遇两段锁协议的语句优化。但这仍然无法避免死锁现象。那么MySQL是如何控制和检测死锁的呢?
图4-1 MySQL发现死锁并回滚整个事务
上图所展示的是MySQL处理死锁的一种方式:主动死锁检测,发现死锁后,主动回滚其中某一个事务。这也是我们通常情况下所采取的策略。它是由innodb_deadlock_detect
控制的,默认为开启状态。主动检测在高并发同时更新同一行时,是有额外负担的。它是通过检测发生锁等待的事务所依赖的线程有没有被别的线程锁住,构建一个死锁依赖的图,最后判断是否出现了循环等待。可想而知,当高并发同时更新同一行时,他的开销也是极其大的。
MySQL处理死锁的另一种方式:发生等待后,就持续等待,直至超时,然后回滚这个事务。通过innodb_lock_wait_timeout
来控制超时时长,默认值是50s。
图4-2 innodb_lock_wait_timeout默认值
如果采用这个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但我们又不能把它设的太小,否则会影响大多数事务的正常执行,误伤正常事务。所以MySQL通常还是使用主动死锁检测。
5.并发极高下的死锁控制
上面我们说过在极大并发同时更新同一行时,主动检测死锁是十分损耗性能的。甚至于我们MySQL数据库的CPU资源都被死锁检测所耗费了,影响其他业务的执行。那么我们应该如何避免这个问题呢?
首先,如果我们能够保证业务不会出现死锁,我们就可以暂时关闭数据库的死锁检测。但我们大多数在业务设计时是考虑不到死锁的。
其次我们可以通过减少整个系统的并发度来避免死锁检测所带来的高额资源消耗。如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。通过在客户端进行限流,但是由于我们的客户端并非单个,多个客户端汇总到数据库服务端以后,峰值并发数也可能很高。所以我们要将限流设计在数据库的服务端。修改MySQL源码或者利用中间件。总而言之,对于向同行的更新,先将其阻塞在数据库存储引擎外,避免大量死锁检测工作。
此外我们还可以在设计上避免,例如在保证数据正确的基础上把数据库中一行数据分成多行,例如将一行余额分成多行存储,余额总和等于每个记录的值的总和,能减少概率冲突。但这会导致其他有关的业务在设计上有额外的难度。
这篇博客,我们了解了MySQL的行锁、两段锁协议、死锁和死锁避免。涉及到了事务,事务回滚的概念,同时MySQL还有其他类型的行锁,例如next-key锁,间隙锁,自增锁等这都和事务和事务的隔离机制离不开关系,下篇博客就谈谈事务的有关内容吧。