首页 > 其他分享 >go高并发之路——缓存击穿

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

时间:2024-05-05 20:23:13浏览次数:21  
标签:缓存 互斥 res redis DB 并发 key go

缓存击穿,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次可能都获取不到正常结果,最后返回了一个空结果给前端。感兴趣的朋友可以想想这个方案还有什么问题,然后能怎么优化,欢迎指出

标签:缓存,互斥,res,redis,DB,并发,key,go
From: https://www.cnblogs.com/lmz-blogs/p/18173813

相关文章

  • 如何基于surging跨网关跨语言进行缓存降级
    概述      surging是一款开源的微服务引擎,包含了rpc服务治理,中间件,以及多种外部协议来解决各个行业的业务问题,在日益发展的今天,业务的需求也更加复杂,单一语言也未必能抗下所有,所以在多语言行业解决方案优势情况下,那么就需要多语言的协同研发,而对于协同研发环境下,统一配置......
  • Django-rest-framework框架
    【一】drf入门规范【二】序列化组件【三】请求与响应【四】视图组件【五】路由组件【六】认证组件【七】权限组件【八】频率组件【九】过滤与排序【十】异常捕获【十一】分页组件【十二】生成接口文档【十三】序列化类源码分析【十四】JWT介绍【十五】simple-jwt简......
  • 为什么我要使得GOLang重写SAAS(软件即服务)服务端
    引言“道”在中国哲学中,是一个重要的概念,表示“终极真理”。“道”这一概念,不单为哲学流派诸子百家所重视,也被宗教流派道教等所使用。大道至简的意思就是大道理是极其简单的,简单到一两句话就能说明白。所谓“真传一句话,假传万卷书”。正文在开启独立创作之路之前,我主要用不用......
  • 20240504 —— Goodbye 2024(1/3).
    很久没有用心写过随笔了。写随笔对我来说是个很困难的事情,因为我文笔烂完了。每次都是写前觉得有一堆东西可以写,写的时候就不知道咋连结在一起,最后乱写了一堆发出来。2024/5/422:39看了会物理书后累了打了会块(TETR.IO),3:5,DEFEAT。怎么回事呢。打完前两局感觉对手硬实力不是......
  • spring三级缓存
    第一级缓存:singletonObjects第二级缓存:earlySingletonObjects第三级缓存:singletonFactories先从“第一级缓存”找对象,有就返回,没有就找“二级缓存”;找“二级缓存”,有就返回,没有就找“三级缓存”;找“三级缓存”,找到了,就获取对象,放到“二级缓存”,从“三级缓存”移除。 在第......
  • Goose:Go语言渐进式的数据库迁移工具
    Goose:Go语言渐进式的数据库迁移工具原创 K8sCat 源自开发者 2024-05-0422:57 广东 听全文源自开发者专注于提供关于Go语言的实用教程、案例分析、最新趋势,以及云原生技术的深度解析和实践经验分享。214篇原创内容公众号数据库迁移是软件开发过程......
  • go高并发之路——出发
    工作7年有余了,B端和C端业务都做过不少,打算整理分享一些自己在实际工作中所遇到的高并发的场景和解决方案,也是对自己本人职业生涯中的一些经验的总结和感悟。与其他博文略有不同的是,这些基本上都是自己实际工作中遇到的,并且线上的解决和处理方案,即真正的理论和实践的结合。先交代......
  • Go-高性能实用指南(全)
    Go高性能实用指南(全)原文:zh.annas-archive.org/md5/CBDFC5686A090A4C898F957320E40302译者:飞龙协议:CCBY-NC-SA4.0前言《Go高性能实战》是一个完整的资源,具有经过验证的方法和技术,可帮助您诊断和解决Go应用程序中的性能问题。本书从性能概念入手,您将了解Go性能背后的......
  • Go-分布式计算(全)
    Go分布式计算(全)原文:zh.annas-archive.org/md5/BF0BD04A27ACABD0F3CDFCFC72870F45译者:飞龙协议:CCBY-NC-SA4.0前言Go编程语言是在Google开发的,用于解决他们在为其基础设施开发软件时遇到的问题。他们需要一种静态类型的语言,不会减慢开发人员的速度,可以立即编译和执行,利......
  • Go-标准库秘籍(全)
    Go标准库秘籍(全)原文:zh.annas-archive.org/md5/F3FFC94069815F41B53B3D7D6E774406译者:飞龙协议:CCBY-NC-SA4.0前言感谢您给予本书机会!本书是一本指南,带您了解Go标准库的可能性,其中包含了许多开箱即用的功能和解决方案。请注意,本书涵盖的解决方案主要是对标准库实现的简......