Spring data - 启用乐观锁定

Posted

技术标签:

【中文标题】Spring data - 启用乐观锁定【英文标题】:Spring data - enable optimistic locking 【发布时间】:2018-04-27 03:36:15 【问题描述】:

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


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

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

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

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

我在 spring boot 项目的上下文中使用 spring 数据,版本 1.4.5。

测试:

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> 

【问题讨论】:

【参考方案1】:

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

您指的是 JPA 规范第 93 页上的 P2。本节开头:

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

但是您的测试并没有创建这样的场景。方法lock 永远不会被调用。因此不会发生相关的锁定。特别是仅仅加载一个实体不会调用lock

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

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

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

【讨论】:

【参考方案2】:

您可以像这样设置乐观锁定策略:

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

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

乐观锁定完全由 Hibernate 处理。

乐观锁概念

使用场景:在事务结束时并发更新很少时,检查是否被任何其他事务更新

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

【讨论】:

【参考方案3】:

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

@Entity
public class User 
    @Version
    @Column(nullable = false)
    private Long version;

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

【讨论】:

我在我的项目中使用了这个 @Version 注释,它确实通过我们的休息服务工作。我在回答中提到的确实来自以前的经验。谢谢【参考方案4】:

乐观锁背后的原因是为了防止从以前的状态更新表。例如:

    您获得 id 为 1 的用户 另一个用户更新并将 ID 为 1 的用户提交到新状态 您更新用户 1(您在步骤 1 中加载)并尝试将其提交到数据库

在这种情况下,在第 3 步中,您将覆盖其他人在第 2 步中所做的更改,而您需要抛出异常。

我相信 spring 使用与数据库中的 version 列相对应的 @version 属性来做到这一点。结果是这样的:

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

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

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

【讨论】:

1) 从我在规范中看到的情况来看,乐观锁定比您所说的要多。它必须防止脏读和不可重复读。 2)我认为处理版本号的不是spring,而是不可靠的JPA提供程序(在这种情况下为休眠) 3)Spring不使用字符串作为版本,这是JPA,您可以在一组之间进行选择。 4)如我所说,如果我对findOne方法进行注解,则会抛出乐观锁,所以这意味着你刚才所说的是错误的。

以上是关于Spring data - 启用乐观锁定的主要内容,如果未能解决你的问题,请参考以下文章

乐观锁定的重试机制(spring data + JPA)

Spring Data Jpa如何实现审计和乐观锁功能

Spring Data Jpa如何实现审计和乐观锁功能

如何在 Spring Data MongoDB 中使用乐观锁定?

Spring认证中国教育管理中心-Spring Data Couchbase教程三

使用 Hibernate 和 Spring 实现乐观锁