当前位置: 首页 > 图灵资讯 > 技术篇> 悲观锁和乐观锁?

悲观锁和乐观锁?

来源:图灵教育
时间:2023-06-11 09:12:23

Java 实现悲观锁和乐观锁

锁(locking)在实现业务逻辑的过程中,往往需要保证数据访问的排他性。如金融系统的日终结算 在处理过程中,我们希望处理cut-off时间点的数据,而不是在结算过程中 (可能是几秒钟,也可能是几个小时),数据再次发生变化。在这个时候,我们需要确保这些数据在某个操作过程中不会被外界修改,这就是所谓的 “锁”,即锁定我们选定的目标数据,使其无法被其他程序修改。 Hibernate支持两种锁机制:“悲观锁”(Pessimistic Locking)” 和“乐观锁(Optimistic Locking)”。

一 :悲观锁(PessimisticLocking)悲观锁,就像它的名字一样,是指修改数据被外部世界(包括系统当前的其他事务,以及外部系统的事务处理)的保守态度。因此,在整个数据处理过程中,数据被锁定 状态。悲观锁的实现往往取决于数据库提供的锁机制(只有数据库层提供的锁机制) 真正保证数据访问的排他性,否则,即使加锁机制在系统中实现,外部系统也无法保证 不会修改数据)。 依赖数据库的典型悲观锁调用:select * from account where name=”Erica” for update 这条sql 语句锁定account 所有检索条件均符合表中的检索条件(name=”Erica)记录。 事务提交前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。 基于数据库的锁机制,实现了Hibernate的悲观锁。 以下代码实现了查询记录的加锁:

  String hqlStr    =       "   from TUser as user where user.name=’Erica’   "   ; 2   Query query    =    session.createQuery(hqlStr); 3   query.setLockMode(   "   user   "   ,LockMode.UPGRADE);    //   加锁   4     List userList    =    query.list();   //   执行查询,

获取数据query.setlockmode锁定查询句中特定别名对应的记录(我们是为了 TUser类指定了一个别名“user),这里也就是锁定返回的所有user记录。 观察Hibernate在运行期间生成的SQL语句:

select tuser0_____.id as id, tuser0_____.name as name, tuser0_____.group_id as group_id, tuser0_____.user_type as user_type, tuser0___.sex as sex from t_user tuser0___ where (tuser0____.name   =   ’Erica’ )    for    update————————————————

悲观锁机制。 Hibernate的加锁模式有: LockMode.NONE : 无锁机制。? LockMode.WRITE :Hibernate在Insert和Update记录时自动记录 获取。? LockMode.READ : Hibernate在阅读记录时会自动获取。以上三种锁机制一般由Hibernate内部使用,如Hibernate,以确保Update 对象在实现save方法时,不会被外界修改,会自动为目标对象添加WRITE锁。? LockMode.UPGRADE :使用数据库的for 加锁update子句。? LockMode. UPGRADE_NOWAIT :Oracle的特定实现,利用Oracle的forre update nowait子句实现加锁。上述两种锁机制在应用层中比较常用,通常采用以下方法加锁:Criteria.setLockModeQuery.setLockModeSession.lock注意,只有在查询开始前(即Hiberatete) 生成SQL 之前)设置加锁,然后再设置加锁 真正通过数据库的锁机制加锁,否则,不包括for的数据已经通过 update 子句的Select SQL加载,所谓数据库加锁是不可能的。

二 :乐观锁(Optimistic Locking)与悲观锁相比,乐观锁机制采用了更宽松的锁机制。大多数情况下,悲观锁是基于 实现数据库的锁定机制,以确保操作的最大独占性。但随之而来的是数据库 大量的性能费用,尤其是长期事务,往往是无法承受的。 例如,在金融系统中,当操作员读取用户数据并在读取用户数据的基础上进入时 如果采用悲观锁定机制进行修改(如更改用户账户余额),则意味着整个操作已经完成 整个过程(从操作员读取数据,开始修改,直到提交修改结果,甚至包括操作 员工中途去煮咖啡的时间),数据库记录总是处于加锁状态,可想而知,如果面对几 数百人并发,这种情况会导致什么后果。 乐观锁定机制在一定程度上解决了这个问题。 乐观锁机制在一定程度上解决了这个问题。乐观锁 大部分是基于数据版本的 (Version)实现记录机制。什么是数据版?在基于数据的基础上添加一个版本标识 在数据库表的版本解决方案中,通常会添加一个“数据库表”version”字段来 实现。 读取数据时,将此版本号一起读取,然后更新时,将此版本号添加到一个。此时,将提 如果提交的数据与数据库表对应记录的当前版本信息进行比较,则提交的数据 如果版本号大于数据库表当前版本号,则更新,否则将被视为过期数据。 假设以上修改用户账户信息的例子 :在数据库中的帐户信息表中 version字段,目前值为1;而当前账户余额字段(balance)为$100。1 :操作员A 这个时候读出来(version=1)并从其账户余额中扣除$50 ($100-$50)。2:在操作员A操作过程中,操作员B也读取此用户信息(version=1),并 从其账户余额中扣除$20($100-$20)。3: 操作员A完成了修改工作,并添加了数据版本号(version=2),与账户一起扣除 除后余额(balance=$50),提交到数据库更新,此时由于提交的数据版本很大 在当前版本的数据库记录中,数据更新,数据库记录version更新为2。4: 操作员B完成了操作,并添加了版本号(version=2)试图向数据库提交数量 据(balance=$80),但在比较数据库记录版本时,发现操作员B提交了 数据版本号为2,当前版本的数据库记录也为2,不符合“提交版本必须大于记录” 只有记录当前版本,才能实施更新“乐观锁策略”。因此,操作员B 拒绝提交。 这样就避免了操作员B 基于version=1 修改旧数据的结果涵盖了操作 员A操作结果的可能性。从以上例子可以看出,乐观锁机制避免了长期事务中的数据库锁定费用(操作员A 在操作员B的操作过程中,没有锁定数据库数据),大大提高了大并发量下的系统 整体性能表现。 需要注意的是,乐观锁定机制往往是基于系统中的数据存储逻辑,因此也有一定的局 例如,在最后一个例子中,由于乐观的锁定机制是在我们的系统中实现的,来自外部系统的用户 余额更新操作不受我们系统的控制,可能会导致脏数据更新到数据库。在 在系统设计阶段,应充分考虑这些情况的可能性,并进行相应的调整(如 在数据库存储过程中实现乐观锁策略,基于此存储过程只对外开放数据更新途径 直径,而不是直接向公众披露数据库表)。Hibernate 乐观锁实现内置在其数据访问引擎中。如果不考虑外部系统的对数 利用Hibernate提供的透明乐观锁实现数据库的更新操作,将大大提升我们 生产力。 可通过class描述符的optimistic-lock属性结合versionate 描述符指定。现在,我们在之前的例子中为TUSER增加了乐观的锁定机制。1. 首先为TUser的class描述符添加optimistic-lock属性:

<   hibernate   -   mapping   >       <   class    name   =   "   org.hibernate.sample.TUser   "    table   =   "   t_user   "    dynamic   -   update   =   "   true   "    dynamic   -   insert   =   "   true   "    optimistic   -   lock   =   "   version   "       >    ……    </   class   >       </   hibernate   -   mapping   >    

optimistic-lock属性有以下可选值: none 无乐观锁? version 乐观锁通过版本机制实现 dirty 乐观锁是通过检查变化的属性来实现的 all 通过检查所有属性来实现乐观锁,其中通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁,同时也是如此 它是Hibernate中唯一一个在数据对象从Session修改后仍然有效的锁机 制。因此,一般情况下,我们都选择version作为Hibernate乐观锁的实现机制。2. 添加Version属性描述符代码内容

 <   hibernate   -   mapping   >      2     <   class      3   name   =   "   org.hibernate.sample.TUser   "      4   table   =   "   t_user   "      5   dynamic   -   update   =   "   true   "      6   dynamic   -   insert   =   "   true   "      7   optimistic   -   lock   =   "   version   "      8     >      9     <   id  10   name   =   "   id   "     11   column   =   "   id   "     12   type   =   "   java.lang.Integer   "     13     >     14     <   generator    class   =   "   native   "   >     15     </   generator   >     16     </   id   >     17     <   version  18   column   =   "   version   "     19   name   =   "   version   "     20   type   =   "   java.lang.Integer   "     21     />     22   ……  23     </   class   >     24     </   hibernate   -   mapping   >     

注意version ID中必须出现节点 节点之后。 在这里,我们声明了一个version属性,用于存储用户的版本信息,并将其保存在tuser表中 version字段中。 此时,如果我们试图编写代码,更新TUSER表中的记录数据,例如:代码内容

 Criteria criteria    =    session.createCriteria(TUser.   class   );  2   criteria.add(Expression.eq(   "   name   "   ,   "   Erica   "   ));  3   List userList    =    criteria.list();  4   TUser user    =   (TUser)userList.get(   0   );  5   Transaction tx    =    session.beginTransaction();  6   user.setUserType(   1   );    //   更新Usertype字段    7     tx.commit();  

每次更新TUSER,我们都会发现数据库中的version正在增加。 如果我们试试tx,.commit 之前,启动另一个Session,称为Erica 的用 家庭操作,模拟并发更新的情况:代码内容

Session session   =    getSession();   2   Criteria criteria    =    session.createCriteria(TUser.   class   );   3   criteria.add(Expression.eq(   "   name   "   ,   "   Erica   "   ));   4   Session session2    =    getSession();   5   Criteria criteria2    =    session2.createCriteria(TUser.   class   );   6   criteria2.add(Expression.eq(   "   name   "   ,   "   Erica   "   ));   7   List userList    =    criteria.list();   8   List userList2    =    criteria2.list();TUser user    =   (TUser)userList.get(   0   );   9   TUser user2    =   (TUser)userList2.get(   0   );  10   Transaction tx    =    session.beginTransaction();  11   Transaction tx2    =    session2.beginTransaction();  12   user2.setUserType(   99   );  13   tx2.commit();  14   user.setUserType(   1   );  15   tx.commit();  16   

代码将在tx执行上述代码.commit()将Staleobjectstateeexception抛出 通常,并指出版本检查失败,目前的事务正试图提交过期数据。通过捕捉这种异常,我 乐观锁校验失败时,可相应处理。