Lock Mode Type 之 Optimistic 使用场景

JPA规范中,Lock Mode的值中包含了Optimistic, 一看就知道是用于乐观锁控制, 但是我一直被这个问题困扰,原因在于,在Entity中,已经使用@Version做了乐观锁控制,Hibernate会自动更新比对,version值,那么设置Lock Mode 为Optimistic又有何用处呢?
先看一段代码

    @PersistenceContext
    EntityManager entityManager;
    @Transactional()
    public Order t3(String str) {
        Order order67 = entityManager.find(Order.class, 67L, LockModeType.OPTIMISTIC);
        order67.setCancelReason(str);
        return order67;
    }

这段代码中使用了entity manager查询order,并且指定了乐观锁,然后在更新order,最初我以为这段代码会触发两条sql语句

select ... from `order` ...;
update `order` set .... where id = ? and version = ?;

但是出乎我意料的是,我看到了第三条sql语句

select version from `order` where id = ?;

于是乎我在find时,去除了Optimistic的配置, 发现这第三条sql就不会触发。我百思不得其解,这Optimistic如同鸡肋一样。无意中找到在stack overflow上找到一篇帖子,看到其中一段对话,豁然开朗(链接)在文章中,有一段话是重点,我在这里黏贴出来

Normally you would never use the lock() API for optimistic locking. JPA will automatically check any version columns on any update or delete.

The only purpose of the lock() API for optimistic locking is when your update depends on another object that is not changed/updated. This allows your transaction to still fail if the other object changes.

When to do this depends on the application and the use case. OPTIMISTIC will ensure the other object has not been updated at the time of your commit. OPTIMISTIC_FORCE_INCREMENT will ensure the other object has not been updated, and will increment its version on commit.

Optimistic locking is always verified on commit, and there is no guarantee of success until commit. You can use flush() to force the database locks ahead of time, or trigger an earlier error.

上面提到了Lock Mode Type 的 Optimistic类型的使用场景, 用中国话来说就是,在一个事务中,A对象的改变依赖于B对象,如果B对象在A的事物期间内发生了改变,那么A所在的事物将失败并且回滚。这种情况下就可以使用entityManager.lock(B,OPTIMISTIC)对B对象使用乐观锁。这一句话的作用在于,它会在A对象更改后,但事物提交前,查询一次B对象的version并做比对,如果比对成功A的事物保存成功,否则A事物失败,回滚。
继续看另一端代码

    @PersistenceContext
    EntityManager entityManager;
    @Transactional()
    public Order test(String str) {
        Order order67 = entityManager.find(Order.class, 67L);
        order67.setCancelReason(str);
        Order order66 = entityManager.find(Order.class, 66L);
        entityManager.lock(order66, LockModeType.OPTIMISTIC);
        return order67;
    }

这段代码中,查询出了order67和order66, order67在其事务期间,如果order66发生变化,order67的事物将失败。代码中entityManager.lock(order66, LockModeType.OPTIMISTIC);这一句给order66加了乐观锁,所以事物提交之前,会先check order66的version,来判断order66是否被修改。为了配合实验,在数据库执行如下代码(假定order66的version值为12)

start transaction;

select * from `order` where id = 67 for update;
update `order` set version = 13 where id = 66;

开启事物, 使用排它锁将order67锁住,再更新order66对应的version,暂时不要提交。然后通过API调用刚才的test函数,此时test函数会阻塞,因为order67刚才被排它锁锁住,test函数事务提交时,会阻塞在order67的更新操作上。此时回到mysql 命令行 commit刚才的事务。按照最初的设想,在order67事务结束前,order66的version发生了变化,order67应当提交失败,函数应该会抛出ObjectOptimisticLockingFailureException,但是我却意外地发现函数执行成功了,触发了四条sql语句

select order67
select order66
update order67
select version from order67

这与我们之前讲的明显不一样,后来我突然意识到mysql的默认事务隔离级别是repeatable-read,那么就算其他事务提交,原有已开启的事务,是读不到变化的值。所以在这里我重新修改了函数,代码如下

    @PersistenceContext
    EntityManager entityManager;
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public Order t3(String str) {
        Order order67 = entityManager.find(Order.class, 67L);
        order67.setCancelReason(str);
        Order order66 = entityManager.find(Order.class, 66L);
        entityManager.lock(order66, LockModeType.OPTIMISTIC);
        return order67;
    }

这段代码在@Transactional指定了isolation为READ_COMMITTED,此时再重复刚才的试验,程序如预期的一样抛出了ObjectOptimisticLockingFailureException。

关于Locking read(lock in share mode 和 for update),可以上mysql官网查看,里面讲的很清楚(友情链接)。

意外发现了一篇和我这篇比较相像的文章,链接在这里,我大致浏览了下,基本是对的,作者最后貌似忘了将数据库隔离级别更改为read-committed。
我又陆续看了作者的几篇文章讲的真好,这篇讲的是OPTIMISTIC-INCREMENT的使用场景,非常之贴切。
极力推荐这位大牛的博客,有事没事可以去转转

你可能感兴趣的:(JPA)