【软件测试】盘一盘工作中遇到的 Redis 异常测试
在测试工作中,涉及到与 redis 交互的场景变的越来越多了。关于redis本身就不作赘述了,网上随便搜,本人也做过一些整理。
今天只来复盘一下,在测试过程中与 redis 的二三事儿。其中提到的案例是经过抽象化的,用作辅助说明作用,仅供参考。
一、更新 Key 异常
注意点:先删除原 key 再存,还是直接覆盖原 key?
比如:之前 A 服务每8小时去查询一次数据库,更新到缓存里去。后来需求调整,变成当数据库里有变动的时候就会发送MQ消息给服务 A,然后A就去全量拉取库里数据,再更新到缓存。
开发小哥实现的是先删除key再更新,那么可能会导致这个时间如果有大量的请求进来,就不能命中缓存。于是乎建议,当从数据库拉来数据之后,可以先和redis中原来的key值进行对比,删除多余的缓存,其他的覆盖更新。
二、Key的删除和丢失
注意点:考虑key被删除,或者key丢失后对上游的影响。
比如:服务A 会同步一类数据到 redis,然后发消息告诉 服务B。B 收到消息后,拿到 redis 数据去找自己那边 MongoDB里的对应 key,做更新操作,若查不到key,就会删除数据。
此时如果 redis 里产生了数据丢失,key就不存在了,那么同步过后,会导致 MongoDB 里的数据被勿删。
于是乎这里建议方案是:redis 那边涉及要删除key的话,就更新key的值为空[]
,这时候 MongoDB 查询到值为空的key,就去删除对应数据。
另外,如果redis那边key 丢失了,MongoDB这边也别就删数据了,去调用一个实时接口去查询数据然后更新。
三、KEY 过期策略不当造成内存泄漏
首先回顾一下 redis 中 ttl key
指令:
- 当 key 不存在时,返回 -2
- 当 key 存在但没有设置剩余生存时间时,返回 -1
- 否则,返回 key 的剩余生存时间,单位是 s
通常,大多数业务用到redis 都会设置过期时间。接下来,了解一下 key 过期是如何清理的。
定期清理
Redis会定期主动淘汰一批已过期的key(随机抽取一批key检查)。
缺点:可能存在很多KEY已过期,仍未清理。
惰性清理
在获取某个 key 的时候,redis 会检查一下这个 key 如果设置了过期时间并且已经过期,就会删除这个 key,不会返回任何东西。
缺点:如果存在很多未去查询的过期key,就没法走到惰性删除,于是可能会有大量过期的key堆积在内存里,导致内存耗尽。
一般来说,业务会惰性和定期清理配合使用。
内存淘汰机制
但是,如果定期清理漏掉了很多过期的key,然后你也没及时去查,也就没走惰性删除。此时依旧有可能大量过期的key堆积在内存里,导致内存耗尽。
这时候需要内存淘汰机制,有如下几个:
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。这个一般很少用。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key,这个是最常用的。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
以上可以作个了解。
四、查询Redis异常时处理
很多时候,redis 只是做一个缓存机制,如果redis异常或者未取到数据,是否有实时获取数据的兜底方案(查接口 or 查库?),需要考虑。
五、redis 穿透、击穿、雪崩
穿透
用户想要查询一个数据,发现redis内存数据库中没有,也就是说没有命中缓存,也是会向持久层数据库查询,发现也没有,那么本次查询失败。
如果此时,用户很多,高并发场景下都去查这个数据,由于缓存都没有命中,于是压力直接打到持久层数据库那里,这就是缓存穿透。
解决方案可以用布隆过滤器、返回空对象(设置过期时间)。
击穿
缓存击穿,是指一个key非常热点,在不停的扛着高并发,如果这个key失效了,在失效的瞬间,持续的并发量就会穿破缓存,直接打到持久层数据库,就像一个防御墙被凿开一个洞。
解决方案可以设置热点数据永不过期、加互斥锁等。
雪崩
是指在某一个时间段,缓存集中过期失效,或者redis宕机了。
解决方案:
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
关于这3个,之前有过一篇整理:https://www.cnblogs.com/pingguo-softwaretesting/p/14911080.html
六、Redis死锁
Redis锁,小心使用不当造成锁不能释放,陷入死锁。
目前常用的2种锁:
SET Key UniqId Seconds
仅在单实例的场景下是安全的。如果不使用setnx+expire+del
中间环节断了仍可能造成死锁;
如果不用SET Key UnixTimestamp Seconds NX
,高并发下可能存在相同时间戳。
分布式Redis锁:Redlock
此种方式比原先的单节点的方法更安全。
- 安全性:在同一时间不允许多个Client同时持有锁。
- 活性死锁:锁最终应该能够被释放,即使Client端crash或者出现网络分区(通常基于超时机制)。
- 容错性:只要超过半数Redis节点可用,锁都能被正确获取和释放。
七、Redis持久化
当Redis数据需要长久有效时,需要考虑是否做RDB和AOF持久化,一般RDB和AOF配合使用,但做持久化,会影响性能。
目前接触到的业务做持久化的很少见。比如有个推荐系统Redis数据是长久有效的,但却为了响应快不影响性能,未做持久化,而采用了其他的降级方案Hbase,以及业务的兜底等。
八、缓存与数据库双写时的数据一致性
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,允许缓存跟数据库偶尔不一致的情况,那么最后好不要做这个一致性方案。
如果实现这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
但是串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
还有一种适中的方式就是,就是先更新数据库,然后再删除缓存。可能会暂时产生不一致的情况,但是发生的几率特别小。这时候通常并行写数据库和缓存,可以加个事务,都写成功才成功,有一个环节失败了就回滚事务,全失败。
关于双写一致性的问题,其实可以另起一个篇幅来说了,有兴趣的可以网上搜索一下,后续可能会再进行整理。