Spring Data MongoRepository save (T) не работает... иногда

Итак, это небольшое приложение Angular + Java + Spring Boot + MongoDB, с которым я работаю. В последнее время он получает довольно много действий (читай: модификации кода), но классы доступа к данным в значительной степени остались нетронутыми AFAIK.
Однако, похоже, MongoRepository внезапно решил прекратить сохранять изменения, которые я save() в DB.

mongod.log это то, что я вижу, когда save() работает:

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

И это то, что я вижу, когда это не так:

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)

Выполняя tail -f 1 в файле журнала во время отладки, я видел, что эти соединения появляются правильно, когда мой код вызывает findById() или save(), поэтому кажется, что приложение может попасть в БД.

Это (более или менее) соответствующий Java-код:

/* 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);
}

Я прошел через эту часть сто раз во время отладки и все, кажется, в порядке:

  • findById(id) корректно возвращает ожидаемый BookData original объект BookData original: проверьте ✓
  • newData содержит ожидаемые значения, которые будут использоваться для обновления: проверьте ✓
  • прямо перед вызовом save(original), original был правильно изменен с использованием значений newData: проверьте ✓
  • save() выполняется без ошибок: проверьте ✓
  • save() возвращает новую BookData с правильно обновленными значениями: к моему собственному удивлению, проверьте ✓
  • после того, как save() возвращения, db.bookData.find() запрос в Монго Shell показывает, что значения были обновлены: сбой.
  • после возврата save() объект BookData полученный новыми вызовами findById() содержит обновленные значения: fail (иногда это происходит, иногда это не так).

Просто похоже, что MongoDB ждет какой-то flush(), но это не репозиторий JPA, где можно было бы вместо этого вызвать saveAndFlush().

Любые идеи, почему это может произойти?

EDIT: версии (по запросу):

  • Java 8
  • Весенняя игра 1.4.5
  • MongoDB 3.2.6
  • Windows 10

Я также включил BookData выше.

Ответы

Ответ 1

Задача решена.
Другой асинхронный вызов от JS-клиента, к другой конечной точке в бэкэнде Java, переписывал мой обновленный документ в другой поток с исходными значениями.

Обе операции обновления findById перед сохранением. Проблема заключалась в том, что они делали это одновременно, поэтому они получали одинаковые первоначальные значения.
Затем они продолжаются с обновлением соответствующих полей и вызовом save в конце, в результате чего другой поток эффективно отменяет мои изменения.
Каждый вызов регистрировался только с соответствующими измененными полями, поэтому я не понимал, что один из них переписывает изменения другого.

Как только я добавил systemLog.verbosity: 3 в MongoDB config.cfg чтобы он регистрировал все операции, было ясно, что одновременно выполнялись две разные операции WRITE (~ 500 мс друг от друга), но с использованием разных значений.
Тогда это было просто перемещение findById ближе к save и обеспечение того, чтобы вызовы JS выполнялись по порядку (сделав одно из обещаний зависимым от другого).

Оглядываясь назад, это, вероятно, не произошло бы, если бы я использовал MongoOperations или MongoTemplate, которые предлагают одно update и методы findAndModify которые также допускают MongoRepository операции, а не MongoRepository где я вынужден сделать это за 3 шага (find, изменить возвращаемого объекта, save) и работать с полным документом.


EDIT: Я не очень, как мой первый "двигаться findById ближе save " подход, так что в конце концов, я сделал то, что считал правильным и реализован пользовательский сохранить методы, используемые MongoTemplate более мелкозернистой update API. Итоговый код:

/* 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);

Таким образом, каждая конечная точка может update так часто, как это необходимо через MongoTemplate не беспокоясь о перезаписи несвязанных полей, и я все еще сохраняю методы MongoRepository на основе MongoRepository для таких вещей, как создание новых сущностей, методы findBy, аннотированные @Query и т.д.

Ответ 2

MongoDB по своей сути является хранилищем кэш-памяти, под которым я подразумеваю, что содержимое не гарантируется как последнее или обязательно правильно. Я не смог найти параметры конфигурации для времени очистки (но они были бы настроены в самой БД), но MongoDB добавила функции, чтобы вы могли выбрать быстрый + грязный или медленный + чистый. Этот фактор "свежести", скорее всего, относится к вашей проблеме, если вы видите такую проблему. (Даже если вы не используете дистрибутив, существует разница во времени между подтверждением запроса и запросом)

Вот ссылка на сообщение относительно "чистого чтения" (ключевой момент в следующей цитате)

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

Я призываю пользователей MongoDB размещать себя (или, по крайней мере, свою прикладную деятельность) в одной из следующих групп:

"Я хочу слабую латентность". Грязные чтения в порядке, пока все идет быстро. Используйте w = 1 и читайте "local". (Это настройки по умолчанию.) "Я хочу согласованность". Грязные чтения не в порядке, даже за счет латентности или слегка устаревших данных. Используйте w = "большинство" и читайте "большинство". использовать MongoDB v1.2.0;

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

дальнейшее чтение, которое может быть полезным или не может быть полезным

Обновить

Если вы работаете в многопоточной среде, убедитесь, что ваши потоки не попирают другие обновления. Вы можете проверить, если это происходит, настроив уровень регистрации системы или запросов на 5. https://docs.mongodb.com/manual/reference/log-messages/#log-messages-configure-verbosity