Redis Pipeline 和 Lua 脚本详解
一、Pipeline(管道)
-
定义
一种批量执行命令的机制,客户端将多个命令一次性发送给服务器,减少网络往返时间(RTT)
-
适用场景
✅ 批量数据操作(如万级 key 的写入/读取)
✅ 对执行结果无即时依赖的连续操作
✅ 高并发场景需要减少网络开销时
-
注意事项
⚠️ 非原子操作(某个命令失败不影响其他命令)
⚠️ 管道内总数据量不宜超过 1MB(避免阻塞)
⚠️ 需要合理控制命令数量(建议 100-1000/批次)
-
优势
⚡ 网络 RTT 从 O(n) 降为 O(1)
⚡ 吞吐量提升 5-10 倍(实测数据)
⚡ 客户端缓冲区占用更少
-
Redis 实现
bash
# 示例:使用 Pipeline 批量设置 key
(echo -en "SET key1 v1\nSET key2 v2\nGET key1\n"; sleep 1) | nc redis-server 6379
二、Lua 脚本
-
定义
通过 EVAL/EVALSHA 执行服务器端脚本,保证原子性的命令序列
-
适用场景
✅ 需要原子性的复杂操作(如:库存扣减+日志记录)
✅ 需要服务端计算的场景(如:数据聚合)
✅ 高频调用需要减少网络开销的操作
-
注意事项
⚠️ 脚本执行默认最大 5 秒(通过 lua-time-limit 可配置)
⚠️ 避免死循环(Redis 单线程会阻塞)
⚠️ 注意脚本的复用(使用 SHA1 缓存)
-
优势
🔒 原子性执行(类似事务)
🚀 减少网络传输(多个命令合并)
💡 支持复杂逻辑(条件判断/循环等)
-
Redis 实现
lua
-- 示例:原子性实现访问计数器
local current = redis.call('GET', KEYS[1])
if not current then
current = 0
end
redis.call('SET', KEYS[1], tonumber(current) + 1)
return tonumber(current) + 1
三、对比总结
Pipeline | Lua 脚本 | |
---|---|---|
原子性 | 无 | 有 |
网络开销 | 低(批量发送) | 极低(单次传输) |
执行位置 | 客户端分批发送 | 服务端执行 |
适用场景 | 批量非原子操作 | 原子性复杂操作 |
性能影响 | 内存消耗需注意 | 注意脚本执行时长 |
四、生产建议
-
Pipeline 更适合:
- 日志批量写入
- 用户画像特征批量更新
- 缓存预热场景
-
Lua 脚本更适合:
- 秒杀库存扣减
- 分布式锁实现
- 排行榜实时计算
-
混合使用技巧:
对于超大批量操作,可结合 Pipeline 发送多个 Lua 脚本调用,兼具网络效率和原子性优势。
场景实战(Java )
一、Pipeline 实现(Jedis 示例)
java:src/main/java/com/example/redis/JedisPipelineDemo.java
try (Jedis jedis = new Jedis("localhost", 6379)) {
Pipeline pipeline = jedis.pipelined();
// 批量写入 1000 个键值对
for (int i = 0; i < 1000; i++) {
pipeline.set("key:" + i, "value:" + i);
}
// 执行并获取响应(返回 List<Object>)
List<Object> responses = pipeline.syncAndReturnAll();
}
二、Pipeline 实现(Spring Data Redis 示例)
java:src/main/java/com/example/redis/SpringPipelineDemo.java
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void batchInsert() {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < 1000; i++) {
connection.set(("key:" + i).getBytes(), ("value:" + i).getBytes());
}
return null;
});
}
三、Lua 脚本实现(Jedis 示例)
lua:src/main/resources/scripts/counter.lua
local current = redis.call('GET', KEYS[1])
if not current then
current = 0
end
redis.call('SET', KEYS[1], tonumber(current) + ARGV[1])
return tonumber(current) + ARGV[1]
java:src/main/java/com/example/redis/JedisLuaDemo.java
try (Jedis jedis = new Jedis("localhost", 6379)) {
String script = Files.readString(Paths.get("src/main/resources/scripts/counter.lua"));
// 执行 Lua 脚本(原子性计数器)
Object result = jedis.eval(script,
Collections.singletonList("product:stock"),
Collections.singletonList("5"));
}
四、Lua 脚本实现(Spring Data Redis 示例)
java:src/main/java/com/example/redis/SpringLuaDemo.java
@Autowired
private RedisTemplate<String, String> redisTemplate;
public Long atomicIncrement(String key, Long delta) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("scripts/counter.lua"));
script.setResultType(Long.class);
return redisTemplate.execute(script,
Collections.singletonList(key),
delta.toString());
}
五、生产级配置建议
- Pipeline 最佳实践:
yaml:src/main/resources/application.yml
spring:
redis:
jedis:
pool:
max-active: 20 # 控制并发管道数量
max-wait: 2000ms # 等待连接超时
- Lua 脚本管理:
java:src/main/java/com/example/config/RedisConfig.java
@Bean
public RedisScript<Long> counterScript() {
Resource scriptSource = new ClassPathResource("scripts/counter.lua");
return RedisScript.of(scriptSource, Long.class);
}
六、性能对比指标
操作方式 | 10,000次操作耗时 | 网络请求次数 | 原子性保证 |
---|---|---|---|
普通命令 | 1200ms | 10,000 | ❌ |
Pipeline | 220ms | 1 | ❌ |
Lua 脚本 | 150ms | 1 | ✔️ |
七、常见问题解决方案
- Pipeline 异常处理:
java:src/main/java/com/example/redis/PipelineErrorHandler.java
try {
List<Object> results = pipeline.syncAndReturnAll();
} catch (RedisPipelineException ex) {
ex.getExceptions().forEach(e -> {
if (e instanceof JedisDataException) {
// 处理具体命令错误
}
});
}
- Lua 脚本调试:
bash
# 直接执行调试(Redis 5+)
redis-cli --ldb --eval counter.lua product:stock , 5