🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
在[数据库(五),事务](http://www.cnblogs.com/dy2903/p/8438209.html)里面我们讲了事务ACID属性,事务最重要的能在异常情况的修复以及并发连接的处理上。 异常情况的修复主要通过**日志**来完成,那么并发连接的处理主要通过**锁**。本章主要整理的是**锁**的相关知识。 # 为什么需要锁? 现在Bob的账户里面有1000块钱,此时程序突然同时来了两个要求,一个要把Bob的钱转给Smith 20块,一个要把Bob的钱转Joe 30块。这两个要求一查Bob的账户,都发现现在Bob有1000块,所以要求A算出现在Bob应该有980块,要求B算出来Bob应有970。要求A的数据被要求B的数据覆盖了。 这样就出问题了,明明应该扣50块钱,现在却只是扣了30块。 **锁**就是用来解决这样的并发访问的问题。当每次访问Bob账户之前,都加一个锁,禁止别人再次访问,只有**等待持有锁的人来释放** ![image.png](http://upload-images.jianshu.io/upload_images/1323506-dadfa6419f8635b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 悲观锁和乐观锁 ## 悲观锁 如果事务A把Bob账户锁住了,事务B自然不能操作Bob账户,也就是说其他线程只能在外面等待。 这种加锁的方式就是**悲观锁**。它每次取读写数据时总认为数据会被别人修改,所以将数据加锁,置于锁定状态,不让别人访问。 缺点是如果持有锁的时间太长,其他用户需要等待很长的时间。 悲观锁主要适用于**并发争抢**比较严重的场景。 ## 乐观锁 悲观锁的问题显而易见,如果将数据加锁了以后,其他的线程是无法访问的,只能等待。如果持有锁的时间太长,需要等待大量的时间。 所以我们引入了**乐观锁**,所谓乐观锁是认为一般情况下不会有太多的人修改余额,所有没有加锁,只有在最后更新的时候才去看是否有冲突。 那**具体怎么做呢?** 可以在日志中加上一个version(版本)字段, - 每次**读**的时候,不仅需要读出余额,还需要读出版本号。 - 等修改了余额以后,往回写之前需要检查一下版本号,看看与读的时候版本号是否一样。 - 如果不一样,说明数据已经被改变了,所以需要放弃写操作,重新读取余额和版本号 - 如果一样,则将新余额写回去,把版本号加1 。 比如 事务1把Bob的余额减去30,此时它读到了(Bob余额=1000,版本=1) 事务2也需要将Bob的余额减去50,他也读到了(Bob余额=1000,版本=1) 然后事务1率先完成计算,把新的余额值970写回了,版本 加 1 ,变成了版本2。 事务2写回去的时候,发现最新的版本号变为2,表示之前读的数据已经改变,所以需要**重新读一遍** 这就是乐观锁,这种方式**适合于冲突不多的场景**,如果冲突很多,数据争用激烈,会导致不断的尝试,反而降低了性能。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-0ee87c4c302e4d9f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 死锁 ## 死锁产生的条件 如果出现如下这种情况 - 有两个**线程**同时参与 - 这两个线程在**不同方向**给同一个资源加锁 - 争抢相同的**资源** 那么很可能出现死锁 比如事务1是Bob给Smith转账,事务2是Smith给Bob转账。 当这两个事务单元同时发生的时候,就有问题呢。 事务单元1会先锁定Bob,然后锁定Smith,而事务单元2会先锁定Smith,然后锁定Bob 事务1会等待事务2把Bob给释放了,而事务2在等待事务1把Smith释放了。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-ce9291d2b340f666.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 如何解决 那么如何解决死锁呢?最好的方法是尽可能不出现死锁,当然很难。或者说如果锁定时间超时了,则强行释放,不过这种方法效率比较低,因为如果有用户的事务本来时间就很长,则每个死锁的检测时间将会很长。 所以最优的方案在于**预测死锁,**可以把**事务单元等待的锁记录下来** 比如下图中,事务单元1持有"Lock Bob"的锁,现在又在申请一把"Lock Smith"的锁,在申请之前,可以查看同样申请了"Lock Smith"的有哪些事务单元。明显事务单元2也申请过这把锁。好了,下一步是看事务单元2在申请什么锁呢,发现它居然在申请"Lock Bob"这把锁,而这把锁目前由事务单元1持有。所以现在已经发现有死锁的可能了,也就是发生了**碰撞**。所以可以提前补救。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-75256e3d01332df6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## U锁 下面来讨论一种死锁的情况。如下图 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-6b72969e647dc84a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 事务1 Trx1 > 开始事务1 读A(读锁) A - 100(读锁需要升级为写锁) 提交事务1 事务2 Trx2 > 开始事务2 读A(读锁) A - 100(读锁需要升级为写锁) 提交事务2(解锁) 事务1和事务2的读锁是可以并行的,所以读锁可以同时进入临界区,但是写锁不能,会被挡在外面。此时事务2又发起了写锁。那么尴尬的局面就产生了。 事务1的写锁需要等事务2的读锁释放资源。 事务2的写锁需要等待事务1的读锁释放资源。 所以形成了死锁。其实这种死锁的形成条件非常的简单,**只需要针对同一个数据进行读写**。比如说`update set A=A-1 where id = 100`如果运行多次,就会出现死锁 解决的办法是引入**U锁**,可以将读锁直接升级为写锁。 对于事务1,读以后马上就是写,所以直接就使用写锁,而不是读锁呢。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-f1aefbc2a654deca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 同理事务2也是如此。 ![image.png](http://upload-images.jianshu.io/upload_images/1323506-87ad69908d3a26d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![image.png](http://upload-images.jianshu.io/upload_images/1323506-944ae00be056cc65.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)