Redis和MySQL的双写一致性指的是在同时使用缓存和数据库存储数据的时候,保证Redis和MySQL中数据的一致性。
用户发起请求,先从Redis中查取数据,有数据就直接返回,没有数据就从MySQL中查询数据,并且存储到Redis中,然后返回。从MySQL中查询到数据再存入Redis中这个步骤称为回写。
上述这种有回写的缓存称为读写缓存,仅仅用于查询的缓存称为只读缓存,只读缓存中的数据是通过命令或者批量脚本从MySQL中写到Redis的。
对于读写缓存,如果需要尽可能保证数据库和缓存数据一致,使用同步直写策略,写数据库后也同步写Redis缓存;如果数据库和缓存的数据同步容许有一定的时间间隔,比如仓库系统,就可以使用异步缓写策略,写数据库的一段时间后再同步缓存,当出现异常情况需要对数据进行修补的时候,也可能需要使用异步换写策略,比如用Kafka或RabbitMQ之类的消息中间件重写数据。
源码地址,文中只展示关键代码。
双检加锁策略
从缓存中查询两次,并且加上互斥锁。
func (dao *UserDAO) FindByID(c context.Context, userID int64) (u domain.User, err error) {
db := dao.db
rdb := dao.rdb
key := fmt.Sprintf("user:%v", userID)
// 1. 从缓存中查询数据,如果有数据就返回
var user domain.User
val, err := rdb.Get(c, key).Result()
if val != "" && err == nil {
err := json.Unmarshal([]byte(val), &user)
if err == nil {
return user, nil
}
}
// 2. 没有查到数据就加锁再查一次
mu.Lock()
defer mu.Unlock()
val, err = rdb.Get(c, key).Result()
// 2.1 从缓存中查到数据就直接返回
if val != "" && err == nil {
err := json.Unmarshal([]byte(val), &user)
if err == nil {
return user, nil
}
}
// 2.2 没有从缓存中查到数据就从数据库中查询
err = db.Where("id=?", userID).First(&user).Error
if err != nil {
return user, err
}
// 3. 将从数据库中拿到的数据写到缓存中
userStr, err := json.Marshal(user)
if err == nil {
rdb.Set(c, key, userStr, 1000*time.Second)
}
return user, nil
}
数据库和缓存一致性的几种更新策略
上面说的是查询策略,接下来说一下数据库和缓存一致性的更新策略。
可以停机的情况:
比如先往MySQL中灌入1万条数据,再同步到Redis中,可以在凌晨升级,给出升级提示。
不可以停机的情况:
先更新数据库,再更新缓存(不可行)
异常情况1:
更新Redis出现异常时导致的问题。
异常情况2:
并发情况下执行顺序的不确定性导致的问题。
先更新缓存,再更新数据库(不可行)
和1一样,因为并发可能造成MySQL和Redis中的数据不一致。并且一般要把MySQL作为底单数据,保证最后解释。
先删除缓存,再更新数据库(不可行)
两个并发操作,一个时更新操作,一个是查询操作,由于执行顺序的不确定性,可能导致缓存中存储的是旧数据,并且一直是旧数据。
可以悲观地认为在A更新数据期间,一定会有B来读取数据,在A写完数据库之后,延迟一段时间,再次删除缓存中的数据。但是当业务中读取数据库和写缓存的时间不好估算时,这个延迟的时间不好设置。
先更新数据库,再删除缓存
先更新数据库也不是完全能保证数据一致性的,但是造成的影响比较小。只是在缓存删除失败或者来不及删除的时候,导致查询请求访问Redis时缓存命中,读取到的是缓存旧值。
func (dao *UserDAO) UpdateUserData(c context.Context, userID int64, name string) (user User, err error) {
db := dao.db
rdb := dao.rdb
key := fmt.Sprintf("user:%v", userID)
user.ID = userID
// 先更新数据库中的数据
u := User{
Name: name,
}
err = db.Model(&user).
Select("Name").
Where("id=?", userID).Updates(u).Error
if err != nil {
return user, err
}
// 再删除缓存中的数据
err = rdb.Del(c, key).Err()
if err != nil {
return user, err
}
return user, nil
}
比较稳妥的方式
通过非业务代码订阅MySQL的binlog日志,将对应的缓存删除,如果没有删除成功,就将未成功的数据发送到消息队列中,从消息队列中读取数据进行删除缓存的重试,删除缓存成功就把对应数据从消息队列中删掉,重试超过一定次数后向业务层报错,提醒开发或者运维人员进行处理。
学习地址
缓存双写一致性:www.bilibili.com/video/BV13R…