Redis 分布式缓存生产实战


01 · 引言:缓存不是银弹,而是双刃剑

在外贸电商、支付网关、库存交易这类高并发系统里,缓存是抗住流量洪峰的第一道闸门。但生产环境中,缓存也是最容易被低估的组件------你以为加了个 Redis 就能高枕无忧,结果上线后 Hot Key 把单节点打挂、Big Key 阻塞整集群、缓存一致性让下游数据"阴晴不定"。

本文基于真实生产场景,从 Redis Cluster 架构、Redisson 分布式锁、Hot/Big Key 治理到缓存一致性,给出一套敢上量、可观测、能兜底的落地方案。代码不是 Hello World,而是带背压、熔断、上下文传播和监控埋点的生产骨架。

指标 目标
峰值 QPS 经缓存层削峰 10w+
P99 读缓存延迟 < 5ms
大集群常见故障根因 3 类(Hot/Big/一致性)

设计原则: 缓存层必须做"横向可扩展 + 纵向热点隔离"。不要只关注集群有多少主节点,要关注每个 key 被访问的频次和大小。


02 · Redis Cluster 架构:分片不是免死金牌

Redis Cluster 采用无中心架构,16384 个槽位分配到多个主节点,每个主节点挂 1-N 个从节点。数据按 key 的 CRC16 取模落在槽位上,客户端通过 MOVED/ASK 重定向定位节点。

text 复制代码
        ┌──────────────────────────────────────┐
        │           Redis Cluster 三主三从        │
        └──────────────────────────────────────┘

   Master-A (slots 0-5460) ──→ Replica-A1, Replica-A2
   Master-B (slots 5461-10922) ──→ Replica-B1, Replica-B2
   Master-C (slots 10923-16383) ──→ Replica-C1, Replica-C2

但分片本身无法解决以下问题:

  • Hot Key:同一 key 被高频访问,所在节点 CPU/带宽被打爆,其他节点空闲。
  • Big Key:单个 value 过大(如 List/Hash 百万级),导致持久化阻塞、主从同步慢。
  • 倾斜:业务 key 分布不均,某些槽位流量是其他的十倍。

03 · Redisson 分布式锁:看门狗不是万能的

生产环境最忌讳"自己实现 Redis 锁"。Redisson 的 RLock 已经封装了加锁、可重入、自动续期(Watch Dog)、订阅释放通知等机制。但默认配置往往不能直接上生产。

3.1 配置:必须显式指定锁等待时间与自动续期策略

java 复制代码
// 生产级 Redisson 配置:多主节点、重试、超时、连接池隔离
@Configuration
public class RedissonConfig {

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useClusterServers()
            .addNodeAddress(
                "redis://10.0.1.10:7000",
                "redis://10.0.1.11:7000",
                "redis://10.0.1.12:7000"
            )
            .setScanInterval(2000)            // 拓扑扫描 2s
            .setMasterConnectionMinimumIdleSize(16)
            .setMasterConnectionPoolSize(64)
            .setSlaveConnectionMinimumIdleSize(16)
            .setSlaveConnectionPoolSize(64)
            .setRetryAttempts(3)
            .setRetryInterval(1500)
            .setConnectTimeout(3000)
            .setTimeout(3000)
            .setPingConnectionInterval(5000);
        return Redisson.create(config);
    }
}

3.2 业务锁:库存扣减 + 上下文传播 + 监控埋点

java 复制代码
@Service
@RequiredArgsConstructor
public class InventoryLockService {

    private final RedissonClient redisson;
    private final MeterRegistry meterRegistry;
    private final Tracer tracer;

    private static final String LOCK_PREFIX = "lock:inventory:";
    private static final long WAIT_SEC = 3;
    private static final long LEASE_SEC = 10;

    public boolean deductWithLock(String skuId, int qty, String traceId) {
        String key = LOCK_PREFIX + skuId;
        RLock lock = redisson.getLock(key);

        // 1. 上下文传播:把 traceId 放进 MDC,后续链路可追踪
        MDC.put("traceId", traceId);
        long start = System.nanoTime();

        try {
            // 2. 加锁:最多等 3s,持有 10s,自动续期默认 30s/3 = 10s
            boolean locked = lock.tryLock(WAIT_SEC, LEASE_SEC, TimeUnit.SECONDS);
            if (!locked) {
                meterRegistry.counter("redis.lock.fail", "sku", skuId).increment();
                return false;
            }

            // 3. 业务执行:双重检查,防止锁外逻辑并发
            int stock = queryStock(skuId);
            if (stock < qty) {
                throw new BusinessException("库存不足");
            }
            updateStock(skuId, stock - qty);
            recordDeductionLog(skuId, qty, traceId);

            meterRegistry.counter("redis.lock.success", "sku", skuId).increment();
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new SystemException("加锁被中断", e);
        } finally {
            // 4. 只有当前线程持有才释放,避免误删他人锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
            long cost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            meterRegistry.timer("redis.lock.duration", "sku", skuId)
                         .record(cost, TimeUnit.MILLISECONDS);
            MDC.remove("traceId");
        }
    }
}

⚠️ 锁失效的常见场景

  1. 锁内业务执行时间超过 leaseTime,看门狗续期失败导致锁被其他线程获取。
  2. 网络抖动导致 unlock 没发到 Redis,锁过期后业务还在执行。
  3. 没有 isHeldByCurrentThread() 保护,A线程释放B线程的锁。

04 · Hot Key & Big Key 治理:分层+探测+拆分

当单个 SKU 做秒杀、或者单个商户订单量爆发时,Redis 节点会出现"局部过载"。治理思路是本地缓存削峰 + 热 key 实时探测 + 大 key 拆分/压缩

4.1 本地缓存 + 概率淘汰策略

java 复制代码
@Component
public class LocalHotKeyCache {

    // Caffeine:本地热点缓存,命中率 80%+,显著降低 Redis 单点压力
    private final LoadingCache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofSeconds(5))   // 短过期,接受弱一致
        .recordStats()
        .build(k -> null);

    public Object get(String key) {
        return localCache.getIfPresent(key);
    }

    public void put(String key, Object value) {
        localCache.put(key, value);
    }

    public double hitRate() {
        return localCache.stats().hitRate();
    }
}

4.2 热 key 探测:基于客户端采样

java 复制代码
@Component
public class HotKeyDetector {

    private final ConcurrentHashMap<String, AtomicLong> hotKeys = new ConcurrentHashMap<>();
    private final int sampleMask = 0xFF; // 每 256 次采样一次

    public void record(String key) {
        // 随机采样降低 CPU,热点 key 即使采样也能被命中
        if ((ThreadLocalRandom.current().nextInt() & sampleMask) != 0) {
            return;
        }
        AtomicLong counter = hotKeys.computeIfAbsent(key, k -> new AtomicLong(0));
        long cnt = counter.incrementAndGet();
        if (cnt > 1000) {
            // 超过阈值,上报到本地缓存或配置中心触发本地缓存预热
            AlarmUtil.send("hotkey", key, cnt);
            counter.set(0);
        }
    }
}

4.3 Big Key 拆分:Hash 分桶,避免单 key 爆炸

java 复制代码
@Service
public class ShardingHashService {

    private final RedissonClient redisson;
    private static final int BUCKET_COUNT = 128;

    public void hSetBig(String key, String field, String value) {
        int bucket = Math.abs(field.hashCode() % BUCKET_COUNT);
        String realKey = key + ":" + bucket;
        RMap<String, String> map = redisson.getMap(realKey);
        map.fastPut(field, value);
    }

    public String hGetBig(String key, String field) {
        int bucket = Math.abs(field.hashCode() % BUCKET_COUNT);
        String realKey = key + ":" + bucket;
        RMap<String, String> map = redisson.getMap(realKey);
        return map.get(field);
    }

    // 批量读取:并发访问多个桶,聚合结果
    public Map<String, String> hGetAllBig(String key) {
        Map<String, String> result = new ConcurrentHashMap<>();
        List<CompletableFuture<?>> futures = new ArrayList<>();
        for (int i = 0; i < BUCKET_COUNT; i++) {
            int bucket = i;
            futures.add(CompletableFuture.runAsync(() -> {
                RMap<String, String> map = redisson.getMap(key + ":" + bucket);
                result.putAll(map.readAllEntrySet().stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
            }));
        }
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        return result;
    }
}

05 · 缓存一致性:没有完美方案,只有 trade-off

缓存与数据库的一致性,从来都不是"先删缓存还是先更新数据库"这种简单二选一。生产上更稳妥的是Cache-Aside + 延时双删 + 消息队列最终一致

text 复制代码
写请求 → 更新 DB → 删除缓存 → 发送 MQ 延时消息 → 500ms 后再次删缓存

5.1 生产级写服务:带重试、幂等、熔断

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductWriteService {

    private final ProductMapper productMapper;
    private final RedissonClient redisson;
    private final KafkaTemplate<String, String> kafkaTemplate;
    private final MeterRegistry meterRegistry;

    private static final String CACHE_KEY = "product:info:";

    @Transactional
    public void updateProduct(ProductDTO dto, String traceId) {
        MDC.put("traceId", traceId);
        long start = System.nanoTime();

        try {
            // 1. 先更新数据库,保证主库为唯一真相源
            productMapper.update(dto);

            // 2. 删除缓存
            String cacheKey = CACHE_KEY + dto.getId();
            deleteCache(cacheKey);

            // 3. 发送延时消息,500ms 后兜底二次删除
            kafkaTemplate.send(
                "cache-invalidate-delay",
                cacheKey,
                cacheKey
            ).whenComplete((res, ex) -> {
                if (ex != null) {
                    meterRegistry.counter("cache.invalidate.mq.fail").increment();
                }
            });

            meterRegistry.timer("product.write.duration")
                         .record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
        } finally {
            MDC.remove("traceId");
        }
    }

    // 删缓存带重试与熔断:防止 Redis 抖动时写失败导致脏读
    private void deleteCache(String key) {
        int attempts = 0;
        while (attempts < 3) {
            try {
                redisson.getBucket(key).delete();
                return;
            } catch (Exception e) {
                attempts++;
                meterRegistry.counter("cache.delete.retry").increment();
                if (attempts >= 3) {
                    // 熔断:记录失败,交给 MQ 消费者兜底
                    meterRegistry.counter("cache.delete.fail").increment();
                    throw new CacheOpsException("缓存删除失败", e);
                }
                LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(50));
            }
        }
    }
}

5.2 读路径:多级缓存 + 互斥锁防击穿

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductReadService {

    private final RedissonClient redisson;
    private final ProductMapper productMapper;
    private final LocalHotKeyCache localCache;
    private final MeterRegistry meterRegistry;

    public ProductDTO getProduct(Long id) {
        String key = "product:info:" + id;

        // 1. 本地缓存(Caffeine)
        Object local = localCache.get(key);
        if (local != null) {
            meterRegistry.counter("cache.hit.local").increment();
            return (ProductDTO) local;
        }

        // 2. Redis 缓存
        RBucket<ProductDTO> bucket = redisson.getBucket(key);
        ProductDTO cached = bucket.get();
        if (cached != null) {
            localCache.put(key, cached);
            meterRegistry.counter("cache.hit.redis").increment();
            return cached;
        }

        // 3. 互斥锁防止缓存击穿
        String lockKey = "lock:product:" + id;
        RLock lock = redisson.getLock(lockKey);
        try {
            boolean locked = lock.tryLock(2, 5, TimeUnit.SECONDS);
            if (!locked) {
                meterRegistry.counter("cache.load.lock.fail").increment();
                return productMapper.selectById(id); // 降级直接查库
            }

            // 双重检查
            cached = bucket.get();
            if (cached != null) {
                return cached;
            }

            ProductDTO db = productMapper.selectById(id);
            if (db != null) {
                bucket.set(db, Duration.ofMinutes(10));
                localCache.put(key, db);
            } else {
                // 空值缓存,防穿透
                bucket.set(ProductDTO.EMPTY, Duration.ofMinutes(2));
            }
            meterRegistry.counter("cache.load.db").increment();
            return db;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new SystemException("读缓存中断", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

06 · 生产级骨架:并发、背压、熔断、监控四位一体

真正上量的系统,不能只在业务代码里 patch。下面是缓存服务的入口切面,整合了Semaphore 并发控制、RateLimiter 背压、CircuitBreaker 熔断、Micrometer 指标

java 复制代码
@Aspect
@Component
@RequiredArgsConstructor
public class CacheAccessAspect {

    private final MeterRegistry meterRegistry;
    private final RateLimiter rateLimiter = RateLimiter.create(5000.0); // 背压
    private final Semaphore concurrency = new Semaphore(200);            // 并发控制

    private final CircuitBreaker cacheBreaker = CircuitBreaker.ofDefaults("redis");

    @Around("@annotation(CacheGuard)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        String method = pjp.getSignature().getName();

        // 1. 背压:超过阈值则快速失败,保护 Redis
        if (!rateLimiter.tryAcquire()) {
            meterRegistry.counter("cache.backpressure.reject", "method", method).increment();
            throw new BackpressureException("请求过载,请稍后重试");
        }

        // 2. 并发控制:避免同一时刻大量线程穿透到缓存
        if (!concurrency.tryAcquire(200, TimeUnit.MILLISECONDS)) {
            meterRegistry.counter("cache.concurrency.reject", "method", method).increment();
            throw new ConcurrencyException("并发限流");
        }

        try {
            // 3. 熔断:Redis 连续失败时直接降级走 DB 或本地缓存
            return cacheBreaker.executeSupplier(() -> {
                try {
                    return pjp.proceed();
                } catch (Throwable t) {
                    throw new RuntimeException(t);
                }
            });
        } catch (CallNotPermittedException | RuntimeException ex) {
            meterRegistry.counter("cache.circuitbreaker.open", "method", method).increment();
            return fallback(pjp); // 降级:本地缓存或数据库
        } finally {
            concurrency.release();
            long cost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            meterRegistry.timer("cache.access.duration", "method", method)
                         .record(cost, TimeUnit.MILLISECONDS);
        }
    }

    private Object fallback(ProceedingJoinPoint pjp) {
        // 实际生产:返回空值/默认值/本地缓存,或抛自定义降级异常
        return null;
    }
}

07 · 监控指标:没有指标,就是瞎子摸象

Redis 上线后,必须建立以下指标看板,才能真正"敢上量"。

指标类别 关键指标 告警阈值建议
性能 latency_p99、qps、connection_used latency_p99 > 20ms 持续 1min 告警
容量 used_memory、memory_fragmentation_ratio、key_count 内存使用率 > 80% 告警
热点 hot_key_top10、key_access_distribution 单 key QPS > 节点 20% 流量
一致性 cache_hit_rate、db_fallback_rate、invalidate_fail 命中率 < 70% 或 fallback 激增
lock_duration、lock_fail、lock_held_timeout 锁失败率 > 1% 或持有时长异常

08 · 生产踩坑:这 6 个问题我们真金白银买过

坑 1:Redisson 默认 leaseTime 太长,故障时锁不释放

修复: 显式设置 leaseTime,业务内再兜底 unlock;避免用默认 30s 锁跑长事务。

坑 2:Big Key 导致 RDB/AOF 阻塞,整集群抖动

修复: Hash 分桶、List 分片、String 压缩;定期用 redis-cli --bigkeys 扫描。

坑 3:缓存穿透被恶意请求打穿数据库

修复: 空值缓存 + BloomFilter;接口层加 WAF/限流。

坑 4:先删缓存后更新 DB,并发读导致旧缓存回填

修复: 延时双删 + MQ 最终一致;读多写少场景优先"先更新 DB 再删缓存"。

坑 5:本地缓存过期时间太长,读到脏数据

修复: 本地缓存只放强一致性要求低的数据,过期时间控制在 3-5s;通过 MQ 主动失效。

坑 6:Redis 单节点 CPU 被打满,以为是集群容量不足

修复: 监控 key 级访问分布;发现倾斜后做本地缓存或 key 副本拆分。


09 · 总结:缓存不是加速,是架构基础设施

Redis 用得好,能把 10w QPS 的 DB 压力削到几千;用得不好,它就是最隐蔽的故障源。生产落地的关键是:

  • 架构层: Cluster 分片 + 读写分离 + 多可用区部署。
  • 代码层: Redisson 锁显式配置、本地缓存削峰、互斥锁防击穿。
  • 一致性层: Cache-Aside + 延时双删 + MQ 兜底,接受最终一致。
  • 治理层: Hot/Big Key 探测、背压、熔断、全链路监控。

缓存设计的最高境界不是"快",而是**"在快的同时,让系统在异常时也能优雅降级"**。


相关推荐
用户095367515835 小时前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌8 小时前
Redis 3.x——集群故障转移
java·数据库·redis·集群
海兰10 小时前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_7275356210 小时前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf
阿标在干嘛10 小时前
从单机到分布式:政策快报爬虫系统的三次重构
分布式·爬虫·重构
笨鸟先飞的橘猫11 小时前
游戏后端分布式学习——开篇
分布式·学习·游戏
William Dawson11 小时前
【华为云 MRS Redis 安全集群 Kerberos 认证接入实战(Spring Boot \+ Jedis)】
redis·安全·华为云
六bring个六11 小时前
分布式软总线认证模块架构与实现分析
分布式·架构·open harmony
zhouhui00112 小时前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
java·spring boot·redis·ai编程