Reputation: 377
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
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
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
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
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