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

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

Проверяя 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 -f1 в файле журнала во время отладки, я увидел, что эти соединения появляются правильно, когда мой код вызывает 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: проверьте ✓
  • newData содержит ожидаемые значения, которые будут использоваться для обновления: отметьте ✓
  • непосредственно перед вызовом save(original), original был правильно изменен с использованием значений newData: проверьте ✓
  • save() работает без ошибок: проверьте ✓
  • save() возвращает новый BookData с правильно обновленными значениями: к моему собственному удивлению, отметьте ✓
  • после возврата save() запрос db.bookData.find() в Mongo Shell показывает, что значения были обновлены: неудача.
  • после того, как save() возвращается, объект BookData, полученный новыми вызовами findById(), содержит обновленные значения: неудача (иногда да, иногда нет).

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

Есть идеи, почему это может происходить?

Обновлено: версии (по запросу):

  • Java 8
  • Весенний ботинок 1.4.5
  • MongoDB 3.2.6
  • Windows 10

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

[1] На самом деле Get-content mongod.log -Tail 10 -Wait в PowerShell, но не совсем актуальный.

walen 12.04.2018 15:42

Я предлагаю вам добавить туда несколько журналов в updateBookData, так как есть нулевая проверка и в некоторых случаях оригинал останется нетронутым. зарегистрируйте bean-компонент перед внесением изменений и прямо перед тем, как save может помочь при ошибках приложения. Можете выложить свой бин BookData ?

Paizo 16.04.2018 14:20

Вы настроили политики WriteResultChecking или WriteConcern?

Anatoly Shamov 16.04.2018 14:59

@Paizo Спасибо за ваше участие. Используя пошаговое выполнение, я подтвердил, что original модифицируется, как ожидалось, до save (как указано в вопросе), так что это не проблема. Я добавлю BookData по запросу.

walen 16.04.2018 15:14

@AnatolyShamov Нет, все, что касается Spring Data и MongoRepository, настолько ванильно, насколько это возможно.

walen 16.04.2018 15:16

@LuisG., Можете еще добавить информацию о сервере mongo? Версия, ОС и т. д.

Tarun Lalwani 16.04.2018 15:19

@TarunLalwani Готово.

walen 16.04.2018 15:26

Можете ли вы попробовать с MongoDB 3.4 или 3.6? docs.mongodb.com/manual/tutorial/install-mongodb-on-windows

Tarun Lalwani 16.04.2018 15:28

@LuisG. У вас есть тестовый пример, чтобы достоверно воспроизвести проблему?

Tezra 16.04.2018 15:30

@TarunLalwani Наш производственный экземпляр MongoDB используется несколькими приложениями, включая мое, поэтому обновление до 3.4 или 3.6 потребует значительных усилий со стороны других команд для тестирования своих приложений на соответствие новой версии и т. д. - чего я бы предпочел пока избегать, если нет какой-либо документированной проблемы с 3.2.x, которая могла бы оправдать обновление.

walen 16.04.2018 15:38

@Tezra К сожалению, нет. Я не могу заставить даже само приложение постоянно выходить из строя, так что ...

walen 16.04.2018 15:39

Может быть, у библиотеки клиентской версии Mongo, которую вы сейчас используете, есть проблема? Я подумал, что это локальный экземпляр, и поэтому предложил обновить

Tarun Lalwani 16.04.2018 15:39

@TarunLalwani Да, для разработки я использую локальный экземпляр. И мой локальный экземпляр, и производственный - 3.2.6. Я имею в виду, что если я обновлю свой локальный экземпляр, и тогда все будет работать ... Я все равно не пойму, почему это иногда не удалось раньше, и мне, вероятно, понадобится более хорошее объяснение, чем это, прежде чем я подключу DevOps и другие команды .

walen 16.04.2018 15:43

Трудно сказать, потому что недавний патч или что-то еще на вашей машине могло повлиять на поведение. Комбинаций может быть много, и они могут быть даже специфичными для вашей машины. Пока ваш коллега не работает в разных средах и не конфигурирует, трудно сказать

Tarun Lalwani 16.04.2018 15:45

@Tezra Как сказано в вопросе и в одном из моих комментариев выше, я подтвердил с помощью пошагового выполнения, что updateBookData выполняется каждый раз, и объект original изменяется до saveing, и вызов save даже возвращается обновленный объект. Но изменения не сохраняются в самой БД. Мне кажется, что это кеширование, но я не знаю ни одной опции кеширования для MongoRepository.

walen 16.04.2018 16:01

@LuisG. Запустите еще несколько тестов, но добавьте 61-секундный сон между сохранением и возвратом. Если это сработает, то это определенно проблема с промывкой (или проблема с блокировкой).

Tezra 16.04.2018 16:23

Не могли бы вы включить ведение журнала уровня отладки для зависимостей Spring mongo и mongodb? Так что при сбое сохранения у вас будет больше информации. Попробуйте logging.level.org.springframework.data.mongodb.core.MongoTem‌​plate=DEBUG logging.level.org.mongodb.driver.protocol.insert=DEBUG logging.level.org.mongodb.driver.protocol.query=DEBUG logging.level.org.mongodb.driver.protocol.command=DEBUG logging.level.org.mongodb.driver.protocol.update=DEBUG в application.properties и сравните логи. Надеюсь, это поможет вам сузить проблему.

s7vr 16.04.2018 18:10

журналы типа MongoTemplate : findOne using query: { "_id" : { "$oid" : "somevalue"}} in db.collection: db.somecollection protocol.command: Sending command {find : BsonString{value='somecollection'}} to database [connectionId{localValue:3, serverValue:67}] to server 127.0.0.1:27017 protocol.command: Command completed MongoTemplate: Calling update using query: { "_id" : { "$oid" : "somevalue"} , "version" : 4} and update: update json in collection: some collection protocol.update: Updating in namespace db.somecollection on connection serverValue:67}] protocol.update: Update completed

s7vr 16.04.2018 18:14

@Veeram Я обнаружил проблему, см. этот комментарий. Я разрываюсь между тем, чтобы сделать одно из презираемых «Обновлено: Я решил это, это было из-за X» правок, и написать какой-то ответ об уровнях ведения журнала и о том, как определить, перезаписывает ли какой-то другой поток изменения.

walen 16.04.2018 18:14

Рад, что вы решили эту проблему, но вам следует настроить ведение журнала, чтобы вы могли отлаживать проблему, когда она возникает. Что произойдет, если вы включите ведение журнала на стороне приложения? Он должен записывать журналы, когда другой поток перезаписывает изменения.

s7vr 16.04.2018 18:24

@Veeram Обе операции обновления регистрировались как на клиенте JS, так и на сервере Java, но в каждом журнале отображались только «соответствующие» измененные поля, поэтому я не осознавал проблему, пока не поднял уровень журнала БД.

walen 16.04.2018 18:45

так что решение разоблачить проблему заключалось в том, чтобы поставить условную точку останова, отфильтровав bookId на repository.save(BookData bookData)? : D

Paizo 17.04.2018 10:55

@Paizo Я думал, что проблема на стороне MongoDB, поэтому моим решением было поднять уровень verbosity (см. Мой ответ), и именно так я обнаружил два обновления. Если бы я уже подозревал, что «два обновления вместо одного» на стороне Java были основной причиной, тогда я, вероятно, сделал бы, как вы говорите, и действительно проблема всплыла бы, так как я бы видел два вызова save поступающие с разных конечных точек. Взгляд в прошлое - 20/20.

walen 17.04.2018 11:40

Поскольку этот вопрос решен, вы должны принять ответ, чтобы другие знали, что он решен.

Tezra 20.04.2018 15:55

@Tezra Что ж, для меня это действительно решено, но (как вы сами сказали) другие пользователи, которые нашли этот вопрос, могут иметь другую проблему. 11 человек проголосовали за вопрос, но только 2 проголосовали за мой (довольно конкретный) ответ, и только я проголосовал за ваш (хотя я уже понял проблему потока к тому времени, когда вы ее упомянули): возможно, ни один из ответов не достаточно хорош для сообщество? Следовательно, почему вопрос и его награда будут оставаться открытыми до окончания льготного периода, на случай, если кто-то отправит более полный / общий ответ (или улучшит текущий;). Не торопитесь с вещами, которые не нужно торопить.

walen 20.04.2018 17:40

Вы принимать исходя из того, что помогло вам лично. Вы можете подождать, пока истечет срок награды, но принимайте ее на свой страх и риск, а не сообщества. Другие с подобной проблемой (если она когда-либо существует) могут открыть свой собственный вопрос для этого сценария. : 3

Tezra 20.04.2018 18:22
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
23
26
11 065
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

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

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

I encourage MongoDB users to place themselves (or at least, their application activities) into one of the following groups:

"I want low latency" – Dirty reads are OK as long as things are fast. Use w=1 and read concern 'local'. (These are the default settings.) "I want consistency" – Dirty reads are not OK, even at the cost of latency or slightly out of date data. Use w='majority' and read concern 'majority. use 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

Спасибо за ответы. Я понимаю, что «возможная согласованность» MongoDB может время от времени приводить к некоторым грязным чтениям, когда между операциями всего несколько секунд. Однако я вижу, что изменения никогда сохраняются. Я только что сделал обновление, дождался 15 минут, прежде чем снова запросить БД, и изменений нигде не было видно. Разумеется, 15 минут должно хватить MongoDB для распространения изменений в конфигурации с одним экземпляром без сегментов?

walen 16.04.2018 16:59

(Кстати, обе нижние ссылки для меня пурпурные, проверял их на прошлой неделе. Вторая была очень интересной, но ничего из того, что там написано, не помогло, так как этот «сброс каждые 60 с, журнал каждые 100 мс», похоже, не соответствует поведению Я вижу. Но все равно спасибо!)

walen 16.04.2018 17:07

@LuisG. Я полагаю, что для большинства людей, которые задаются этим вопросом, это будет их настоящая проблема. В вашем случае, скорее всего, нет. Конечно, это МОЖЕТ быть много чего. Если это не проблема сброса, выполните сравнение свойств, равных объектам сохранения до публикации, чтобы убедиться, что они одинаковы (т. Е. Идентификатор не изменился или что-то еще), и запустите dbstats перед публикацией (docs.mongodb.com/manual/reference/command/dbStats), чтобы увидеть если НИЧЕГО изменилось в БД. Если вы многопоточны, также возможно, что другой поток попирает ваши изменения.

Tezra 16.04.2018 17:11
«Если вы многопоточны, возможно, другой поток попирает ваши изменения». Я увеличил systemLog.verbosity MongoDB до 3, и после тщательного изучения, это, кажется, реальная проблема. Другой поток обновляет тот же документ всего через 500 мс после моего обновления, но с другими изменениями. Таким образом, мои изменения перезаписываются почти мгновенно, и поэтому я не вижу их ни при следующем вызове findById(), ни при запуске db.bookData.find() в Mongo Shell. Ну что ж.
walen 16.04.2018 17:26

Однако вопрос о том, почему mongod.log перестал показывать обновления, остается открытым. Может иметь какое-то отношение к slowms в конфигурации журналирования (например, запросы, которые я видел, были медленнее, чем slowms, и это единственная причина, по которой я их вообще видел, по умолчанию не видеть никаких запросов, если verbosity не увеличен).

walen 16.04.2018 17:31

@LuisG. Конфигурация журнала по умолчанию: 100 + мс - это «медленный» запрос, и регистратор будет регистрировать только «медленные» запросы. (здесь объясняется docs.mongodb.com/manual/reference/method/db.setProfilingLeve‌ l) Чтобы регистрировать все запросы, вам нужно изменить logLevel с 0 (мин.) на 5 (макс.) (docs.mongodb.com/manual/reference/log-messages/… (Установить подробность запроса)). Если вы не изменили настройки по умолчанию, это то, что вы видите.

Tezra 16.04.2018 18:41

Да. Два обещания от клиента JS вызывали две разные конечные точки из серверной части Java, которые, в свою очередь, одновременно создавали два findById. Затем каждый внес свои собственные модификации и назвал save последним, который сделал это, фактически заменив первый.

walen 16.04.2018 22:06

@LuisG. Хорошо. ^ _ ^ Я обновил ответ, включив в него аргументы в пользу многопоточной безопасности для будущих читателей, чтобы им не приходилось копаться в комментариях, и поскольку это был фактический ответ на ваш вопрос. Рад, что смог помочь. : 3

Tezra 16.04.2018 22:22
Ответ принят как подходящий

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

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

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

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


Обновлено: Мне не очень понравился мой первый подход «переместить findById ближе к save», поэтому в конце концов я сделал то, что считал правильным, и реализованы пользовательские методы сохранения, которые использовали более детальный MongoTemplate API update. Окончательный код:

/* 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 на основе сущностей для таких вещей, как создание новых сущностей, методы findBy, аннотированные @Query и т. д.

Вы также можете использовать оптимистическую блокировку (см. docs.spring.io/spring-data/mongodb/docs/current/reference/ht ml /…), чтобы предотвратить эти постоянные обновления. По сути, вам нужно добавить поле «версия» типа long в свой объект BookData, аннотированное @Version, а Spring Data позаботится обо всем остальном за вас!

tinesoft 06.06.2019 17:40

Другие вопросы по теме