web-dev-qa-db-fra.com

Spring, JPA et Hibernate - Comment incrémenter un compteur sans problèmes de simultanéité

Je joue un peu avec Spring et JPA/Hibernate et je suis un peu confus sur la bonne façon d’incrémenter un compteur dans une table.

Mon REST API doit incrémenter et décrémenter une valeur dans la base de données en fonction de l'action de l'utilisateur (dans l'exemple ci-dessous, aimer ou ne pas aimer une balise obligera le compteur à s'incrémenter ou à se décrémenter de une dans la table des balises)

tagRepository est une JpaRepository (Spring-data) et j'ai configuré la transaction comme ceci

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"/>

@Controller
public class TestController {

    @Autowired
    TagService tagService

    public void increaseTag() {
        tagService.increaseTagcount();
    }
    public void decreaseTag() {
        tagService.decreaseTagcount();

    }
}

@Transactional
@Service
public class TagServiceImpl implements TagService {


    public void decreaseTagcount() {
        Tag tag = tagRepository.findOne(tagId);
        decrement(tag)
    }

    public void increaseTagcount() {
        Tag tag = tagRepository.findOne(tagId);
        increment(tag)
    }

    private void increment(Tag tag) {
        tag.setCount(tag.getCount() + 1); 
        Thread.sleep(20000);
        tagRepository.save(tag);
    }

    private void decrement(Tag tag) {
        tag.setCount(tag.getCount() - 1); 
        tagRepository.save(tag);
    }
}

Comme vous pouvez le constater, j’ai volontairement dormi 20 secondes au maximum juste avant la .save() pour pouvoir tester un scénario de concurrence.

compteur d'étiquette initial = 10;

1) Un utilisateur appelle augmentationTag et le code frappe le sommeil pour que la valeur de l'entité = 11 et la valeur dans le DB est toujours 10

2) un utilisateur appelle le signe de réduction et passe en revue tout le code. la valeur est la base de données est maintenant = 9

3) Le sommeil se termine et frappe le .save avec l'entité ayant un comptez 11, puis appuyez sur .save ()

Lorsque je vérifie la base de données, la valeur de cette balise est maintenant égale à 11 .. alors qu'en réalité (du moins ce que je voudrais atteindre), elle serait égale à 10

Ce comportement est-il normal? Ou l'annotation @Transactional ne fait-elle pas son travail?

22
Johny19

La solution la plus simple consiste à déléguer la simultanéité à votre base de données et à vous appuyer simplement sur le verrou de la base de données sur les lignes actuellement modifiées:

L'incrément est aussi simple que cela:

UPDATE Tag t set t.count = t.count + 1 WHERE t.id = :id;

et la requête de décrémentation est:

UPDATE Tag t set t.count = t.count - 1 WHERE t.id = :id;

La requête UPDATE verrouille les lignes modifiées, empêchant les autres transactions de modifier la même ligne, avant que la transaction en cours ne soit validée (tant que vous n'utilisez pas READ_UNCOMMITTED).

45
Vlad Mihalcea

Par exemple, utilisez Optimistic Locking . Ceci devrait être la solution la plus simple pour résoudre votre problème. Pour plus de détails, voir -> https://docs.jboss.org/hibernate/orm/4.0/devguide/en-US/html/ch05.html

1
mh-dev

Une autre solution éventuellement cohérente à ajouter:

  • créer une table counters_increment séparée pour insérer chaque incrément de compteur
  • ajouter un planificateur pour mettre à jour la table counters principale à partir du counters_increment

Plus:

  • une autre base de données pour un stockage counters_increment avec une écriture importante (par exemple, Cassandra, Redis)
  • counters_increment_{period} table par période (par exemple, jour) et suppression/recréation de la table entière après le traitement des données et leur utilisation
0
aux