Appearance
Redis 高级特性
Redis 除了缓存和基本数据结构,还提供了事务、Lua 脚本、Pipeline、发布订阅、Stream 消息队列等高级特性。这些特性让 Redis 的能力远超缓存。
一、Redis 事务
1.1 事务命令
bash
MULTI # 开启事务
SET key1 v1
SET key2 v2
INCR counter
EXEC # 执行事务(所有命令顺序执行)
# 或
DISCARD # 取消事务1.2 事务特性
Redis 事务的特点:
✅ 原子性:事务中的命令要么全执行,要么全不执行(但不支持回滚)
✅ 隔离性:事务中的命令在执行前不会被执行
✅ 顺序性:严格按入队顺序执行
❌ 不支持回滚:EXEC 后即使某条命令失败,其他命令仍会执行
❌ 不支持并发控制:没有悲观锁或 MVCC1.3 WATCH 乐观锁
bash
# 使用 WATCH 实现乐观锁
WATCH balance:user:1 # 监视 key
balance = GET balance:user:1 # 读取当前值
MULTI
SET balance:user:1 (balance - 100) # 扣款
EXEC
# 如果 EXEC 之前,balance:user:1 被其他客户端修改了
# EXEC 返回 nil,事务不执行1.4 事务 vs Lua
| 维度 | 事务(MULTI/EXEC) | Lua 脚本 |
|---|---|---|
| 原子性 | 支持 | 支持 |
| 条件判断 | 不支持(中间不可做判断) | 支持 |
| 回滚 | 不支持 | 不支持 |
| 复用性 | 每次都要发送 | 脚本缓存,复用 |
| 网络开销 | 多条命令多次往返 | 一次发送,一次执行 |
二、Lua 脚本
2.1 为什么用 Lua?
Lua 脚本的优势:
✅ 原子性:整个脚本作为一个原子操作执行
✅ 减少网络开销:多条命令打包发送
✅ 复用性:脚本可以缓存到 Redis,重复使用
✅ 条件逻辑:可以在脚本中做判断和循环2.2 基本用法
java
// 使用 Lua 脚本实现分布式锁释放
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
// 执行脚本
Long result = jedis.eval(script,
Collections.singletonList("lock:order:1001"), // KEYS
Collections.singletonList("uuid-12345")); // ARGV
// 预加载脚本(缓存 SHA1,后续用 evalsha 调用)
String sha1 = jedis.scriptLoad(script);
jedis.evalsha(sha1, keys, args);2.3 实战:限流脚本
lua
-- 滑动窗口限流
-- KEYS[1] = 限流 key
-- ARGV[1] = 窗口大小(毫秒)
-- ARGV[2] = 限流阈值
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = redis.call('TIME')[1] * 1000 + math.floor(redis.call('TIME')[2] / 1000)
-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return 1 -- 放行
else
return 0 -- 限流
end2.4 注意事项
⚠️ Lua 脚本注意事项:
1. 脚本执行是原子的,会阻塞其他命令
2. 脚本不能执行太长时间(默认超时 5 秒)
3. 不要写随机逻辑(影响主从复制一致性)
4. 脚本中所有 key 必须通过 KEYS 传入(集群模式需要 key 路由)三、Pipeline(管道)
3.1 为什么需要 Pipeline?
没有 Pipeline:
客户端 → SET key1 v1 → Redis(RTT 1ms)
客户端 → SET key2 v2 → Redis(RTT 1ms)
客户端 → SET key3 v3 → Redis(RTT 1ms)
...
100 条命令 = 100ms RTT(网络延迟)
使用 Pipeline:
客户端 → [SET key1 v1, SET key2 v2, ..., SET key100 v100] → Redis
100 条命令 = 1ms RTT(一次网络往返)
性能提升 100 倍!3.2 使用方式
java
// Jedis Pipeline
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.set("key:" + i, "value:" + i);
}
pipeline.sync(); // 批量发送,一次性获取结果
// Spring Data Redis
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < 10000; i++) {
connection.set(("key:" + i).getBytes(), ("value:" + i).getBytes());
}
return null;
});3.3 Pipeline vs 事务 vs Lua
| 维度 | Pipeline | 事务(MULTI) | Lua 脚本 |
|---|---|---|---|
| 原子性 | ❌ 不保证 | ✅ 保证 | ✅ 保证 |
| 网络优化 | ✅ 批量发送 | ✅ 批量发送 | ✅ 一次发送 |
| 命令顺序 | 保证顺序 | 保证顺序 | 脚本内顺序执行 |
| 使用场景 | 批量操作,不要求原子性 | 需要原子性的批量操作 | 需要条件判断的原子操作 |
四、发布订阅
4.1 基本命令
bash
# 订阅
SUBSCRIBE channel1 channel2 # 订阅多个频道
PSUBSCRIBE order.* # 模式订阅
# 发布
PUBLISH channel1 "hello world"
# 取消订阅
UNSUBSCRIBE channel14.2 使用场景
java
// 发布者
jedis.publish("order:create", "order:1001");
// 订阅者
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("收到消息:" + channel + " - " + message);
}
}, "order:create");4.3 局限性
Pub/Sub 的局限:
❌ 消息不持久化:订阅者离线期间的消息会丢失
❌ 没有 ACK 机制:无法确认消息是否成功处理
❌ 不支持消息堆积:生产者快、消费者慢时,消息会丢失
如果需要可靠的消息队列,用 Stream五、Stream(消息队列)
5.1 为什么需要 Stream?
Redis 5.0 引入 Stream,专为解决 Pub/Sub 的可靠性问题。支持消息持久化、消费者组、ACK 确认、消息回溯。
Redis Stream 特性:
✅ 消息持久化(存储在内存,可 AOF/RDB 持久化)
✅ 消费者组(多个消费者并行消费,不重复)
✅ ACK 机制(确认消息处理完成)
✅ 消息回溯(可重放历史消息)
✅ 阻塞读取(XREAD BLOCK)5.2 基本操作
bash
# 添加消息
XADD mystream * key1 value1 key2 value2
# * 表示自动生成消息 ID
# 返回:1612345678000-0
# 读取消息
XREAD COUNT 2 STREAMS mystream 0 # 从头读 2 条
XREAD BLOCK 5000 STREAMS mystream $ # 阻塞等待新消息
# 创建消费者组
XGROUP CREATE mystream group1 0 # 从 0 开始消费
# 消费者组消费
XREADGROUP GROUP group1 consumer1 COUNT 2 STREAMS mystream >
# > 表示只读取新消息
# ACK 确认
XACK mystream group1 1612345678000-0
# 查看未确认消息
XPENDING mystream group15.3 Spring Boot 集成
java
// 生产者
@Service
public class OrderProducer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void sendOrder(Order order) {
Map<String, String> message = new HashMap<>();
message.put("orderId", order.getId().toString());
message.put("amount", order.getAmount().toString());
redisTemplate.opsForStream()
.add("order:stream", message);
}
}
// 消费者
@Component
public class OrderConsumer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PostConstruct
public void start() {
new Thread(() -> {
while (true) {
List<MapRecord<String, Object, Object>> messages =
redisTemplate.opsForStream()
.read(Consumer.from("order-group", "consumer-1"),
StreamReadOptions.empty().count(5).block(Duration.ofSeconds(5)),
StreamOffset.create("order:stream", ReadOffset.lastConsumed()));
for (MapRecord<String, Object, Object> msg : messages) {
System.out.println("处理订单:" + msg.getValue());
redisTemplate.opsForStream()
.acknowledge("order:stream", "order-group", msg.getId());
}
}
}).start();
}
}5.4 Stream vs Kafka vs RabbitMQ
| 维度 | Redis Stream | Kafka | RabbitMQ |
|---|---|---|---|
| 吞吐量 | 高(10W+/s) | 极高(百万/s) | 中(3W/s) |
| 持久化 | 依赖 RDB/AOF | 磁盘日志 | 磁盘 + 内存 |
| 消息回溯 | 支持 | 支持 | 不支持 |
| 使用场景 | 轻量级消息队列 | 大数据流处理 | 企业级消息 |
六、面试要点
Q1:Redis 事务支持回滚吗?
不支持。Redis 事务没有回滚机制,EXEC 后即使某条命令失败,其他命令仍会执行。Redis 作者认为这是设计简洁性和性能的权衡。
Q2:Pipeline 和事务的区别?
Pipeline 只是批量发送命令减少 RTT,不保证原子性;事务(MULTI/EXEC)保证原子性,中间的命令不会被打断。Lua 脚本也保证原子性,且支持条件判断。
Q3:Redis 的 Stream 和 Pub/Sub 有什么区别?
Pub/Sub 消息不持久化,离线丢失,无 ACK;Stream 支持消息持久化、消费者组、ACK 确认、消息回溯,是可靠的轻量级消息队列。
Q4:Lua 脚本为什么能保证原子性?
Redis 是单线程执行命令,Lua 脚本在执行期间不会被其他命令打断。整个脚本像一个原子操作,要么全部执行完,要么不执行。
