bresai
bresai

Reputation: 377

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

Upvotes: 4

Views: 9574

Answers (4)

Jens Schauder
Jens Schauder

Reputation: 81930

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.

Upvotes: 4

Jens Schauder
Jens Schauder

Reputation: 81930

With this line

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

you are telling JPA to undo all your changes. From the documentation of the refresh method

Refresh the state of the instance from the database, overwriting changes made to the entity, if any.

Remove it and your code should run as expected.

Upvotes: 0

Alexander Yanyshin
Alexander Yanyshin

Reputation: 1370

This looks like the case for pessimistic locking. Create in your repository method 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);
}

and use it to obtain db level lock which will be held till the end of transaction:

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

There is no need to use application level locks like RegularLock in your code.

Upvotes: 3

shazin
shazin

Reputation: 21883

The problem seems to be eventhough you call,

customerDao.saveAndFlush(customer);

The Commit will not take place until the end of the method is reached and a commit is made because your code inside of a

@Transactional(isolation = Isolation.REPEATABLE_READ)

What you can do is either change the propagation of the Transactional to Propagation.REQUIRES_NEW like below.

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

This will result in a New Transaction being created and committed at the end of the method. And at the end of Transaction the changes will be committed.

Upvotes: 0

Related Questions