Spring Data - 启用乐观锁定

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

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

如果我在使用Repositories的spring数据测试项目中这样做,那么似乎没有激活锁定。实际上,在进行非重复读取测试时(请参见JPA规范第93页的P2),没有抛出OptimisticLockException

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

这是正常的还是我错过了什么?我们是否必须注释所有方法(或创建一个基本存储库实现来获取锁),才能在spring数据中启用乐观行为?

我正在使用spring boot项目的spring data,版本为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> {
}
5个回答

14

使用Spring Data JPA实现的乐观锁由所使用的JPA实现完成。

您引用了JPA规范第93页上的。该部分开头如下:

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

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

当修改对象时情况会改变(规范第93页倒数第二段):

如果版本化对象已经被更新或删除,则实现必须确保满足LockModeType.OPTIMISTIC_FORCE_INCREMENT的要求,即使没有对EntityManager.lock进行显式调用。

注意:您正在使用同一个存储库生成两个线程,这将让它们使用相同的EntityManager。我怀疑EntityManager是否支持此操作,而且我也不确定您是否实际上以这种方式获取了两个事务,但这是另一天的问题。


0

回答你的问题,不需要。但是,如果你想要在保存后使用新版本与实体一起工作,那么需要使用flush()或者是yes。原因是乐观锁定是在事务提交(或刷新)时确定的。


-1
乐观锁的原因是为了防止对表的更新从先前状态开始。例如:
  1. 您获取ID为1的用户
  2. 另一个用户更新并提交ID为1的用户到新状态
  3. 您更新在步骤1中加载的用户1,并尝试将其提交到数据库
在这种情况下,在步骤3中,您将覆盖其他人在步骤2中所做的更改,而实际上您需要抛出异常。
我相信Spring使用@version属性来实现它,该属性对应于数据库中的版本列。结果类似于:
update users set password="p2" where id=1 and version=1; 

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

你没有得到异常是因为只有一个线程在操作数据。你在线程1中读取它,当前版本是1,然后在线程2中你再次读取它 - 版本仍然是1。然后当你尝试保存时,它会比较Hibernate会话中的版本与数据库中的版本,它们匹配 - 一切都正常,所以它继续执行而没有异常。尝试使用updateWithSleep()方法更新它,你应该会得到预期的异常。


1
  1. 从规范中我看到,乐观锁定不仅仅是你所说的那样。它必须防止脏读和不可重复读取。
  2. 我认为处理版本号的不是 Spring,而是底层的 JPA 提供程序(在此情况下是 Hibernate)。
  3. Spring 不使用字符串版本,这是 JPA,并且可以在一组之间进行选择。
  4. 正如我所说,如果我注释 findOne 方法,则会抛出乐观锁异常,因此这意味着您刚刚说的是错误的。
- vratojr

-1
您可以设置乐观锁策略,例如:

乐观锁(可选 - 默认为版本):确定乐观锁定策略。

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

乐观锁完全由Hibernate处理。

乐观锁概念

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

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


-2

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

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

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


我在我的项目中使用了@Version注释,并且它确实通过我们的REST服务工作。我在答案中提到的内容确实来自以前的经验。谢谢。 - Amr Alaa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接