行锁
mysql的行锁是在引擎层由各个引擎各自实现的,但是并不是所有的引擎都支持行锁,MylSAM就不支持行锁。
行锁就是针对表中行记录的锁。事务A和事务B需要对同一行数据更新,只有在事务A更新完之后事务B才可以接着操作。
两阶段锁
假设事务A中,需要更新数据1号 数据2号 数据3号的数据;
开启事务
- 获取数据1号的行数 开始更新数据1号 释放数据1号的行数
- 获取数据2号的行数 开始更新数据2号 释放数据2号的行数
- 获取数据3号的行数 开始更新数据3号 释放数据3号的行数
提交
然后同时来了一个事务B,需要更新数据1号的数据,但是事务A已经拥有了行数,并且事务A还未提交;所以事务B需要等待事务A所所有的更新操作更新完,并且提交之后才可以更新数据1号的数据;
但是如果我们将事务A的执行顺序换一下
- 获取数据3号的行数 开始更新数据3号 释放数据3号的行数
- 获取数据2号的行数 开始更新数据2号 释放数据2号的行数
- 获取数据1号的行数 开始更新数据1号 释放数据1号的行数
在事务B来的时候,事务A还未拥有数据1号的行锁,事务B进行操作,然后提交;事务B提交完之后,事务A才刚刚更新完数据3号,接着更新数据2号和数据1号;这样就省区了前面事务B的等待时间。
因为行锁是在需要的时候才开始加上的,并不是事务开启的时候就已经上锁了,所以将热点数据行的操作放在最靠近释放锁的阶段就可以尽量的较少等待时间,提高了并发度。
死锁
- 事务A启动之后,先拥有id=1的行锁进行操作;
- 事务B启动之后,先拥有id=2的行锁,进行操作
- 事务A想要id=2的行锁进行操作,但是事务B并没有提交所以需要等待锁的释放
- 事务B想要id=1的行锁进行操作,但是事务A也没有提交所以也需要等待事务A的提交从而释放id=1的行锁
- 事务A在等待事务B的提交,事务B在等待事务A的提交,从而陷入了无限循环等待中。
出现死锁以后,有两种策略:
1.一直等待,等待超时。这个超时时间可以用参数innodb_lock_wait_timeout设置,默认是50s
2.另一种策略是死锁检测,发起死锁之后,主动回滚死锁链条中的一个事务,让其他事务能够执行。可以通过数innodb_deadlock_detect查看设置开启和关闭
如果采用第一种策略,一直等待的话,就是需要等待50s,但是这个时间太长了不能接受;但是如果设置为2s,又太短,如果是发生的是正常的锁等待并不是死锁,就会导致误操作回滚数据,也不可行。
正常情况还是采用第二种策略;但是死锁检测也是有代价的。如果有1000个并发线程需要对同一个数据进行操作吗,那每一次操作之前都去做死锁检测,就会耗费大量的CPU资源,但是真正执行的事务确很少,大量的时间都被花在了死锁检测上。
- 控制客户端的并发度。如果同一时间只允许10个线程可以并发操作数据,那就会大量减少压力。但是如果有200个客户端,一个客户端只允许有5个线程并发去操作数据库实例,那也有1000个并发线程。
- 所以并发控制线程数需要做在数据库服务端里,这样才能真正减少同一时间操作数据库的并发线程数。可以考虑中间件,比如RocketMQ,将超过一定数量的线程请求操作放在mq中,延时去发送。也可以考虑修改MySql的源码,也就是在对相同行数据的操作在进入引擎之前需要排队(这个确实不懂,这个操作请求放在哪里?如果更新的数据行被删了咋办?)
- 还可以考虑从设计上优化,就是如果对一个账户余额进行操作,可以将这个账户余额的钱分为10份,每次加减余额的操作可以随机取一条数据进行操作;这样发生所冲突的概率是降低了,但是需要的锁就增加了,同样的死锁检测的次数也就多了;