Spring数据 - 启用乐观锁定

时间:2017-11-14 09:03:35

标签: java spring hibernate jpa spring-data-jpa

注意: 我不需要关于最佳锁定的解释。 这个问题是关于使用乐观锁定时的特定Spring Data行为。

每当实体具有@Version注释字段时,从jpa specs开始,应该在实体上自动启用乐观锁定。

如果我在使用存储库的弹簧数据测试项目中执行此操作,则锁定似乎不会被激活。实际上,在进行不可重复读取测试时,不会抛出OptimisticLockException(请参阅JPA规范第93页上的P2)

但是,从spring docs开始,我看到如果我们使用@Lock(LockModeType.OPTIMISTIC)注释单个方法,那么底层系统会正确抛出OptimisticLockException(然后在春天捕获并传播到以稍微不同的形式堆叠。)

这是正常还是我错过了什么?我们是否有义务对所有方法进行注释(或创建一个采用锁的基本存储库实现)以使用spring数据启用乐观行为?

我在Spring Boot项目1.4.5版本的上下文中使用了spring数据。

测试:

public class OptimisticLockExceptionTest {

    static class ReadWithSleepRunnable extends Thread {

        private OptimisticLockExceptionService service;

        private int id;

        UserRepository userRepository;

        public ReadWithSleepRunnable(OptimisticLockExceptionService service, int id, UserRepository userRepository) {
            this.service = service;
            this.id = id;
            this.userRepository = userRepository;
        }

        @Override
        public void run() {
            this.service.readWithSleep(this.userRepository, this.id);
        }

    }

    static class ModifyRunnable extends Thread {

        private OptimisticLockExceptionService service;

        private int id;

        UserRepository userRepository;

        public ModifyRunnable(OptimisticLockExceptionService service, int id, UserRepository userRepository) {
            this.service = service;
            this.id = id;
            this.userRepository = userRepository;
        }

        @Override
        public void run() {
            this.service.modifyUser(this.userRepository, this.id);
        }

    }

    @Inject
    private OptimisticLockExceptionService service;

    @Inject
    private UserRepository userRepository;

    private User u;

    @Test(expected = ObjectOptimisticLockingFailureException.class)
    public void thatOptimisticLockExceptionIsThrown() throws Exception {

        this.u = new User("email", "p");
        this.u = this.userRepository.save(this.u);

        try {
            Thread t1 = new ReadWithSleepRunnable(this.service, this.u.getId(), this.userRepository);
            t1.start();
            Thread.sleep(50);// To be sure the submitted thread starts
            assertTrue(t1.isAlive());
            Thread t2 = new ModifyRunnable(this.service, this.u.getId(), this.userRepository);
            t2.start();
            t2.join();
            assertTrue(t1.isAlive());
            t1.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

测试服务:

@Component
public class OptimisticLockExceptionService {

    @Transactional
    public User readWithSleep(UserRepository userRepo, int id) {

        System.err.println("started read");
        User op = userRepo.findOne(id);
        Thread.currentThread();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.err.println("read end");
        return op;

    }

    @Transactional
    public User modifyUser(UserRepository userRepo, int id) {

        System.err.println("started modify");
        User op = userRepo.findOne(id);

        op.setPassword("p2");

        System.err.println("modify end");
        return userRepo.save(op);

    }
}

存储库:

@Repository
public interface UserRepository extends CrudRepository<User, Integer> {
}

4 个答案:

答案 0 :(得分:5)

使用Spring Data进行乐观锁定JPA由使用的JPA实现实现。

您指的是JPA规范第93页上的 P2 。该部分以:

开头
  

如果事务T1调用   lock(entity, LockModeType.OPTIMISTIC)    在版本化对象上,实体管理器必须确保不会发生以下任何现象:

但是你的测试并没有创造出这样的场景。方法lock永远不会被调用。因此,不会发生相关锁定。特别是加载实体并不会在其上调用lock

当一个人修改一个对象(第二个但是规范的最后一个段落)时,事情会发生变化:

  

如果版本化对象以其他方式更新或删除,则实现必须确保满足LockModeType.OPTIMISTIC_FORCE_INCREMENT的要求,即使未对EntityManager.lock进行显式调用。

注意:您使用相同的存储库生成两个线程,这反过来会使它们使用相同的EntityManager。我怀疑这是否得到了EntityManager的支持,而且我也不确定你是否真的以这种方式获得了两笔交易,但这是另一天的问题。

答案 1 :(得分:-1)

乐观锁定背后的原因是防止表格从先前状态更新。例如:

  1. 您获得身份1
  2. 的用户
  3. 另一个用户更新并将id为1的用户提交到新状态
  4. 您更新用户1(您在步骤1中加载)并尝试将其提交到数据库
  5. 在这种情况下,在步骤3中,您将覆盖其他人在步骤2中所做的更改,而您需要抛出异常。

    我相信spring使用@version属性来对应于数据库中的版本列。结果如下:

    update users set password="p2" where id=1 and version=1; 
    

    我认为spring实际上使用字符串版本,但我不确定。可能也是一个时间戳,但这是一般的想法。

    您没有异常,因为只有一个线程正在操作数据。你在线程1中读取它,当前版本是例如1,然后在线程2中读取它 - 版本仍然是1.然后当你尝试保存时,它将hibernate会话中的版本与数据库中的版本进行比较,它们匹配 - 一切都井井有条,所以它无一例外地继续下去。尝试使其updateWithSleep(),你应该得到预期的异常。

答案 2 :(得分:-1)

您可以设置乐观锁定策略,如:

optimistic-lock(可选 - 默认为version):确定乐观锁定策略。

乐观锁定策略: version:检查版本/时间戳列, all:检查所有列, dirty:检查更改的列 none:不使用乐观锁定

乐观锁定完全由Hibernate处理。

乐观锁定概念

使用场景:当事务检查结束时并发更新很少,如果由任何其他事务更新

可以使用乐观锁定来处理并发更新。乐观锁定通过检查它将要更新的数据是否已被另一个事务更改,因为它已被读取。例如你搜索了一条记录,经过很长一段时间你要修改那条记录,但在此期间,记录已由其他人更新。实现乐观锁定的一种常用方法是向每个表添加一个版本列,每次更改行时应用程序都会增加该值。每个UPDATE语句的WHERE子句检查版本号自读取后是否未更改。如果行已被另一个事务更新或删除,则应用程序可以回滚事务并重新开始。乐观锁定的名称来源于它假定并发更新很少,而不是阻止它们,应用程序检测并从中恢复。 Optimistic Lock模式仅在用户尝试保存更改时才检测到更改,只有在重新开始时才能正常工作并不会给用户带来负担。在实施用户因为不得不丢弃几分钟而非常恼火的用例时。工作,更好的选择是使用悲观锁。

答案 3 :(得分:-2)

@Version注释用作数据库中的一列,应该添加到每个实体以执行此实体的乐观锁定,例如

message.links

这将确保不会使用错误的版本创建用户。这意味着您无法同时从多个来源更新用户。