许多人对 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 在复杂场景下存在三个隐患:
- 过期时间设置不合理 → 锁在业务执行完之前提前释放,导致并发问题
- 释放时未校验持有者 → 可能误删其他进程的锁
- 不支持自动续期 → 长时间任务跑完之前锁已过期
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 在复杂场景下存在三个隐患:
- 过期时间设置不合理 → 锁在业务执行完之前提前释放,导致并发问题
- 释放时未校验持有者 → 可能误删其他进程的锁
- 不支持自动续期 → 长时间任务跑完之前锁已过期
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
小结
- ✅ 优势:字段级读写,避免全量序列化/反序列化;配合 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 是内存数据库,以上所有特性都建立在"数据放得进内存"这一前提上。在生产环境中合理组合这些能力,可以显著减少系统的中间件数量。大数据集场景下,务必评估内存成本并制定相应的持久化与淘汰策略。