1 双写策略
2025/3/5大约 3 分钟
什么是双写
双写策略,是绝大多数同学刚刚入门的时候接触到的一种保证数据一致性的方法,也是最符合人的逻辑的一种方法:既然两个地方存的都有,那我两边都修改一下不就可以了。 这就是双写的思想,简单易懂好实现。
如果你第一时间想到的是这个方法,那说明你是一个正常人……
我们来实现一下,这次先看代码,再看流程图。
- 双写策略的源码:
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")
key := fmt.Sprintf("book_%s", id)
//双写策略,同时写Redis和DB
_ = db.Rdb.Set(context.Background(), key, name, time.Second*5).Err()
info := db.Info{}
idInt, _ := strconv.Atoi(id)
info.Save(idInt, name)
// 向客户端发送响应
fmt.Fprintf(w, "双写策略完成。")
}
- 双写策略的简图:

- 双写策略的流程图:(以先写数据库为例)

从流程上看,好像不会有太多的问题。但是,我在代码里吃掉了很多err,这一步问题很大。如果我把所有的Err补全,我们再看一次代码:
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
}
key := fmt.Sprintf("book_%s", id)
if err := db.Rdb.Set(context.Background(), key, name, time.Second*5).Err(); err != nil {
//注意,此时更新失败,是否意味着,缓存和数据库的不一致
_, _ = fmt.Fprintf(w, "缓存更新失败!")
return
}
// 向客户端发送响应
_, _ = fmt.Fprintf(w, "双写策略完成。")
}
此时,双写策略的弊端就出现了:你是先写缓存还是先写数据库呢?
双写策略的问题
这个问题就好比是:过年了,就剩一头猪和一头驴了,你先杀哪一个?
答案很简单,哪个都不好使……
重要
无论先写哪一个,紧跟着便是一个要命的问题:第二个写失败了怎么办?
如何解决?我们来看这段代码,这是暑假训练营的同学给的一个思路:
func SetBookV2(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)
//启动事务
db.DB.Begin()
if err := info.Save(idInt, name); err != nil {
db.DB.Rollback()
_, _ = fmt.Fprintf(w, "数据库更新失败!")
return
}
key := fmt.Sprintf("book_%s", id)
if err := db.Rdb.Set(context.Background(), key, name, time.Second*5).Err(); err != nil {
db.DB.Rollback()
_, _ = fmt.Fprintf(w, "缓存更新失败!")
return
}
db.DB.Commit()
// 向客户端发送响应
_, _ = fmt.Fprintf(w, "双写策略完成。")
}