【问题标题】:jpa-spring entitymanager.find timeout not workingjpa-spring entitymanager.find超时不起作用
【发布时间】:2017-09-27 18:43:35
【问题描述】:

我正在尝试在我的 Spring Boot 应用程序中使用 jpa 和 db 作为 mysql 实现悲观锁。我的目标是让存储库首先从数据库中获取一行,然后对其设置锁定。当这个事务正在运行时,没有人应该能够读取同一行。以下是我实现的代码:

@Repository
@Transactional
public class UserRepo {

@PersistenceContext
private EntityManager entityManager;

/**
 *
 * @param token
 * @param data
 * @return
 */
public boolean lockUser(String token, int data) {
        Map<String, Object> props = new HashMap<String, Object>();
    props.put("javax.persistence.query.timeout", 0);
        User usr = entityManager.find(User.class, token, LockModeType.PESSIMISTIC_WRITE, props);
        System.out.println("BEFOREE LOCK = " + 
        Thread.currentThread().getId() + " user="+usr.getPlayerBalance());

        entityManager.lock(usr, LockModeType.PESSIMISTIC_WRITE, props);
        System.out.println("AFTER LOCK = " + Thread.currentThread().getId());
        if (data>2) {
            System.out.println("IN IF BEFORE SLEEP Thread = " + Thread.currentThread().getId());
            Thread.sleep(90000);
            System.out.println("IN IF AFTER SLEEP Thread = " + Thread.currentThread().getId());
        } else {
            System.out.println("IN ELSE Thread = " + Thread.currentThread().getId());
        }  return false;
      }
 }

现在当我运行它时,当第一个请求带有数据 > 3 时,这个获取行然后锁定行并且线程休眠 90 秒。现在,当第二个请求带有 data = 1 时,线程等待锁定(em.find-具有悲观锁定,超时为 0 毫秒)。现在理想情况下它应该抛出异常,因为我已将超时设置为 0。但第二个线程不会立即抛出异常,而且线程从 db 读取行然后等待。

【问题讨论】:

    标签: java hibernate jpa spring-boot


    【解决方案1】:

    LockModeType.PESSIMISTIC_WRITE 用于锁定行,可以轻松测试。

    我将 UserRepo 稍微调整为:

    @Repository
    public class UserRepo {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @Transactional
        public void lockUser(final Long id, final boolean wait) throws InterruptedException {
            entityManager.clear(); // be sure there is nothing in the cache, actually the threads don't share first level cache
    
            final Map<String, Object> props = new HashMap<String, Object>();
            props.put("javax.persistence.query.timeout", 0);
    
            System.out.println("Thread " + Thread.currentThread().getId() + " EXECUTES SELECT FOR UPDATE");
            entityManager.find(User.class, id, LockModeType.PESSIMISTIC_WRITE, props);
    
            if (wait) {
                System.out.println("Thread " + Thread.currentThread().getId() + " started blocking!");
                Thread.sleep(10000);
                System.out.println("Thread " + Thread.currentThread().getId() + " finished blocking!");
            }
    
            System.out.println("Thread " + Thread.currentThread().getId() + " FINISHED QUERY");
        }
    }
    

    我为那个 repo 创建了一个(不是漂亮但实用的)测试:

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.transaction.annotation.Transactional;
    
    @RunWith(SpringRunner.class)
    @Transactional
    @SpringBootTest
    public class UserRepoTests {
    
        @Autowired
        private UserRepo userRepo;
    
        @Test
        public void testSelectForUpdate() throws InterruptedException {
    
            final Runnable requestOne = () -> {
                try {
                    userRepo.lockUser(1L, true); // this one should wait and block the others
                } catch (InterruptedException e) {
                }
            };
    
            final Runnable requestTwo = () -> {
                try {
                    userRepo.lockUser(1L, false);
                } catch (InterruptedException e) {
                }
            };
    
            final Runnable requestThree = () -> {
                try {
                    userRepo.lockUser(1L, false);
                } catch (InterruptedException e) {
                }
            };
    
            final Thread threadOne = new Thread(requestOne);
            threadOne.start();
    
            Thread.sleep(1000); // give the first one some time to start
    
            final Thread threadTwo = new Thread(requestTwo);
            threadTwo.start();
            final Thread threadThree = new Thread(requestThree);
            threadThree.start();
    
            Thread.sleep(20000); // wait before destroying context
        }
    
    }
    

    如果我们现在假设有一个 ID 为 1(Long)的 User 类型的实体,则输出为:

    Thread 16 EXECUTES SELECT FOR UPDATE
    Hibernate: select user0_.id as id1_31_0_, user0_.player_balance as player_b2_31_0_ from "user" user0_ where user0_.id=? for update
    Thread 16 started blocking!
    Thread 17 EXECUTES SELECT FOR UPDATE
    Hibernate: select user0_.id as id1_31_0_, user0_.player_balance as player_b2_31_0_ from "user" user0_ where user0_.id=? for update
    Thread 18 EXECUTES SELECT FOR UPDATE
    Hibernate: select user0_.id as id1_31_0_, user0_.player_balance as player_b2_31_0_ from "user" user0_ where user0_.id=? for update
    Thread 16 finished blocking!
    Thread 16 FINISHED QUERY
    Thread 17 FINISHED QUERY
    Thread 18 FINISHED QUERY
    

    因此,在调用entityManager.find(... LockModeType.PESSIMISTIC_WRITE...); 之后,此查询的所有后续执行都会等待第一个(因为SELECT ... FOR UPDATE),因此不需要entityManager.lock(...) 调用。

    丢失的异常可能是由于查询超时只是一个提示,您的数据库可能没有考虑到这一事实。见the docs

    QueryTimeoutException:查询花费的时间超过指定的超时时间(请参阅 javax.persistence.query.timeout - 此属性是一个提示,可能不会被遵循)

    或者也在同一页面上:

    javax.persistence.query.timeout 查询超时,以毫秒为单位(整数或字符串),这是 Hibernate 使用的提示,但需要底层数据库的支持(TODO 是 100% 正确,还是我们使用其他技巧)。

    所以你不应该依赖超时异常。

    【讨论】:

    • 嗨 Kevin,我使用了 SET GLOBAL innodb_lock_wait_timeout = 0;犯罪;在 mysql 中,现在我遇到了异常。还有一件事我如何解锁该行。我试过 entityManager.lock(usr, LockModeType.NONE);但这不会解除锁定。如果您能提供帮助,那就太好了。
    • 我想在事务结束前以编程方式解除锁定。
    • 事务结束时释放锁。也许这会有所帮助:vladmihalcea.com/2015/02/24/… TransactiobTemplate 可以帮助实现这一点:docs.spring.io/spring/docs/current/javadoc-api/org/…
    猜你喜欢
    • 2010-12-09
    • 1970-01-01
    • 1970-01-01
    • 2015-10-18
    • 2020-07-11
    • 1970-01-01
    • 2017-10-10
    • 2020-11-14
    • 2018-09-12
    相关资源
    最近更新 更多