4

I am developing webapp using Spring MVC and have a such methods in my application:

@Transactional
public void methodA(Long id, String color) {
    Fruit fruit = entityManager.createNamedQuery("Fruit.findById", Fruit.class).setParameter(1, id).getSingleResult();
    fruit.setColor("color");
    entityManager.merge(fruit);
}

@Transactional
public void methodB(Long id, int price) {
    Fruit fruit = entityManager.createNamedQuery("Fruit.findById", Fruit.class).setParameter(1, id).getSingleResult();
    fruit.setPrice(price);
    entityManager.merge(fruit);
}

This two methods are often called almost at the same time, and because of that a race condition occurs. Is there a way to fix this problem? I think putting both of them in one synchronized method wouldn't be a good idea, because I expect a lot of calls of these methods (thousands) by different users at the same time, so it can cause delays. Fix me if I am wrong.

red_white_code
  • 111
  • 2
  • 10

3 Answers3

1

Typical approaches to deal with race conditions are locks. In a pessimistic scenario you would forbid the database to accept any transactions on a resource if another transaction is currently active.

Another option is optimistic locking. Before writing a resource back its state is compared with the one when it was initially read. If they differ, another process has changed that resource, which typically ends up in a OptimisticLockException. The good thing is, you may catch it and retry updating that resource right away. Just the same you could tell the user about the conflict. It's your choice.

Both solutions would work well for your use case. Which one to pick depends on many factors. I'd propose you read up on locks and choose for yourself.

You might also want to consider if is absolutely necessary to commit the resources to the DB right away. If you expect them to be altered in the very next second anyway, you could store them in memory and flush them every n seconds which could save you some DB overhead. In most cases this proposal is probably a bad idea. It's just a thought without having a deeper insight into your application.

Jan B.
  • 6,030
  • 5
  • 32
  • 53
0

EntityManager.merge(T entity) merges the state of the given entity into the current persistence context. Depending on the underlying datastore, by the time it merges the entity, the same entity record from the repository may already be altered with different information so any of that altered information may be lost and overwritten by the later merge.

Instead of using EntityManager.merge(T entity), use EntityManager.createQuery(CriteriaUpdate updateQuery).executeUpdate(). This should only update the values of the specified attributes you provide.

@Transactional
public void methodA(Long id, String color) {
    final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    final CriteriaUpdate<Fruit> updateColor = cb.createCriteriaUpdate(Fruit.class);
    final Root<Fruit> updateRoot = updateColor.from(Fruit.class);
    updateColor.where(cb.equal(updateRoot.get(Fruit_.id), id));
    updateColor.set(updateRoot.get(Fruit_.id), id);
    entityManager.createQuery(updateColor).executeUpdate();
}

@Transactional
public void methodB(Long id, int price) {
    final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    final CriteriaUpdate<Fruit> updatePrice = cb.createCriteriaUpdate(Fruit.class);
    final Root<Fruit> updateRoot = updatePrice.from(Fruit.class);
    updatePrice.where(cb.equal(updateRoot.get(Fruit_.id), id));
    updatePrice.set(updateRoot.get(Fruit_.price), price);
    entityManager.createQuery(updatePrice).executeUpdate();
}

As long as no other transaction updates the same fields as either of these methods, then there should no longer be any issues with this update.

andrewh
  • 465
  • 2
  • 10
0

Based on the answer for this question Would transactions/spring Transaction propagation solve this concurrency issue? you could try to put the @transactional on the @service instead each method from repository. You would have something like this:

@Service
@Transactional
class MyService {

    @Autowired
    MyRepo repository;
    public void methodA(Data data){
         repository.methodA(data);
    }
    public void methodB(Data data){
         repository.methodB(data);
    }
}

I know that the problem from this post is not the same that you have, but this could solve your problem.

Community
  • 1
  • 1
Kim Aragon Escobar
  • 166
  • 1
  • 2
  • 15