数据库乐观锁和悲观锁的理解和实现

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

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

    String hqlStr ="from TUser as user where user.name=Max"; 

    Query query = session.createQuery(hqlStr); 

    query.setLockMode("user",LockMode.UPGRADE); //加锁 

    List userList = query.list();//执行查询,获取数据 

 

     观察运行期Hibernate生成的SQL语句:

[html] view plain copy

    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

    <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乐观锁实现机制。

实现二、添加一个Version属性描述符

[html] view plain copy

    <hibernate-mapping> 

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

        <id name="id" column="id" type="java.lang.Integer"> 

            <generator class="native"/> 

        </id> 

        <version column="version" name="version" type="java.lang.Integer"/> 

    …… 

         </class> 

    </hibernate-mapping> 

 

     注意version 节点必须出现在ID 节点之后。这里声明了一个version属性,用于存放用户的版本信息,保存在TUser表的version字段中。

 

测试:

 

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

[html] view plain copy

 

    Criteria criteria = session.createCriteria(TUser.class); 

    criteria.add(Expression.eq("name","Max")); 

    List userList = criteria.list(); 

    TUser user =(TUser)userList.get(0); 

    Transaction tx = session.beginTransaction(); 

    user.setUserType(1); //更新UserType字段 

    tx.commit(); 

 

     每次对TUser进行更新的时候,我们可以发现,数据库中的version都在递增。而如果我们尝试在tx.commit 之前,启动另外一个Session,对名为Max的用户进行操作,下面模拟并发更新时的情况:

[html] view plain copy

 

    Session session= getSession(); 

    Criteria criteria = session.createCriteria(TUser.class); 

    criteria.add(Expression.eq("name","Max")); 

    Session session2 = getSession(); 

    Criteria criteria2 = session2.createCriteria(TUser.class); 

    criteria2.add(Expression.eq("name","Max")); 

    List userList = criteria.list(); 

    List userList2 = criteria2.list();TUser user =(TUser)userList.get(0); 

    TUser user2 =(TUser)userList2.get(0); 

    Transaction tx = session.beginTransaction(); 

    Transaction tx2 = session2.beginTransaction(); 

    user2.setUserType(99); 

    tx2.commit(); 

    user.setUserType(1); 

    tx.commit(); 

 

     执行并发更新的代码,在tx.commit()处抛出StaleObjectStateException异常,并指出版本检查失败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行相应处理。

 

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

四、总结

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

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

     乐观锁的管理跟SVN管理代码版本的原理很像,如果在我提交代码之前用本地代码的版本号与服务器做对比,如果本地版本号小于服务器上最新版本号,则提交失败,产生冲突代码,让用户决定选择哪个版本继续使用。

     在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法        另外,Mysql在处理并发访问数据上,还有添加读锁(共享锁)、写锁(排它锁),控制锁粒度【表锁(table lock)、行级锁(row lock)】等实现,有兴趣可以继续研究。

 

数据库乐观锁和悲观锁的理解和实现

上一篇:SQL重复记录查询的几种方法


下一篇:Oracle——基础知识