Spring数据jpa在多线程下获得旧结果

时间:2016-12-23 08:57:46

标签: java multithreading spring-boot spring-data-jpa

在多线程下,我不断从存储库中获取旧结果。

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void updateScore(int score, Long userId) {
    logger.info(RegularLock.getInstance().getLock().toString());
    synchronized (RegularLock.getInstance().getLock()) {
        Customer customer = customerDao.findOne(userId);
        System.out.println("start:": customer.getScore());
        customer.setScore(customer.getScore().subtract(score));
        customerDao.saveAndFlush(customer);
    }

}

CustomerDao看起来像

@Transactional
public T saveAndFlush(T model, Long id) {
    T res = repository.saveAndFlush(model);
    EntityManager manager = jpaContext.getEntityManagerByManagedType(model.getClass());
    manager.refresh(manager.find(model.getClass(), id));
    return res;
}
来自saveAndFlush()

JpaRepository用于立即保存更改并锁定整个代码。但我仍然保持旧的结果。

java.util.concurrent.locks.ReentrantLock@10a9598d[Unlocked]
start:710
java.util.concurrent.locks.ReentrantLock@10a9598d[Unlocked]
start:710

我正在使用springboot和spring data jpa。

我将所有代码都放在测试控制器中,问题仍然存在

@RestController
@RequestMapping(value = "/test", produces = "application/json")
public class TestController {
    private static Long testId;

    private final CustomerBalanceRepository repository;

    @Autowired
    public TestController(CustomerBalanceRepository repository) {
        this.repository = repository;
    }

    @PostConstruct
    public void init() {
//        CustomerBalance customer = new CustomerBalance();
//        repository.save(customer);
//        testId = customer.getId();
    }


    @SystemControllerLog(description = "updateScore")
    @RequestMapping(method = RequestMethod.GET)
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public CustomerBalance updateScore() {
        CustomerBalance customerBalance = repository.findOne(70L);
        System.out.println("start:" + customerBalance.getInvestFreezen());
        customerBalance.setInvestFreezen(customerBalance.getInvestFreezen().subtract(new BigDecimal(5)));
        saveAndFlush(customerBalance);
        System.out.println("end:" + customerBalance.getInvestFreezen());
        return customerBalance;
    }

    @Transactional
    public CustomerBalance saveAndFlush(CustomerBalance customerBalance) {
        return repository.saveAndFlush(customerBalance);
    }
}

,结果是

start:-110.00
end:-115.00
start:-110.00
end:-115.00
start:-115.00
end:-120.00
start:-120.00
end:-125.00
start:-125.00
end:-130.00
start:-130.00
end:-135.00
start:-130.00
end:-135.00
start:-135.00
end:-140.00
start:-140.00
end:-145.00
start:-145.00
end:-150.00

4 个答案:

答案 0 :(得分:3)

我试图重现问题但失败了。我把你的代码,只需很少的更改放入一个Controller并通过请求localhost:8080/test执行它,并且可以在日志中看到分数按预期减少。 注意:它实际上会产生异常,因为我没有配置视图,但这应该是无关紧要的。

因此,我建议采取以下行动: 从下面拿我的控制器,尽可能少地将它添加到您的代码中。验证它确实有效。然后逐步修改它,直到它与您当前的代码相同。请注意开始产生当前行为的更改。这可能会使事业变得非常明显。如果没有用您找到的内容更新问题。

@Controller
public class CustomerController {
    private static String testId;

    private final CustomerRepository repository;

    private final JpaContext context;

    public CustomerController(CustomerRepository repository, JpaContext context) {
        this.repository = repository;
        this.context = context;
    }

    @PostConstruct
    public void init() {
        Customer customer = new Customer();
        repository.save(customer);
        testId = customer.id;
    }


    @RequestMapping(path = "/test")
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public Customer updateScore() {
        Customer customer = repository.findOne(testId);
        System.out.println("start:" + customer.getScore());
        customer.setScore(customer.getScore() - 23);
        saveAndFlush(customer);
        System.out.println("end:" + customer.getScore());
        return customer;
    }

    @Transactional
    public Customer saveAndFlush(Customer customer) {
        return repository.saveAndFlush(customer);
    }
}

从OP更新并进行了一些讨论后,我们似乎已将其固定下来:

问题只发生在多个线程上(OP使用JMeter来做这件事10次/秒)。

事务级别序列化似乎也解决了这个问题。

<强>诊断

这似乎是一个丢失的更新问题,会导致以下影响:

Thread 1: reads the customer score=10 
Thread 2: reads the customer score= 10 
Thread 1: updates the customer to score 10-4 =6
Thread 2: updates the customer to score 10-3 =7 // update from Thread 1 is gone.

为什么这不会被同步阻止?

这里的问题很可能是在问题中显示的代码之前发生了读取,因为EntityManager是第一级缓存。

如何修复 这应该被JPA的乐观锁定所捕获,因为这需要一个用@Version注释的列。

事务级别Serializable可能是更好的选择,如果经常发生这种情况。

答案 1 :(得分:1)

这看起来像是悲观锁定的情况。在您的存储库方法findOneWithLock中创建lke this:

import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.JpaRepository;
import org.springframework.data.repository.query.Param;

import javax.persistence.LockModeType;

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select c from Customer c where c.id = :id")
    Customer findOneWithLock(@Param("id") long id);
}

并使用它来获取数据库级锁定,这将持续到事务结束:

@Transactional
public void updateScore(int score, Long userId) {
    Customer customer = customerDao.findOneWithLock(userId);
    customer.setScore(customer.getScore().subtract(score));
}

您的代码中无需使用像RegularLock这样的应用程序级锁。

答案 2 :(得分:0)

问题似乎是在你打电话的时候,

customerDao.saveAndFlush(customer);

提交将不会发生,直到达到方法结束并提交,因为您的代码在

@Transactional(isolation = Isolation.REPEATABLE_READ)

您可以做的是将交易的propagation更改为Propagation.REQUIRES_NEW,如下所示。

@Transactional(isolation = Isolation.REPEATABLE_READ, propagation=Propagation.REQUIRES_NEW)

这将导致在方法结束时创建并提交新事务。在交易结束时,将提交更改。

答案 3 :(得分:0)

使用此行

manager.refresh(manager.find(model.getClass(), id));

您告诉JPA撤消所有更改。来自refresh method

的文档
  

从数据库刷新实例的状态,覆盖对实体所做的更改(如果有)。

删除它,您的代码应按预期运行。