为了解决这个问题,可以使用Redis分布式锁,优化代码,代码片段如下:
其中实现分布式锁的核心方法是setexnx(),该方法最终调用的是Redis的 set Key Value EX seconds NX 命令,该命令只在Key不存在时才对Key进行设置操作,并返回OK。如上面代码所示如果setexnx返回结果不是OK,则表明锁已存在,那么本次加锁操作失败,业务报错。
这里有一个需要注意的是finally代码块最终会释放锁,那为什么在加锁的时候还要设置过期时间呢?这个主要是为了防止意外发生,程序在走到finally之前,比如程序退出,导致无法释放锁,所以设置了一个兜底的策略,60s后自动释放锁。
三、Redis测试案例
案例一:用户分组查询接口升级
项目背景:用户分组查询从原来的RPC接口升级为gRPC接口,升级前后业务逻辑需保持完全一致。
发现的问题:
在测试过程中,发现分组信息缓存失效后,分组信息无法正常加载。
问题产生原因:
通过分析得知,查询分组信息,首先会从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大Key优化测试
测试分析:
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有一些比较经典的问题,如缓存穿透,缓存击穿和缓存雪崩。在介绍这几个概念之前,我们先来看一个正常的缓存模型。
正常缓存模型
正常的请求过程
1、用户通过雪球APP访问雪球后端服务
2、后端服务先请求Redis,检查请求内容是否存在
3、如果有数据,Redis将结果返回给后端服务,并执行7;如果没有则会继续往下执行
4、后端服务从数据库中查询请求的数据
5、数据库将查询的结果返回给后端服务
6、如果数据库有返回数据,则将返回的结果添加到Redis
7、将请求的数据返回给客户端
缓存穿透
在高并发场景下,通过接口访问一个缓存和数据库都不存在的数据,此时缓存起不到保护数据库的作用,就像被穿透了一样,导致数据库存在被打挂的风险。