likes
comments
collection
share

Redis和MySQL双写一致性

作者站长头像
站长
· 阅读数 48

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
}

Redis和MySQL双写一致性

数据库和缓存一致性的几种更新策略

上面说的是查询策略,接下来说一下数据库和缓存一致性的更新策略。

可以停机的情况:

​ 比如先往MySQL中灌入1万条数据,再同步到Redis中,可以在凌晨升级,给出升级提示。

不可以停机的情况:

  1. 先更新数据库,再更新缓存(不可行)

    异常情况1:

    更新Redis出现异常时导致的问题。

Redis和MySQL双写一致性

异常情况2:

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

Redis和MySQL双写一致性

  1. 先更新缓存,再更新数据库(不可行)

    和1一样,因为并发可能造成MySQL和Redis中的数据不一致。并且一般要把MySQL作为底单数据,保证最后解释。

  2. 先删除缓存,再更新数据库(不可行)

    两个并发操作,一个时更新操作,一个是查询操作,由于执行顺序的不确定性,可能导致缓存中存储的是旧数据,并且一直是旧数据。

    可以悲观地认为在A更新数据期间,一定会有B来读取数据,在A写完数据库之后,延迟一段时间,再次删除缓存中的数据。但是当业务中读取数据库和写缓存的时间不好估算时,这个延迟的时间不好设置。

Redis和MySQL双写一致性

  1. 先更新数据库,再删除缓存

    先更新数据库也不是完全能保证数据一致性的,但是造成的影响比较小。只是在缓存删除失败或者来不及删除的时候,导致查询请求访问Redis时缓存命中,读取到的是缓存旧值。

Redis和MySQL双写一致性


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
}
  1. 比较稳妥的方式

    通过非业务代码订阅MySQL的binlog日志,将对应的缓存删除,如果没有删除成功,就将未成功的数据发送到消息队列中,从消息队列中读取数据进行删除缓存的重试,删除缓存成功就把对应数据从消息队列中删掉,重试超过一定次数后向业务层报错,提醒开发或者运维人员进行处理。

学习地址

缓存双写一致性:www.bilibili.com/video/BV13R…