许多人对 Redis 的认知停留在高性能缓存层,这个定位没有错,但只用来做缓存,相当于买了一台工作站只用来刷网页。Redis 内置的布隆过滤器、Stream 消息队列、GEO 地理检索、HyperLogLog 基数统计,每一项都可以在特定场景替代掉额外引入的中间件,同时享受 Redis 本身的性能和运维优势。
高并发系统中,缓存穿透是一个经典的棘手问题——大量请求携带数据库中根本不存在的 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,则"可能存在",需进一步查询数据库确认。
@Componentpublic class BloomFilterService {@Autowiredprivate RedissonClient redissonClient;private RBloomFilter<String> 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 去重、垃圾邮件过滤
直接使用
SET key value NX EX seconds 在复杂场景下存在三个隐患:
Redisson 通过 Lua 脚本保证操作原子性,并内置 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)等高级锁模式,可按业务需要选用。
订单 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<Order> blockingQueue;private RDelayedQueue<Order> 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;}}}}
| 对比维度 | 简单计数器 | 令牌桶 |
| 突发流量 | 硬截断,体验差 | 允许桶容量内的突发 |
| 实现复杂度 | 低 | 中(Lua 脚本) |
| 流量平滑度 | 差(窗口边界抖动) | 好 |
| 适用场景 | 简单 QPS 控制 | API 限流、秒杀入口 |
Redis 结合 Lua 脚本可以在单个原子操作内实现令牌桶,无需额外引入 Sentinel 等限流中间件。
@Componentpublic class RateLimiterService {@Autowiredprivate RedisTemplate<String, Object> 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<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
limit,
intervalSec
);return result != null && result == 1L;}}
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,则"可能存在",需进一步查询数据库确认。
@Componentpublic class BloomFilterService {@Autowiredprivate RedissonClient redissonClient;private RBloomFilter<String> 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);}}
直接使用
SET key value NX EX seconds 在复杂场景下存在三个隐患:
Redisson 通过 Lua 脚本保证操作原子性,并内置 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)等高级锁模式,可按业务需要选用。
订单 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<Order> blockingQueue;private RDelayedQueue<Order> 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;}}}}
| 对比维度 | 简单计数器 | 令牌桶 |
| 突发流量 | 硬截断,体验差 | 允许桶容量内的突发 |
| 实现复杂度 | 低 | 中(Lua 脚本) |
| 流量平滑度 | 差(窗口边界抖动) | 好 |
| 适用场景 | 简单 QPS 控制 | API 限流、秒杀入口 |
Redis 结合 Lua 脚本可以在单个原子操作内实现令牌桶,无需额外引入 Sentinel 等限流中间件。
@Componentpublic class RateLimiterService {@Autowiredprivate RedisTemplate<String, Object> 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<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
limit,
intervalSec
);return result != null && result == 1L;}}
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<String, Object> 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<Long>) connection -> connection.bitCount(key.getBytes()));}// 查询某用户某天是否签到public Boolean hasSignIn(Long userId, LocalDate date) {
String key = "sign:" + date.toString();return redisTemplate.opsForValue().getBit(key, userId);}}
BITCOUNT 统计速度极快统计独立访客数(UV)面临经典矛盾:
对于大多数流量分析场景,0.81% 的误差完全可接受,而内存节省是数量级级别的。
@Componentpublic class UVService {@Autowiredprivate RedisTemplate<String, Object> 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<String> dates) {
String destKey = "uv:weekly:" + LocalDate.now();for (String date : dates) {
redisTemplate.opsForHyperLogLog().union(destKey, "uv:" + date);}return redisTemplate.opsForHyperLogLog().size(destKey);}}
注意:HyperLogLog 无法判断某个具体元素是否存在于集合中,只能返回估算的基数总量。如需判断存在性,应使用布隆过滤器。
PFMERGE 合并多集合Redis 3.2 引入的 GEO 命令族基于 Sorted Set 实现,以 GeoHash 编码存储经纬度,支持:
GEORADIUS):查找某坐标 N 公里内的所有 POIGEOSEARCH):按边界框筛选GEODIST):两点间精确距离外卖骑手调度、同城附近门店搜索、打车匹配——这类"附近的 X"场景直接用 Redis GEO 即可,不必单独引入 Elasticsearch 或专业地理检索引擎。
@Componentpublic class GeoService {@Autowiredprivate RedisTemplate<String, Object> 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<Store> findNearbyStores(double lng, double lat, double radiusKm) {
Circle circle = new Circle(new Point(lng, lat),new Distance(radiusKm, Metrics.KILOMETERS));
GeoResults<RedisGeoCommands.GeoLocation<Object>> 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());}}
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<String, String> stream;@PostConstructpublic void init() {
stream = redissonClient.getStream("order-stream");// 消费者组不存在时自动创建
stream.createGroup("order-group", StreamMessageId.AUTO);}// 生产者:写入消息public void publish(String orderId) {
Map<String, String> data = Map.of("orderId", orderId,"timestamp", String.valueOf(System.currentTimeMillis()));
stream.add(data);}// 消费者:批量拉取并处理@Asyncpublic void consume() {while (true) {
Map<StreamMessageId, Map<String, String>> 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); // 无消息时短暂休眠}}}}
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<String, Object> 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<Long> 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<Long> script =new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
value,
expireSec
);return result != null && result == 1L;}}
注意:Lua 脚本执行期间会阻塞 Redis 处理其他请求。脚本逻辑应尽量简洁,避免执行耗时操作(如大量循环)。
不使用 RedisJSON 时,在 Redis 中存储 JSON 的常规做法:
读取 → JSON.parse() → 修改字段 → JSON.stringify() → 写回
每次修改一个字段都需要读取整个文档,序列化/反序列化开销大,且在并发场景下存在一致性风险。
RedisJSON 模块(Redis Stack 内置)允许:
# 存储完整 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
@Componentpublic class RedisJsonService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;// 存储完整 JSON 文档public void saveUser(Long userId, User user) {
redisTemplate.execute((RedisCallback<Object>) connection ->
connection.execute("JSON.SET",("user:" + userId).getBytes(),"$".getBytes(),
objectMapper.writeValueAsBytes(user)));}// 仅更新单个字段,不影响其他字段public void updateAge(Long userId, int age) {
redisTemplate.execute((RedisCallback<Object>) connection ->
connection.execute("JSON.SET",("user:" + userId).getBytes(),"$.age".getBytes(),
String.valueOf(age).getBytes()));}// 读取单个字段public String getCity(Long userId) {return (String) redisTemplate.execute((RedisCallback<Object>) connection ->
connection.execute("JSON.GET",("user:" + userId).getBytes(),"$.city".getBytes()));}}
| # | 用法 | 核心价值 | 关键约束 | 典型场景 |
| 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 是内存数据库,以上所有特性都建立在"数据放得进内存"这一前提上。在生产环境中合理组合这些能力,可以显著减少系统的中间件数量。大数据集场景下,务必评估内存成本并制定相应的持久化与淘汰策略。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70026953/viewspace-3116883/ ,如需转载,请注明出处,否则将追究法律责任。