web-dev-qa-db-fra.com

La sauvegarde (T) de Spring Data MongoRepository ne fonctionne pas ... parfois

Donc, il y a ce petit Angular + Java + Spring Boot + MongoDB app avec lequel je travaille. Il reçoit beaucoup d'action (lire: modifications de code) récemment, mais les classes d'accès aux données sont restées largement intactes AFAIK.
Cependant, il semble que MongoRepository ait soudainement décidé d'arrêter de persister les modifications que je save() ing vers DB.

En inspectant mongod.log, Voici ce que je vois lorsque la save() fonctionne:

2018-04-11T15:04:06.840+0200 I COMMAND  [conn6] command pdfviewer.bookData command: find { find: "bookData", filter: { _id: "ID_1" }, limit: 1, singleBatch: true } planSummary: IDHACK keysExamined:1 docsExamined:1 idhack:1 cursorExhausted:1 keyUpdates:0 writeConflicts:0 numYields:1 nreturned:1 reslen:716 locks:{ Global: { acquireCount: { r: 4 } }, Database: { acquireCount: { r: 2 } }, Collection: { acquireCount: { r: 2 } } } protocol:op_query 102ms
2018-04-11T17:30:19.615+0200 I WRITE    [conn7] update pdfviewer.bookData query: { _id: "ID_1" } update: { _class: "model.BookData", _id: "ID_1", config: { mode: "normal", offlineEnabled: true }, metadata: { title: "PDFdePrueba3pag   copia  6 ", ...}, downloaded: false, currentPageNumber: 2, availablePages: 3, bookmarks: [], stats: { _id: "c919e517-3c68-462c-8396-d4ba391762e6", dateOpen: new Date(1523460575872), dateClose: new Date(1523460575951), timeZone: "+2", ... }, ... } keysExamined:1 docsExamined:1 nMatched:1 nModified:1 keyUpdates:0 writeConflicts:1 numYields:1 locks:{ Global: { acquireCount: { r: 2, w: 2 } }, Database: { acquireCount: { w: 2 } }, Collection: { acquireCount: { w: 2 } } } 315ms
2018-04-11T17:30:19.615+0200 I COMMAND  [conn7] command pdfviewer.$cmd command: update { update: "bookData", ordered: false, updates: [ { q: { _id: "ID_1" }, u: { _class: "model.BookData", _id: "ID_1", config: { mode: "normal", offlineEnabled: true }, metadata: { title: "PDFdePrueba3pag   copia  6 ", ...}, downloaded: false, currentPageNumber: 2, availablePages: 3, bookmarks: [], stats: { _id: "c919e517-3c68-462c-8396-d4ba391762e6", dateOpen: new Date(1523460575872), dateClose: new Date(1523460575951), timeZone: "+2", ... }, ... }, upsert: true } ] } keyUpdates:0 writeConflicts:0 numYields:0 reslen:55 locks:{ Global: { acquireCount: { r: 2, w: 2 } }, Database: { acquireCount: { w: 2 } }, Collection: { acquireCount: { w: 2 } } } protocol:op_query 316ms

Et c'est ce que je vois quand ce n'est pas le cas:

2018-04-11T18:13:21.864+0200 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:64271 #1 (1 connection now open)
2018-04-11T18:18:51.425+0200 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:64329 #2 (2 connections now open)
2018-04-11T18:19:06.967+0200 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:64346 #3 (3 connections now open)

En faisant un tail -f1 sur le fichier journal lors du débogage, j'ai vu ces connexions apparaître juste lorsque mon code appelle findById() ou save(), il semble donc que l'application peut atteindre la base de données.

C'est (plus ou moins) le code Java Java pertinent:

/* BookData.Java */
@Document
public class BookData {

    @Id private String id;
    // Some more non-Id Strings...
    private Config config;
    private Metadata metadata;
    private Boolean downloaded;
    private Integer currentPageNumber;
    private int availablePages;
    private List<Bookmark> bookmarks;
    private StatsModel stats;

    @Transient private byte[] contents;

    public BookData() {}

    // getters and setters
}

/* BookDataRepository.Java */
// MongoRepository comes from spring-boot-starter-parent-1.4.5.RELEASE
public interface BookDataRepository extends MongoRepository<BookData, String> {
    BookData findById(String id);
}

/* BookDataServiceImpl.Java */
public BookData updateBookData(String id, BookData newData) {
    final BookData original = bookDataRepository.findById(id);
    if (original == null) {
        return null;
    }
    original.setCurrentPageNumber(Optional.ofNullable(newData.getCurrentPageNumber()).orElseGet(original::getCurrentPageNumber));
    // similar code for a couple other fields

    return bookDataRepository.save(original);
}

J'ai parcouru cette partie cent fois lors du débogage et tout semble bien se passer:

  • findById(id) renvoie correctement l'objet BookData original attendu: vérifiez ✓
  • newData contient les valeurs attendues à utiliser pour la mise à jour: cochez ✓
  • juste avant d'appeler save(original), original a été correctement modifié en utilisant les valeurs newData: vérifier ✓
  • save() s'exécute sans erreur: vérifier ✓
  • save() renvoie un nouveau BookData avec des valeurs correctement mises à jour: à ma grande surprise, cochez ✓
  • après le retour de save(), une requête db.bookData.find() dans Mongo Shell montre que les valeurs ont été mises à jour: fail.
  • après le retour de save(), l'objet BookData récupéré par de nouveaux appels à findById() contient les valeurs mises à jour: fail (parfois il le fait, parfois il non).

Il semble que MongoDB attend une sorte de flush(), mais ce n'est pas un référentiel JPA où l'on pourrait appeler saveAndFlush() à la place.

Des idées pourquoi cela pourrait se produire?

EDIT: versions (comme demandé):

  • Java 8
  • Spring Boot 1.4.5
  • MongoDB 3.2.6
  • Windows 10

J'ai également inclus BookData ci-dessus.

21
walen

Problème résolu.
Un appel asynchrone différent du client JS, vers un point de terminaison différent dans le backend Java, écrasait mon document mis à jour dans un thread différent avec les valeurs d'origine.

Les deux opérations de mise à jour appelaient findById avant l'enregistrement. Le problème était qu'ils le faisaient en même temps, donc ils obtenaient les mêmes valeurs d'origine.
Chacun a ensuite continué à mettre à jour ses champs pertinents et à appeler save à la fin, ce qui a eu pour effet que l'autre thread a effectivement remplacé mes modifications.
Chaque appel était enregistré avec uniquement les champs modifiés pertinents, donc je ne savais pas que l'un remplaçait les changements de l'autre.

Une fois que j'ai ajouté systemLog.verbosity: 3 à MongoDB config.cfg donc il a journalisé toutes les opérations , il était clair que 2 opérations WRITE différentes se produisaient en même temps (à ~ 500 ms d'intervalle) mais en utilisant des valeurs différentes .
Ensuite, il s'agissait simplement de rapprocher le findById du save et de s'assurer que les appels JS se faisaient dans l'ordre (en faisant dépendre l'une des promesses de l'autre).

Rétrospectivement, cela ne serait probablement pas arrivé si j'avais utilisé MongoOperations ou MongoTemplate, qui proposent des méthodes simples update et findAndModify qui permettent également un champ unique opérations, au lieu de MongoRepository où je suis obligé de le faire en 3 étapes (find, modifier l'entité retournée, save) et de travailler avec le document complet.


EDIT: Je n'ai pas vraiment aimé ma première approche "déplacer findById plus près de save", donc à la fin j'ai fait ce que je pensais être juste et implémenté des méthodes de sauvegarde personnalisées qui utilisé l'API MongoTemplate plus fine de update. Code final:

/* MongoRepository provides entity-based default Spring Data methods */
/* BookDataRepositoryCustom provides field-level update methods */
public interface BookDataRepository extends MongoRepository<BookData, String>, BookDataRepositoryCustom {

    BookData findById(String id);

}

/* Interface for the custom methods */
public interface BookDataRepositoryCustom {

    int saveCurrentPage(String id, Integer currentPage);
}

/* Custom implementation using MongoTemplate. */
@SuppressWarnings("unused")
public class BookDataRepositoryImpl implements BookDataRepositoryCustom {
    @Inject
    MongoTemplate mongoTemplate;

    @Override
    public int saveCurrentPage(String id, Integer currentPage) {
        Query query = new Query(Criteria.where("_id").is(id));
        Update update = new Update();
        update.set("currentPage", currentPage);

        WriteResult result = mongoTemplate.updateFirst(query, update, BookData.class);

        return result == null ? 0 : result.getN();
    }
}

// Old code: get entity from DB, update, save. 3 steps with plenty of room for interferences.
//        BookData bookData = bookDataRepository.findById(bookDataId);
//        bookData.setCurrentPage(currentPage);
//        bookDataRepository.save(bookData);
// New code: update single field. 1 step, 0 problems.
        bookDataRepository.saveCurrentPage(bookDataId, currentPage);

Ce faisant, chaque point de terminaison peut update aussi souvent que nécessaire via MongoTemplate sans jamais se soucier de remplacer les champs non liés, et je conserve toujours les méthodes MongoRepository basées sur l'entité pour des choses comme création d'une nouvelle entité, findBy méthodes, annotées @Querys etc.

7
walen

MongoDB est intrinsèquement une mémoire cache, par laquelle je veux dire, le contenu n'est pas garanti d'être le plus récent ou nécessairement correct. Je n'ai pas pu trouver les options de configuration pour le temps de vidage (mais elles seraient configurées dans la base de données elle-même), mais MongoDB a ajouté des fonctionnalités pour que vous puissiez choisir rapide + sale ou lent + propre. Ce facteur de "fraîcheur" est probablement votre problème si vous rencontrez ce type de problème. (Même si vous n'exécutez pas de distribution, il existe une différence de synchronisation entre la demande d'accusé de réception et la demande validée)

Voici un lien pour poster concernant la "lecture propre" (Point clé dans la citation suivante)

http://www.dagolden.com/index.php/2633/no-more-dirty-reads-with-mongodb/

J'encourage les utilisateurs de MongoDB à se placer (ou au moins, leurs activités d'application) dans l'un des groupes suivants:

"Je veux une faible latence" - Les lectures sales sont OK tant que les choses sont rapides. Utilisez w = 1 et lisez "local". (Ce sont les paramètres par défaut.) "Je veux de la cohérence" - Les lectures sales ne sont pas correctes, même au prix d'une latence ou de données légèrement obsolètes. Utilisez w = "majorité" et lisez "majorité". utiliser MongoDB v1.2.0;

my $mc = MongoDB->connect(
    $uri,
    {
        read_concern_level => 'majority',
        w => 'majority',
    }
);

lecture complémentaire qui peut être utile ou non

Mise à jour

Si vous exécutez dans un environnement multi-thread, assurez-vous que vos threads ne piétinent pas les mises à jour d'un autre. Vous pouvez vérifier si cela se produit en configurant le niveau de journalisation du système ou de la requête sur 5. https://docs.mongodb.com/manual/reference/log-messages/#log-messages-configure-verbosity =

4
Tezra