在[数据库(五),事务](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)