1. 风控场景下的频次计算挑战
在金融支付、电商交易等实时风控场景中,频次计算是最基础也最关键的防线。想象一下双11零点支付洪峰时,每秒要处理数十万笔交易请求,同时还要实时判断"这个用户1分钟内是否已经发起第5次支付"这类规则。传统数据库在这种压力下会直接崩溃,而基于内存的Redis正是解决这一痛点的银弹。
去年我主导某跨境支付平台的风控系统升级时,就遇到过单Redis节点扛不住突发流量的问题。当时峰值QPS冲到15万+,Redis CPU直接飙到100%,触发了风控熔断。事后复盘发现,问题出在简单的incr命令滥用和未做热点数据隔离。这个教训让我深刻认识到:超高并发频次计算不是简单套个Redis就能解决的,需要从数据结构、集群策略到算法优化的全链路设计。
2. 核心架构设计
2.1 分层削峰设计
面对百万级QPS,单层架构必然崩溃。我们的方案采用三级缓冲:
- 本地缓存层:使用Caffeine做JVM内计数,拦截90%的低频请求
- 分布式计数层:Redis Cluster处理跨节点的精准计数
- 持久化层:异步将计数结果落库到TiDB
// 本地缓存使用示例 LoadingCache<String, Long> localCounter = Caffeine.newBuilder() .maximumSize(100_000) .expireAfterWrite(1, TimeUnit.MINUTES) .build(key -> redisTemplate.opsForValue().increment(key));关键点:本地缓存过期时间必须小于Redis的过期时间,避免脏数据
2.2 Redis数据结构选型
不同风控规则需要不同的数据结构支撑:
| 规则类型 | 数据结构 | 优势 |
|---|---|---|
| 滑动窗口计数 | Redis SortedSet | 精确到毫秒的时间窗口控制 |
| 固定周期计数 | String+incr | 内存占用小,性能极高 |
| 设备指纹去重 | HyperLogLog | 海量数据下仅需12KB内存 |
| 复杂规则组合 | Lua脚本 | 原子性执行避免网络往返 |
实测发现,使用ZSET实现滑动窗口时,单个key在1分钟窗口、1000次操作场景下,内存占用比String方案高出40倍。因此我们最终采用分段计数策略:
// 分段计数伪代码 String segmentKey = "rate:" + userId + ":" + (timestamp / segmentDuration); redisTemplate.opsForValue().increment(segmentKey); redisTemplate.expire(segmentKey, windowSize + 10, TimeUnit.SECONDS);3. 性能优化实战
3.1 热点Key解决方案
618大促期间监控发现,某些热门商品的频次计算Key达到了50万QPS。我们采用三种手段组合解决:
- Key分片:将
product:123拆分为product:123:shard1~product:123:shard8 - 本地合并:使用Guava的LongAdder在应用层合并计数
- 读写分离:配置Redis副本专门处理读请求
// 热点key分片示例 int shard = (productId.hashCode() & 0x7FFFFFFF) % shardCount; String shardKey = "product:" + productId + ":shard" + shard;3.2 内存优化技巧
通过Redis内存分析工具发现,大量1分钟过期的Key导致了内存碎片。优化方案:
- 将过期时间从60秒改为63秒(避免整点集中过期)
- 对小于10的计数使用共享计数器
- 启用Redis的active defrag配置
内存对比测试结果:
| 优化措施 | 内存下降幅度 | QPS影响 |
|---|---|---|
| 共享计数器 | 35% | +2% |
| 过期时间离散化 | 18% | 无 |
| 启用内存压缩 | 22% | -5% |
4. 容灾与监控
4.1 降级策略
设计三级降级预案:
- 初级降级:关闭非核心规则检查
- 中级降级:放宽时间窗口(1分钟→5分钟)
- 完全降级:启用本地缓存模式
降级决策基于以下指标实时计算:
public boolean shouldDegrade() { double load = redisMetrics.getInstantLoad(); long timeoutCount = circuitBreaker.getTimeoutCount(); return load > 0.8 || timeoutCount > 100; }4.2 监控埋点
关键监控指标配置:
- 每个规则的99分位延迟
- Redis节点的网络吞吐量
- 内存碎片率
- 热点Key自动检测
我们开发了基于Prometheus+Grafana的监控看板,特别增加了"规则有效性"指标,可以实时显示每条风控规则的命中率与误杀率。
5. 典型问题排查
5.1 计数不准确问题
曾出现本地缓存与Redis计数偏差达15%的情况,根本原因是:
- 本地缓存TTL设置比Redis短2秒
- 网络抖动导致Redis更新失败
解决方案:
// 双重校验机制 Long localCount = localCounter.get(key); Long redisCount = redisTemplate.opsForValue().get(key); if (Math.abs(localCount - redisCount) > threshold) { localCounter.invalidate(key); }5.2 Redis集群脑裂
某次机房网络分区导致出现双主节点,造成计数混乱。现在采用以下预防措施:
- 设置
min-slaves-to-write 1 - 启用
cluster-require-full-coverage no - 部署Redis哨兵集群跨机房监控
6. Java调优经验
6.1 连接池配置
Lettuce客户端最佳实践:
spring.redis.lettuce.pool.max-active=500 spring.redis.lettuce.pool.max-idle=50 spring.redis.lettuce.pool.min-idle=10 spring.redis.lettuce.pool.time-between-eviction-runs=30s注意:在K8s环境中需要额外配置
spring.redis.lettuce.shutdown-timeout=100ms
6.2 序列化优化
测试发现JSON序列化占用了15%的CPU时间。改用Protostuff后:
RedisTemplate<String, Long> template = new RedisTemplate<>(); template.setValueSerializer(new ProtostuffSerializer());性能提升对比:
| 序列化方式 | 吞吐量(万QPS) | CPU使用率 |
|---|---|---|
| JDK | 12.5 | 65% |
| JSON | 15.8 | 45% |
| Protostuff | 18.3 | 38% |
7. 未来演进方向
当前架构在压测中已达到单集群30万QPS的稳定输出,但还有优化空间:
- 试验Redis 7.0的Function特性替代Lua脚本
- 测试KeyDB的多线程版本
- 探索基于Rust编写的RediSQL模块
在最近一次全链路压测中,我们意外发现Nginx的stream模块竟然成为了瓶颈。这提醒我们:超高并发系统每个组件都可能成为短板,需要持续全链路优化。