文章

时间修改成功,但是没同步?

时间修改成功,但是没同步?

问题引出

OJ的一次考试中,中途延长考试后,有一些同学需要重新登录,但是他们登录上去所获取的开放时间依旧是原来的时间,因此一直显示“考试已结束”,无法答题。我迅速排查日志内容,发现并未有ERROR,数据库的题目集时间也是正确的。但在我仔细检查登录方法时,看到登录时需要从Redis获取题目集时间,赶紧打开Redis一看,缓存的时间和数据库对不上了

解决方案

很多人写更新缓存数据代码时,先删除缓存,然后再更新数据库,利用后续的业务请求把数据重新装载进缓存。

其实这个逻辑是错误的。因为这两步操作不是原子性的,在并发环境下,可能线程A删除了Redis缓存后,还未写库,线程B就来读取,发现缓存为空,则会查询数据库写入缓存,此时缓存中为脏数据。

1
2
3
4
5
6
7
8
9
10
    @Override
    public ResponseResult<String> resetTimer(UpdateTopiSetTimeDTO info){
        // 分别获取原状态和新状态
        TopicsetTimeVo timeVo = getBaseMapper().getTopicSetTime(info.getTopicSetId());
        if (timeVo.getTopicsetStatus() == TopicSetStatus.OPEN) {
            return ResponseResult.errorResult(HttpCodeEnum.ERROR.getCode(), "题目集已开放,不可修改开始时间");
        }
        redisCache.deleteObject(RedisConstants.TOPICSET_STATUS);
        getBaseMapper().updateTopicsetTime(info,newStatus);
}

更新缓存的Design Pattern 有四种:Cache Aside、Read Through、Write Through、Write Behind Caching

Cache Aside Pattern

这是最常用的 Pattern,总结来说:

  • 读:先读缓存,未命中则读数据库库,然后取出数据放入缓存
  • 写:先更新数据库,再删除缓存

其具体逻辑如下:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。
1
2
        getBaseMapper().updateTopicsetTime(info,newStatus); // 更新数据库
        redisCache.deleteObject(RedisConstants.TOPICSET_STATUS); // 删除缓存

那么,这种方式是否可以没有文章前面提到过的那个问题呢?

线程A查询数据,线程B更新数据,因为不再是先删除缓存,因此缓存有效,线程A获取的是缓存内容(旧),但是线程B马上就让缓存失效了,后续的查询再从数据库获取数据,同步到缓存。

这是标准的design pattern,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。为什么不是写完数据库后更新缓存?你可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据。

Read/Write Through Pattern 其实就是将同步操作交给缓存代理,读操作由调用方负责把数据载入缓存,写操作如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库;

Write Behind Caching Pattern(Write Back),在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。Write Back 的有点是 I/O 快,还可以合并同一数据的多次操作(视频点赞等);缺点是数据不是强一致性的,可能会丢失。

参考

https://coolshell.cn/articles/17416.html#google_vignette

https://pdai.tech/md/db/nosql-redis/db-redis-x-cache.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E5%92%8C%E7%BC%93%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7

本文由作者按照 CC BY 4.0 进行授权