Spring Data MongoRepository save(T)不工作......有时候

wal*_*len 21 java mongodb spring-data spring-boot

所以我正在使用这个小的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
Run Code Online (Sandbox Code Playgroud)

这就是我没看到的情况:

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)
Run Code Online (Sandbox Code Playgroud)

通过在调试时对日志文件执行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);
}
Run Code Online (Sandbox Code Playgroud)

我在调试时已经完成了那一部分一百次,一切似乎都没问题:

  • 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
  • Spring Boot 1.4.5
  • MongoDB 3.2.6
  • Windows 10

我也包括BookData在上面.

Tez*_*zra 7

MongoDB 本质上是一个缓存存储,我的意思是,不能保证内容是最新的或一定是正确的。我还没有找到刷新时间的配置选项(但它们会在 DB 本身中配置),但 MongoDB 增加了功能,因此您可以选择快速+脏或慢+干净。如果您看到此类问题,则这种“新鲜度”因素很可能是您的问题。(即使您没有运行分布式,请求确认和请求提交之间也存在时间差异)

这是有关“干净阅读”的帖子链接(以下引用中的要点)

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

我鼓励 MongoDB 用户将自己(或至少是他们的应用程序活动)放入以下组之一:

“我想要低延迟”——只要速度快,脏读就可以了。使用 w=1 并阅读关注“本地”。(这些是默认设置。)“我想要一致性”——脏读是不行的,即使以延迟或稍微过时的数据为代价。使用 w='majority' 并阅读关注 'majority. 使用 MongoDB v1.2.0;

my $mc = MongoDB->connect(
    $uri,
    {
        read_concern_level => 'majority',
        w => 'majority',
    }
);
Run Code Online (Sandbox Code Playgroud)

可能有用也可能没用的进一步阅读

更新

如果在多线程环境中运行,请确保您的线程不会践踏他人的更新。您可以通过将系统或查询日志记录级别配置为 5 来验证是否发生这种情况。 https://docs.mongodb.com/manual/reference/log-messages/#log-messages-configure-verbosity

  • @路易斯G。好的。^_^ 我已经更新了答案,为未来的读者提供了多线程安全案例,这样他们就不必仔细阅读评论,因为那是您问题的实际答案。很高兴我能帮上忙。:3 (2认同)

wal*_*len 7

问题解决了.
从JS客户端到Java后端的不同端点的不同异步调用是在具有原始值的不同线程中覆盖我更新的文档.

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

一旦我添加systemLog.verbosity: 3到MongoDB,config.cfg所以它记录了所有操作,很明显,同时发生了2个不同的WRITE操作(相距约500毫秒),但使用了不同的值.
然后,只需要移动findById越接近save并确保JS调用按顺序完成(通过使其中一个承诺依赖于另一个).

事后看来,这可能不会发生,如果我用的MongoOperations还是MongoTemplate,它提供单updatefindAndModify方法也允许单场操作,而不是MongoRepository在那里我被迫做的3个步骤(find修改返回实体save)和使用完整的文档.


编辑:我真的不喜欢我的第一个" findById接近save"的方法,所以最后我做了我认为是正确的,并实现了使用MongoTemplate更细粒度的updateAPI的自定义保存方法.最终代码:

/* 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);
Run Code Online (Sandbox Code Playgroud)

通过这样做,每个端点可以update根据需要经常使用,MongoTemplate而不必担心覆盖不相关的字段,并且我仍然保留基于实体的MongoRepository方法,例如新实体创建,findBy方法,注释@Query等.