Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring data jpa get old result under multi threading

Under multi threading, I keep getting old result from repository.

@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);
    }

}

And CustomerDao looks like

@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() from JpaRepository is used in order to save the change instantly and the entire code is locked. But I still keep getting old result.

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

I'm using springboot with spring data jpa.

I put all code in a test controller, and the problem remains

@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);
    }
}

and the results are

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
like image 617
bresai Avatar asked Mar 11 '23 08:03

bresai


1 Answers

I tried to reproduce the problem and failed. I put your code, with very little changes into a Controller and executed it, by requestion localhost:8080/test and could see in the logs, that the score gets reduced as expected. Note: it actually produces an exception because I don't have a view resulution configured, but that should be irrelevant.

I therefore recommend the following course of action: Take my controller from below, add it to your code with as little changes as possible. Verify that it actually works. Then modify it step by step until it is identical with your current code. Note the change that starts producing your current behavor. This will probably make the cause really obvious. If not update the question with what you have found.

@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);
    }
}

After update from OP and a little discussion we seemed to have it pinned down:

The problem occurs ONLY with multiple threads (OP used JMeter to do this thing 10times/second).

Also Transaction level serializable seemed to fix the problem.

Diagnosis

It seems to be a lost update problem, which causes effects like the following:

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.

Why isn't this prevented by the synchronization?

The problem here is most likely that the read happens before the code shown in the question, since the EntityManager is a first level cache.

How to fix it This should get caught by optimistic locking of JPA, for this one needs a column annotated with @Version.

Transaction Level Serializable might be the better choice if this happens often.

like image 158
Jens Schauder Avatar answered Mar 12 '23 23:03

Jens Schauder