小议“悲观锁和乐观锁”的原理、场景、示例

[1] 博由

前几天与一些朋友谈到这个问题,之前有一些概念的上的涉及,但是并没有相对深入的了解,因此找一些资料来帮助自己理解悲观锁和乐观锁的概念理解、场景、然后通过示例来阐述乐观锁和悲观锁的实现方式。

[2] 摘要

    本文将从三个方面来阐述悲观锁和乐观锁,以理论到实践的思维方式呈现出个人对悲观锁和乐观锁的理解。
    [1] 悲观锁和乐观锁的理论知识
    [2] 悲观锁和乐观锁的一般使用场景&优缺点
    [3] 简单实现乐观锁和悲观锁的

[3] 理论知识

[QA]什么是悲观锁和乐观锁?

术语描述常见案例
乐观锁每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据版本号控制,适用于多读少写的场景
悲观锁每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁DB的行锁、表锁等,适用于数据一致性比较高的场景

[?] 个人理解
假设每个操作的对象是一个或者多个资源,悲观锁:可以理解为很悲观的看待资源权限的访问,因此每次去操作资源时,总是会try操作,问一问是否可以去访问(这个问一问,就是去尝试获取锁),只有获取了锁之后,才会开始放心操作资源了;然后乐观锁却是相反的,很乐观的看待资源,不关心这个资源是否有锁,而是直接去访问资源,至多检查一下当前资源是不是最新的。
用伪代码解释一下:
[1] 悲观锁: 
while (!lock.tryGet) { // 一直等待获取锁权限
    // do something,会经历等待锁、获取锁、释放锁的过程,是比较占用资源的,但是确保了资源的并发访问可能出现的问题。
    lock.release
    break
}
[2] 乐观锁:
if (checkResourceVersion) { // 检查资源版本是否一致
    // do something
}else {
    // 过期, 更新数据无效
}

[4] 案例

在大概了解了,悲观锁和乐观锁的概念之后,我们看看在实际生产中,具体有那些案例和场景使用到了悲观锁和乐观锁,以及其对应的一般问题解决方案。

[4.1] 乐观锁案例

一般实现乐观锁的方式:
[1] 版本号控制
[2] 时间戳控制

[4.1.1] 乐观锁 - 版本号控制案例

一般会在数据库表增加一个version字段,这个字段标识当前数据的版本,每次更新操作都会version+=1;流程下图:

版本控制流程情况 
[图片引用] 
http://www.javaweb1024.com/java/JavaWebzhongji/2015/09/06/847.html

[1] 过程描述
1,start transaction  
2,first_version = get_cur_version() // 获取当前数据版本
3,update_data(version+=1)           // 更新操作版本号+1
4,cur_version = get_cur_version()   // 提交更新时,获取版本号
5,if first_version == cur_version // 比较提交时的版本号与第一次获取的版本号,如果一致,那么认为资源是最新的,可以更新
  then commit 
  else rollback or raise exception // 否则回滚或者抛出异常
[2] 原理描述
最关键的点,在于确保每次提交的信息是最新的,认为是没有竞争的或者说很少竞争的,通过version来标识每一次的数据更新操作,当存在并发时,同一个数据,会又多个用户进行更新操作,如果通过乐观锁来实现,在多写的情况下,会频繁出现异常或者回滚,因此一般使用在多读少写的情况,以提高系统吞吐量。

[4.1.2] 乐观锁 - 时间戳控制案例

时间戳的方式与版本号实际上原理差不多,每次更新数据时,会更新该时间戳字段,以标识数据的更新情况。
[过程描述]
1, start transaction
2, first_timestamp = get_cur_timestamp()
3, update_data(timestamp=get_sys_cur_timestamp)
4, if first_timestamp = get_cur_timestamp()
   then commit 
   else rollback or raise Exception
[原理]
每次更新数据时,时间戳会记录更新时间,如果出现并发更新,会导致A,B事务更新提交时读取的不是事务起初读取的时间戳,因而导致失败,同样适合于多读少写的场景。

[4.2] 悲观锁案例

    悲观锁的实现一般都是通过锁机制来实现的,锁可以简单理解为资源的访问的入口。如果要对一个具有锁属性的资源执行访问时,在更新操作时,需要持锁权才能进行操作,但是往往这种操作可以保证数据的一致性和完整性。

在数据库中,表锁、行锁都是通过悲观锁形式来实现的,通过模拟一下mysql的行锁形式,来阐述悲观锁的运行机制: 
行锁

事务A,事务B,当事务A对id=1的记录加了for update行锁之后,事务B如果想访问id=1的记录,会出现block,因为锁已经被事务A占有了,要么事务A操作完成,然后执行事务B block的操作,要不等待超时。

[5] 场景

我们知道了乐观锁和悲观锁的概念,已经一般的使用方式,那么我们还需要了解到的是:什么时候使用悲观锁,什么时候使用乐观锁?

[5.1] 什么时候使用悲观锁?

    一旦通过悲观锁锁定一个资源,那么其他需要操作该资源的使用方,只能等待直到锁被释放,好处在于可以减少并发,但是当并发量非常大的时候,由于锁消耗资源,并且可能锁定时间过长,容易导致系统性能下降,资源消耗严重。因此一般我们可以在并发量不是很大,并且出现并发情况导致的异常用户和系统都很难以接受的情况下,会选择悲观锁进行。

[5.2] 什么时候使用乐观锁?

    乐观锁实际上并没用实际的锁资源操作,就如上面概述的版本号和时间戳方式一样,使用方都可以操作相应的资源,而当第一个使用方提交之后,其他使用方提交时,会出现异常(例如:代码版本控制器SVN,GIT),其可以增加系统的并发处理能力,但是如果并发导致了资源提交冲突,其他使用方需要重新读取资源,会增加读的次数,但是可以面对高并发场景,前提是如果出现提交失败,用户是可以接受的。因此一般乐观锁只用在高并发、多读少写的场景。
    其中:GIT,SVN,CVS等代码版本控制管理器,就是一个乐观锁使用很好的场景,例如:A、B程序员,同时从SVN服务器上下载了code.html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器。这其实就是乐观锁的实现全过程。如果此时使用的是悲观锁,那么意味者所有程序员都必须一个一个等待操作提交完,才能访问文件,这是难以接受的。


参考

[1]http://www.javaweb1024.com/java/JavaWebzhongji/2015/09/06/847.html 
[2]http://blog.csdn.net/sd4015700/article/details/50162965

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值