Spring Data MongoRepository的save(T)方法有时候不起作用

26

我正在处理一个使用Angular + Java + Spring Boot + MongoDB的小应用程序。最近有很多活动(也就是代码修改),但据我所知,数据访问类似乎一直没有受到影响。
然而,似乎MongoRepository突然决定停止对我保存到数据库中的更改进行持久化。

检查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 代码(或者说更多相关的 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() 返回后,在 Mongo Shell 中进行的 db.bookData.find() 查询显示值已更新:失败
  • save() 返回后,通过新的调用 findById() 检索到的 BookData 对象包含更新的值:失败(有时是这样,有时不是)。

看起来 MongoDB 只是在等待某种形式的flush(),但这不是可以调用 saveAndFlush() 的 JPA 存储库。

有什么想法为什么会发生这种情况吗?

编辑:版本(按要求):

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

我还在上面包含了 BookData


你是否定制了WriteResultChecking或WriteConcern策略? - Anatoly Shamov
@TarunLalwani 完成。 - walen
你现在使用的Mongo客户端版本库可能有问题吗?我认为这是一个本地实例,所以建议升级。 - Tarun Lalwani
@TarunLalwani 是的,我正在使用本地实例进行开发。我的本地实例和生产实例都是3.2.6版本。我的意思是,如果我升级了本地实例,然后一切正常...我仍然不知道为什么之前会_有时_失败,我可能需要更好的解释,然后才能让DevOps和其他团队参与进来。 - walen
@Paizo 我原本以为问题出在 MongoDB 的一方,所以我的解决方案是提高 verbosity 级别(请参见我的答案),这就是我发现了两个更新的原因。如果我已经怀疑“Java 端有两个更新而不是一个”是根本原因,那么我可能会像你说的那样去做,实际上问题也会浮出水面,因为我会看到来自不同端点的两个 save 调用。事后诸葛亮。 - walen
显示剩余21条评论
2个回答

9
问题得到解决。
JS客户端的另一个异步调用向Java后台的不同终端点,使用原始值重写了在不同线程中更新的文档。

两个更新操作都在保存之前调用了findById。问题是它们同时这样做,因此它们获得了相同的原始值。
然后每个操作继续更新它们相关的字段并在最后调用save,导致其他线程有效地覆盖了我的更改。
每个调用都记录了相关修改的字段,所以我没有意识到其中一个正在覆盖另一个的更改。

一旦我在MongoDB的config.cfg中添加了systemLog.verbosity: 3,使其记录所有操作,就清楚了同时发生了2个不同的WRITE操作(间隔约500毫秒),但使用了不同的值。
然后只需将findById移到save的附近,并确保JS调用按顺序进行(通过让一个承诺依赖于另一个)。

事后看来,如果我使用MongoOperationsMongoTemplate,就不会发生这种情况,它们提供单个updatefindAndModify方法,还允许单字段操作,而不是强制我使用MongoRepository这种需要分为三个步骤(find、修改返回的实体和save)并且要处理整个文档的方法。


编辑:我不太喜欢我的第一个“将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);

通过这样做,每个端点都可以使用MongoTemplate尽可能频繁地进行update,而不必担心覆盖不相关的字段,我仍然保留了基于实体的MongoRepository方法,用于新实体创建、findBy方法、带注释的@Query等操作。

你也可以使用乐观锁定(参见 https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo-template.optimistic-locking)来防止并发更新。基本上,你需要在 BookData 实体中添加一个类型为 long 的 'version' 字段,用 @Version 进行注释,Spring Data 将会为你处理其余部分! - tinesoft
如果您想要防止脏读、不可重复读,请使用事务。如果 T1 读取文档 D1,然后 T2 在 T1 之前也读取并更新了该文档,则 T1 将会收到写入错误。 - Khanna111
@Khanna111,MongoDB事务在2018年4月之前不存在,它们是在MongoDB 4.2中引入的 - 我当时正在使用MongoDB 3.2.6。 - walen
谢谢。我会把这个评论留在那里作为可能当前解决方案的占位符,还有你对引入事务版本的评论。 - Khanna111

8
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
1
@LuisG。我想对于大多数遇到这个问题的人来说,这将是他们真正的问题。在你的情况下,很可能不是这样。当然,有很多可能性。如果这不是刷新问题,请进行属性相等比较以验证预先和后保存的对象是否相同(即,ID是否更改或其他任何内容),并运行预先和后保存的dbstats(https://docs.mongodb.com/manual/reference/command/dbStats/)以查看数据库中是否有任何更改。如果您是多线程的,则还可能会有另一个线程破坏您的更改。 - Tezra
如果您是多线程的,那么另一个线程可能会覆盖您的更改。我将MongoDB的systemLog.verbosity增加到3,并经过仔细检查,发现这似乎是实际问题。另一个线程在我的更新后仅500毫秒就更新了同一文档,但更改不同。因此,我的更改几乎立即被覆盖,这就是为什么我无法通过下一次调用findById()或在Mongo Shell中运行db.bookData.find()来看到它们的原因。哦,好吧。 - walen
关于 mongod.log 停止显示更新的问题仍然未解决。可能与日志配置中的 slowms 有关(例如,我看到的查询比 slowms 慢,这是我能看到它们的唯一原因,除非增加 verbosity,否则默认情况下不会看到任何查询)。 - walen
2
@LuisG. 很好。^_^ 我已更新答案,包括多线程安全案例,以便未来读者不必深入评论中查找,因为那才是你问题的实际答案。很高兴能帮到你。:3 - Tezra
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接