• MySQL结合Redis缓存如何实现双写一致性?
  • 发布于 2个月前
  • 350 热度
    0 评论
  • 苒青绾
  • 0 粉丝 26 篇博客
  •   

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)
        // 堆代码 duidaima.com
	// 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.先更新数据库,再更新缓存(不可行)
异常情况1:
更新Redis出现异常时导致的问题。

异常情况2:
并发情况下执行顺序的不确定性导致的问题。

2.先更新缓存,再更新数据库(不可行)
和1一样,因为并发可能造成MySQL和Redis中的数据不一致。并且一般要把MySQL作为底单数据,保证最后解释。

3.先删除缓存,再更新数据库(不可行)
两个并发操作,一个时更新操作,一个是查询操作,由于执行顺序的不确定性,可能导致缓存中存储的是旧数据,并且一直是旧数据。可以悲观地认为在A更新数据期间,一定会有B来读取数据,在A写完数据库之后,延迟一段时间,再次删除缓存中的数据。但是当业务中读取数据库和写缓存的时间不好估算时,这个延迟的时间不好设置。

4.先更新数据库,再删除缓存
先更新数据库也不是完全能保证数据一致性的,但是造成的影响比较小。只是在缓存删除失败或者来不及删除的时候,导致查询请求访问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
}
5.比较稳妥的方式
通过非业务代码订阅MySQL的binlog日志,将对应的缓存删除,如果没有删除成功,就将未成功的数据发送到消息队列中,从消息队列中读取数据进行删除缓存的重试,删除缓存成功就把对应数据从消息队列中删掉,重试超过一定次数后向业务层报错,提醒开发或者运维人员进行处理。

用户评论