Hibernate深入浅出(六)事务2――锁locking

业务逻辑的实现过程中,往往需要保证数据访问的排他性。

这里就是所谓的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改。

Hibernate支持两种锁机制:即通常所说的“悲观锁(Pessimistic Locking)”和“乐观锁(Optimistic Locking)”。

1. 悲观锁(Pessimistic Locking)

指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此在整个数据处理过程中,将数据处于锁定状态。

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性。

一个典型的,依赖数据库实现的悲观锁调用:
select * from account where name=”Erica” for update
通过for update子句,这条SQL锁定了account表中所有符合检索条件的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
Hibernate的悲观锁,也是基于数据库的锁机制实现。

例子:

String hqlStr = "from TUser as user where" +
                " user.name='Erica'";
        Query query = session.createQuery(hqlStr);
        query.setLockMode("user", LockMode.UPGRADE);//加锁
        List userList = query.list();//执行查询,获取数据

执行后的输出:

Hibernate: select tuser0_.id as id3_, tuser0_.name as name3_, tuser0_.age as age3_, tuser0_.group_id as group4_3_ from t_user3 tuser0_ where tuser0_.name='Erica' for update


query.setLockMode对查询语句中,特定别名所对应的记录进行加锁(我们通过”from TUser as user”为TUser类指定了一个别名“user”),这里也就是对返回的所有user记录进行加锁

可以看到Hibernate通过使用数据的for update子句实现了悲观锁机制。

2. Hibernate的加锁模式

1) LockMode.NONE: 无锁机制

2) LockMode.WRITE: Hibernate在Insert和Update记录的时候会自动获取

3) LockMode.READ: Hibernate在读取记录的时候会自动获取

以上这3种锁机制一般由Hibernate内部使用,如Hibernate为了保证update过程中对象不会被外界修改,会在save方法中自动为目标对象加上WRITE锁,这些都是Hibernate内部对数据的锁定机制,与数据库无关。

4) LockMode.UPGRADE: 利用数据库的for update子句加锁

5) LockMode.UPGRADE_NOWAIT: Oracle的特定实现,利用Oracle的for update nowait子句实现加锁

上面这两种锁机制是我们在应用层较常用的,依赖数据库的悲观锁机制。


加锁一般通过以下方法实现:

Criteria.setLockMode
Query.setLockMode
Session.lock
注:只有在查询开始之前(也就是Hibernate生成SQL之前)设定加锁,才会真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含for update子句的select SQL加载进来,所谓数据库加锁也就无从谈起。

3. 乐观锁(Optimistic Locking)

悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销。

乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。

读取出数据时,将此版本号一同读出,之后更新时,对此版本加1。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行对比,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

注:乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的操作不受我们系统的控制,因此可能会造成非法数据被更新到数据库中。


在系统设计阶段,我们应充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

Hibernate在其数据访问引擎中内置了乐观锁实现。

例子:

1》为TUser的class描述符添加optimistic-lock属性:

<hibernate-mapping>
    <class name=”…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发生修改的情况下依然有效的锁机制。

2》 添加一个version属性描述符

<hibernate-mapping>
    <class name=”…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”>
        </generator>
    </id>
    <version column=”version”
        name=”version”
        type=”java.lang.Integer”
    />
    …
</class>
</hibernate-mapping>

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

3》测试

编写一段代码,更新TUser表中记录的数据

Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq(“name”,”Erica”));
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,对名为Erica的用户进行操作,并模拟并发更新时的情形:

Session session=getSession();
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq(“name”,”Erica”));
Session session2=getSession();
Criteria criteria2=session2.createCriteria(TUser.class);
criteria2.add(Expression.eq(“name”,”Erica”));
List userList = criteria.list();
List userList2 = criteria2.list();
TUser user = userList.get(0);
TUser user2 = userList2.get(0);
Transaction tx = session.beginTransaction();
Transaction tx2 = session2.beginTransaction();
user2.setUserType(99);
tx2.commit();
user.setUserType(1);
tx.commit();

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

你可能感兴趣的:(数据库,update,account,locking,排他性)