2 读更新 写删除
1. 用事务来保证数据一致性
有同学肯定能够想出一个相当靠谱的方式来保证数据的一致性,比如有些同学提出了一个想法:
我先起一个事务,将数据写到数据库里,但是不提交,等到Redis的更新成功以后,再提交事务。如果Redis更新失败,那么我就回滚事务。这样子就可以保证数据库和缓存的一致了。
我只能说,牛啊。提前很多个版本就领悟到了**两阶段提交。**这个思路很好,但是需要写得代码,还有业务上的成本都非常高。并且一定会出现问题:
当缓存不可用时,想要通过接口去修改数据库里的值,怕是再也无法成功了。如果真的出现极端情况。那么有没有又简单又好用的方法?读更新,写删除。
重要
读更新,写删除是目前最常用的方案,请大家务必掌握,并尽可能去使用。这个策略仅仅是再回溯的基础上增加一个更新数据后,清除缓存的操作。
2. 读更新,写删除
2.1. 代码块
func GetBook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "只支持 GET 请求", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
//先获取redis
key := fmt.Sprintf("book_%s", id)
cache, _ := db.Rdb.Get(context.Background(), key).Result()
if cache != "" {
_, _ = fmt.Fprintf(w, "这是缓存的结果:"+fmt.Sprint(cache))
return
}
//redis 不存在就查询数据库
info := db.Info{}
data := info.Get(1)
//如果数据存在,就写缓存
if data.ID > 0 {
_ = db.Rdb.Set(context.Background(), key, fmt.Sprint(data), time.Second*5).Err()
}
// 向客户端发送响应
_, _ = fmt.Fprintf(w, "这是查数据库的结果:"+fmt.Sprint(data))
}
func SetBook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "只支持 Get 请求", http.StatusMethodNotAllowed)
return
}
name := r.URL.Query().Get("name")
id := r.URL.Query().Get("id")
//先写数据库,然后删除掉缓存。
info := db.Info{}
idInt, _ := strconv.Atoi(id)
if err := info.Save(idInt, name); err != nil {
_, _ = fmt.Fprintf(w, "数据库更新失败!")
return
}
//这里不再更新缓存,而是直接删除数据。
key := fmt.Sprintf("book_%s", id)
_ = db.Rdb.Del(context.TODO(), key).Err()
_, _ = fmt.Fprintf(w, "数据更新完成,缓存已经清除!")
}
重要
非常重要:redis在使用的过程中,一般不会建议使用del命令。因为,存在一个大Key删除的问题。我们依然会在项目结束后再讨论这个问题。
所以,我们在这里将删除缓存优化为,将缓存有效期改为0。
func SetBookV1(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "只支持 Get 请求", http.StatusMethodNotAllowed)
return
}
name := r.URL.Query().Get("name")
id := r.URL.Query().Get("id")
//先写数据库,然后删除掉缓存。
info := db.Info{}
idInt, _ := strconv.Atoi(id)
if err := info.Save(idInt, name); err != nil {
_, _ = fmt.Fprintf(w, "数据库更新失败!")
return
}
//将del命令改造为过期时间改为0 这是错误写法
key := fmt.Sprintf("book_%s", id)
_ = db.Rdb.Expire(context.TODO(), key, 0).Err()
db.Rdb.PExpire(context.TODO(), key,time.Millisecond*1).Err()
_, _ = fmt.Fprintf(w, "数据更新完成,缓存已经清除!")
}
解释一下:
//设置为永不过期
db.Rdb.Expire(context.TODO(), key, 0).Err()
//设置有效期为 1 ms
db.Rdb.PExpire(context.TODO(), key,time.Millisecond*1).Err()
提示
引申: 这里用到了 Redis 淘汰缓存数据的策略
2.1 流程图

“读更新,写删除”的逻辑非常非常简单,也是最常用的一种方法。但它也不是万能的,比如我们再来讨论下这个问题:你是先更新数据库再删缓存,还是先删缓存再写数据库呢?
3. 存在哪些问题
提示
很遗憾,猪和驴的故事又上演了。哪个都不好使……
先更新数据库,再删除缓存。这个方法和之前双写存在一样的问题,缓存更新失败,会导致数据不一致。先删缓存,再更新数据库。则存在并发问题,也即更新数据库的时候有其他人把旧数据回溯到缓存了,依然存在数据不一致的情况。或者,存在读写分离的策略,数据更新需要一定的事件。
虽然读更新,写删除依然存在问题,但它仍然是最常用的缓存一致性方案。因为它足够简单,并且在很大程度上能够保证可靠。
相关信息
引申:既然存在删除缓存失败的问题,那我加上重试机制不就解决了么?