首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Redis缓存穿透、缓存击穿、热key问题优化 + 内存缓存

Redis缓存穿透、缓存击穿、热key问题优化 + 内存缓存

原创
作者头像
garyhwang
发布于 2020-06-25 08:59:42
发布于 2020-06-25 08:59:42
2.6K0
举报
文章被收录于专栏:潇洒哥写字潇洒哥写字

Redis缓存穿透、缓存击穿问题优化 + 内存缓存

1 背景

广交会线上举办,在第三方服务不能保证稳定性的情况下,为保证官网稳定性,新增数据聚合服务,用于缓存数据,并保护第三方服务,且在第三方服务失败的情况下,能够返回缓存的数据,保证前台能够拿到返回数据。

数据聚合服务使用Redis集群来做数据缓存服务,但是用户可以通过恶意构造数据的方式,让请求越过Redis层,每次都打到第三方请求(缓存穿透);同时缓存的数据有生存期,在数据失效的那一刻,可能有大量请求打到第三方服务(缓存击穿)。

2 实例

品牌橱窗、CF奖展品 这两个接口,其中品牌橱窗是个性化数据,当用户未登录时(游客),看到的是相同的数据,用户登录之后,看到的是个性化的数据(每个人看到的不同);CF奖展品无论是否登录,每个人看到的都是相同的。

2.1 接口协议
2.1.1 品牌橱窗推荐

接口名:DescribeBrandGoodList

请求参数

参数名称

必选

类型

描述

eventId

是

int64

请求id

interfaceName

是

string

接口名

para

struct

accessToken, page, size, lang

accessToken

否

string

用户信息

uid

否

string

用户id

pageIndex

是

int64

页号(第0页开始)

pageSize

是

int64

页容量

lang

否

string

语言(可选zh和en,默认为zh)

2.1.2 CF奖产品推荐

接口名:DescribeCfGoodList

请求参数

参数名称

必选

类型

描述

eventId

是

int64

请求id

interfaceName

是

string

接口名

para

struct

access_token, page, size, lang

pageIndex

是

int64

页号(第0页开始)

pageSize

是

int64

页容量

lang

否

string

语言(可选zh和en,默认为zh)

2.2 分析

数据请求过程是,前端发起请求到数据聚合服务,处理的流程图如下:

Redis key的设计为:

  • 未登录 apiName_page_size_language
  • 登录 uid_apiName_page_size_language

未登录的场景:

  1. 前台正常请求的page是规律的,size是固定的,language只会传入zh或en,但是如果用户恶意改变这三个值来请求,每次就无法在打到redis的key,请求会到第三方服务,这样会造成缓存穿透。
  2. 当请求到第三方服务失败之后,没有数据写入redis,这样大量请求时也会出现缓存穿透
  3. 这两个接口的数据,每五分钟会改变一次,所以redis中缓存的数据需要设置(逻辑)生存期(5min),以免用户永远拿到相同的数据;当数据失效的那一刻,如果大量的请求打过来,会全部请求第三方服务,造成缓存击穿。
  4. 接口请求首页时,所有的请求都是同一个redis key,当有大量的请求时,会全部命中到同一个key,热key的问题会影响redis的性能。

用户登录场景:

用户登录后会有一个uid,这个uid可以认为是无法伪造的,所以可以等同于未登录的场景来考虑。但是带uid的redis key需要设置真实的过期时间(可以较长,如6小时),避免百万用户请求数据始终缓存。

3 解决方案

3.1 缓存穿透问题

针对pageSize和language的问题,可以和前台约定好,后端固定pageSize,不依赖前端传入;language固定两种入参校验。pageNum这个字段不太好控制,理论上我们不应该限制用户选择查看的页数,但是针对当前场景,用户无法直接选择查看的页号,每次"换一批"只能向前翻动一页,在合理的情况的下,我们限制最大的查看页号为500,如果超过500,认为为非法请求,返回第1页内容。

如果第三方服务请求失败,要在redis中set一个空值,如"null",并设置一个较短的生存期(2秒),防止短期内大量请求打到服务端,设置较短的生存期避免请求长时间拿不到数据。这里还要考虑缓存击穿的问题。

3.2 缓存击穿问题

每个redisKey的逻辑过期时间为5min,针对redisKey失效,大量请求同时并发打到后台服务的问题,这里使用redis实现一个分布式锁来解决。

采用setnx命令来实现redis分布式锁,若setnx成功,表示获取到了锁,则请求第三方服务,并将数据更新到redis,释放锁;没有获取到锁的进程,可以直接返回默认数据。这样可以保证当redisKey失效的那一刻,只有一个进程请求第三方服务并更新redis。

3.3 redis热key问题

redis中的数据是一个定时任务(3min执行一次,缓存前20页)异步请求第三方服务,更新到redis的,未登录的情况下,理论上请求不会到第三方服务,都会命中redis且没有逻辑过期。我们对redis热key拼接上0~19的数字,将一个热key转化为20个key,每次请求的时候,生成一个0~19的随机数,这样就能将热key打散,避免redis热key的问题。定时更新redis的时候,更新20个key,请求时redisKey重新设计为apiName_page_size_language_randomNum

4 内存缓存

虽然redis的性能已经比较优秀了,但是为了保证在大规模并发请求时(压测)服务稳定性,我们考虑在redis缓存之前,加上一层内存缓存,将前20页的数据缓存在本地内存,请求若命中内存则直接返回,这样使得请求响应更快更稳定。

image.png
image.png

使用ristretto来做内存缓存,可以控制缓存的时间、数量、大小、淘汰策略等。

经过内存缓存的优化之后,压测接口的响应时间从ms级提升到了µs级,且压测锯齿明显减少。

下表是一轮压测的情况:

处理耗时

数量

占比

< 1ms

7236615

99.972%

< 5ms

1141

< 10ms

301

< 15ms

106

< 30ms

151

< 50ms

183

< 100ms

128

> 100ms

0

总计

7238625

这里有一个现象是,虽然请求都是命中内存就直接返回了,但是在并发请求量太大的情况下,仍然会存在100ms左右的响应时间。

5 结束

本文介绍了广交会项目后台用到的两种缓存和相关的优化方法。使用两级缓存还有一个问题就是缓存数据的实时性的问题,这里缓存的过期时间和更新时间需要设置好,不然会出现一致性的问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Redis缓存穿透、缓存击穿问题优化 + 内存缓存
    • 1 背景
    • 2 实例
      • 2.1 接口协议
      • 2.1.1 品牌橱窗推荐
      • 2.1.2 CF奖产品推荐
      • 2.2 分析
    • 3 解决方案
      • 3.1 缓存穿透问题
      • 3.2 缓存击穿问题
      • 3.3 redis热key问题
    • 4 内存缓存
    • 5 结束
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档