乐观锁和悲观锁

122 篇文章 0 订阅
乐观锁是在同一个 数据库 事务中我们常采取的策略,因为它能使得我们的系统保持高的性能的情况下,提高很好的并发访问控制。乐观锁,顾名思义就是保持一种乐观的态度,我们认为系统中的 事务并发更新不会很频繁,即使冲突了也没事,大不了重新再来一次。它的基本思想就是每次提交一个 事务更新时,我们想看看要修改的东西从上次读取以后有没有被其它事务修改过,如果修改过,那么更新就会失败。
因为 乐观锁其实并不会锁定任何记录,所以 数据库事务隔离级别设置为读取已提交或者更低的隔离界别,那么是不能避免 不可重复读问题的(因为此时读事务不会阻塞其它事务),所以采用乐观锁的时候,系统应该要容许不可重复读问题的出现。
一般可以采用以下三种方法:
版本(Version)字段:在我们的实体中增加一个 版本控制字段,每次 事务更新后就将版本字段的值加1.
时间戳(timestamps):采取这种策略后,当每次要提交更新的时候就会将系统当前时间和实体加载时的时间进行比较,如果不一致,那么就报告 乐观锁失败,从而 回滚事务或者重新尝试提交。采用 时间戳有一些不足,比如在集群环境下,每个 节点时间同步也许会成问题,并且如果并发 事务间隔时间小于当前平台最小的时钟单位,那么就会发生覆盖前一个事务结果的问题。因此一般采用版本字段比较好。
基于所有属性进行检测:采用这种策略的时候,需要比较每个字段在读取以后有没有被修改过,所以这种策略实现起来比较麻烦,要求对每个属性都进行比较,如果采用hiernate的话,因为Hibernate在 一级缓存中可以进行脏检测,那么可以判断哪些字段被修改过,从而动态的生成sql语句进行更新。
在JDBC和Hibernate中使用 乐观锁
JDBC中使用 乐观锁:如果我们采用JDBC来实现 持久层的话,那么就可以采用以上将的三种支持乐观锁的策略,在实体中增加一个version字段或者一个Date字段,也可以采用基于所有属性的策略,下面就采用version字段来做一演示:
假如系统中有一个Account的实体类,我们在Account中多加一个version字段,那么我们JDBC Sql语句将如下写:
Select a.version....from Account as a where (where condition..)
Update Account set version = version+1.....(another field) where version =?...(another contidition)
可以通过更新结果的行数来进行判断,如果更新结果的行数为0,那么说明实体从加载以来已经被其它 事务更改了,所以就抛出自定义的乐观锁定异常(或者也可以采用Spring 封装的异常体系)。具体实例如下:
1
2
3
4
5
6
.......
introwsUpdated=statement.executeUpdate(sql);
If(rowsUpdated== 0 ){
throwsnewOptimisticLockingFailureException();
}
........
在使用JDBC API的情况下,需要在每个update语句中,都要进行版本字段的更新以及判断,因此如果稍不小心就会出现版本字段没有更新的问题,相反当前的 ORM框架却为我们做好了一切,需要做的就是在每个实体中都增加version或者是Date字段。
Hibernate中使用 乐观锁:如果采用Hibernate做为 持久层的框架,那么实现乐观锁将变得非常容易,因为框架会帮我们生成相应的sql语句,不仅减少了开发人员的负担,而且不容易出错。下面同样采用version字段的方式来总结一下:
同样假如系统中有一个Account的实体类,我们在Account中多加一个version字段,
1
2
3
4
5
6
7
publicclassAccount{
Longid;
.......
@Version //也可以采用XML文件进行配置
Intversion
.......
}
提交 事务时,hibernate内部会生成相应的SQL语句将版本字段加1,并且进行相应的版本检测,如果检测到并发乐观锁定异常,那么就抛出StaleObjectStateExceptio n.

悲观锁

所谓悲观锁,顾名思义就是采用一种悲观的态度来对待 事务并发问题,系统中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样就需要采用真正意义上的锁来进行实现。悲观锁的基本思想就是每次一个 事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者 回滚解除锁。
最后还是需要明确一个问题,假如 数据库 事务隔离级别设置为读取已提交或者更低,那么通过 悲观锁,控制了 不可重复读的问题,但是不能避免幻影读的问题(因为要想避免我们就需要设置 数据库隔离级别为Serializable,而一般情况下会采取读取已提交或者更低隔离级别,并配合乐观或者悲观锁来实现 并发控制,所以幻影读问题是不能避免的,如果想避免幻影读问题,那么只能依靠数据库的serializable隔离级别(幸运的是幻影读问题一般情况下不严重)。
下面就分别以JDBC和Hibernate来总结一下:
JDBC中使用 悲观锁:在JDBC中使用悲观锁,需要使用select for update语句,假如我们系统中有一个Account的类,我们可以采用如下的方式来进行:
Select * from Account where ...(where condition).. for update.
当使用了for update语句后,每次在读取或者加载一条记录的时候,都会锁住被加载的记录,那么当其他 事务如果要更新或者是加载此条记录就会因为不能获得锁而阻塞,这样就避免了 不可重复读以及 脏读的问题,但是其他事务还是可以插入和删除记录,这样也许同一个事务中的两次读取会得到不同的 结果集,但是这不是 悲观锁所造成的问题,这是数据库 隔离级别所造成的问题。
最后还需要注意的一点就是每个冲突的 事务中,必须使用select for update 语句来进行 数据库的访问,如果一些事务没有使用select for update语句,那么就会很容易造成错误,这也是采用JDBC进行悲观控制的缺点。
Hibernate中使用 悲观锁:相比于JDBC使用悲观锁来说,在Hibernate中使用悲观锁将会容易很多,因为Hibernate有API让我们来调用,从而避免直接写SQL语句。下面就Hibernate使用 悲观锁做一总结:
首先先要明确一下Hibernate中支持 悲观锁的两种模式LockMode.UPGRADE以LockMode.UPGRADE_NO_WAIT.(PS:在JPA中,对应的锁模式是LockModeType.Read,这与Hibernate是不一样的呵呵)
假如系统中有一个Account的类,那么具体的操作可以像这样:
1
2
3
......
session.lock(account,LockMode.UPGRADE);
......
或者也可以采用如下方式来加载 对象
1
session.get(Account. class ,identity,LockMode.UPGRADE).
这样以来当加载 对象时,hibernate内部会生成相应的select for update语句来加载对象,从而锁定对应的记录,避免其它 事务并发更新。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值