为什么要用锁?
解决并发中出现的问题,解决不可重复读(丢失更新)问题:
事务A在对数据进行操作时,事务B同样在对数据进行操作,在二者操作结束后得到的不是人们所希望的正确的结果。
如事务A读取并操作数据,此时读取了商品的库存为20件,取出其中5件,并更新数据。此时更新数据尚未提交,事务B对数据库进行访问,它读取到的库存也为20件,从中取出10件,并先事务A一步提交数据。此时事务A提交数据后,查看数据库发现库存剩余15件,但实际上我们取出了15件商品,事务B对数据库的更新丢失了。这就是丢失更新现象,也称不可重复读。
悲观锁
实现策略
通常依赖于数据库机制,整个过程中将数据锁定,其他任何用户不能读取或修改,适用于短事务的操作。
实际运用
从数据开读取数据时加入LockMode.UPGRADE锁
首先使用load加载数据:
注意:划线部分中的UPGRADE方法出现了中划线,说明这是一种过时的方法,这种情况一般我们都能在源码中找到代替的方法:
注释中给出了用来代替此方法的方法:PESSIMISTIC_WRITE
继续代码执行到断点处,我们查看执行日志
我们明明使用的是load进行懒加载,但Hibernate还是发送了sql语句向数据库中查找数据。这说明,悲观锁不支持懒加载。
此时我们模拟多线程并发的情景,直接运行testLoad2方法
只要当前锁没有解开,其他线程就无法对数据进行访问
小结:
- 使用悲观锁,访问数据库记录时,就给这条记录加锁,只有当前用户才能对数据进行操作,其他用户将无法访问被枷锁的记录;
- 数据提交,锁开启,记录属于可操作状态;
- 悲观锁不支持懒加载。
乐观锁
本质上不是锁,是一种冲突检测手段,其并发性好
使用策略
一般是在数据库表中加入一个version字段,读取数据时将版本号一同读出,之后更新数据时版本号加一。如果提交数据时版本号与数据表中的版本号不符,则认为数据是过期的,否则给予更新。
实际运用
定义实体类
在实体类中额外增加一个整型属性。
定义hbm.xml映射文件
- 在class标签中增加optimistic-lock关键字,将version字段作为乐观锁字段;
- 使用version标签增加一个字段,映射整型的属性,映射成版本字段,Hibernate将这个版本字段作为乐观锁的控制机制。
创建表:
Hibernate: create table t_inventory (itemNo integer not null auto_increment, version integer not null, itemName varchar(255), quantity integer, primary key (itemNo)) engine=MyISAM
通过表的结构,我们看到版本字段version是不能为null的
表中的初始版本号,也就是当前版本号为1
使用懒加载load加载数据:
查看执行日志
Hibernate没有发送sql语句,将断点下移
当需要使用数据时,Hibernate才发送sql语句访问数据库,这说明乐观锁是支持懒加载的。
同样,我们模拟多线程并行情景,调用testLoad2:
执行并查看执行结果:
我们看到两个方法拿到的版本号都为0,此时,testLoad2方法已经对数据进行了更新,我们将testLoad1方法断点下移,执行commit:
一执行commit立刻出现了OptimisticLockException异常
数据库中版本号增加一位。
小结:
- 乐观锁允许多个线程同时运行;
- 只要其中一个用户一旦执行commit,通过版本号成功更新后,版本号自增1位;
- 如果其他用户更新时依旧使用旧版本号更新,这时抛出异常,其他用户就成了过期用户;
乐观锁在时间戳中应用
- 实体类中添加时间属性;
- 将时间作为乐观锁的控制字段;
- 设定的时间作为初始时间;
- 更新数据时,将初始时间改为当前时间;
- 其他用户还是用初始时间更新数据时,就会出现错误,其他用户就成了过期用户。
乐观锁和悲观锁的区别:
使用悲观锁时,线程必须串行,其他进程处于等待状态;
使用乐观锁时,线程可以并发运行,但程序可能直接产生异常;
为了保证程序的健壮性,一般情况下使用悲观锁,不使用乐观锁。
说明:本文仅用作学习笔记,无其他用途,如有冒犯可联系本人删除