看了掘金小册以后,自认为对mysql的锁有所认知,但是反而看的越多越困惑, 在mysql 的innodb 引擎下, 默认的隔离级别下, mysql已经通过读使用mvcc,写使用加锁 的方式解决了并发的四大数据库问题 脏写, 脏读, 不可重复读, 幻读. 所以看到这里的时候我就在想,既然mysql底层机制已经解决了并发问题,那么为什么还有好多人说自己通过版本号机制 来实现乐观锁避免并发问题呢?
这里的并发问题到底是什么问题? 如果是脏写这种问题,mysql不是已经解决了吗? 多此一举用版本号的意义是什么呢?
首选要理清为什么要用乐观锁,我们要看看悲观锁和乐观锁到底是什么
乐观锁
这其实是一种思想,当线程去拿数据的时候,认为别的线程不会修改数据,就不上锁,但是在更新数据的时候会去判断以下其他线程是否修改了数据。通过版本来判断,如果数据被修改了就拒绝更新,之所以叫乐观锁是因为并没有加锁。
悲观锁
当线程去拿数据的时候,总以为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞。
1.首先,乐观锁和悲观锁是一种理念,也就是说,它不是你认为的"锁", 具体来说,乐观和悲观锁是一种 "锁的实现机制".
2. 在mysql里,没有乐观锁,只有悲观锁,mysql里的比如说共享锁,独占锁等等, 都是具有真实的数据内存结构,它们是真正的"锁"
在举例子之前,我们要先了解,默认情况下, 对于select操作, mysql是不加锁的 ,对于写操作 update 是会加锁的, select 不加锁如何解决幻读 ,脏读这些问题 暂时不属于我们现在讨论范畴. insert delete 也不属于我们讨论的范畴.
举个栗子:
假如现在你接手一个需求, 为用户账户充值金额(充值10元), 比如你写了两个sql,并且你很有心的加上了手动事务
1. begin
2. select 该用户的 余额, 得到 100元
3. update 该用户的 余额 , 具体操作是 set money = 100 + 10 ,
4. commit , 成功更新余额为 110 元
很简单的需求,看似没什么困难,但是,如果有两个人恰好同时给这一个账户进行充值,会发生什么问题呢?
你可能觉得也没什么问题, 因为你觉得你机智的加上了事务, 你认为即使两个请求同事过来, 事务也会挨个排队执行, A事务执行完,B事务才会执行, 真的是这样吗?
显然不是, 由于mysql并没有使用串行化 这个隔离级别, 由于 读 并没有加锁 ,所以真实执行情况可能是下面这样的.
发生时间
|
Session A
|
Session B
|
1.
|
Begin |
|
2.
|
select money = 100 (查到此时账户余额是100) |
|
3. |
|
Begin
|
4. |
|
select money = 100 (查到此时账户余额是100) |
5. |
update money = 100 + 10 (更新金额为100+ 10) |
|
6. |
commit
|
|
7. |
|
update money = 100 + 10 (更新金额为100+ 10) |
8. |
|
commit
|
9. |
|
|
上面的例子仔细看, 并没有发生脏读,脏写,不可重复读,幻读 等任何问题,,但是明明充值了两次10元, 但是余额却还是 110 元,! 显然! 是不对的, (如果不明白脏读 ,幻读这些的定义,可以去百度一下, 这里不展开讲了.)
由此可见,默认情况下,对于某些特定的业务需求, 必须要进行额外的处理,不然显然会发生大问题.
所以, 对于这种情况下的解决方案, 网上有两种方案,使用 乐观锁或者悲观锁 ,(再次强调,乐观锁悲观锁是一种思想)
悲观锁,就是某个事物操作数据的时候害怕别的事务也操作,所以加上"锁" 给锁住,这样别的事务将会被阻塞,
在mysql中, 执行select的时候 如果 加上 for update ,那么这条语句即使是select ,也会加上独占锁(独占锁就是不管别的操作是什么,只要是需要获取锁的操作,必须等待我把自己的锁给释放了,别人才能执行)
发生时间
|
Session A
|
Session B
|
1.
|
Begin |
|
2.
|
select money = 100 for update (查到此时账户余额是100,加上了独占锁,B事务无法插入,必须等待A事务提交后把锁释放) |
|
3. |
update money = 100 + 10 (更新金额为100+ 10) |
|
4. |
Commit (这条记录的独占锁被释放了,此时B事务才可以操作)
|
|
5. |
|
Begin
|
6. |
|
select money = 110 for update (查到此时账户余额是110,由于B事务select的时候也加了for update ,那么 意味着它也需要获取这个记录的锁才能读,显然由于A事务先拿到了锁,所以B事务必须等A commit) |
7. |
|
update money = 110 + 10 (更新金额为110+ 10) 更新成功为 120 元 |
8. |
|
commit
|
9. |
|
|
乐观锁常见的机制是使用version,每个事务都会先查一下版本号,更新的时候时候同时再比对库里的版本号与刚才查的版本号是否一直,如果不一致,说明有其他事务更新了这个记录, 说明自己当前操作的数据不是最新的, 所以会尝试.
如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本,这种业务场景下,select 加 独占锁和使用version实现乐观锁 都是可选的.
发生时间
|
Session A
|
Session B
|
1.
|
Begin |
|
2.
|
select money = 100 ,version = 1 (查到此时账户余额是100,版本号是1)
|
|
3. |
|
Begin
|
4. |
|
select money = 100 ,version = 1 (查到此时账户余额是100,版本号是1)
|
5. |
update money = 100 + 10 (Update同时会比对当前库里的version是否等于 1,显然是等于的,所以更新金额为100+ 10) |
|
6. |
Commit (这个时候数据库的这个记录的version已经为2了)
|
|
7. |
|
update money = 100 + 10 , (update会比对version,由于此时数据库version=2,但是自己刚才查到的version是1 , 所以无法跟新 |
8. |
|
Rollback 这里由于更新失败,会再次尝试 |
9. |
|
Begin
|
10.
|
|
select money = 110 ,version = 2 (查到此时账户余额是110,版本号是2)
|
11. |
|
update money = 110 + 10 , (update会比对version,此时数据库version=2,自己刚才查到的version也是2 , 所以可以更新 |
12. |
|
Commit 充值成功了 |
使用乐观锁的场景大家常说是并发情况下,这里的并发出现的问题,是业务逻辑上的问题,并不是数据库本身在并发情况下发生的脏读, 不可重复读 等问题. 请一定要悉知.