»Redis 进阶实战:缓存三大问题、Redisson 分布式协调与秒杀全链路深度解析

2026-06-192026-06-19数据库13 分钟读完(约 3457 字)

Redis 远不止一个简单的 key-value 缓存。在企业级高并发架构中,它是缓存层、分布式锁协调器、限流哨兵、消息中转站的多面手。本文将沿着缓存治理 → 分布式协调 → 高并发实战的主线,逐层深入 Redis 进阶体系。


Redis 核心数据结构速览

在深入缓存问题之前,先快速建立 Redis 数据模型的全景认知:

数据结构底层实现核心操作典型场景
StringSDS(简单动态字符串)/ intSET GET INCR SETNX缓存对象、计数器、分布式锁
Hash压缩列表 / 哈希表HSET HGET HINCRBY对象存储(用户信息)、购物车
List快速列表(QuickList)LPUSH BRPOP LRANGE消息队列、最新消息 Timeline
Set整数集合 / 哈希表SADD SINTER SISMEMBER标签、共同好友、去重
ZSet压缩列表 / 跳跃表ZADD ZRANGE ZREVRANK排行榜、延迟队列、优先级队列
StreamRax 树XADD XREAD XGROUP持久化消息队列、消费者组
BitmapString 的位操作SETBIT BITCOUNT BITOP签到打卡、日活统计
HyperLogLog基数估计算法PFADD PFCOUNT PFMERGEUV 统计、去重计数(误差 0.81%)
GeoZSet 封装GEOADD GEORADIUS GEODIST附近的人、门店搜索

一、Redis 缓存三大问题及生产级解决方案

在高并发架构中,缓存的引入带来了三大致命问题:缓存穿透缓存击穿缓存雪崩

┌──────────────────────────────────────────────────────────────────┐
│                    缓存三大问题全景图                                │
│                                                                  │
│    缓存穿透                    缓存击穿                    缓存雪崩   │
│                                                                  │
│  请求不存在的数据            热点Key过期瞬间             大量Key同时过期 │
│  (缓存无→DB也无)            (缓存无→DB有)             (缓存层整体崩溃) │
│        │                        │                         │       │
│        ▼                        ▼                         ▼       │
│   每次直接打DB              并发全部打DB             所有请求打DB     │
│        │                        │                         │       │
│        ▼                        ▼                         ▼       │
│  布隆过滤器+空对象缓存       互斥锁+逻辑过期          随机TTL+多级缓存 │
└──────────────────────────────────────────────────────────────────┘

1.1 缓存穿透(Cache Penetration)

核心概念:用户查询的数据在缓存中不存在,在数据库中也不存在。由于两层都未命中,每次请求都直接打到数据库上。通常由非法参数、恶意攻击或爬虫引起。

正常请求:Client → Redis(有) → 返回
穿透攻击:Client → Redis(无) → DB(也无) → 每一次都查到底!
                                             ↑ 恶意循环

方案一:缓存空对象

public Product getProductWithPassThrough(Long id) {
    String cacheKey = "cache:product:" + id;

    // 1. 从缓存中获取数据
    String json = stringRedisTemplate.opsForValue().get(cacheKey);

    // 2. 判断是否命中
    if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(cacheKey))) {
        if (json == null || "EMPTY_OBJECT".equals(json)) {
            return null; // 命中空缓存标记,直接返回
        }
        return JSON.parseObject(json, Product.class);
    }

    // 3. 缓存未命中,查询数据库
    Product product = productMapper.selectById(id);
    if (product == null) {
        // 4. 数据库也没有 → 缓存空对象,设置短 TTL 防止穿透
        stringRedisTemplate.opsForValue()
            .set(cacheKey, "EMPTY_OBJECT", 2, TimeUnit.MINUTES);
        return null;
    }

    // 5. 写入正常缓存
    stringRedisTemplate.opsForValue()
        .set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
    return product;
}

方案二:布隆过滤器(Bloom Filter)

布隆过滤器通过多个 Hash 函数将所有合法 Key 映射到 Bitmap 中。请求先过过滤器,不存在则直接拦截:

@Component
public class BloomFilterService {

    private final RedissonClient redisson;
    private RBloomFilter<String> productBloomFilter;

    public BloomFilterService(RedissonClient redisson) {
        this.redisson = redisson;
    }

    @PostConstruct
    public void init() {
        // 初始化布隆过滤器:预计 100 万元素,误判率 1%
        productBloomFilter = redisson.getBloomFilter("product:bloom");
        productBloomFilter.tryInit(1_000_000L, 0.01);

        // 启动时加载全部合法 Key
        List<Long> allIds = productMapper.selectAllIds();
        for (Long id : allIds) {
            productBloomFilter.add("product:" + id);
        }
    }

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

        // 布隆过滤器预判:不存在则直接拦截
        if (!productBloomFilter.contains(key)) {
            return null; // 绝对不存在,拒绝请求
        }

        // 可能存在(有 1% 误判率)→ 继续查缓存 + DB
        // ... 正常的缓存→数据库查询逻辑
        return queryWithCache(id);
    }
}

两种方案对比

维度缓存空对象布隆过滤器
内存占用随攻击 Key 数量线性增长固定大小,极省内存
实现复杂度极简单需预加载合法 Key,需处理新增 Key
误判存在(可调参降至 <1%)
Key 删除支持天然支持 TTL 过期原生不支持删除(需 Counting Bloom Filter)
适用场景穿透 Key 数量少、变化频繁穿透 Key 数量大、合法 Key 集合稳定

1.2 缓存击穿(Cache Breakdown)

核心概念:一个高并发访问的热点 Key(如爆款商品),在过期的瞬间,大量请求同时发现缓存失效,涌向数据库重建缓存,造成数据库瞬时压力飙升。

时间线:
  热点Key过期 ─┬─ 请求1 → 缓存无 → 查DB(慢)────────────────→ 重建缓存
              ├─ 请求2 → 缓存无 → 查DB(慢)→ 查DB(慢)────→ 重建缓存
              ├─ 请求3 → 缓存无 → 查DB(慢)→ 查DB(慢)→ 查DB──→ 重建缓存
              └─ 请求N → ...
                                ↑ 所有请求同时穿透,数据库崩溃

方案一:互斥锁(Mutex)

只让一个线程去查 DB 重建缓存,其余线程自旋等待:

public Product getProductWithMutex(Long id) {
    String cacheKey = "cache:product:" + id;
    String lockKey = "lock:product:" + id;

    // 1. 先查缓存
    String json = stringRedisTemplate.opsForValue().get(cacheKey);
    if (StringUtils.hasText(json)) {
        return JSON.parseObject(json, Product.class);
    }

    // 2. 缓存未命中,获取互斥锁
    boolean isLock = false;
    try {
        // setIfAbsent = SETNX,返回 true 表示获取锁成功
        isLock = Boolean.TRUE.equals(
            stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS));

        if (!isLock) {
            // 未获取到锁,休眠后递归重试
            Thread.sleep(50);
            return getProductWithMutex(id);
        }

        // 3. 获取锁成功,Double Check 缓存
        json = stringRedisTemplate.opsForValue().get(cacheKey);
        if (StringUtils.hasText(json)) {
            return JSON.parseObject(json, Product.class);
        }

        // 4. 查数据库 + 重建缓存
        Product product = productMapper.selectById(id);
        if (product != null) {
            stringRedisTemplate.opsForValue()
                .set(cacheKey, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        }
        return product;

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException(e);
    } finally {
        if (isLock) {
            stringRedisTemplate.delete(lockKey); // 释放锁
        }
    }
}

方案二:逻辑过期(Logical TTL)

缓存永不过期,Value 内包含 expire 字段。发现过期后异步重建,当前请求先返回旧数据:

@Data
public class RedisData {
    private LocalDateTime expireTime; // 逻辑过期时间
    private Object data;              // 实际数据
}

public Product getProductWithLogicalExpire(Long id) {
    String cacheKey = "cache:product:" + id;
    String lockKey = "lock:product:" + id;

    // 1. 查缓存
    String json = stringRedisTemplate.opsForValue().get(cacheKey);
    if (!StringUtils.hasText(json)) {
        return null; // 缓存中完全没有,返回空
    }

    // 2. 反序列化
    RedisData redisData = JSON.parseObject(json, RedisData.class);
    Product product = JSON.parseObject((String) redisData.getData(), Product.class);
    LocalDateTime expireTime = redisData.getExpireTime();

    // 3. 判断是否逻辑过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        return product; // 未过期,直接返回
    }

    // 4. 逻辑过期 → 尝试获取互斥锁
    boolean isLock = Boolean.TRUE.equals(
        stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS));

    if (isLock) {
        // 5. 获取锁成功,开启独立线程异步重建缓存
        threadPoolExecutor.submit(() -> {
            try {
                Product newProduct = productMapper.selectById(id);
                RedisData newRedisData = new RedisData();
                newRedisData.setData(JSON.toJSONString(newProduct));
                newRedisData.setExpireTime(LocalDateTime.now().plusMinutes(30));
                stringRedisTemplate.opsForValue()
                    .set(cacheKey, JSON.toJSONString(newRedisData));
            } finally {
                stringRedisTemplate.delete(lockKey); // 释放锁
            }
        });
    }

    // 6. 无论是否获得锁,当前请求都返回旧数据(保证可用性)
    return product;
}

互斥锁 vs 逻辑过期对比

维度互斥锁逻辑过期
一致性强一致,查到的永远是最新数据最终一致,可能短期读到旧数据
性能/QPS线程需阻塞等待,吞吐量略降零阻塞,性能最高
实现复杂度简单较复杂(需线程池异步重建)
适用场景金融/交易类系统资讯/商品展示/社交类

1.3 缓存雪崩(Cache Avalanche)

核心概念:极短时间内大量缓存 Key 同时到期失效,或 Redis 整个集群宕机,导致流量洪峰瞬间压向数据库。

正常状态:
  Client → Redis(分散过期) → DB(低负载)

雪崩状态:
  Client → Redis(大量Key同时过期/集群宕机) → DB(过载崩溃) → 级联故障

治理策略

策略一:随机扰动过期时间(TTL 打散)

public void setWithRandomTTL(String key, Object value, long baseMinutes) {
    // 在基础过期时间上叠加随机 1~5 分钟
    long randomMinutes = ThreadLocalRandom.current().nextLong(1, 6);
    long ttl = baseMinutes + randomMinutes;

    stringRedisTemplate.opsForValue()
        .set(key, JSON.toJSONString(value), ttl, TimeUnit.MINUTES);
}

策略二:多级缓存架构

┌─────────┐    ┌──────────────┐    ┌──────────────┐    ┌────────┐
│ Nginx   │ →  │ 本地缓存       │ →  │ Redis 分布式  │ →  │ MySQL  │
│ 限流    │    │ (Caffeine)    │    │ 缓存          │    │        │
│         │    │ 命中率 ~60%   │    │ 命中率 ~35%   │    │ 兜底    │
└─────────┘    └──────────────┘    └──────────────┘    └────────┘
@Configuration
public class MultiLevelCacheConfig {

    @Bean
    public Cache<String, Object> localCache() {
        return Caffeine.newBuilder()
            .initialCapacity(100)
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()
            .build();
    }

    public Object getWithMultiLevel(String key) {
        // L1: 本地缓存
        Object value = localCache().getIfPresent(key);
        if (value != null) return value;

        // L2: Redis 分布式缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) {
            localCache().put(key, JSON.parse(json));
            return JSON.parse(json);
        }

        // L3: 数据库
        Object dbValue = queryFromDB(key);
        if (dbValue != null) {
            // 回写 L2 + L1
            stringRedisTemplate.opsForValue()
                .set(key, JSON.toJSONString(dbValue), 30, TimeUnit.MINUTES);
            localCache().put(key, dbValue);
        }
        return dbValue;
    }
}

策略三:熔断降级

// 使用 Sentinel 对数据库查询接口做熔断保护
@SentinelResource(value = "queryFromDB", fallback = "queryFallback")
public Product queryFromDB(Long id) {
    return productMapper.selectById(id);
}

// 降级方法:返回兜底数据
public Product queryFallback(Long id, Throwable e) {
    log.error("数据库查询熔断降级,id: {}", id, e);
    return Product.DEFAULT; // 返回默认商品信息
}

二、Redis 持久化:RDB vs AOF

理解 Redis 的持久化机制,是为了在性能和数据安全之间做出正确取舍:

维度RDB(快照)AOF(追加日志)
原理每隔一段时间将内存数据快照写入磁盘将每条写命令追加到日志文件
文件体积压缩的二进制,体积小文本协议,体积大(可重写压缩)
数据恢复速度快,直接加载二进制慢,需逐条重放命令
数据安全可能丢失最后两次快照间的数据默认每秒 fsync,最多丢 1 秒数据
影响性能fork 子进程时短暂阻塞持续写盘,但可配置 fsync 策略
恢复风险快照文件损坏则全部丢失日志可修复,更可靠

生产环境推荐配置

# redis.conf
# ===== RDB =====
save 900 1      # 900 秒内至少 1 次修改则触发快照
save 300 10     # 300 秒内至少 10 次修改则触发快照
save 60 10000   # 60 秒内至少 10000 次修改则触发快照

# ===== AOF =====
appendonly yes
appendfsync everysec       # 每秒 fsync 一次(推荐)
auto-aof-rewrite-percentage 100  # AOF 文件增长 100% 时自动重写
auto-aof-rewrite-min-size 64mb   # AOF 文件最小 64MB 才触发重写

结论:生产环境建议 RDB + AOF 双开,用 RDB 做快速恢复,用 AOF 保证数据不丢。


三、Redis 内存淘汰与过期策略

过期键删除策略

Redis 采用惰性删除 + 定期删除的组合策略:

策略机制特点
惰性删除访问 Key 时检查是否过期,过期则删除CPU 友好,内存不友好
定期删除每秒 10 次随机抽检一批 Key,过期比例 >25% 则继续抽检CPU 和内存的折中

内存淘汰策略(8 种)

当 Redis 内存达到 maxmemory 上限时,根据配置的策略淘汰 Key:

策略行为适用场景
noeviction不淘汰,写入直接报错不推荐,会导致服务不可用
allkeys-lru所有 Key 中淘汰最久未使用的通用缓存,推荐
volatile-lru有过期时间的 Key 中淘汰最久未使用的需保留部分永久 Key
allkeys-lfu所有 Key 中淘汰访问频率最低的热点数据场景
volatile-lfu有过期时间的 Key 中淘汰访问频率最低热点 + 永久 Key 混合
allkeys-random所有 Key 中随机淘汰不常用
volatile-random有过期时间的 Key 中随机淘汰不常用
volatile-ttl有过期时间的 Key 中淘汰 TTL 最短的接近过期优先淘汰
# redis.conf 推荐配置
maxmemory 4gb                      # 根据服务器内存设定上限
maxmemory-policy allkeys-lru       # 通用场景推荐 LRU

四、缓存与数据库一致性方案

缓存和数据库的一致性是高并发架构的核心难题:

┌──────────────────────────────────────────────────────────────────┐
│                      缓存一致性模式                                │
│                                                                  │
│  Cache-Aside(旁路缓存)  Write-Through(写穿)  Write-Behind(写回)│
│                                                                  │
│  应用手动管理缓存        应用只写缓存             应用只写缓存       │
│  读:缓存→DB            缓存同步写DB             缓存异步批量写DB   │
│  写:更新DB→删缓存      写穿保证一致             延迟最高,风险最大  │
│                                                                  │
│  ★ 最常用,最灵活          一致性强                 高性能但可能丢数据│
└──────────────────────────────────────────────────────────────────┘

推荐方案:Cache-Aside + 延迟双删

@Transactional
public void updateProduct(Product product) {
    String cacheKey = "cache:product:" + product.getId();

    // 1. 先删除缓存
    stringRedisTemplate.delete(cacheKey);

    // 2. 更新数据库
    productMapper.updateById(product);

    // 3. 延迟双删:等待数据库主从同步完成后再次删除
    //    防止并发读请求在此期间写入旧缓存
    CompletableFuture.runAsync(() -> {
        try {
            Thread.sleep(500); // 等待时间 > 主从同步延迟
            stringRedisTemplate.delete(cacheKey);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }, delayedExecutor);
}

Cache-Aside 口诀读时加载,写时删除。先更库后删缓存,延迟双删保一致。


五、Redisson 分布式锁与高级组件全景

Redisson 不仅仅是一个 Redis 客户端,更是一个功能完备的分布式协调框架。

5.1 Watchdog(看门狗)续期机制 — 核心内核

Redisson 分布式锁采用 Redis 的 Hash 数据结构:

Key:   lock:order:1001
Field: UUID:ThreadId (如 "a3f2b1-1")  ← 标识持锁线程
Value: 3                              ← 重入次数(可重入)
加锁时的 Lua 脚本逻辑:
  if (redis.call('exists', KEYS[1]) == 0) then
      redis.call('hincrby', KEYS[1], ARGV[2], 1);   -- Field+1(首次加锁)
      redis.call('pexpire', KEYS[1], ARGV[1]);        -- 设 30s TTL
      return nil;
  elseif (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
      redis.call('hincrby', KEYS[1], ARGV[2], 1);    -- 重入 +1
      redis.call('pexpire', KEYS[1], ARGV[1]);        -- 续期
      return nil;
  end;
  return redis.call('pttl', KEYS[1]);                  -- 锁被他人持有,返回剩余 TTL

Watchdog 工作原理

加锁成功 → 开启定时任务(每 10 秒)
    │
    ├── 第 10 秒 → 检查锁是否仍被当前线程持有
    │             → 是 → 发送 Lua 脚本续期到 30 秒
    │
    ├── 第 20 秒 → 再次检查并续期
    │
    └── ...循环直到 unlock() 或 JVM 宕机

关键注意事项

// ✅ 不指定 leaseTime → 看门狗自动续期(默认 30s)
lock.lock();                    // 看门狗生效
lock.tryLock(10, -1, TimeUnit.SECONDS); // leaseTime=-1,看门狗生效

// ❌ 指定 leaseTime → 看门狗失效,锁到期强制释放
lock.lock(10, TimeUnit.SECONDS); // 10 秒后必释放,看门狗不工作

5.2 Redisson 组件全家桶

① 可重入锁(Reentrant Lock)

@Autowired
private RedissonClient redisson;

public void doBusinessWithLock() {
    RLock lock = redisson.getLock("order:lock:1001");
    try {
        // tryLock(等待时间, 释放时间(-1=看门狗), 时间单位)
        if (lock.tryLock(10, -1, TimeUnit.SECONDS)) {
            // 执行业务逻辑
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock(); // 重入次数归零才真正释放
        }
    }
}

② 公平锁(Fair Lock)

RLock fairLock = redisson.getFairLock("order:fairLock");
fairLock.lock();
try {
    // 线程严格按照请求顺序排队进入,杜绝"饥饿"
} finally {
    fairLock.unlock();
}

③ 读写锁(ReadWriteLock)

RReadWriteLock rwLock = redisson.getReadWriteLock("product:rwLock");

// 读操作 — 读读不互斥
RLock readLock = rwLock.readLock();
readLock.lock();
try {
    return productMapper.selectById(id);
} finally {
    readLock.unlock();
}

// 写操作 — 读写互斥、写写互斥
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
    productMapper.updateById(product);
} finally {
    writeLock.unlock();
}
锁关系读锁写锁
读锁✅ 不互斥(共享)❌ 互斥
写锁❌ 互斥❌ 互斥

④ 红锁(RedLock)— 跨多节点

// 适用于 Redis 集群环境,防止主从切换导致锁丢失
RLock lock1 = redissonClient1.getLock("lock1");
RLock lock2 = redissonClient2.getLock("lock2");
RLock lock3 = redissonClient3.getLock("lock3");

RLock redLock = redisson.getMultiLock(lock1, lock2, lock3);
// 向 N 个独立节点申请加锁,成功 ≥ N/2+1 个才算加锁成功
redLock.lock();
try {
    // 安全执行关键业务
} finally {
    redLock.unlock();
}

⑤ 信号量(Semaphore)

RSemaphore semaphore = redisson.getSemaphore("db:connection:pool");
semaphore.trySetPermits(10); // 最多 10 个并发

semaphore.acquire(); // 获取许可,无许可时阻塞
try {
    // 执行受控操作
} finally {
    semaphore.release(); // 归还许可
}

⑥ CountDownLatch

// 主服务等待 5 个子服务完成
RCountDownLatch latch = redisson.getCountDownLatch("task:batch:sync");
latch.trySetCount(5);

// 各子服务完成后
latch.countDown();

// 主服务等待
latch.await(30, TimeUnit.SECONDS);

⑦ 限流器(RateLimiter)

RRateLimiter limiter = redisson.getRateLimiter("api:user:rate");
// 每 1 秒产生 100 个令牌
limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);

if (limiter.tryAcquire()) {
    // 获取令牌成功,执行业务
} else {
    throw new RateLimitException("请求过于频繁");
}

⑧ 布隆过滤器(RBloomFilter)

RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user:bloom");
bloomFilter.tryInit(1_000_000L, 0.01); // 100 万预期,1% 误判率

bloomFilter.add("user:1001");
bloomFilter.contains("user:1001"); // true
bloomFilter.contains("user:9999"); // false(大概率)

六、Redis Pipeline 批量优化

在网络延迟敏感的场景中,逐条命令的 RTT(往返时间)是性能杀手:

逐条执行:                  Pipeline 批量执行:
Client → GET key1        Client → GET key1
        ← value1                  GET key2
Client → GET key2                 GET key3
        ← value2                 ← value1, value2, value3
Client → GET key3         一次 RTT,三条结果
        ← value3
3 次 RTT                  1 次 RTT
public List<Object> batchGet(List<String> keys) {
    return stringRedisTemplate.executePipelined(
        (RedisCallback<Object>) connection -> {
            for (String key : keys) {
                connection.stringCommands().get(key.getBytes());
            }
            return null; // 返回值由 executePipelined 收集
        }
    );
}

Pipeline 注意事项

  • Pipeline 不是原子操作 — 批中的命令可能与其他客户端的命令交错执行
  • 单次 Pipeline 不宜过大(建议 ≤ 1000 条),否则会长时间阻塞 Redis 单线程
  • 需要原子性用 Lua 脚本,需要吞吐量用 Pipeline

七、Redis 高可用架构

┌──────────────────────────────────────────────────────────────┐
│ 单机 → 主从 → Sentinel 哨兵 → Cluster 集群(演进路径)          │
│                                                              │
│  单机       主从复制     Sentinel        Cluster             │
│  无高可用   读写分离     自动故障转移     数据分片 + 高可用    │
│  数据可能丢  手动切换     3 节点起        3 主 3 从起          │
└──────────────────────────────────────────────────────────────┘

架构对比

架构高可用数据分片最小节点数适用规模
单机1开发/测试
主从复制❌(手动切换)2读写分离,小规模
Sentinel 哨兵✅ 自动故障转移3中小规模生产
Cluster 集群✅ 16384 槽位6(3 主 3 从)大规模生产,TB 级数据

Sentinel 配置示例

# sentinel.conf(每个 Sentinel 节点配置相同)
sentinel monitor mymaster 192.168.1.10 6379 2
# ↑ 监控主节点,2 表示至少 2 个哨兵同意才进行故障转移

sentinel down-after-milliseconds mymaster 5000
# ↑ 主节点 5 秒无响应判定为下线

sentinel failover-timeout mymaster 60000
# ↑ 故障转移超时时间 60 秒

sentinel parallel-syncs mymaster 1
# ↑ 故障转移后,每次只允许 1 个从节点同步新主节点

八、秒杀高并发业务全链路实战

秒杀具有"瞬时高并发"、"读多写少"、"突发流量大"的特征。直接打 MySQL 必然崩溃。标准架构方案是:Redis 拦截 99% 无效流量 + 消息队列异步落库

8.1 秒杀架构黄金链路

┌──────────────────────────────────────────────────────────────────────┐
│                          秒杀全链路架构                                 │
│                                                                      │
│  [用户请求]                                                           │
│      │                                                               │
│      ▼                                                               │
│  [Nginx 层] ── 连接数限制 + 限流(limit_req)                           │
│      │                                                               │
│      ▼                                                               │
│  [网关层]   ── 黑名单拦截 + IP 频次控制                                 │
│      │                                                               │
│      ▼                                                               │
│  [Redis 层] ── Lua 脚本原子扣库存 + 用户幂等校验 + 预减库存               │
│      │                                                               │
│      ├── 抢购失败(库存不足/重复购买)→ 直接返回                           │
│      │                                                               │
│      ▼ 抢购成功                                                       │
│  [消息队列] ── 发抢购凭证到 MQ,削峰填谷                                  │
│      │                                                               │
│      ▼                                                               │
│  [订单服务] ── 异步消费,低频写 MySQL,生成真实订单                         │
└──────────────────────────────────────────────────────────────────────┘

8.2 核心 Lua 脚本:原子扣库存 + 防超卖 + 限购

-- seckill.lua
-- KEYS[1]: 商品库存 Key     (seckill:stock:1001)
-- KEYS[2]: 已购用户集合 Key  (seckill:order:1001)
-- ARGV[1]: 当前用户 ID

local stockKey = KEYS[1]
local orderKey = KEYS[2]
local userId = ARGV[1]

-- Step 1: 幂等校验 — 用户是否已抢购(限购 1 件)
if redis.call('sismember', orderKey, userId) == 1 then
    return -1  -- 重复购买
end

-- Step 2: 检查库存
local currentStock = redis.call('get', stockKey)
if not currentStock or tonumber(currentStock) <= 0 then
    return 0   -- 库存不足
end

-- Step 3: 原子扣库存 + 记录用户
redis.call('decr', stockKey)
redis.call('sadd', orderKey, userId)

return 1  -- 抢购成功

8.3 秒杀服务完整代码

@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private DefaultRedisScript<Long> seckillScript;

    @PostConstruct
    public void initScript() {
        seckillScript = new DefaultRedisScript<>();
        seckillScript.setLocation(new ClassPathResource("seckill.lua"));
        seckillScript.setResultType(Long.class);
    }

    @Override
    public String executeSeckill(String skuId, String userId) {
        // 1. 组装 Redis Key
        String stockKey = "seckill:stock:" + skuId;
        String orderKey = "seckill:order:" + skuId;

        // 2. 执行 Lua 脚本(单线程原子操作)
        Long result = stringRedisTemplate.execute(
            seckillScript,
            List.of(stockKey, orderKey),
            userId
        );

        // 3. 根据返回值响应
        if (result == null) {
            log.error("秒杀脚本执行异常,skuId: {}, userId: {}", skuId, userId);
            return "系统繁忙,请稍后再试";
        }

        switch (result.intValue()) {
            case -1:
                return "您已参与过该商品的秒杀活动,每人限购一件!";
            case 0:
                return "很遗憾,商品已被抢光!";
            case 1:
                // 4. Redis 预扣成功 → 发 MQ 异步下单
                sendToMessageQueue(skuId, userId);
                return "抢购成功!订单正在创建中,请稍后查看。";
            default:
                return "未知错误";
        }
    }

    private void sendToMessageQueue(String skuId, String userId) {
        SeckillMessageDTO message = new SeckillMessageDTO(skuId, userId);
        rabbitTemplate.convertAndSend(
            "seckill.exchange",
            "seckill.order",
            message
        );
        log.info("秒杀凭证已发送到 MQ,userId: {}, skuId: {}", userId, skuId);
    }
}

8.4 库存预热

@Component
public class SeckillStockPreloader {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 秒杀活动开始前,将库存加载到 Redis
     * 由运营后台或定时任务触发
     */
    public void preloadStock(String skuId, int totalStock) {
        String stockKey = "seckill:stock:" + skuId;
        String orderKey = "seckill:order:" + skuId;

        // 设置库存
        stringRedisTemplate.opsForValue().set(stockKey, String.valueOf(totalStock));
        // 清空上轮已购用户集合
        stringRedisTemplate.delete(orderKey);

        log.info("秒杀库存预热完成,skuId: {}, 库存量: {}", skuId, totalStock);
    }
}

8.5 秒杀优化要点总结

优化点手段效果
库存扣减Lua 脚本原子操作,杜绝超卖正确性保证
限购防刷Redis Set 记录已购用户一人一单
流量削峰MQ 异步下单DB TPS 从 10000+ → 100
库存预热活动开始前 Redis 预加载Redis 纯内存,ns 级响应
前端静默按钮置灰 + 倒计时 + CDN拦截 50% 无效请求
Nginx 限流limit_req_zone单 IP 限制 QPS
读库存分离库存查询走本地缓存不占用 Redis 连接

九、总结与最佳实践清单

序号实践要点说明
TTL 必须加随机扰动TTL = base + random(1~5分钟),杜绝雪崩
热点 Key 用逻辑过期读多写少场景优先逻辑过期,0 阻塞
金融场景用互斥锁涉及金额/库存一致性用互斥锁
Watchdog 不设 leaseTimeleaseTime = -1 或直接用 lock() 才能激活看门狗
秒杀必须 Lua 脚本GET + DECR 分开执行必超卖
Redisson unlock 加守护判断isHeldByCurrentThread() 判断,防止跨线程误解锁
Pipeline 不保证原子性需要原子性用 Lua,需要吞吐量用 Pipeline
生产必须配 maxmemory防止 OOM 导致 Redis 不可用
持久化 RDB + AOF 双开RDB 快速恢复 + AOF 数据安全
写时删除缓存,延迟双删Cache-Aside 解决 99% 缓存一致性问题

记住一句话:缓存穿透靠布隆/空对象,缓存击穿靠互斥锁/逻辑过期,缓存雪崩靠随机 TTL + 多级缓存,秒杀靠 Lua 原子 + MQ 削峰。

Redis 进阶实战:缓存三大问题、Redisson 分布式协调与秒杀全链路深度解析 | Shanhai