go高并发之路——缓存击穿

4 篇文章 0 订阅
3 篇文章 0 订阅
本文讨论了在直播业务中如何应对缓存击穿问题,如直播间优惠券抢购时大量并发导致DB压力过大。通过分析,作者提出了使用互斥锁来限制并发请求,确保在缓存失效时只有一个请求访问数据库,从而减轻DB负担。
摘要由CSDN通过智能技术生成

缓存击穿,Redis中的某个热点key不存在或者过期,但是此时有大量的用户访问该key。比如xxx直播间优惠券抢购、xxx商品活动,这时候大量用户会在某个时间点一同访问该热点事件。但是可能由于某种原因,redis的这个热点key没有设置,或者过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,造成DB突刺,CPU和内存瞬间被打满,最终导致服务崩溃。

本人所负责的业务就存在这样的场景,以直播间邀请榜单为例,顾名思义就是会查询该直播间实时的邀请人数,统计前30名邀请人数最多的用户展示在直播间里面,通过榜单去刺激C端用户的分享参与热情。下面一起分析下这个场景遇到的问题和解决方案。

问题1:
统计邀请榜单需要加载实时的,即我邀请一个人进来,假设在前30名,那我不得上榜吗?那问题来了,这种数据我是不是得实时去查数据库呢?

解决方案:这种业务,我们一般会设置一个短时间的缓存,比如30秒左右。也就是在缓存失效后,即30秒去查一次数据库,不然数据库肯定是顶不住的。

问题2:
我们常规的设置缓存的代码逻辑可能是下面这种。(代码片段错误处理等细节请自行处理,这是一段精简版的代码,主要介绍Redis的处理逻辑)

	//step1:读缓存,存在则返回结果
	ctx := context.Background()

	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "123456",
		DB:       0,
	})

	redisKey := "xxx_xxx_xxx" //邀请榜单数据的key

	res, err := rdb.Get(ctx, redisKey).Result()
	if err == nil {
		return res
	}

	//step2:不存在缓存,读DB
	//此处省略,查DB的数据,结果为res

	//step3:设置缓存,并返回结果
	args := redis.SetArgs{
		TTL:  time.Second * 30,
		Mode: "EX",
	}
	_, _ = rdb.SetArgs(ctx, redisKey, res, args).Result()
	
	return res

这种代码逻辑在并发量小的情况下是没有任何问题的,事实上我平时写一些业务,基本上就把它当成一个“公式”来用,用的非常多。然而,在一些高并发的场景下,这种逻辑就会出现问题。试想一下这个场景:假如某个大直播(用户量巨大)是在晚上8点开播,那么8点一到,那个瞬间就会有大量的C端用户进入直播间,去调用后端的接口,假如此时接口的Redis缓存已经过期或者不存在,那么这一刻就会有大量的请求落到DB上,可想而知这一刻DB的压力是多么巨大(这谁顶得住啊)。这就是一个典型的缓存击穿的业务场景。
那么我们需要怎么做,才能让我们的服务抵抗住瞬时的请求洪峰呢?

解决方案:
解决缓存击穿的常见方法有几种:
1、设置该key永不过期,那么就不会存在缓存失效、过期等问题。但这种方法很明显不适合我这种场景,因为我上面提到过,我这个key值存的是邀请榜单的数据,是动态更新的,在直播中,这个榜单的数据是会变化的,所以只能设30秒的缓存时间。该方案行不通。

2、人工干预该key,比如写一个脚本去定时读DB数据,然后更新这个key,然后业务侧(对接前端的接口)只能通过读该key的缓存去获取结果数据,而不能直接读DB。这样也能解决问题,但是貌似维护成本有点高,而且业务侧不能读DB也很不灵活,你想下如果每个热点key都这样去设置维护,那估计会很烦吧。该方案也行不通。

3、使用互斥锁,即在缓存失效的时候,只有一个请求可以获取到互斥锁,然后去查DB,最后重建缓存。这种方案就能很好地解决缓存击穿这个问题,也是我在工作中用来应对缓存击穿问题的最常用的方案。下面是精简版代码:

	//step1:读缓存,存在则返回结果
	ctx := context.Background()

	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "123456",
		DB:       0,
	})

	redisKey := "xxx_xxx_xxx" //邀请榜单数据的key

	res, err := rdb.Get(ctx, redisKey).Result()
	if err == nil {
		return res
	}
	
	//step2:不存在缓存,加互斥锁,读缓存
	lockKey := "yyy_yyy_yyy" //互斥锁的key

	argsLock := redis.SetArgs{
		TTL:  time.Second * 3,
		Mode: "NX", //不存在时才执行
	}

	_, err = rdb.SetArgs(ctx, lockKey, "1", argsLock).Result()
	if err != nil { //获取互斥锁失败
		for i := 0; i < 3; i++ { //重复三次去读缓存值
			res, errRetry := rdb.Get(ctx, redisKey).Result()
			if errRetry == nil { //重试读缓存成功,则返回结果
				return res 
			}
			time.Sleep(10 * time.Millisecond) //这里睡眠时间根据业务来定,取的是另一个线程从读数据库到设置缓存成功的大概时间区间
		}
		return nil //如果循环三次,都读不到缓存,则返回空结果
	}

	//step3:获取互斥锁成功,则表明当前的线程/协程拥有查DB的权力
	//此处省略,查DB的数据,结果为res

	//step4:设置缓存,删除互斥锁,并返回结果
	args := redis.SetArgs{
		TTL:  time.Second * 30,
		Mode: "EX",
	}
	_, _ = rdb.SetArgs(ctx, redisKey, res, args).Result()
	
	rdb.Del(ctx, lockKey) //删除互斥锁
	
	return res

以上就是个人在线上的一些项目面对缓存击穿问题,所做的一些处理方案了。当然这个方案也不是完美的,例如当获取到互斥锁的当前线程/协程,出现异常,导致设置缓存失败,那么其他线程/协程就重试3次可能都获取不到正常结果,最后返回了一个空结果给前端。感兴趣的朋友可以想想这个方案还有什么问题,然后能怎么优化,欢迎指出

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值