为什么需要锁(并发控制)?
在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
典型的冲突有:
l 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
l 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
为了解决这些并发带来的问题。 我们需要引入并发控制机制。
1、悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系
统不会修改数据)。
2、乐观锁( Optimistic Locking )
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后update时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
并发控制机制
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。
乐观锁应用
1. 使用自增长的整数表示数据版本号。更新时检查版本号是否一致,比如数据库中数据版本为6,更新提交时version=6+1,使用该version值(=7)与数据库version+1(=7)作比较,如果相等,则可以更新,如果不等则有可能其他程序已更新该记录,所以返回错误。
2. 使用时间戳来实现.
注:对于以上两种方式,Hibernate自带实现方式:在使用乐观锁的字段前加annotation: @Version, Hibernate在更新时自动校验该字段。
悲观锁应用
需要使用数据库的锁机制,比如SQL SERVER 的TABLOCKX(排它表锁) 此选项被选中时,SQL Server 将在整个表上置排它锁直至该命令或事务结束。这将防止其他进程读取或修改表中的数据。
SqlServer中使用
Begin Tran
select top 1 @TrainNo=T_NO
from Train_ticket with (UPDLOCK) where S_Flag=0
update Train_ticket
set T_Name=user,
T_Time=getdate(),
S_Flag=1
where T_NO=@TrainNo
commit
我们在查询的时候使用了with (UPDLOCK)选项,在查询记录的时候我们就对记录加上了更新锁,表示我们即将对此记录进行更新. 注意更新锁和共享锁是不冲突的,也就是其他用户还可以查询此表的内容,但是和更新锁和排它锁是冲突的.所以其他的更新用户就会阻塞.
结论
在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.
乐观锁
使用举例:以MySQL InnoDB为例
还是拿之前的实例来举:商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。
下单操作包括3步骤:
1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
那么为了使用乐观锁,我们首先修改t_goods表,增加一个version字段,数据默认version值为1。
t_goods表初始数据如下:
- mysql> select * from t_goods;
- +----+--------+------+---------+
- | id | status | name | version |
- +----+--------+------+---------+
- | 1 | 1 | 道具 | 1 |
- | 2 | 2 | 装备 | 2 |
- +----+--------+------+---------+
- 2 rows in set
- mysql>
对于乐观锁的实现,我使用MyBatis来进行实践,具体如下:
Goods实体类:
- /**
- * ClassName: Goods <br/>
- * Function: 商品实体. <br/>
- * date: 2013-5-8 上午09:16:19 <br/>
- * @author chenzhou1025@126.com
- */
- public class Goods implements Serializable {
- /**
- * serialVersionUID:序列化ID.
- */
- private static final long serialVersionUID = 6803791908148880587L;
- /**
- * id:主键id.
- */
- private int id;
- /**
- * status:商品状态:1未下单、2已下单.
- */
- private int status;
- /**
- * name:商品名称.
- */
- private String name;
- /**
- * version:商品数据版本号.
- */
- private int version;
- @Override
- public String toString(){
- return "good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;
- }
- //setter and getter
- }
GoodsDao
- /**
- * updateGoodsUseCAS:使用CAS(Compare and set)更新商品信息. <br/>
- *
- * @author chenzhou1025@126.com
- * @param goods 商品对象
- * @return 影响的行数
- */
- int updateGoodsUseCAS(Goods goods);
mapper.xml
- <update id="updateGoodsUseCAS" parameterType="Goods">
- <![CDATA[
- update t_goods
- set status=#{status},name=#{name},version=version+1
- where id=#{id} and version=#{version}
- ]]>
- </update>
GoodsDaoTest测试类
- @Test
- public void goodsDaoTest(){
- int goodsId = 1;
- //根据相同的id查询出商品信息,赋给2个对象
- Goods goods1 = this.goodsDao.getGoodsById(goodsId);
- Goods goods2 = this.goodsDao.getGoodsById(goodsId);
- //打印当前商品信息
- System.out.println(goods1);
- System.out.println(goods2);
- //更新商品信息1
- goods1.setStatus(2);//修改status为2
- int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);
- System.out.println("修改商品信息1"+(updateResult1==1?"成功":"失败"));
- //更新商品信息2
- goods1.setStatus(2);//修改status为2
- int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods1);
- System.out.println("修改商品信息2"+(updateResult2==1?"成功":"失败"));
- }
输出结果:
- good id:1,goods status:1,goods name:道具,goods version:1
- good id:1,goods status:1,goods name:道具,goods version:1
- 修改商品信息1成功
- 修改商品信息2失败
说明:
在GoodsDaoTest测试方法中,我们同时查出同一个版本的数据,赋给不同的goods对象,然后先修改good1对象然后执行更新操作,执行成功。然后我们修改goods2,执行更新操作时提示操作失败。此时t_goods表中数据如下:
- mysql> select * from t_goods;
- +----+--------+------+---------+
- | id | status | name | version |
- +----+--------+------+---------+
- | 1 | 2 | 道具 | 2 |
- | 2 | 2 | 装备 | 2 |
- +----+--------+------+---------+
- 2 rows in set
- mysql>
我们可以看到 id为1的数据version已经在第一次更新时修改为2了。所以我们更新good2时update where条件已经不匹配了,所以更新不会成功,具体sql如下:
- update t_goods
- set status=2,version=version+1
- where id=#{id} and version=#{version};
这样我们就实现了乐观锁
乐观锁 时间戳 我们一定会碰到这样的情况:银行A与银行B几乎同时打开你的账户并看到你的账户上原有1000元存款,然后两家银行都想在你的账户上加上500元存款。那么,银行A便将1000元改成1500元,同时,银行B也将1000元改成了1500元。这样就糟糕了!最后,你的银行账户上最后只有1500元而不是理应的2000元,等于白白损失了500元!这就是在没有锁定数据的情况下修改造成的严重问题。然而,我们可以通过时间戳来巧妙解决这个问题。
我们来看思路:
在银行account表中建立时间戳字段timestamp,设定为文本类型varchar。
当银行A读取account表中的存款字段时,同时也读取时间戳字段,比如123456。
当银行A修改完存款数值后,进行存盘操作时,将先前读取的时间戳123456与当时表中的时间戳进行一次对比,如果一致,那么允许存盘,然后生成一个新的时间戳比如456789替换表中原有的时间戳123456。
这样做会带来什么好处呢。
我们再来看一开始的那个情况:银行A与银行B几乎同时打开你的账户并看到你的账户上原有1000元存款,与此同时两个银行业同时读取了时间戳123456,接下来就有区别了,当银行A把1000元改成1500元后,存盘,系统将对比先前的时间戳123456是否与存盘时表中的时间戳一致,显然,现在应该是一致的,那么允许存盘,并生成新的时间戳456789替换了旧的时间戳123456。接下去,B银行也将1000元修改成了1500元,存盘,系统对比先前的时间戳123456是否与存盘时表中的时间戳一致,发现先前的时间戳123456已经与现在的时间戳456789相异,系统拒绝存盘,要求刷新数据,那么数据刷新之后1000元已经因为之前A银行存入了500元而成为了1500元,那么B银行就会在1500元的基础上改为2000元,再次存盘,系统允许。这样,我们就避免了重复修改数据所带来的错误!
悲观锁:http://blog.itpub.net/22207394/viewspace-1736342/