外观
从支付峰值宕机到零异常:缓存三层防御实战
金融支付场景缓存问题的高危背景
支付系统作为金融核心链路,对数据一致性、可用性要求极高,一旦Redis缓存出现穿透、击穿、雪崩三类问题,轻则导致数据库压力陡增触发宕机,重则引发重复支付、订单状态错乱等资损事故。我们此前在大促峰值场景下,曾遭遇过恶意请求穿透缓存扫库、热key过期瞬间万级请求直打数据库的故障,单小时内资损风险超过百万,因此针对性搭建了三层防御体系,经过多次大促验证可以做到三类问题零发生。
三类缓存问题方案选型对比
我们对比了行业常见的各类解决方案,针对支付场景的强一致性要求做了筛选:1. 缓存穿透场景,常见的空值缓存、布隆过滤器两种方案,空值缓存存在额外内存占用问题,布隆过滤器存在极低误判率,最终我们采用「布隆过滤器+空值短缓存」的组合方案,兼顾性能与准确性;2. 缓存击穿场景,常见的互斥锁、逻辑过期两种方案,逻辑过期会存在数据不一致窗口,不符合支付场景强一致要求,最终我们采用分布式互斥锁的方案,确保同一时间只有一个请求回源更新缓存;3. 缓存雪崩场景,常见的过期时间加随机偏移、多级缓存方案,最终我们采用「过期时间打散+本地二级缓存兜底」的组合方案,避免大面积缓存同时失效引发雪崩。
三层防御体系实操步骤与Redis代码示例
第一层:缓存穿透防御实现
首先针对支付订单这类核心查询场景,我们把所有合法的订单ID提前存入布隆过滤器,所有请求先经过布隆过滤器校验,不存在的ID直接返回参数错误,直接拦截99%以上的恶意穿透请求,Redis实操命令如下:
redis
# 初始化布隆过滤器,错误率设置为0.01,预计元素数量1000万
BF.RESERVE pay_order_bf 0.01 10000000
# 批量插入合法订单ID到布隆过滤器
BF.MADD pay_order_bf order_1001 order_1002 order_1003 order_1004
# 校验请求的订单ID是否存在,返回1代表存在,0代表不存在
BF.EXISTS pay_order_bf order_9999对于布隆过滤器误判的少量不存在的请求,我们设置30秒的短空值缓存,避免这类请求反复穿透到数据库,空值缓存的设置命令如下:
redis
# 不存在的订单ID设置空值缓存,过期时间30秒
SET nonexist_order_9999 "" EX 30第二层:缓存击穿防御实现
针对支付场景的热key,我们采用Redis分布式锁的方式,同一时间只放行一个请求回源查询数据库,其余请求自旋等待缓存更新完成,锁的过期时间设置为比数据库查询最大耗时多2秒,避免死锁,核心操作命令如下:
redis
# 尝试获取订单查询锁,锁过期时间3秒,防止死锁
SET order_query_lock_1001 "uuid_123456" NX EX 3
# 回源查询数据库完成后,写入缓存,过期时间5分钟
SET order_info_1001 "{\"orderId\":1001,\"status\":\"PAID\",\"amount\":1000}" EX 300
# 释放分布式锁,校验锁标识避免误删他人锁
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 order_query_lock_1001 uuid_123456第三层:缓存雪崩防御实现
针对所有缓存key的过期时间,我们在基础过期时间上增加0-60秒的随机偏移,避免大量key同时过期引发雪崩,同时针对TOP 1%的最热支付订单key,我们额外增加Caffeine本地二级缓存兜底,即使Redis集群整体宕机,也能承接90%以上的读请求,避免数据库被直接打垮。为了进一步降低雪崩风险,我们还对核心支付缓存做了Redis集群多副本部署,单节点宕机可以秒级切换到副本节点,不会影响正常服务。
支付场景缓存治理避坑总结
我们在落地这套体系的过程中踩过不少生产故障的坑,总结出3条必须严格遵守的实践准则:
- 布隆过滤器必须提前做扩容预案,常见错误现象是业务量级超出布隆过滤器初始化的预估容量,导致误判率飙升,大量合法订单ID被误拦截,引发用户查询订单失败。修正方法是初始化布隆过滤器时按照预估容量的3倍设置参数,同时开启布隆过滤器的自动扩容配置,预留足够的冗余空间,上线后每季度定期校验布隆过滤器的元素占比,超过70%就提前做扩容迁移。
- 分布式锁绝对不能使用固定字符串作为锁值,常见错误现象是请求A拿到锁之后业务执行超时,锁被自动释放,请求B拿到锁之后写入缓存,此时请求A执行完操作之后直接删除请求B持有的锁,导致锁机制完全失效,大量请求直接打向数据库。修正方法是锁值必须使用全局唯一的UUID标识,删除锁的时候必须通过Lua脚本校验当前锁值和自己持有的标识是否一致,才能执行删除操作,确保锁的安全性,自旋等待的次数不能超过10次,避免大量空转占用CPU资源。
- 空值缓存的过期时间绝对不能设置过长,常见错误现象是不存在的订单ID被设置了几小时甚至几天的长缓存,后续用户刚好创建了相同ID的合法订单,用户查询订单的时候直接读取到空值,引发订单不存在的资损投诉。修正方法是空值缓存的过期时间必须控制在1分钟以内,同时在订单创建成功之后,主动删除对应ID的空值缓存,确保数据一致性,绝对不能把空值缓存的过期时间设置超过5分钟,避免极端场景下的数据不一致问题。 这套三层防御体系我们已经在支付生产环境运行超过18个月,经历过6次大促峰值的验证,缓存穿透拦截率达到99.99%,从未出现过缓存击穿、雪崩引发的数据库宕机事故,整体缓存可用性从原来的99.9%提升到99.999%,完全满足金融支付场景的高可用要求。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。