zoukankan      html  css  js  c++  java
  • 数据库乐观锁和悲观锁的理解和实现(转载&总结)

    数据的锁定分为两种,第一种叫作悲观锁,第二种叫作乐观锁。

    1、悲观锁,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。【数据锁定:数据将暂时不会得到修改】

    2、乐观锁,认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息。让用户决定如何去做。


    理解:

    1. 乐观锁是一种思想,具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝。

    之所以叫乐观,因为这个模式没有从数据库加锁。

    2. 悲观锁是读取的时候为后面的更新加锁,之后再来的读操作都会等待。这种是数据库锁

    乐观锁优点程序实现,不会存在死锁等问题。他的适用场景也相对乐观。阻止不了除了程序之外的数据库操作。

    悲观锁是数据库实现,他阻止一切数据库操作。

    再来说更新数据丢失,所有的读锁都是为了保持数据一致性。乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。具体业务具体分析


    实现:

    一、悲观锁
        1、排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。

        2、实现:大多数情况下依靠数据库的锁机制实现

         一般使用 select ...for update 对所选择的数据进行加锁处理,例如select * from account where name=”Max” for update, 这条sql 语句锁定了account 表中所有符合检索条件(name=”Max”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

    二、乐观锁
        1、如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。

        2、实现:大多数基于数据版本(Version)记录机制实现

         具体可通过给表加一个版本号或时间戳字段实现,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。

    三、ORM框架中悲观锁乐观锁的应用

         一般悲观锁、乐观锁都需要都通过sql语句的设定、数据的设计结合代码来实现,例如乐观锁中的版本号字段,单纯面向数据库操作,是需要自己来实现乐观锁的,简言之,也就是版本号或时间戳字段的维护是程序自己维护的,自增、判断大小确定是否更新都通过代码判断实现。数据库进提供了乐观、悲观两个思路进行并发控制。

         对于常用java 持久化框架,对于数据库的这一机制都有自己的实现,以Hibernate为例,总结一下ORM框架中悲观锁乐观锁的应用

    1、Hibernate的悲观锁:

         基于数据库的锁机制实现。如下查询语句:

    [html] view plain copy
    1. String hqlStr ="from TUser as user where user.name=Max";  
    2. Query query = session.createQuery(hqlStr);  
    3. query.setLockMode("user",LockMode.UPGRADE); //加锁  
    4. List userList = query.list();//执行查询,获取数据  
         观察运行期Hibernate生成的SQL语句:
    [html] view plain copy
    1. 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通过使用数据库的for update子句实现了悲观锁机制。对返回的所有user记录进行加锁。
    2、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的for update nowait子句实现加锁。
         注意,只有在查询开始之前(也就是Hiberate 生成SQL 之前)设定加锁,才会真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含for update子句的Select SQL加载进来,所谓数据库加锁也就无从谈起。

    3、Hibernate的乐观锁

         Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的生产力。Hibernate中可以通过class描述符的optimistic-lock属性结合version描述符指定。具体实现方式如下:
         现在,我们为之前示例中的TUser加上乐观锁机制。
    实现一、 配置optimistic-lock属性:

    [html] view plain copy
    1. <hibernate-mapping>  
    2.      <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version">  
    3.            ……  
    4.      </class>  
    5. </hibernate-mapping>  
    optimistic-lock属性有如下可选取值:
         Ø none:无乐观锁
         Ø version:通过版本机制实现乐观锁
         Ø dirty:通过检查发生变动过的属性实现乐观锁
         Ø all:通过检查所有属性实现乐观锁

         通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,目前唯一在数据对象脱离Session发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。
    实现二、添加一个Version属性描述符

    [html] view plain copy
    1. <hibernate-mapping>  
    2.      <class name="org.hibernate.sample.TUser" table="t_user"   dynamic-update="true" dynamic-insert="true" optimistic-lock="version">   
    3.     <id name="id" column="id" type="java.lang.Integer">  
    4.         <generator class="native"/>  
    5.     </id>  
    6.     <version column="version" name="version" type="java.lang.Integer"/>  
    7. ……  
    8.      </class>  
    9. </hibernate-mapping>  
         注意version 节点必须出现在ID 节点之后。这里声明了一个version属性,用于存放用户的版本信息,保存在TUser表的version字段中。

    测试:

         此时如果我们尝试编写一段代码,更新TUser表中记录数据,如:

    [html] view plain copy
    1. Criteria criteria = session.createCriteria(TUser.class);  
    2. criteria.add(Expression.eq("name","Max"));  
    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,对名为Max的用户进行操作,下面模拟并发更新时的情况:
    [html] view plain copy
    1. Session sessiongetSession();  
    2. Criteria criteria = session.createCriteria(TUser.class);  
    3. criteria.add(Expression.eq("name","Max"));  
    4. Session session2 = getSession();  
    5. Criteria criteria2 = session2.createCriteria(TUser.class);  
    6. criteria2.add(Expression.eq("name","Max"));  
    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();  
         执行并发更新的代码,在tx.commit()处抛出StaleObjectStateException异常,并指出版本检查失败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行相应处理。

         这就是hibernate实现悲观锁和乐观锁的主要方式。

    四、总结

         悲观锁相对比较谨慎,设想现实情况应该很容易就发生冲突,所以我还是独占数据资源吧。

         乐观锁就想得开而且非常聪明,应该是不会有什么冲突的,我对表使用一个时间戳或者版本号,每次读、更新操作都对这个字段进行比对,如果在我之前已经有人对数据进行更新了,那就让它更新,大不了我再读一次或者再更新一次。

         乐观锁的管理跟SVN管理代码版本的原理很像,如果在我提交代码之前用本地代码的版本号与服务器做对比,如果本地版本号小于服务器上最新版本号,则提交失败,产生冲突代码,让用户决定选择哪个版本继续使用。
         在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法        另外,Mysql在处理并发访问数据上,还有添加
    读锁(共享锁)、写锁(排它锁),控制锁粒度【表锁(table lock)、行级锁(row lock)】等实现,有兴趣可以继续研究。

  • 相关阅读:
    PHP 开发 APP 接口 学习笔记与总结
    Java实现 LeetCode 43 字符串相乘
    Java实现 LeetCode 43 字符串相乘
    Java实现 LeetCode 43 字符串相乘
    Java实现 LeetCode 42 接雨水
    Java实现 LeetCode 42 接雨水
    Java实现 LeetCode 42 接雨水
    Java实现 LeetCode 41 缺失的第一个正数
    Java实现 LeetCode 41 缺失的第一个正数
    Java实现 LeetCode 41 缺失的第一个正数
  • 原文地址:https://www.cnblogs.com/duende99/p/10664003.html
Copyright © 2011-2022 走看看