likes
comments
collection
share

社区平台Redis测试实践

作者站长头像
站长
· 阅读数 13

社区平台Redis测试实践

Redis作为一款优秀的内存存储系统,深受互联网公司的青睐;同样也是我们雪球的技术选型之一,比如雪球的行情股票信息,帖子信息等都会用到Redis作为缓存服务。QA在日常测试的过程中,经常也会遇到和Redis相关的一些问题,也踩过一些坑,通过本文今天在这跟大家分享一下。

接下来我们先来快速了解一下Redis。

一、Redis快速入门

Redis简介

REmote DIctionary Server(Redis) 是一个基于 key-value的内存存储系统,是跨平台的非关系型数据库。

社区平台Redis测试实践

现在很多公司都会用到Redis,那它为什么这么受欢迎呢,这就要从Redis的架构设计说起,接下来看看Redis的架构是如何设计的。

Redis架构设计

Redis的架构设计总的来说满足高性能、高可靠和可扩展。

社区平台Redis测试实践

Redis常用数据结构

Redis常用的数据结构主要有以下5种:

社区平台Redis测试实践

二、Redis使用场景

缓存

社区平台Redis测试实践

如上图为雪球APP手机号验证码登录界面,输入手机号,点击发送验证码,手机会收到一条短信验证码信息。这里就用了Redis来存储验证码,并设置了过期时间为5分钟。

服务端验证码存储方法:

public void setAndExpireSmsVerificationCache(UcTelephone ucTelephone, SmsVerification smsVerification, int expireAt) {
    String cacheKey = SmsCacheKeyEmum.SMS_VERIFICATION.key(ucTelephone.areaCode(), ucTelephone.number());
    usercenterV4.setAndExpire(cacheKey, JsonUtils.write(smsVerification),expireAt);
}

Redis存储:

redis-cli -h ${host} -p ${port} ttl sms:verification:86:${tel}
300  
redis-cli -h ${host} -p ${port} get sms:verification:86:${tel}
{"code":"2294","createdAt":"Jun 14, 2022 10:45:28 AM","times":1,"verifyTimes":0,"status":false}

计数器

社区平台Redis测试实践

上图为雪球APP,关注用户功能,业务限制普通用户每天最多允许关注100个用户,超过则提示;

我们可以想象一下,服务端应该是有一个计数器,用户每关注成功一人,计数器+1;每次点击关注按钮,会先判断当前计数器是否超过100,超过则提示「你今天已关注100人,请改天再试」。

在本例中,查询Redis,用户uid1在2023-02-07的日关注数Redis的值,已达到100,所以当天无法继续关注用户。

redis-cli -h ${host} -p ${port} get DAILYFUSER_2023-02-07_uid1
100

如下为Redis计数服务端代码实现,因为是用String数据结构存储的,Redis提供了incr()接口来实现+1操作。

Long followerNum = JedisCluster.master().incr(RelationKeys.createrFollowDailyKey(follower.getId()));

分布式锁

实现分布式锁也是Redis非常常见的使用场景。

下面以雪球的直播业务为例来介绍。用户在雪球直播,需要创建直播间,业务上是一个用户只能创建一个直播间。在测试的过程中,发现存在这么一个问题:多个用户并发创建直播间时会存在Bug,不同的用户会同时创建成功同一个直播间。

如下图:

社区平台Redis测试实践

为了解决这个问题,可以使用Redis分布式锁,优化代码,代码片段如下:

社区平台Redis测试实践

其中实现分布式锁的核心方法是setexnx(),该方法最终调用的是Redis的 set Key Value EX seconds NX 命令,该命令只在Key不存在时才对Key进行设置操作,并返回OK。如上面代码所示如果setexnx返回结果不是OK,则表明锁已存在,那么本次加锁操作失败,业务报错。

这里有一个需要注意的是finally代码块最终会释放锁,那为什么在加锁的时候还要设置过期时间呢?这个主要是为了防止意外发生,程序在走到finally之前,比如程序退出,导致无法释放锁,所以设置了一个兜底的策略,60s后自动释放锁。

三、Redis测试案例

案例一:用户分组查询接口升级

项目背景:用户分组查询从原来的RPC接口升级为gRPC接口,升级前后业务逻辑需保持完全一致。

社区平台Redis测试实践

发现的问题:

在测试过程中,发现分组信息缓存失效后,分组信息无法正常加载。

问题产生原因:

通过分析得知,查询分组信息,首先会从Redis里查询用户的分组成员,当缓存无数据时,会从DB加载数据并回写到缓存。接下来对比下接口升级前后Redis里的数据。

升级前Redis里的分组数据:

/**升级前Redis value格式为:uid -> 创建时间时间戳取负值 **/
redis-cli -h ${host} -p ${port} zrange MEMBER_MEMBERLIST_GROUPID_3615231762_14257441 0 -1 withscores
9485866208 -1669558282413 
7371699344 -1663674830000

升级后Redis里的分组数据:

/**升级后Redis value格式为:uid -> 创建时间时间戳 **/
redis-cli -h ${host} -p ${port} zrange MEMBER_MEMBERLIST_GROUPID_3615231762_14257441 0 -1 withscores
7371699344 1663674830000
9485866208 1669558282413 

通过对比发现,分组信息的score值不一致。升级前score值为创建时间时间戳取负值,升级后score值为创建时间时间戳,没有对时间戳取负值,最终导致分组信息无法正常加载。

解决方案:

对创建时间戳取负值,保持与原有写入逻辑一致。

总结:对于此类问题,测试过程中不但需要关注Redis值是否写入了,还要保证数据格式的正确性,包括数据结构、字段类型以及各字段的写入格式。

案例二:个股页新帖流数据不更新

问题:

用户反馈,雪球个股页:中天科技(SH600522),新帖流数据不更新。

问题产生原因:

当天雪球内网出现短暂故障,个股页新帖流接口在设置了Redis值后,由于网络错误,导致缓存过期时间设置失败,缓存永不过期(正常情况缓存60s会过期),从而出现部分个股页新帖流下的帖子一直不更新。

参照下图,由于设置Redis值和设置Redis过期时间,不是原子操作,在极端情况下,确实会出现缓存没有过期时间的问题。那从QA的角度,如何避免这种问题呢?

解决方案:

1、Code Review阶段,关注代码里设置Redis值和缓存过期时间,是否使用原子操作,如String结构,可以直接使用setex命令。测试过程中要去关注系统是否会存在缓存过期时间设置失败的问题。

2、一旦发生了缓存过期时间设置失败的问题,是否有相应的应对方案。从测试的角度要多考虑不同场景下系统异常的处理方案,对于极端情况下会出现的缓存无过期时间,系统要增加缓存过期检查机制,对于没有过期时间的缓存,程序要有设置过期时间的机制等。

社区平台Redis测试实践

案例三:行情逐笔Redis大Key优化测试

社区平台Redis测试实践

测试分析:

1、Key读写方式的变化:从原来的读写一级Key变为二级Key,且Key的名称已被改变。

2、二级Key读写必然存在跨Key读写数据的场景,如何进行场景覆盖。 3、由于行情逐笔交易数据量较大,如何验证数据的准确性。

测试覆盖:

1、股票品类覆盖

按照股票品类维度划分,每个品类选择至少一支股票覆盖。

2、Redis Key值覆盖

此次优化,涉及到不同的Redis Key,如盘前:trade:ext_pre:{symbol},盘中:trade:{symbol},盘后:trade:ext_after:{symbol} 等共6类Redis Key。

3、相关业务回归

梳理相关涉及到的此次的Redis Key的相关业务,并进行测试覆盖。

因新老key数据量较大,通过如下脚本来验证数据量是否一致。

#!/bin/bash
key=$1
sum=0
levelkey="shard_${key}"
/** 查询老key里的数据量**/
old=$(sh connect_redis.sh zcard ${key} | awk ' $1>0{print $1}')
/** 查询新key里的数据量**/
list=$(sh connect_redis.sh zrange ${levelkey} 0 -1 | grep trade | grep -v zrange)
for var in ${list}
do
 i=`sh connect_redis.sh zcard ${var} | awk '$1>0{ print $1}'`
 sum=$(($sum+$i))
done
if [ ! -n "$old" ]
 then
   old=0
fi
if [ ! -n "$sum" ]
 then
    sum=0
fi
echo "原KEY数据总数" ${old}
echo "新KEY数据总数" ${sum}
if [ $old -eq $sum ]
then
  echo "数据总数一致"
else
  echo "数据总数不一致"
fi

风险评估:

测试通过后,接下来就会下掉对老Redis Key的读写。

1、如何确定没有别的业务在调用老的Key?

因行情服务较多,如何保证没有任何服务在调用老的Redis Key了,主要是从两个方面,一方面,开发从代码的角度梳理相关服务调用,测试从业务的角度梳理并进行覆盖;另一方面,为了防止有梳理遗漏的地方,可以通过Redis的访问日志,持续观察老Redis Key还有没有访问量。

2、如何将上线风险降到最低?

一是线上逐步放量,持续观察;二是增加新老服务开关,一旦出问题可以快速的切换到老服务。

通过以上案例,介绍了测试过程中曾踩过的一些比较典型的坑,以及相应的应对方案。总结下Redis测试的一些关注点:

1、缓存数据存储逻辑的合理性。缓存数据写入的正确性以及缓存的写入数据格式是否合理。

2、缓存读取逻辑的合理性。有缓存要优先读缓存,无缓存查询数据库,并回写缓存。

3、缓存更新逻辑的合理性。什么情况下要更新缓存,以及缓存失效后是否会更新缓存内容。

4、缓存时间设置的合理性。缓存时间设置太短,会导致频繁访问数据库;时间设置太长,一方面会占用过多内存,造成资源浪费;另一方面会造成用户访问到的一直是老数据。因此要根据业务数据的实际更新频次,设置合理的过期时间。

5、缓存数据是否会重复。同样的数据,应只存在一条,重复的数据会浪费资源。

四、Redis常见问题

Redis有一些比较经典的问题,如缓存穿透,缓存击穿和缓存雪崩。在介绍这几个概念之前,我们先来看一个正常的缓存模型。

正常缓存模型

社区平台Redis测试实践

正常的请求过程

1、用户通过雪球APP访问雪球后端服务

2、后端服务先请求Redis,检查请求内容是否存在

3、如果有数据,Redis将结果返回给后端服务,并执行7;如果没有则会继续往下执行

4、后端服务从数据库中查询请求的数据

5、数据库将查询的结果返回给后端服务

6、如果数据库有返回数据,则将返回的结果添加到Redis

7、将请求的数据返回给客户端

缓存穿透

在高并发场景下,通过接口访问一个缓存和数据库都不存在的数据,此时缓存起不到保护数据库的作用,就像被穿透了一样,导致数据库存在被打挂的风险。

社区平台Redis测试实践

解决方案:

1、对接口请求进行鉴权和非法参数校验

日常我们接口测试的过程中,一些非法参数都需要做校验拦截,比如负数的用户ID、非法的日期格式等,以防止非法请求数据穿透缓存直接打到数据库。

2、当数据库返回空值时,将空值缓存到Redis,并设置一个合理的过期时间

3、使用布隆过滤器,过滤不存在的Key

使用布隆过滤器存储所有可能使用的Key,不存在的Key直接被过滤掉,存在的Key再进一步访问缓存和数据库。

缓存击穿

社区平台Redis测试实践

某个热点数据,比如雪球的一个非常热门的话题,在缓存过期的一瞬间,有大量的用户请求打过来,由于缓存过期,导致大量请求都走到数据库,造成瞬时数据库压力骤增,存在被大打挂的风险。

解决方法:

1、加互斥锁

当热点数据缓存过期,大量请求涌入时,只有第一个用户的请求能获取锁且会阻塞其他用户的请求,此时会给用户相应的提示。第一个请求去查询数据库并把数据写入缓存后释放锁,后续所有的请求都可以直接走到缓存了。

2、对于特别热门的数据可以不设置过期时间或者在缓存快过期时给缓存续期

缓存雪崩

社区平台Redis测试实践

大量的热点数据过期时间相同,导致数据在同一时刻集体失效。造成瞬间数据库请求量大、压力大增,引起雪崩,导致数据库存在被打挂的风险。

解决方案:

1、将热点数据的过期时间打散。

2、加互斥锁。

3、热门话题设置缓存不过期或者临近过期时,给热点数据续期。

以上介绍了缓存穿透、缓存击穿和缓存雪崩,缓存使用过程中非常经典的三大问题,一旦缓存使用不当,就有可能发生。所以在日常测试过程中,需要我们去关注。如下是我们在测试过程中,发现的一个具体的缓存穿透的实例。

需求背景:因业务需要,接口新增返回用户注册时间字段。历史用户注册时间均只存了数据表。

测试发现的问题:

1、大量历史用户及所有的匿名用户,注册时间缓存均不存在。

2、按照当前的业务量对接口压测,大量请求穿透到数据库,使得数据库QPS暴增,存在风险。

解决方案:

1、对无注册时间的用户,增加空缓存,value=0。

2、用户注册,或匿名用户转非匿名用户时,同步更新缓存。

接下来我们一起看看雪球是如何解决缓存相关问题的:

1、Redis数据是否需要设置过期时间以及过期时间设置是否合理。

2、是否存在会被频繁刷库的可能,是否有相应的应对措施。

3、整个系统是否会存在缓存雪崩的可能,一旦发生是否有应对方案。

4、系统是否存在缓存穿透的可能,是否有相应的应对方案。

5、系统是否存在缓存击穿的可能,是否有相应的应对方案。

五、总结

Redis以其高性能、高可靠、可扩展等特性,目前正在被很多公司使用,除了可以被用来做缓存中间件也可以作为数据库存储,用途非常广泛。虽然它已经被验证足够成熟稳定,但使用不当也会导致业务出现各类问题,如文中讲到的缓存穿透、缓存击穿和缓存雪崩等。本文主要讲述了Redis的架构设计、常用数据结构、业务使用场景等,并以QA的视角阐述了我们在测试Redis相关业务场景时的实践经验,希望能给大家提供一些参考与借鉴。