实战项目,是如何保证缓存跟数据库数据一致性的?

实战项目,是如何保证缓存跟数据库数据一致性的?

解决方案goocz2025-04-11 15:01:5714A+A-

本文跟大家说说,实战项目中,缓存跟数据库数据一致性,我们是如何保证的。因为最近给我的星球项目引入本地缓存Caffeine,因此说说保证本地缓存和数据库数据一致性,是如何处理

1. 基于Cache-Aside缓存模式的读请求

日常开发中,我们是如何使用缓存的呢?如果是读请求,一般都是这样吧:

  • 读的时候,先读缓存,缓存命中的话,直接返回数据
  • 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。

假设我们查询数据信息,如下:

 @Cacheable("book")
 public BookVO queryBookById(Integer bookId) {
      BookPO bookPO = bookRepository.queryBookById(bookId);
      BookVO bookVO = new BookVO();
      BeanUtils.copyProperties(bookPO, bookVO);
      log.info("查询书本ID:{}", bookId);
      return bookVO;
  }

这个加了@Cacheable("book")注解,执行流程如下:

  • 查询缓存:当调用被 @Cacheable 注解的方法时,Spring Cache 会首先检查指定的缓存(在这个例子中是名为 "book" 的缓存)中是否存在与给定参数(这里是 bookId)相关的缓存项。
  • 缓存未命中:如果缓存中没有找到对应的项(即缓存未命中),那么方法将被执行,以从数据库获取数据。
  • 执行方法:在你的例子中,queryBookById 方法会被执行,从 bookRepository 中查询 BookPO 对象,然后将其转换为 BookVO 对象。
  • 设置缓存:方法执行完成后,其返回值(BookVO 对象)会被存储到缓存中,与调用方法时使用的参数(bookId)相关联。
  • 返回结果:最终,方法返回的值(从缓存中获取或直接计算得到的)会被返回给调用者。

2. 如何保证数据一致性

2.1 更新数据时,让缓存失效

既然我们读的时候,是先读缓存的。为了保证缓存和数据库的数据一致,当更新数据库数据的时候,则需要让缓存失效

  //乐观锁只更新数量
  int result = updateBookByVersion(bookPO, request);
  if (result > 0) {
      //让缓存失效
      Objects.requireNonNull(cacheManager.getCache("book")).evict(bookPO.getId());
  }

2.2 重试删除

很多伙伴可能会有疑问,就是执行让缓存失效时,如果执行失败了,怎么办呢?缓存和数据库的数据是不是就不一致了?

就是执行这一步,失败

Objects.requireNonNull(cacheManager.getCache("book")).evict(bookPO.getId());

其实一般很少很少失败的,如果你比较谨慎的话,可以重试几次哈

2.3 延迟双删

还有些伙伴比较有这方面考虑,就是你让缓存失效的时候,查询接口那边,极端情况,查到书,把旧的数据放进去缓存呢?

比如这个流程:

  1. 线程B发起读请求,去读缓存,发现缓存刚好到期失效了。
  2. 接着线程B去查数据库的数据,查到了旧的数据。
  3. 这时候,线程A过来了,它把数据库的数据更新了
  4. 然后线程A它去让缓存失效
  5. 这时,线程B才把旧的数据库,更新到缓存

酱紫就有问题啦,缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据

这可以考虑延迟双删啦:

在更新数据库之前,先删除缓存中的旧数据。然后,在更新数据库之后,再次删除缓存.在两次删除缓存之间设置一个合理的延迟时间

代码实现如下:

// 第一次删除缓存

Objects.requireNonNull(cacheManager.getCache("book")).evict(bookPO.getId());

//乐观锁只更新数量
int result = updateBookByVersion(bookPO, request);
if (result > 0) {
    // 等待延迟(这里使用简单的Thread.sleep作为示例,实际中应使用更合适的机制)
    try {
          Thread.sleep(DELAY_DELETE_TIME); // 延迟时间应该根据实际情况设定
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复中断状态
        // 可以选择抛出异常或记录日志等
    }

  Objects.requireNonNull(cacheManager.getCache("book")).evict(bookPO.getId());
}

有关于这个延迟双删的时间,设置为多久,大家可以看我这篇文章哈:字节一面:延迟双删,要延迟多久呢?

2.4 最终一致性,给缓存设置一定的过期时间

当然,有些极端请款,或者一些bug产生,导致缓存和数据库数据不一致。因为缓存不要设置永久过期时间

为了保证数据最终一致性,我们时要给缓存设置一定的过期时间的

Caffeine caffeineCacheBuilder() {
    return Caffeine.newBuilder()
            .maximumSize(500) // 设置缓存的最大容量
            .expireAfterWrite(10, TimeUnit.MINUTES); // 设置写入后过期时间
}


来源:
https://mp.weixin.qq.com/s/UVHMeFDO4NYTnSwHZc9f1A

点击这里复制本文地址 以上内容由goocz整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

果子教程网 © All Rights Reserved.  蜀ICP备2024111239号-5