📦 本文基于的完整项目源码:https://github.com/meirongdev/shop

2026-04 实践更新 当前主线代码已经把 buyer-bff / auth-server / activity-service / api-gateway 的 Redis 访问统一切到 RedissonClient;其中网关限流仍然保留 Lua 原子脚本,但执行器也已经从 StringRedisTemplate 切到 RScript。本文下面的示例已按仓库当前实现同步。

Redis 在 Shop Platform 中承担的角色远不止"缓存"——它是限流的令牌桶状态(Gateway Lua + RScript)、抢红包的原子存储(activity-service Redis List)、反作弊的状态机(Anti-CheatGuard)、幂等检查的 Bloom Filter(shop-starter-idempotency),以及游客购物车的存储(buyer-bff)。

本文从五个维度整理 Spring Boot 3.5 下 Redis 的一些实践记录。


Lettuce 连接池调优#

Spring Boot 3.5 默认使用 Lettuce 作为 Redis 客户端。Lettuce 基于 Netty,连接本身是线程安全的;对大多数普通 GET / SET / HSET / EVAL 场景来说,共享连接就已经够用。连接池更适合拿来解决阻塞命令、事务、Pub/Sub 或需要 dedicated connection 的场景,而不是“高并发就一定要开池”。

什么时候才需要连接池#

如果你确实要跑阻塞命令、事务,或者要为特定操作准备专用连接,可以再启用连接池:

spring:
  data:
    redis:
      host: ${REDIS_HOST:redis}
      port: ${REDIS_PORT:6379}
      lettuce:
        pool:
          max-active: 20      # 最大连接数
          max-idle: 10        # 最大空闲连接
          min-idle: 5         # 最小空闲连接
          max-wait: 2000ms    # 获取连接最大等待时间
参数 说明 常见起点
max-active 连接池最大连接数 CPU 核心数 × 2 ~ × 4
max-idle 最大空闲连接数 max-active 的 50%-80%
min-idle 最小空闲连接数 max-active 的 20%-30%
max-wait 获取连接超时 1-3 秒(过长说明连接池不够)

连接池依赖#

Spring Boot 不会自动引入连接池,需要手动添加 commons-pool2:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

有了这个依赖,Spring Boot 自动配置会创建 LettuceConnectionFactory 并启用连接池。没有它,spring.data.redis.lettuce.pool.* 配置会被忽略。

需要补一句现状说明:当前 shop 仓库里没有统一提交 spring.data.redis.lettuce.pool.* 覆盖配置,说明项目目前主要依赖 Lettuce 的共享连接模型,而不是提前把池子调到一个固定值。

参考:Spring Boot Redis ReferenceLettuce Connection PoolingRedis: Connection pools and multiplexing


序列化策略对比#

Spring Data Redis 支持多种序列化方式,选择哪种取决于场景:

方案对比#

序列化方式 可读性 性能 空间 适用场景
JDK 序列化 ❌ 二进制 通常不作为首选
StringRedisTemplate ✅ JSON 字符串 简单 KV、调试友好
GenericJackson2JsonRedisSerializer ✅ JSON 复杂对象、类型自动推断
自定义 Jackson2JsonRedisSerializer ✅ JSON 固定类型、高性能
Redisson JSON ✅ JSON Redisson 用户

shop 项目的选择#

游客购物车RedissonClient RBucket<String> + 手动 JSON

@Component
public class GuestCartStore {
    private final RedissonClient redissonClient;
    private final ObjectMapper objectMapper;

    public void addToCart(String buyerId, OrderApi.CartItemRequest item) {
        List<OrderApi.CartItemResponse> items = listCart(buyerId);
        RBucket<String> bucket = redissonClient.getBucket(CART_KEY_PREFIX + buyerId);
        bucket.set(objectMapper.writeValueAsString(items),
                DEFAULT_TTL.toMillis(), TimeUnit.MILLISECONDS);
    }
}

原因:购物车数据结构简单(List),手动序列化依旧可控且调试友好;同时统一走 RedissonClient 后,项目里的 Redis 接入风格不再在 StringRedisTemplate / Redisson 之间来回切换。

Bloom Filter / 分布式锁:Redisson

// RedissonBloomFilterAutoConfiguration
@Bean
BloomFilter bloomFilter(RedissonClient redissonClient,
                         BloomFilterProperties properties) {
    RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(properties.getRedisKey());
    bloomFilter.tryInit(properties.getExpectedInsertions(),
            properties.getFalseProbability());
    return new RedissonBloomFilter(bloomFilter);
}

Redisson 封装了 Bloom Filter 的底层 Redis 命令,不需要手动实现。

参考:Spring Data Redis Reference(Serializer 示例)Redisson Bloom Filter


Redisson 分布式锁#

基本用法#

RLock lock = redissonClient.getLock("my-resource:" + resourceId);
try {
    if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
        // 获取锁成功,执行业务
        doSomething();
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

shop 项目的应用场景#

场景 锁 Key 说明
商品库存变更 shop:marketplace:inventory:mutate:{productId} MarketplaceApplicationService 在扣减 / 回补库存前串行化写入
优惠券模板发放 shop:promotion:coupon-template:issue:{templateId} CouponTemplateService 防止并发超发
定时任务单实例执行 shop:order:scheduler:cancel-expiredshop:order:scheduler:auto-completeshop:subscription:scheduler:renewal 多副本部署时避免重复调度

Redisson vs Lua 脚本#

维度 Redisson RLock Lua 脚本
实现方式 标准 RLock 语义 + WatchDog / lease time 原子执行脚本
适用场景 通用分布式锁 精细原子操作
性能 中(多次 Round-Trip) 高(一次 Round-Trip)
灵活性 标准锁语义 完全自定义

shop 项目中,抢红包、网关限流 这类“多步 Redis 操作要一次性完成”的场景继续用 Lua;库存变更、优惠券发放、定时任务单实例执行 这类标准互斥需求则交给 Redisson RLock

参考:Redisson Distributed Locks


Bloom Filter 自动配置#

什么是 Bloom Filter#

Bloom Filter 是一种概率数据结构,用于判断一个元素可能存在于集合中在当前过滤器状态下可判定为不存在。它有误判率(false positive),但不会有漏判(false negative)。

shop-starter-idempotency 中的 Bloom Filter#

// RedisIdempotencyGuard.java
public <T> T executeOnce(String key, Supplier<T> action, Supplier<T> fallback) {
    // Bloom Filter 快速检查
    if (!bloomFilter.contains(key)) {
        missCounter.increment();
        return executeAction(key, action, fallback);
    }
    // 可能存在 → 查 DB 确认
    if (repository.existsByKey(key)) {
        duplicateHitCounter.increment();
        return fallback.get();
    }
    falsePositiveHitCounter.increment();
    return executeAction(key, action, fallback);
}

两层检查的设计:

  1. Bloom Filter 说"不存在" → 在 Bloom Filter 语义下可直接走快速路径,直接执行
  2. Bloom Filter 说"可能存在" → 查 DB 确认(因为 Bloom Filter 有误判率)
  3. Redis 不可用 → 降级到 DB-only 路径

配置参数#

shop:
  idempotency:
    bloom-filter:
      enabled: true
      redis-key: "shop:wallet:idempotency:bf"
      expected-insertions: 1000000   # 预期插入量
      false-probability: 0.001       # 误判率 0.1%

expected-insertionsfalse-probability 共同决定了 Bloom Filter 的底层位数组大小。设置过小会频繁误判,设置过大会浪费内存。

自动配置#

@Bean
@ConditionalOnProperty(prefix = "shop.idempotency.bloom-filter",
        name = "enabled", havingValue = "true")
BloomFilter bloomFilter(RedissonClient redissonClient,
                         BloomFilterProperties properties) {
    RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(properties.getRedisKey());
    bloomFilter.tryInit(properties.getExpectedInsertions(),
            properties.getFalseProbability());
    return new RedissonBloomFilter(bloomFilter);
}

通过 @ConditionalOnProperty 控制,不配置 enabled=true 时不会创建 Bloom Filter,降级为纯 DB 幂等。

参考:Redisson Bloom FilterBloom Filter 原理


Lua 脚本加载策略#

为什么用 Lua#

在 Redis 中,单个命令是原子的,但多个命令组合不是。Lua 脚本保证整个脚本原子执行:

// RateLimitingFilter.java — 网关令牌桶限流
Long allowed = redissonClient.getScript(StringCodec.INSTANCE).eval(
        RScript.Mode.READ_WRITE,
        TOKEN_BUCKET_SCRIPT,
        RScript.ReturnType.INTEGER,
        List.<Object>of(baseKey + ":tokens", baseKey + ":ts"),
        String.valueOf(ratePerSec),
        String.valueOf(capacity),
        String.valueOf(nowMs));

Lua 脚本的两种加载方式#

方式一:内联脚本(shop 项目当前方案)

String script = """
        if redis.call('HEXISTS', KEYS[2], ARGV[1]) == 1 then
            return {-1, redis.call('HGET', KEYS[2], ARGV[1])}
        end
        local amount = redis.call('LPOP', KEYS[1])
        if not amount then
            return {-2}
        end
        redis.call('HSET', KEYS[2], ARGV[1], amount)
        return {1, amount}
        """;
redissonClient.getScript(StringCodec.INSTANCE)
        .eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.MULTI, keys, args);

优点:简单直接,不需要额外文件管理。 缺点:脚本较长时影响可读性。

方式二:外部脚本文件

@Bean
DefaultRedisScript<Long> rateLimitScript() {
    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
    script.setScriptSource(new ResourceScriptSource(
            new ClassPathResource("scripts/rate-limit.lua")));
    script.setResultType(Long.class);
    return script;
}

优点:脚本独立管理,可单元测试。 缺点:需要额外的文件管理和部署。

什么时候不该用 Lua#

场景 更常见的方案 原因
简单计数器 INCR 单命令 本身原子,不需要 Lua
Hash 字段更新 HSET 单命令 本身原子
多步条件操作 Lua 脚本 需要原子性
跨 Key 操作 Redis 事务 / Lua 需要原子性

Lua 脚本的核心价值是"多步操作原子化"。如果单个 Redis 命令本身满足需求,不需要 Lua。

参考:Redis Lua ScriptingSpring Data Redis Scripting


Redis Key 设计规范#

shop 项目的全局 Redis key 命名空间:

buyer:guest:cart:{buyerId}              # 游客购物车
rl:{id}:tokens                          # 网关令牌桶剩余令牌
rl:{id}:ts                              # 网关令牌桶上次补充时间
gateway:canary:{routeId}                # 灰度路由白名单
re:packets:{gameId}                     # 抢红包金额
re:claims:{gameId}                      # 抢红包记录
activity:ac:player:{gameId}:{buyerId}   # 玩家限频
activity:ac:ip:{gameId}:{ip}            # IP 限频
activity:ac:device:{gameId}:{fingerprint} # 设备指纹
shop:wallet:idempotency:bf              # 钱包幂等 Bloom Filter(当前默认值之一)

命名规范:

  • 前缀分层{domain}:{subsystem}:{detail}
  • 避免冲突:不同功能用不同前缀
  • 便于清理:活动结束时可以按前缀批量删除

参考与实现位置#


小结#

Spring Boot 3.5 下 Redis 实战的核心要点:

  • Lettuce 连接池:尽量不要把 pooling 当默认答案;只有 blocking / transactional / dedicated connection 场景才优先考虑
  • 序列化策略:当前仓库已统一到 RedissonClient;简单 KV 场景继续保存 JSON 可读性,Bloom Filter / 锁 / Lua 原子脚本共享同一套客户端基线
  • 分布式锁:Redisson RLock(标准锁)vs Lua 脚本(精细原子操作),按需选择
  • Bloom Filter 自动配置:双层检查(Bloom Filter → DB),失败降级到 DB-only
  • Lua 脚本:多步操作原子化,简单命令不需要 Lua
  • Key 设计规范:前缀分层、避免冲突、便于清理

项目仓库:github.com/meirongdev/shop