艾体宝干货|【Redis实用技巧#19】Redis 十种进阶用法

许多人对 Redis 的认知停留在高性能缓存层,这个定位没有错,但只用来做缓存,相当于买了一台工作站只用来刷网页。Redis 内置的布隆过滤器、Stream 消息队列、GEO 地理检索、HyperLogLog 基数统计,每一项都可以在特定场景替代掉额外引入的中间件,同时享受 Redis 本身的性能和运维优势。

1. 布隆过滤器:拦截缓存穿透


问题背景

高并发系统中,缓存穿透是一个经典的棘手问题——大量请求携带数据库中根本不存在的 Key,Cache 无法命中,全部打穿到数据库。常见的攻击型流量(例如遍历不存在的用户 ID)或程序 Bug 都可能触发这一问题。

布隆过滤器(Bloom Filter)是解决这一问题的标准工具,其核心特性:


特性 说明
内存效率 1 亿个元素通常仅需约 100 MB,远低于直接存储所有合法 Key
判断精度 "不存在"的判断百分百准确;"存在"的判断有极低概率误判(可控,通常 < 1%)
删除支持 标准布隆过滤器不支持删除,需要删除时可考虑 Counting Bloom Filter 变体


工作原理

元素 Key

  │
  ├─── Hash 1 ──→ bit[23] = 1 ─┐
  ├─── Hash 2 ──→ bit[67] = 1 ─┼─→ 三个 bit 全为 1 → "可能存在"(需回查 DB)
  └─── Hash 3 ──→ bit[91] = 1 ─┘

                                   有任意 bit = 0 → "一定不存在"(直接返回)


多个哈希函数将元素映射到位数组的不同位置。查询时,只要有一个 bit 为 0,该元素必然不存在;若全部为 1,则"可能存在",需进一步查询数据库确认。

代码示例(Redisson)

@Componentpublic class BloomFilterService {@Autowiredprivate RedissonClient redissonClient;private RBloomFilter bloomFilter;@PostConstructpublic void init() {

       bloomFilter = redissonClient.getBloomFilter("user:bloom");// 初始化:预计 100 万个元素,误判率 0.01
       bloomFilter.tryInit(1000000L, 0.01);}// 系统启动时将所有合法 userId 写入过滤器public void addUser(Long userId) {
       bloomFilter.add(userId.toString());}public User getUserById(Long userId) {// 过滤器判定不存在 → 直接返回,不查数据库if (!bloomFilter.contains(userId.toString())) {return null;}// 可能存在 → 走正常缓存 + 数据库查询链路return queryFromDB(userId);}}


小结

  • ✅ 优势:内存极省;有效拦截缓存穿透
  • ❌ 劣势:存在一定概率的误判;不支持删除元素
  • ? 适用场景:防恶意请求打穿缓存、爬虫 URL 去重、垃圾邮件过滤


2. Redisson 分布式锁

为什么不能直接用 SET NX?

直接使用 SET key value NX EX seconds 在复杂场景下存在三个隐患:

  1. 过期时间设置不合理 → 锁在业务执行完之前提前释放,导致并发问题
  2. 释放时未校验持有者 → 可能误删其他进程的锁
  3. 不支持自动续期 → 长时间任务跑完之前锁已过期

Redisson 通过 Lua 脚本保证操作原子性,并内置 Watchdog 看门狗机制自动续期,从根本上规避上述问题。

Watchdog 续期机制

业务线程持有锁(TTL = 30s)

      │
      ▼
 [Watchdog 每 10s 检测]
      │
      ├── 线程仍持有锁? → 是 → 续期至 30s → 继续监听
      │
      └── 线程不再持有? → 停止续期,锁自然过期


代码示例

@Servicepublic class OrderService {@Autowiredprivate RedissonClient redissonClient;public void processOrder(String orderId) {

       RLock lock = redissonClient.getLock("order:lock:" + orderId);try {// 等待最多 10s 获取锁;持有时间 30s(Watchdog 会自动续期)if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {doProcess(orderId);} else {throw new RuntimeException("获取锁失败,请稍后重试");}} catch (InterruptedException e) {
           Thread.currentThread().interrupt();throw new RuntimeException("线程被中断", e);} finally {// 只释放由当前线程持有的锁,避免误删他人的锁if (lock.isHeldByCurrentThread()) {
               lock.unlock();}}}}

扩展:Redisson 还支持可重入锁、读写锁( RReadWriteLock)、红锁(Redlock)等高级锁模式,可按业务需要选用。

小结

  • ✅ 优势:Watchdog 自动续期防死锁;可重入;支持读写锁和 Redlock
  • ❌ 劣势:引入 Redisson 依赖,比原生 Redis 命令稍重
  • ? 适用场景:分布式任务调度、库存扣减、订单创建等互斥操作


3. 延迟队列

问题背景

订单 30 分钟未支付自动关闭、消息 N 分钟后重试——这类"延迟执行"场景在业务系统中极为常见。传统方案是轮询数据库定时扫描,效率低、精度差,且对数据库造成不必要的压力。

Redisson 延迟队列基于 Sorted Set 实现,以任务触发时间戳作为 Score,精度和可靠性都显著优于轮询方案。


数据流转过程

addOrder(order, 30, MINUTES)

        │
        ▼
 [Delayed Queue]           到期
 {score: 触发时间戳} ──────────────→ [Blocking Queue]
                                            │
                                            ▼
                                     Consumer.take()
                                            │
                                            ▼
                                     processExpiredOrder()


代码示例

@Componentpublic class DelayQueueService {@Autowiredprivate RedissonClient redissonClient;private RBlockingQueue blockingQueue;private RDelayedQueue  delayedQueue;@PostConstructpublic void init() {

       blockingQueue = redissonClient.getBlockingQueue("order:queue");
       delayedQueue  = redissonClient.getDelayedQueue(blockingQueue);}// 下单时添加延迟任务:30 分钟后触发public void addOrder(Order order, long delay, TimeUnit unit) {
       delayedQueue.offer(order, delay, unit);}// 消费者线程:阻塞等待,任务到期后自动返回@Asyncpublic void startConsumer() {while (true) {try {
               Order order = blockingQueue.take(); // 到期前一直阻塞processExpiredOrder(order);} catch (InterruptedException e) {
               Thread.currentThread().interrupt();break;}}}}


小结

  • ✅ 优势:精度高;分布式;数据自动持久化
  • ❌ 劣势:消费失败的重试逻辑需自行实现
  • ? 适用场景:订单超时关闭、延迟通知、定时任务分发


4. 令牌桶限流

令牌桶 vs. 计数器

对比维度 简单计数器 令牌桶
突发流量 硬截断,体验差 允许桶容量内的突发
实现复杂度 中(Lua 脚本)
流量平滑度 差(窗口边界抖动)
适用场景 简单 QPS 控制 API 限流、秒杀入口

Redis 结合 Lua 脚本可以在单个原子操作内实现令牌桶,无需额外引入 Sentinel 等限流中间件。


代码示例

@Componentpublic class RateLimiterService {@Autowiredprivate RedisTemplate redisTemplate;// Lua 脚本在 Redis 服务端原子执行,无并发安全问题private static final String LUA_SCRIPT ="local key      = KEYS[1]\n" +"local limit    = tonumber(ARGV[1])\n" +"local interval = tonumber(ARGV[2])\n" +"local current  = redis.call('get', key)\n" +"if current and tonumber(current) >= limit then\n" +"    return 0\n" +                   // 超限,拒绝"else\n" +"    redis.call('incr', key)\n" +"    redis.call('expire', key, interval)\n" +"    return 1\n" +                   // 放行"end";public boolean tryAcquire(String key, int limit, int intervalSec) {

       DefaultRedisScript script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
       Long result = redisTemplate.execute(
           script,
           Collections.singletonList(key),
           limit,
           intervalSec
       );return result != null && result == 1L;}}


小结

  • ✅ 优势:原子操作无竞态;支持瞬时突发流量平滑处理
  • ❌ 劣势:精细化控制建议配合滑动窗口算法
  • ? 适用场景:API 限流、反欺诈、秒杀入口流量控制


5. Bitmap 统计

核心思路

Redis Bitmap 本质上是对 String 类型的位操作封装——用一个 bit 表示一个布尔值(0/1),适合大规模布尔型数据的紧凑存储。

特性 说明
内存效率 1 亿个元素通常仅需约 100 MB,远低于直接存储所有合法 Key
判断精度 "不存在"的判断百分百准确;"存在"的判断有极低概率误判(可控,通常 < 1%)
删除支持 标准布隆过滤器不支持删除,需要删除时可考虑 Counting Bloom Filter 变体


工作原理

元素 Key

  │
  ├─── Hash 1 ──→ bit[23] = 1 ─┐
  ├─── Hash 2 ──→ bit[67] = 1 ─┼─→ 三个 bit 全为 1 → "可能存在"(需回查 DB)
  └─── Hash 3 ──→ bit[91] = 1 ─┘

                                   有任意 bit = 0 → "一定不存在"(直接返回)

多个哈希函数将元素映射到位数组的不同位置。查询时,只要有一个 bit 为 0,该元素必然不存在;若全部为 1,则"可能存在",需进一步查询数据库确认。


码示例(Redisson)

@Componentpublic class BloomFilterService {@Autowiredprivate RedissonClient redissonClient;private RBloomFilter bloomFilter;@PostConstructpublic void init() {

       bloomFilter = redissonClient.getBloomFilter("user:bloom");// 初始化:预计 100 万个元素,误判率 0.01
       bloomFilter.tryInit(1000000L, 0.01);}// 系统启动时将所有合法 userId 写入过滤器public void addUser(Long userId) {
       bloomFilter.add(userId.toString());}public User getUserById(Long userId) {// 过滤器判定不存在 → 直接返回,不查数据库if (!bloomFilter.contains(userId.toString())) {return null;}// 可能存在 → 走正常缓存 + 数据库查询链路return queryFromDB(userId);}}


小结

  • ✅ 优势:内存极省;有效拦截缓存穿透
  • ❌ 劣势:存在一定概率的误判;不支持删除元素
  • ? 适用场景:防恶意请求打穿缓存、爬虫 URL 去重、垃圾邮件过滤


2. Redisson 分布式锁

为什么不能直接用 SET NX?

直接使用  SET key value NX EX seconds 在复杂场景下存在三个隐患:

  1. 过期时间设置不合理 → 锁在业务执行完之前提前释放,导致并发问题
  2. 释放时未校验持有者 → 可能误删其他进程的锁
  3. 不支持自动续期 → 长时间任务跑完之前锁已过期

Redisson 通过 Lua 脚本保证操作原子性,并内置 Watchdog 看门狗机制自动续期,从根本上规避上述问题。

Watchdog 续期机制

业务线程持有锁(TTL = 30s)

      │
      ▼
 [Watchdog 每 10s 检测]
      │
      ├── 线程仍持有锁? → 是 → 续期至 30s → 继续监听
      │
      └── 线程不再持有? → 停止续期,锁自然过期


代码示例

@Servicepublic class OrderService {@Autowiredprivate RedissonClient redissonClient;public void processOrder(String orderId) {

       RLock lock = redissonClient.getLock("order:lock:" + orderId);try {// 等待最多 10s 获取锁;持有时间 30s(Watchdog 会自动续期)if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {doProcess(orderId);} else {throw new RuntimeException("获取锁失败,请稍后重试");}} catch (InterruptedException e) {
           Thread.currentThread().interrupt();throw new RuntimeException("线程被中断", e);} finally {// 只释放由当前线程持有的锁,避免误删他人的锁if (lock.isHeldByCurrentThread()) {
               lock.unlock();}}}}

扩展:Redisson 还支持可重入锁、读写锁( RReadWriteLock)、红锁(Redlock)等高级锁模式,可按业务需要选用。


小结

  • ✅ 优势:Watchdog 自动续期防死锁;可重入;支持读写锁和 Redlock
  • ❌ 劣势:引入 Redisson 依赖,比原生 Redis 命令稍重
  • ? 适用场景:分布式任务调度、库存扣减、订单创建等互斥操作


3. 延迟队列

问题背景

订单 30 分钟未支付自动关闭、消息 N 分钟后重试——这类"延迟执行"场景在业务系统中极为常见。传统方案是轮询数据库定时扫描,效率低、精度差,且对数据库造成不必要的压力。

Redisson 延迟队列基于 Sorted Set 实现,以任务触发时间戳作为 Score,精度和可靠性都显著优于轮询方案。

数据流转过程

addOrder(order, 30, MINUTES)

        │
        ▼
 [Delayed Queue]           到期
 {score: 触发时间戳} ──────────────→ [Blocking Queue]
                                            │
                                            ▼
                                     Consumer.take()
                                            │
                                            ▼
                                     processExpiredOrder()


代码示例

@Componentpublic class DelayQueueService {@Autowiredprivate RedissonClient redissonClient;private RBlockingQueue blockingQueue;private RDelayedQueue  delayedQueue;@PostConstructpublic void init() {

       blockingQueue = redissonClient.getBlockingQueue("order:queue");
       delayedQueue  = redissonClient.getDelayedQueue(blockingQueue);}// 下单时添加延迟任务:30 分钟后触发public void addOrder(Order order, long delay, TimeUnit unit) {
       delayedQueue.offer(order, delay, unit);}// 消费者线程:阻塞等待,任务到期后自动返回@Asyncpublic void startConsumer() {while (true) {try {
               Order order = blockingQueue.take(); // 到期前一直阻塞processExpiredOrder(order);} catch (InterruptedException e) {
               Thread.currentThread().interrupt();break;}}}}


小结

  • ✅ 优势:精度高;分布式;数据自动持久化
  • ❌ 劣势:消费失败的重试逻辑需自行实现
  • ? 适用场景:订单超时关闭、延迟通知、定时任务分发


4. 令牌桶限流

令牌桶 vs. 计数器

对比维度 简单计数器 令牌桶
突发流量 硬截断,体验差 允许桶容量内的突发
实现复杂度 中(Lua 脚本)
流量平滑度 差(窗口边界抖动)
适用场景 简单 QPS 控制 API 限流、秒杀入口

Redis 结合 Lua 脚本可以在单个原子操作内实现令牌桶,无需额外引入 Sentinel 等限流中间件。

代码示例

@Componentpublic class RateLimiterService {@Autowiredprivate RedisTemplate redisTemplate;// Lua 脚本在 Redis 服务端原子执行,无并发安全问题private static final String LUA_SCRIPT ="local key      = KEYS[1]\n" +"local limit    = tonumber(ARGV[1])\n" +"local interval = tonumber(ARGV[2])\n" +"local current  = redis.call('get', key)\n" +"if current and tonumber(current) >= limit then\n" +"    return 0\n" +                   // 超限,拒绝"else\n" +"    redis.call('incr', key)\n" +"    redis.call('expire', key, interval)\n" +"    return 1\n" +                   // 放行"end";public boolean tryAcquire(String key, int limit, int intervalSec) {

       DefaultRedisScript script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
       Long result = redisTemplate.execute(
           script,
           Collections.singletonList(key),
           limit,
           intervalSec
       );return result != null && result == 1L;}}

小结

  • ✅ 优势:原子操作无竞态;支持瞬时突发流量平滑处理
  • ❌ 劣势:精细化控制建议配合滑动窗口算法
  • ? 适用场景:API 限流、反欺诈、秒杀入口流量控制


5. Bitmap 统计

核心思路

Redis Bitmap 本质上是对 String 类型的位操作封装——用一个 bit 表示一个布尔值(0/1),适合大规模布尔型数据的紧凑存储。

用户 ID 作为 bit 偏移量,值 1 = 当天签到/活跃:


key: "sign:2026-04-10"

offset:  0   1   2   3  ...  userId  ...
bit:    [0] [1] [0] [1] ...   [1]   ...
                               ↑
                         该用户今天签到了

内存对比:

方案 1 亿用户签到状态
Set 存储所有签到 userId \~800 MB(每个 long 8 字节)
Bitmap \~12 MB


代码示例

@Componentpublic class BitmapService {@Autowiredprivate RedisTemplate redisTemplate;// 用户签到:key = "sign:2026-04-10",offset = userIdpublic void signIn(Long userId, LocalDate date) {

       String key = "sign:" + date.toString();
       redisTemplate.opsForValue().setBit(key, userId, true);}// 统计某日总签到人数(BITCOUNT 命令,效率极高)public Long countSignIn(LocalDate date) {
       String key = "sign:" + date.toString();return redisTemplate.execute((RedisCallback) connection -> connection.bitCount(key.getBytes()));}// 查询某用户某天是否签到public Boolean hasSignIn(Long userId, LocalDate date) {
       String key = "sign:" + date.toString();return redisTemplate.opsForValue().getBit(key, userId);}}


小结

  • ✅ 优势:内存极省(\~12 MB / 亿用户); BITCOUNT 统计速度极快
  • ❌ 劣势:只能表达布尔状态(0/1),不适合复杂统计
  • ? 适用场景:用户签到、在线状态追踪、布隆过滤器底层结构


6. HyperLogLog 基数估算

精确计数 vs. HyperLogLog

统计独立访客数(UV)面临经典矛盾:

  • 精确计数:需要存储所有去重 ID,内存随数据量线性增长
  • HyperLogLog:固定约 12 KB 内存估算亿级数据集的基数,误差率约 0.81%

对于大多数流量分析场景,0.81% 的误差完全可接受,而内存节省是数量级级别的。

代码示例

@Componentpublic class UVService {@Autowiredprivate RedisTemplate redisTemplate;// 记录一次访问(自动去重)public void addVisit(String date, String userId) {
        redisTemplate.opsForHyperLogLog().add("uv:" + date, userId);}// 查询当日 UVpublic Long countUV(String date) {return redisTemplate.opsForHyperLogLog().size("uv:" + date);}// 合并多日 UV,计算周活跃用户数(PFMERGE 原生支持)public Long countWeeklyUV(List dates) {
        String destKey = "uv:weekly:" + LocalDate.now();for (String date : dates) {
            redisTemplate.opsForHyperLogLog().union(destKey, "uv:" + date);}return redisTemplate.opsForHyperLogLog().size(destKey);}}

注意:HyperLogLog 无法判断某个具体元素是否存在于集合中,只能返回估算的基数总量。如需判断存在性,应使用布隆过滤器。

小结

  • 优势:内存固定约 12 KB,支持 PFMERGE 合并多集合
  • 劣势:结果为估算值(误差 \~0.81%);无法查询单个元素
  • ? 适用场景:UV 统计、大规模去重计数、流量趋势分析


7. GEO 地理检索

为什么用 Redis GEO?

Redis 3.2 引入的 GEO 命令族基于 Sorted Set 实现,以 GeoHash 编码存储经纬度,支持:

  • 半径搜索GEORADIUS):查找某坐标 N 公里内的所有 POI
  • 矩形搜索GEOSEARCH):按边界框筛选
  • 距离计算GEODIST):两点间精确距离

外卖骑手调度、同城附近门店搜索、打车匹配——这类"附近的 X"场景直接用 Redis GEO 即可,不必单独引入 Elasticsearch 或专业地理检索引擎。

代码示例

@Componentpublic class GeoService {@Autowiredprivate RedisTemplate redisTemplate;// 录入门店坐标(lng = 经度,lat = 纬度)public void addStore(Long storeId, double lng, double lat) {
        redisTemplate.opsForGeo().add("stores:geo", new Point(lng, lat), storeId.toString());}// 查询指定坐标 5 公里内的门店,按距离排序public List findNearbyStores(double lng, double lat, double radiusKm) {
        Circle circle = new Circle(new Point(lng, lat),new Distance(radiusKm, Metrics.KILOMETERS));
        GeoResults> results =
            redisTemplate.opsForGeo().radius("stores:geo", circle);return parseResults(results); // 解析并构造 Store 对象列表}// 计算两个门店之间的直线距离public Distance distanceBetweenStores(Long storeId1, Long storeId2) {return redisTemplate.opsForGeo().distance("stores:geo", storeId1.toString(), storeId2.toString());}}

小结

  • 优势:无需额外中间件;支持半径/矩形搜索和精确距离计算
  • 劣势:GeoHash 精度有上限;坐标频繁更新时需重建索引
  • ? 适用场景:外卖骑手调度、附近门店推荐、打车匹配


8. Stream 消息队列

Stream vs. 传统消息队列

Redis 5.0 引入的 Stream 是持久化的消息流结构,核心能力对比:

能力 Redis Stream RabbitMQ / Kafka
消息持久化
消费者组
ACK 确认
历史消息回放 ✅(Kafka)
独立部署成本 无(复用 Redis) 需单独维护
吞吐量上限 中等 高(Kafka)
适用规模 轻量级异步任务 高吞吐、强可靠性场景

消费者组模型

Producer ──XADD──→ [Stream 持久化日志]
                   │ msg-1 │ msg-2 │ msg-3 │ ...
                          │
                   XREADGROUP
                     /      \
              consumer-1   consumer-2     ← 同组内消息不重复消费
                  │
                 处理
                  │
                 ACK ──→ 从 PEL(待确认列表)移除
                          未 ACK 的消息可被重新投递


代码示例

@Componentpublic class StreamService {@Autowiredprivate RedissonClient redissonClient;private RStream stream;@PostConstructpublic void init() {
        stream = redissonClient.getStream("order-stream");// 消费者组不存在时自动创建
        stream.createGroup("order-group", StreamMessageId.AUTO);}// 生产者:写入消息public void publish(String orderId) {
        Map data = Map.of("orderId",   orderId,"timestamp", String.valueOf(System.currentTimeMillis()));
        stream.add(data);}// 消费者:批量拉取并处理@Asyncpublic void consume() {while (true) {
            Map> messages =
                stream.readGroup("order-group", "consumer-1", 10);for (var entry : messages.entrySet()) {process(entry.getValue());
                stream.ack("order-group", entry.getKey()); // 确认消费}if (messages.isEmpty()) {
                Thread.sleep(1000); // 无消息时短暂休眠}}}}


小结

  • 优势:消息持久化;支持消费者组、ACK 确认和历史回放;复用已有 Redis 实例
  • 劣势:功能丰富度不及专业消息队列;不适合超高吞吐量场景
  • ? 适用场景:异步任务队列、日志采集、站内通知系统


9. Lua 脚本:原子复合操作

为什么需要 Lua 脚本?

Redis 是单线程命令执行模型,但多个命令之间仍然可能被其他客户端请求插入。Lua 脚本在 Redis 服务端 原子执行——脚本运行期间不处理其他请求,相当于一个原生事务,且比 MULTI/EXEC 更灵活,支持条件判断和复杂逻辑。

典型竞态场景

// 非原子的"查询 + 扣减",高并发下会超卖:
int stock = redis.get("stock:101");   // 线程 A 读到库存 = 1
                                       // 线程 B 也读到库存 = 1
if (stock > 0) redis.decr("stock:101"); // 两个线程都执行了扣减!
// Lua 脚本版本:整个判断+扣减是原子操作,不会被打断


代码示例:库存扣减 + 分布式锁

@Componentpublic class LuaScriptService {@Autowiredprivate RedisTemplate redisTemplate;// ── 场景一:库存扣减(查询 + 扣减原子完成,防超卖)──private static final String DECREASE_STOCK_SCRIPT ="local stock = redis.call('get', KEYS[1])\n" +"if not stock or tonumber(stock) < tonumber(ARGV[1]) then\n" +"    return 0\n" +        // 库存不足,拒绝"else\n" +"    redis.call('decrby', KEYS[1], ARGV[1])\n" +"    return 1\n" +        // 扣减成功"end";public boolean decreaseStock(String productId, int count) {
        DefaultRedisScript script =new DefaultRedisScript<>(DECREASE_STOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList("stock:" + productId),
            count
        );return result != null && result == 1L;}// ── 场景二:SETNX + EXPIRE 原子化(解决两条命令之间宕机的问题)──private static final String LOCK_SCRIPT ="if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then\n" +"    redis.call('expire', KEYS[1], ARGV[2])\n" +"    return 1\n" +"else\n" +"    return 0\n" +"end";public boolean lock(String key, String value, int expireSec) {
        DefaultRedisScript script =new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(
            script,
            Collections.singletonList(key),
            value,
            expireSec
        );return result != null && result == 1L;}}

注意:Lua 脚本执行期间会阻塞 Redis 处理其他请求。脚本逻辑应尽量简洁,避免执行耗时操作(如大量循环)。


小结

  • 优势:原子性保证;减少网络往返;支持条件判断
  • 劣势:调试困难;执行期间阻塞其他请求,需控制脚本复杂度
  • ? 适用场景:库存扣减防超卖、限流计数、需要 CAS(Compare-And-Swap)语义的条件更新


10. RedisJSON:直接操作 JSON 文档

传统方案的痛点

不使用 RedisJSON 时,在 Redis 中存储 JSON 的常规做法:

读取 → JSON.parse() → 修改字段 → JSON.stringify() → 写回

每次修改一个字段都需要读取整个文档,序列化/反序列化开销大,且在并发场景下存在一致性风险。

RedisJSON 的能力

RedisJSON 模块(Redis Stack 内置)允许:

  • 字段级读写:直接用 JSONPath 语法操作嵌套字段
  • 原子修改:无需取出整个文档
  • 索引查询:配合 RediSearch 模块对 JSON 字段建立二级索引,支持全文检索和复杂查询


核心命令

# 存储完整 JSON 文档
JSON.SET user:1001 $ '{"name":"张三","age":28,"city":"上海","tags":["vip","active"]}'# 只读取某个字段(无需加载整个文档)
JSON.GET user:1001 $.city
# → ["上海"]# 只更新 age 字段
JSON.SET user:1001 $.age 29
# 向数组追加元素
JSON.ARRAPPEND user:1001 $.tags '"senior"'# 原子自增(无需 GET → 修改 → SET)
JSON.NUMINCRBY user:1001 $.age 1


代码示例(Java,使用 Lettuce 底层)

@Componentpublic class RedisJsonService {@Autowiredprivate RedisTemplate redisTemplate;// 存储完整 JSON 文档public void saveUser(Long userId, User user) {
        redisTemplate.execute((RedisCallback) connection ->
            connection.execute("JSON.SET",("user:" + userId).getBytes(),"$".getBytes(),
                objectMapper.writeValueAsBytes(user)));}// 仅更新单个字段,不影响其他字段public void updateAge(Long userId, int age) {
        redisTemplate.execute((RedisCallback) connection ->
            connection.execute("JSON.SET",("user:" + userId).getBytes(),"$.age".getBytes(),
                String.valueOf(age).getBytes()));}// 读取单个字段public String getCity(Long userId) {return (String) redisTemplate.execute((RedisCallback) connection ->
            connection.execute("JSON.GET",("user:" + userId).getBytes(),"$.city".getBytes()));}}


小结

  • 优势:字段级读写,避免全量序列化/反序列化;配合 RediSearch 支持索引查询
  • 劣势:需安装 RedisJSON 模块;复杂嵌套文档内存占用相对较高
  • ? 适用场景:用户配置存储、Session 管理、动态表单、需要局部更新的复杂对象


总结:选型参考

# 用法 核心价值 关键约束 典型场景
1 布隆过滤器 极省内存拦截"一定不存在"的请求 有误判;不支持删除 缓存穿透防护
2 Redisson 分布式锁 Watchdog 自动续期,解决锁过期问题 引入 Redisson 依赖 互斥操作
3 延迟队列 精确定时触发,替代数据库轮询 消费失败需自行重试 订单超时关闭
4 令牌桶限流 Lua 原子操作,支持突发流量平滑 精细控制需结合滑动窗口 API 限流
5 Bitmap 1 亿用户布尔状态仅需 12 MB 只能表达 0/1 状态 签到统计
6 HyperLogLog 固定 ~12 KB 内存估算亿级基数 误差率 ~0.81% UV 统计
7 GEO 检索 附近搜索/距离计算,无需额外引擎 坐标更新需重建索引 附近门店
8 Stream 队列 持久化 + 消费者组 + ACK,轻量 MQ 不及专业 MQ 吞吐量高 异步任务
9 Lua 脚本 原子复合操作,消除竞态窗口 执行期间阻塞;调试困难 库存扣减
10 RedisJSON 字段级 JSON 操作,配合索引查询 需安装模块 Session 存储

最后提示:Redis 是内存数据库,以上所有特性都建立在"数据放得进内存"这一前提上。在生产环境中合理组合这些能力,可以显著减少系统的中间件数量。大数据集场景下,务必评估内存成本并制定相应的持久化与淘汰策略。