Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis MySQL双写一致性

Redis和MySQL保证双写一致性的问题解析

作者:仍沫

Redis和MySQL的双写一致性指的是在同时使用缓存和数据库存储数据的时候,保证Redis和MySQL中数据的一致性,那么如何才能保证他们的一致性呢,下面小编就来为大家详细讲讲

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.先更新数据库,再更新缓存(不可行)

异常情况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日志,将对应的缓存删除,如果没有删除成功,就将未成功的数据发送到消息队列中,从消息队列中读取数据进行删除缓存的重试,删除缓存成功就把对应数据从消息队列中删掉,重试超过一定次数后向业务层报错,提醒开发或者运维人员进行处理。

到此这篇关于Redis和MySQL保证双写一致性的问题解析的文章就介绍到这了,更多相关Redis MySQL双写一致性内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文