从悲观锁和乐观锁的角度怎么避免死锁

为什么共享锁会发生死锁的情况?

当我们使用共享锁的时候会出现死锁的风险,比如

首先客户端 1 开启事务,然后采用读锁的方式对 user_id=912178 的数据行进行查询,这时事务没有提交的时候,这两行数据行上了读锁。

当我们执行的时候客户端 2 会一直等待,因为客户端 1 也获取了该数据的读锁,不需要客户端 2 对该数据进行修改。这时客户端 2 会提示等待超时,重新执行事务。


你能看到当有多个事务对同一数据获得读锁的时候,可能会出现死锁的情况。

从程序员的角度对进行划分

如果从程序员的视角来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式。

乐观锁(Optimistic Locking)认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。

乐观锁的版本号机制

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行 UPDATE ... SET version=version+1 WHERE version=version 。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

这种方式类似我们熟悉的 SVN、CVS 版本管理系统,当我们修改了代码进行提交时,首先会检查当前版本号与服务器上的版本号是否一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。

乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。

你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

悲观锁(Pessimistic Locking)也是一种思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。


从这两种锁的设计思想中,你能看出乐观锁和悲观锁的适用场景:

  1. 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。

  2. 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写和写 - 写的冲突。

总结

在客户端 1 获取某数据行共享锁的同时,另一个客户端 2 也获取了该数据行的共享锁,这时任何一个客户端都没法对这个数据进行更新,因为共享锁会阻止其他事务对数据的更新,当某个客户端想要对锁定的数据进行更新的时候,就出现了死锁的情况。当死锁发生的时候,就需要一个事务进行回滚,另一个事务获取锁完成事务,然后将锁释放掉,很像交通堵塞时候的解决方案。


避免死锁

1、以固定的顺序访问表和行。比如两个更新数据的事务,事务A 更新数据的顺序 为1,2;事务B更新数据的顺序为2,1。这样更可能会造成死锁。
2、大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
3、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
4、降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
5、为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。

6、业务上尽量将加锁的操作后移,降低锁时间。

7、调整innodb_lock_wait_timeout参数。默认是50,即等待50秒还未获取锁,当前语句报错,一般设置20-30

8、主动死锁检测,innodb_deadlock_detect,5.7后默认开启,但在高并发业务下会影响应能,建议关闭,调整innodb_lock_wait_timeout

当然在数据库中,也有一些情况是不会发生死锁的,比如采用乐观锁的方式。另外在 MySQL MyISAM 存储引擎中也不会出现死锁,这是因为 MyISAM 总是一次性获得全部的锁,这样的话要么全部满足可以执行,要么就需要全部等待。

 

posted @ 2019-06-14 20:15  温柔的风  阅读(805)  评论(0编辑  收藏  举报