#### redis 缓存穿透、击穿、雪崩、数据一致性 ####

38 篇文章 0 订阅
// 解决缓存穿透
if exist := bloomFilter.find(ID); !exist {
    return err("not found")
}


// 查询redis
if v, ok := redis.get(ID); ok {
    return v
}


lock(ID) // 使用redis实现的分布式锁,解决缓存击穿
defer unLock(ID)
// 再查询一次redis
if v, ok := redis.get(ID); ok {
    return v
}
// 查询mysql
if v, ok := mysql.get(ID); ok {
    redis.set(ID)
    return v
}


return err("not found")

缓存穿透

请求没有的数据,所以每次都会redis查不到mysql也查不到。

预防:

1. redis放空值,设置过期

2. bloom filter:类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。google的guava中有实现BloomFilter算法

缓存击穿

大量请求去mysql访问同一条数据,之前redis并没有去缓存这条记录。

避免方案:

    1、个别数据提前预热到Redis,例如在活动整点开始的场景

    2、分库分表 增加mysql读性能

缓存雪崩

预防:

1. redis挂掉的情况

redis高可用集群

2. 缓存集体过期的情况

错开数据过期时间

应对:

降级熔断

mysql和redis数据一致性

摘要:

【1】双删为什么要延时?因为update连的是主库,get连的是从库(回源过程),所以:延时时长=db主从同步时长

【2】延时双删的方式:

        (1)key过期-修改db 。【延时双删在实践中会使用置key过期来实现,即:先过期key然后再改db】

        (2)key过期-修改db-修改key。【相较(2)主动修改key以提高性能】

        (3)另起协程睡眠。【弊端:无持久化机制,大量协程gc耗资源】

首先看下,一般情况下是如何操作Redis与数据库的一致的:

方案1:

  • 更新的时候,先更新数据库,然后再删除缓存
  • 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。

乍一看,一致性问题貌似很好的得到了解决。但仔细一想,你会发现还是有问题:如果先更新了数据库,删除缓存的时候失败了怎么办?那么数据库中是新数据,缓存中是老数据,数据出现不一致了。

方案1的改进,方案2:

先删除缓存,后更新数据库。因为即使后面更新数据库失败了,缓存是空的,读的时候会从数据库中重新拉,虽然都是旧数据,但数据是一致的。

所以方案就变成了:

  • 更新的时候,先删除缓存,然后再更新数据库
  • 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。

但是也不行,因为在高并发的场景下,会出现这样的情况:数据发生了变更,先删除了缓存,然后去修改数据库。此时还没来得及修改,一个请求过来了,去读缓存,发现缓存空了,去读数据库,读到了准备修改前的旧数据,并且把旧数据放到了缓存。随后,数据变更程序完成了数据库的修改。那么完了,这个时候发生数据不一致了......

所以,可行的方案有二:

可行方案1. 延时双删

下面的第五步,线程A再去删一次缓存,可以添加到延时消息队列,消费时重复尝试删除n次,一定要确保删除成功

可行方案2. 串行化(感兴趣的可以看下)

实例:

// 新增:添加到布隆过滤器,添加到数据库,通过kafka添加到es
// 修改:删掉redis当前记录,修改数据库当前记录,通过kafka删掉es当前记录,延时双删redis
// 删除:删掉redis当前记录,删掉数据库当前记录,通过kafka删掉es当前记录,延时双删redis
// 获取:布隆过滤,从redis获取,如果redis没有则从数据库获取,期间加锁,如果锁被抢占,则延时后再从redis获取,获取到则返回,否则去数据库查询
// es里的获取:不走redis

func (service *Service) CreateTopic(ctx context.Context, topicDetail *model.TopicDetail) error {
	if err := dao.TiDBInstance.CreateTopic(ctx, topicDetail); err != nil {
		currErr := fmt.Errorf("[service] CreateTopic dao.TiDBInstance.CreateTopic err: %v", err)
		service.Log.Error(currErr)
		sentry.CaptureException(currErr)
		return err
	}

	f := func() error {
		// 添加到bloom filter
		if err := dao.RedisInstance.SetBitTopics(ctx, []int64{topicDetail.ID}); err != nil {
			currErr := fmt.Errorf("[service] CreateTopic BloomFilter dao.RedisInstance.SetBitTopics err: %v", err)
			service.Log.Error(currErr)
			sentry.CaptureException(currErr)

			// TiDB 补偿
			rowsAffected, delErr := dao.TiDBInstance.DelTopicByIdsWithoutUserBehavior(ctx, []int64{topicDetail.ID})
			if delErr != nil {
				currDelErr := fmt.Errorf("[service] CreateTopic 补偿 dao.TiDBInstance.DelTopicByIds err: %v ", delErr)
				service.Log.Error(currDelErr)
				sentry.CaptureException(currDelErr)
				return currDelErr
			}
			if rowsAffected != 1 {
				rowsAffectedErr := fmt.Errorf(
					"[service] CreateTopic 补偿 dao.TiDBInstance.DelTopicByIds rowsAffected: %v != 1", rowsAffected)
				service.Log.Error(rowsAffectedErr)
				sentry.CaptureException(rowsAffectedErr)
				return rowsAffectedErr
			}

			return err
		}

		// Index to es
		basicUtil.PubEvent(service.Pub, topic.EV_DM_TOPIC, event.DataEventUint64_NEW, topicDetail.ID)

		return nil
	}

	// lock
	return dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockTopic(topicDetail.ID), f)
}

func (service *Service) UpdateTopic(ctx context.Context, topicDetail *model.TopicDetail) (int64, error) {
	var rowsAffected int64

	f := func() error {
		// Redis
		if err := dao.RedisInstance.Del(ctx, []string{model.GetKeyForTopic(topicDetail.ID)}); err != nil {
			currErr := fmt.Errorf("[service] UpdateTopic dao.RedisInstance.Del err: %v, key: %v",
				err, model.GetKeyForTopic(topicDetail.ID))
			service.Log.Error(currErr)
			sentry.CaptureException(currErr)
			return err
		}

		// TiDB
		var updateErr error
		rowsAffected, updateErr = dao.TiDBInstance.UpdateTopicWithoutUserBehavior(ctx, topicDetail)
		if updateErr != nil {
			currErr := fmt.Errorf("[service] UpdateTopic dao.TiDBInstance.UpdateTopicWithoutUserBehavior err: %v id: %v",
				updateErr, topicDetail.ID)
			service.Log.Error(currErr)
			sentry.CaptureException(currErr)
			return updateErr
		}
		if rowsAffected != 1 {
			service.Log.Infof("[service] UpdateTopic dao.TiDBInstance.UpdateTopic rowsAffected: %v != 1", rowsAffected)
			return nil
		} else {
			// Index to es
			basicUtil.PubEvent(service.Pub, topic.EV_DM_TOPIC, event.DataEventUint64_NEW, topicDetail.ID)
		}

		// 延时双删
		go func() {
			time.Sleep(200 * time.Millisecond)

			ddl := time.Now().Add(time.Duration(60) * time.Second)
			for tries := 0; time.Now().Before(ddl); tries++ {
				if err := dao.RedisInstance.Del(context.Background(), []string{model.GetKeyForTopic(topicDetail.ID)}); err != nil {
					currErr := fmt.Errorf("[service] UpdateTopic 延时双删 dao.RedisInstance.Del err: %v, key: %v",
						err, model.GetKeyForTopic(topicDetail.ID))
					service.Log.Error(currErr)
					sentry.CaptureException(currErr)
				} else {
					return
				}

				time.Sleep(time.Second << uint(tries))
			}
		}()

		return nil
	}

	// lock
	return rowsAffected, dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockTopic(topicDetail.ID), f)
}

func (service *Service) DelTopicById(ctx context.Context, id int64) (int64, error) {
	var rowsAffected int64

	f := func() error {
		// Redis
		if err := dao.RedisInstance.Del(ctx, []string{model.GetKeyForTopic(id)}); err != nil {
			currErr := fmt.Errorf("[service] DelTopicById dao.RedisInstance.Del err: %v key: %v", err, model.GetKeyForTopic(id))
			service.Log.Error(currErr)
			sentry.CaptureException(currErr)
			return err
		}

		// TiDB
		var delErr error
		rowsAffected, delErr = dao.TiDBInstance.DelTopicByIdsWithoutUserBehavior(ctx, []int64{id})
		if delErr != nil {
			currErr := fmt.Errorf("[service] DelTopicById dao.TiDBInstance.Del err: %v id: %v", delErr, id)
			service.Log.Error(currErr)
			sentry.CaptureException(currErr)
			return delErr
		}
		if rowsAffected != 1 {
			rowsAffectedErr := fmt.Errorf(
				"[service] DelTopicByIds dao.TiDBInstance.DelTopicByIds rowsAffected: %v != 1", rowsAffected)
			service.Log.Info(rowsAffectedErr)
			return rowsAffectedErr
		} else {
			basicUtil.PubEvent(service.Pub, topic.EV_DM_TOPIC, event.DataEventUint64_DELETE, id)
		}

		// 延时双删
		go func() {
			time.Sleep(200 * time.Millisecond)

			ddl := time.Now().Add(time.Duration(60) * time.Second)
			for tries := 0; time.Now().Before(ddl); tries++ {
				if err := dao.RedisInstance.Del(context.Background(), []string{model.GetKeyForTopic(id)}); err != nil {
					currErr := fmt.Errorf("[service] DelTopicById 延时双删 dao.RedisInstance.Del err: %v, key: %v",
						err, model.GetKeyForTopic(id))
					service.Log.Error(currErr)
					sentry.CaptureException(currErr)
				} else {
					return
				}

				time.Sleep(time.Second << uint(tries))
			}
		}()

		return nil
	}

	// lock
	return rowsAffected, dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockTopic(id), f)
}

func (service *Service) DelTopicByIds(ctx context.Context, ids []int64) (int64, error) {
	var rowsAffected int64

	for _, id := range ids {
		r, err := service.DelTopicById(ctx, id)
		if err != nil {
			continue
		}
		rowsAffected += r
	}

	return rowsAffected, nil
}

func (service *Service) GetTopicByIds(ctx context.Context, preIds []int64, withStatistics, withUserBehavior bool,
	userID string) (map[int64]*model.TopicInfo, map[int64]*model.TopicStatistic, error) {

	topicInfos := make(map[int64]*model.TopicInfo, 0)
	topicStatistics := make(map[int64]*model.TopicStatistic, 0)

	// TODO 因redis暂不支持withUserBehavior
	if withUserBehavior {
		currErr := fmt.Errorf("[service] GetTopicByIds withUserBehavior 暂不支持")
		service.Log.Error(currErr)
		sentry.CaptureException(currErr)
		return topicInfos, topicStatistics, currErr
	}

	// 缓存穿透
	ids, err := dao.RedisInstance.GetBitTopics(ctx, preIds)
	if err != nil {
		currErr := fmt.Errorf("[service] GetTopicByIds BitMap dao.RedisInstance.GetBitTopics err: %v", err)
		service.Log.Error(currErr)
		sentry.CaptureException(currErr)
		return topicInfos, topicStatistics, currErr
	}
	if len(ids) == 0 {
		return topicInfos, topicStatistics, &common.InternalError{
			ErrCode: int32(pb.GetTopicByIdsResp_NOT_FOUND),
			ErrMsg:  pb.GetTopicByIdsResp_ErrCode_name[int32(pb.GetTopicByIdsResp_NOT_FOUND)],
		}
	}

	sw := sync.WaitGroup{}
	// 从bi-svc获取统计数据
	var biErr error
	if withStatistics {
		sw.Add(1)
		go func() {
			biCtx, _ := context.WithTimeout(ctx, 3*time.Second)
			service.Log.Infof("[service] GetTopicByIds bi-svc.TopicStatisticsFromBI ids: %v", ids)
			topicStatistics, biErr = service.TopicStatisticsFromBI(biCtx, ids)

			sw.Done()
		}()
	}

	// 首次从redis获取
	topicInfosRedis, redisGetErr := dao.RedisInstance.GetTopics(ctx, ids)
	if redisGetErr != nil {
		currErr := fmt.Errorf("[service] GetTopicByIds dao.RedisInstance.GetTopics err: %v", redisGetErr)
		service.Log.Error(currErr)
		sentry.CaptureException(currErr)
		return topicInfos, topicStatistics, redisGetErr
	}

	lackArr := make([]int64, 0)        // redis缺失 首次
	lackMap := make(map[int64]bool, 0) // redis缺失 首次 map
	lastLackArr := make([]int64, 0)    // redis缺失 最终

	for _, id := range ids {
		if v, ok := topicInfosRedis[id]; ok {
			topicInfos[id] = v
		} else {
			lackArr = append(lackArr, id)
			lackMap[id] = true
		}
	}

	// 从TiDB获取
	if len(lackArr) != 0 {
		for _, lackID := range lackArr {
			// 缓存击穿
			lockedIDs := make([]int64, 0) // 锁被抢占
			if err := dao.RedisInstance.Lock(ctx, model.GetKeyForLockTopicForGetsByTiDB(lackID)); err != nil {
				lockedIDs = append(lockedIDs, lackID)
			}

			// 锁被抢占 延时再次从redis获取
			if len(lockedIDs) != 0 {
				time.Sleep(500 * time.Millisecond)

				// again: get topicInfo from redis
				internalTopicInfosRedis, internalRedisGetErr := dao.RedisInstance.GetTopics(ctx, lockedIDs)
				if internalRedisGetErr != nil {
					currErr := fmt.Errorf("[service] GetTopicByIds dao.RedisInstance.GetTopics err: %v", internalRedisGetErr)
					service.Log.Error(currErr)
					sentry.CaptureException(currErr)
					return topicInfos, topicStatistics, internalRedisGetErr
				}

				// 添加到结果,并与lackArr差集生成lastLackArr。包含len(internalTopicInfosRedis)==0的情况
				for _, v := range internalTopicInfosRedis {
					if v.TopicDetail != nil {
						topicInfos[v.TopicDetail.ID] = v
						if _, ok := lackMap[v.TopicDetail.ID]; ok {
							lackMap[v.TopicDetail.ID] = false
						}
					}
				}
				for i, v := range lackMap {
					if v {
						lastLackArr = append(lastLackArr, i)
					}
				}
			} else { // 全部加锁成功
				// 生成lastLackArr
				lastLackArr = lackArr
			}
		}

		// 到TiDB
		if len(lastLackArr) != 0 {
			// 仅对lastLackArr解锁
			defer func() {
				for _, lackID := range lastLackArr {
					if unLockErr := dao.RedisInstance.UnLock(ctx, model.GetKeyForLockTopicForGetsByTiDB(lackID)); unLockErr != nil {
						currErr := fmt.Errorf("[redis] GetTopicByIds Redis UnLock return unLockErr: %v", unLockErr)
						service.Log.Error(currErr)
						sentry.CaptureException(currErr)
					}
				}
			}()

			topicInfosTiDB, err := dao.TiDBInstance.GetTopicByIds(ctx, lastLackArr, withUserBehavior, userID)
			if err != nil {
				currErr := fmt.Errorf("[service] GetTopicByIds [从tidb补充] dao.TiDBInstance.GetTopicByIds err: %v", err)
				service.Log.Error(currErr)
				sentry.CaptureException(currErr)

				return topicInfos, topicStatistics, err
			}
			// 添加到结果
			for _, v := range topicInfosTiDB {
				if v.TopicDetail != nil {
					topicInfos[v.TopicDetail.ID] = v
				}
			}

			// 写入redis
			topicInfosTiDBArr := make([]*model.TopicInfo, 0)
			for _, v := range topicInfosTiDB {
				topicInfosTiDBArr = append(topicInfosTiDBArr, v)
			}
			redisSetErr := dao.RedisInstance.SetTopics(ctx, topicInfosTiDBArr)
			if redisSetErr != nil {
				currErr := fmt.Errorf("[service] GetTopicByIds [补充到redis] dao.RedisInstance.SetTopics err: %v", redisSetErr)
				service.Log.Error(currErr)
				sentry.CaptureException(currErr)
				return topicInfos, topicStatistics, redisSetErr
			}
		}
	}

	// 从bi-svc获取统计数据
	sw.Wait()
	if biErr != nil {
		currErr := fmt.Errorf("[service] GetTopicByIds bi-svc.TopicStatisticsFromBI err: %v", biErr)
		service.Log.Error(currErr)
		sentry.CaptureException(currErr)

		return topicInfos, topicStatistics, biErr
	}

	return topicInfos, topicStatistics, nil
}

以上是redis的key有过期策略的情况。

还有一种是redis和数据库强一致,redis的key没有过期策略,强一致的更新数据库和redis,实例:

func (service *Service) CreateItem(ctx context.Context, item *model.Item) error {
	// random key/secret
	item.Key = common.RandStringBytes(32)
	item.Secret = common.RandStringBytes(32)

	// TiDB
	if err := dao.TiDBInstance.CreateItem(ctx, item); err != nil {
		service.Log.Error(err)
		sentry.CaptureException(err)
		return err
	}

	f := func() error {
		// Redis
		if err := dao.RedisInstance.HMSetItem(ctx, model.GetKeyForItemHM(item.Key), item.ConvToRedis()); err != nil {
			service.Log.Error(err)
			sentry.CaptureException(err)

			// TiDB 补偿
			rowsAffected, delErr := dao.TiDBInstance.DelItemByIds(ctx, []int64{item.ID})
			if delErr != nil {
				service.Log.Error(delErr)
				sentry.CaptureException(delErr)
				return delErr
			}
			if rowsAffected != 1 {
				rowsAffectedErr := fmt.Errorf(
					"[service] CreateItem TiDBInstance.DelItemByIds 补偿 rowsAffected: %v != 1", rowsAffected)
				service.Log.Error(rowsAffectedErr)
				sentry.CaptureException(rowsAffectedErr)
				return rowsAffectedErr
			}

			return err
		}
		return nil
	}

	// lock
	return dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockItemString(item.ID), f)
}

func (service *Service) UpdateItem(ctx context.Context, item *model.Item) error {
	f := func() error {
		// TiDB get ori
		oriItems, getErr := dao.TiDBInstance.GetItemByIds(ctx, []int64{item.ID})
		if getErr != nil {
			service.Log.Error(getErr)
			sentry.CaptureException(getErr)
			return getErr
		}
		if len(oriItems) != 1 {
			getItemByIdsErr := fmt.Errorf("[service] UpdateItem dao.TiDBInstance.GetItem notFound, len(oriItems): %v", len(oriItems))
			service.Log.Error(getItemByIdsErr)
			sentry.CaptureException(getItemByIdsErr)
			return getItemByIdsErr
		}

		// TiDB update curr
		rowsAffected, updateErr := dao.TiDBInstance.UpdateItem(ctx, item)
		if updateErr != nil {
			service.Log.Error(updateErr)
			sentry.CaptureException(updateErr)
			return updateErr
		}
		if rowsAffected != 1 {
			service.Log.Infof("[service] UpdateItem dao.TiDBInstance.UpdateItem rowsAffected: %v != 1", rowsAffected)
			return nil
		}

		// Redis update curr
		if err := dao.RedisInstance.HMSetItem(ctx, model.GetKeyForItemHM(oriItems[0].Key), item.ConvToRedis()); err != nil {
			service.Log.Error(err)
			sentry.CaptureException(err)

			// TiDB 补偿
			oriRowsAffected, oriUpdateErr := dao.TiDBInstance.UpdateItem(ctx, oriItems[0])
			if oriUpdateErr != nil {
				service.Log.Error(oriUpdateErr)
				sentry.CaptureException(oriUpdateErr)
				return oriUpdateErr
			}
			if oriRowsAffected != 1 {
				updateOriItemErr := fmt.Errorf("[service] UpdateItem dao.TiDBInstance.UpdateOriItem 补偿 oriRowsAffected: %v != 1", oriRowsAffected)
				service.Log.Error(updateOriItemErr)
				sentry.CaptureException(updateOriItemErr)
				return updateOriItemErr
			}

			return err
		}

		return nil
	}

	// lock
	return dao.RedisInstance.LockWrap(ctx, model.GetKeyForLockItemString(item.ID), f)
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值