Redis限流与分布式锁实战解析
1. Redis限流与分布式锁的核心价值在分布式系统架构中流量控制和并发控制是两个永恒的话题。我经历过太多因为突发流量导致服务雪崩的深夜告警也处理过无数因资源竞争引发的数据不一致问题。Redis作为高性能的内存数据库在这两个领域都有着不可替代的作用。限流算法本质上是系统流量的守门人。当外部请求超过系统处理能力时合理的限流策略能像交通信号灯一样确保系统在可控压力下稳定运行。而分布式锁则是协调多个服务实例访问共享资源的仲裁者特别是在秒杀、库存扣减等场景中没有可靠的分布式锁就像没有交通规则的十字路口必然导致混乱。2. 令牌桶限流实现详解2.1 令牌桶算法原理令牌桶算法模拟了一个物理意义上的令牌桶以恒定速率向桶中添加令牌请求到达时消耗令牌。当桶空时拒绝请求。这种机制既允许突发流量桶中有积累的令牌时又能限制长期平均速率。Redis实现关键命令组合-- KEYS[1] 令牌桶key -- ARGV[1] 令牌桶容量 -- ARGV[2] 令牌生成速率个/秒 -- ARGV[3] 当前时间戳 local result redis.call(hmget, KEYS[1], last_time, tokens) local last_time tonumber(result[1]) local tokens tonumber(result[2] or ARGV[1]) if last_time then local delta math.floor((ARGV[3] - last_time) * ARGV[2]) tokens math.min(tokens delta, ARGV[1]) end if tokens 1 then redis.call(hmset, KEYS[1], last_time, ARGV[3], tokens, tokens-1) return true else return false end2.2 生产级实现要点时间同步问题所有实例必须使用相同时间源推荐使用Redis的TIME命令获取时间冷启动处理首次访问时初始化桶状态集群环境适配确保限流key通过hash tag落到同一节点性能优化使用pipeline批量执行命令减少网络往返关键指标监控桶剩余令牌数、拒绝请求数、实际通过QPS。当拒绝率持续超过5%时应考虑扩容。3. 漏桶算法对比实现3.1 算法差异对比特性令牌桶漏桶突发流量处理允许消耗积累令牌严格平滑固定流出速率实现复杂度较高需维护令牌数较低仅需记录水位典型应用场景API限流、用户行为控制流量整形、防止下游过载Redis实现命令HINCRBY 时间计算LIST的RPUSHBLPOP3.2 漏桶Redis实现def leaky_bucket(key, capacity, rate_per_sec): current_time time.time() # 移除已处理的请求 redis.zremrangebyscore(key, 0, current_time - 1/rate_per_sec) # 检查当前水位 if redis.zcard(key) capacity: redis.zadd(key, {str(uuid.uuid4()): current_time}) return True return False4. 分布式锁深度解析4.1 正确实现四要素互斥性SETNX 唯一标识防止误删防死锁必须设置过期时间原子操作加锁和过期时间设置必须原子化可重入可选通过ThreadLocal计数器实现4.2 Redlock算法争议Martin Kleppmann与Redis作者Antirez曾就Redlock的可靠性展开著名论战。实际应用中建议对可靠性要求极高的场景使用Zookeeper/etcd一般场景单Redis实例SET NX PX足够折中方案多主Redis多数派确认// 正确加锁示例 public boolean tryLock(Jedis jedis, String lockKey, String clientId, int expireTime) { String result jedis.set(lockKey, clientId, NX, PX, expireTime); return OK.equals(result); } // 危险的反例 - 非原子操作 public void wrongLock(Jedis jedis, String lockKey) { Long result jedis.setnx(lockKey, 1); if (result 1) { jedis.expire(lockKey, 30); // 非原子操作可能崩溃导致死锁 } }5. 面试实战要点5.1 高频问题拆解Q如何解决锁过期但业务未执行完的问题方案1守护线程定期续期Redisson实现方案2业务完成后检查锁是否仍属于自己需记录客户端ID折中设置足够长的过期时间 熔断机制Q集群环境下限流如何保证精确性使用Redis集群的hash tag确保相同限流key路由到同一节点考虑10%的误差容忍度避免过度设计对于严格要求精确的场景可使用本地限流Redis限流双层校验5.2 性能压测数据在AWS c5.xlarge机型测试结果方案QPS平均延迟99分位延迟单节点令牌桶12,0002ms8ms集群模式限流8,5003ms15ms分布式锁无竞争9,2001ms5ms分布式锁高竞争1,50065ms210ms6. 生产环境避坑指南令牌桶的桶大小设置建议 平均QPS * 允许的突发秒数。例如平均100QPS允许2秒突发则桶容量设为200Redis阻塞命令风险BLPOP等阻塞命令会占用连接池资源建议设置合理的超时时间// 不好的实践 jedis.blpop(0, my_list); // 好的实践 jedis.blpop(5, my_list); // 5秒超时锁续期的最佳间隔锁过期时间的1/3。例如30秒过期的锁每10秒续期一次限流异常处理被限流的请求应该返回429状态码响应头携带Retry-After日志记录但不要打印完整请求体防止日志爆炸Redis内存优化对于大规模限流场景可以使用# 将令牌桶的hash结构转换为ziplist编码 redis-cli config set hash-max-ziplist-entries 512我在实际项目中曾遇到一个典型故障某促销活动期间由于没有对获取优惠券接口做限流导致Redis CPU飙升至100%进而引发分布式锁大面积失效。事后我们采用了令牌桶本地缓存的二级限流方案将突发流量对Redis的冲击降低了80%。关键配置如下# 限流配置示例 rate-limiter: global: capacity: 1000 # 全局限流桶大小 rate: 500 # 每秒令牌生成速率 api: /coupon/get: capacity: 200 # 接口级限流 rate: 100对于分布式锁的实现我强烈建议直接使用Redisson而不要重复造轮子。其内置的看门狗机制解决了锁续期的难题且支持多种锁类型RLock lock redisson.getLock(orderLock); try { // 尝试加锁最多等待100秒上锁后30秒自动解锁 if (lock.tryLock(100, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { lock.unlock(); }最后分享一个容易被忽视的细节Redis的主动过期清理是定期惰性的双重策略。这意味着即使key已过期可能仍会短暂存在内存中。对于分布式锁这种对时效性要求高的场景建议设置稍短的过期时间并配合续期机制而不是依赖长过期时间。