难度:⭐⭐⭐ | 适合人群:想优化Redis性能的开发者
💥 开场:一次"恐怖"的性能问题
时间: 周二早上
地点: 公司
事件: 性能优化任务
技术经理: "小李啊,用户反馈首页加载太慢了,你优化一下。"
我: "好的。"(开始排查)
查看首页接口:
@GetMapping("/homepage")
public HomePageVO getHomePage() {
HomePageVO vo = new HomePageVO();
// 1. 获取轮播图(5个)
for (int i = 1; i <= 5; i++) {
String banner = redisTemplate.opsForValue().get("banner:" + i);
vo.addBanner(banner);
}
// 2. 获取热门商品(20个)
for (int i = 1; i <= 20; i++) {
String product = redisTemplate.opsForValue().get("hot:product:" + i);
vo.addProduct(product);
}
// 3. 获取推荐商品(30个)
for (int i = 1; i <= 30; i++) {
String recommend = redisTemplate.opsForValue().get("recommend:" + i);
vo.addRecommend(recommend);
}
return vo;
}
性能测试:
总共:55次Redis查询
耗时:550ms(每次10ms)
我: "怎么这么慢?每次Redis查询才10ms啊..." 🤔
哈吉米走过来: "你这是55次网络往返!"
我: "啊?" 😯
南北绿豆画了个图:
sequenceDiagram
participant 应用服务器
participant Redis
应用服务器->>Redis: GET banner:1
Redis-->>应用服务器: 返回
Note over 应用服务器,Redis: 网络往返:10ms
应用服务器->>Redis: GET banner:2
Redis-->>应用服务器: 返回
Note over 应用服务器,Redis: 网络往返:10ms
Note over 应用服务器,Redis: ... 重复55次
Note over 应用服务器,Redis: 总耗时:10ms × 55 = 550ms
阿西噶阿西: "每次查询的网络往返时间(RTT)才是瓶颈!Redis执行命令只需要微秒级,网络往返却要毫秒级!"
我: "那怎么优化?" 😓
哈吉米: "用Pipeline!一次性发送所有命令,一次性接收所有结果!"
🎯 第一问:什么是Pipeline?
Pipeline原理
Pipeline(管道): 批量发送命令,减少网络往返次数
不用Pipeline:
发送命令1 → 等待响应1 → 发送命令2 → 等待响应2 → ...
↓
n个命令 = n次网络往返
使用Pipeline:
发送命令1、命令2、...命令n → 等待所有响应
↓
n个命令 = 1次网络往返
时序对比
不用Pipeline:
sequenceDiagram
participant Client
participant Redis
Client->>Redis: GET key1
Redis-->>Client: value1
Note over Client,Redis: RTT: 10ms
Client->>Redis: GET key2
Redis-->>Client: value2
Note over Client,Redis: RTT: 10ms
Client->>Redis: GET key3
Redis-->>Client: value3
Note over Client,Redis: RTT: 10ms
Note over Client,Redis: 总耗时:30ms
使用Pipeline:
sequenceDiagram
participant Client
participant Redis
Client->>Redis: GET key1<br/>GET key2<br/>GET key3
Note over Redis: 执行3个命令
Redis-->>Client: value1<br/>value2<br/>value3
Note over Client,Redis: RTT: 10ms<br/>总耗时:10ms
性能提升:3倍!
💻 第二问:Pipeline使用方法
Java使用Pipeline
Jedis方式:
@Service
public class PipelineService {
@Autowired
private JedisPool jedisPool;
/**
* 批量设置(使用Pipeline)
*/
public void batchSet(Map<String, String> data) {
try (Jedis jedis = jedisPool.getResource()) {
// 1. 创建Pipeline
Pipeline pipeline = jedis.pipelined();
// 2. 批量添加命令(不会立即执行)
for (Map.Entry<String, String> entry : data.entrySet()) {
pipeline.set(entry.getKey(), entry.getValue());
}
// 3. 执行Pipeline(一次性发送所有命令)
List<Object> results = pipeline.syncAndReturnAll();
System.out.println("批量设置完成,共" + results.size() + "个");
}
}
/**
* 批量获取(使用Pipeline)
*/
public List<String> batchGet(List<String> keys) {
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
// 添加命令
List<Response<String>> responses = new ArrayList<>();
for (String key : keys) {
responses.add(pipeline.get(key));
}
// 执行
pipeline.sync();
// 获取结果
List<String> values = new ArrayList<>();
for (Response<String> response : responses) {
values.add(response.get());
}
return values;
}
}
}
RedisTemplate方式:
@Service
public class PipelineService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 批量操作(使用Pipeline)
*/
public List<Object> batchOperations(Map<String, String> data) {
return redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
// 批量SET
for (Map.Entry<String, String> entry : data.entrySet()) {
connection.set(
entry.getKey().getBytes(),
entry.getValue().getBytes()
);
}
// 必须返回null
return null;
}
});
}
/**
* 批量GET(使用Pipeline)
*/
public List<String> batchGet(List<String> keys) {
List<Object> results = redisTemplate.executePipelined(
new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) {
for (String key : keys) {
connection.get(key.getBytes());
}
return null;
}
}
);
// 转换结果
return results.stream()
.map(obj -> obj == null ? null : new String((byte[]) obj))
.collect(Collectors.toList());
}
}
优化首页接口
回到开场的问题:
@GetMapping("/homepage")
public HomePageVO getHomePage() {
HomePageVO vo = new HomePageVO();
// 使用Pipeline批量获取
List<String> keys = new ArrayList<>();
// 添加所有要查询的key
for (int i = 1; i <= 5; i++) {
keys.add("banner:" + i);
}
for (int i = 1; i <= 20; i++) {
keys.add("hot:product:" + i);
}
for (int i = 1; i <= 30; i++) {
keys.add("recommend:" + i);
}
// 一次性获取所有数据
List<String> values = pipelineService.batchGet(keys);
// 解析结果
vo.setBanners(values.subList(0, 5));
vo.setHotProducts(values.subList(5, 25));
vo.setRecommends(values.subList(25, 55));
return vo;
}
性能对比:
优化前:
55次查询 × 10ms = 550ms
优化后:
1次Pipeline = 15ms
性能提升:36倍!⚡
📊 第三问:Pipeline vs 其他批量方式
方式对比
哈吉米: "Redis有多种批量操作方式。"
1. 原生批量命令
# MGET:批量获取
MGET key1 key2 key3
# MSET:批量设置
MSET key1 val1 key2 val2 key3 val3
# HMSET:Hash批量设置
HMSET user:1 name "张三" age "25" email "xxx@qq.com"
Java使用:
// MGET
List<String> values = redisTemplate.opsForValue().multiGet(keys);
// MSET
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
redisTemplate.opsForValue().multiSet(map);
2. Pipeline
// Pipeline:支持任意命令组合
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.set("key1".getBytes(), "value1".getBytes());
connection.get("key2".getBytes());
connection.incr("counter".getBytes());
connection.hSet("user:1".getBytes(), "name".getBytes(), "张三".getBytes());
return null;
});
3. Lua脚本
// Lua脚本:支持逻辑判断
String script =
"local val1 = redis.call('get', KEYS[1]) " +
"local val2 = redis.call('get', KEYS[2]) " +
"if tonumber(val1) > tonumber(val2) then " +
" return val1 " +
"else " +
" return val2 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(script, String.class), keys);
对比表格
| 方式 | 命令限制 | 原子性 | 网络往返 | 适用场景 | 推荐度 |
|---|---|---|---|---|---|
| 原生批量命令 | 只支持特定命令 | ✅ 是 | 1次 | 同类型操作 | ⭐⭐⭐⭐ |
| Pipeline | 任意命令 | ❌ 否 | 1次 | 批量任意操作 | ⭐⭐⭐⭐⭐ |
| Lua脚本 | 任意命令 | ✅ 是 | 1次 | 需要逻辑判断 | ⭐⭐⭐⭐⭐ |
| 循环单次 | 任意命令 | ✅ 是 | n次 | 不推荐 | ⭐ |
选择建议
你的需求是?
│
├─ 批量GET/SET相同类型
│ └─ 使用:MGET/MSET(最简单)
│
├─ 批量不同类型操作,不需要原子性
│ └─ 使用:Pipeline(推荐)
│
├─ 批量操作 + 需要逻辑判断
│ └─ 使用:Lua脚本
│
└─ 批量操作 + 需要原子性
└─ 使用:Lua脚本
⚡ 第四问:Pipeline性能测试
性能对比实验
@Test
public void performanceTest() {
int count = 10000;
// 方式1:循环单次操作
long start1 = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
redisTemplate.opsForValue().set("key:" + i, "value" + i);
}
long time1 = System.currentTimeMillis() - start1;
// 方式2:Pipeline批量操作
long start2 = System.currentTimeMillis();
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < count; i++) {
connection.set(("key:" + i).getBytes(), ("value" + i).getBytes());
}
return null;
});
long time2 = System.currentTimeMillis() - start2;
// 方式3:MSET批量操作
long start3 = System.currentTimeMillis();
Map<String, String> map = new HashMap<>();
for (int i = 0; i < count; i++) {
map.put("key:" + i, "value" + i);
}
redisTemplate.opsForValue().multiSet(map);
long time3 = System.currentTimeMillis() - start3;
System.out.println("========== 性能测试(" + count + "次操作)==========");
System.out.println("循环单次:" + time1 + "ms");
System.out.println("Pipeline:" + time2 + "ms,提升" + (time1/time2) + "倍");
System.out.println("MSET: " + time3 + "ms,提升" + (time1/time3) + "倍");
}
测试结果:
========== 性能测试(10000次操作)==========
循环单次:5230ms
Pipeline:486ms,提升10倍
MSET: 125ms,提升41倍
结论:
- Pipeline比循环快10倍
- MSET比Pipeline还快(原子命令更优)
RTT(往返时间)分析
南北绿豆: "理解RTT很重要!"
RTT(Round-Trip Time)= 一次网络往返时间
同机房:
Redis和应用在同一机房
RTT:0.1-1ms
跨机房:
Redis和应用在不同机房
RTT:10-50ms
公网:
Redis在云服务器
RTT:50-200ms
影响:
10000次操作:
同机房(RTT=0.5ms):
循环:10000 × 0.5 = 5000ms = 5秒
Pipeline:1 × 0.5 = 0.5ms
跨机房(RTT=20ms):
循环:10000 × 20 = 200000ms = 200秒
Pipeline:1 × 20 = 20ms
性能差距:10000倍!
🔧 第五问:Pipeline注意事项
注意1:Pipeline不是原子的
阿西噶阿西: "Pipeline只是批量发送,不保证原子性!"
// Pipeline中的命令可能被其他客户端的命令打断
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.set("stock".getBytes(), "100".getBytes());
connection.decr("stock".getBytes()); // 减1
connection.decr("stock".getBytes()); // 再减1
return null;
});
// 其他客户端可能在这期间执行:
// SET stock 200 ← 打断了Pipeline
// 结果不可控!
如果需要原子性,用Lua脚本!
注意2:控制Pipeline大小
// ❌ 不推荐:一次性Pipeline 100万个命令
List<String> hugeKeys = ... ; // 100万个key
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (String key : hugeKeys) { // 100万次
connection.get(key.getBytes());
}
return null;
});
// 问题:
// - 客户端内存占用大
// - 响应数据太大
// - 超时风险
✅ 推荐:分批Pipeline
public List<String> batchGetLarge(List<String> keys) {
List<String> allResults = new ArrayList<>();
int batchSize = 1000; // 每批1000个
// 分批处理
for (int i = 0; i < keys.size(); i += batchSize) {
int end = Math.min(i + batchSize, keys.size());
List<String> batchKeys = keys.subList(i, end);
// Pipeline处理一批
List<Object> batchResults = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (String key : batchKeys) {
connection.get(key.getBytes());
}
return null;
}
);
// 转换结果
batchResults.stream()
.map(obj -> obj == null ? null : new String((byte[]) obj))
.forEach(allResults::add);
}
return allResults;
}
注意3:Pipeline不支持事务回滚
// Pipeline + 事务
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) {
operations.multi(); // 开启事务
operations.opsForValue().set("key1", "value1");
operations.opsForValue().set("key2", "value2");
// 即使这里某个命令失败,也不会回滚
return operations.exec(); // 提交事务
}
});
💡 第六问:实战案例
案例1:批量初始化缓存
@Service
public class CacheInitService {
@Autowired
private ProductDao productDao;
/**
* 初始化商品缓存
*/
@PostConstruct
public void initProductCache() {
System.out.println("开始初始化商品缓存...");
long start = System.currentTimeMillis();
// 1. 查询所有商品
List<Product> products = productDao.findAll();
// 2. 分批Pipeline写入Redis
int batchSize = 1000;
for (int i = 0; i < products.size(); i += batchSize) {
int end = Math.min(i + batchSize, products.size());
List<Product> batch = products.subList(i, end);
// Pipeline批量写入
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (Product product : batch) {
String key = "product:" + product.getId();
String value = JSON.toJSONString(product);
// SET + EXPIRE
connection.setEx(key.getBytes(), 3600, value.getBytes());
}
return null;
});
System.out.println("已缓存:" + end + "/" + products.size());
}
long time = System.currentTimeMillis() - start;
System.out.println("缓存初始化完成,耗时:" + time + "ms");
}
}
性能对比:
10000个商品:
循环单次:
10000次 × 10ms = 100秒
Pipeline(批量1000):
10批 × 15ms = 150ms
提升:666倍!
案例2:批量查询用户信息
@Service
public class UserService {
/**
* 批量获取用户(优化前)
*/
public List<User> batchGetUsersOld(List<Long> userIds) {
List<User> users = new ArrayList<>();
// 循环查询(慢)
for (Long userId : userIds) {
String key = "user:" + userId;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
users.add(JSON.parseObject(json, User.class));
} else {
// 缓存未命中,查数据库
User user = userDao.findById(userId);
if (user != null) {
users.add(user);
// 回写缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
1, TimeUnit.HOURS);
}
}
}
return users;
}
/**
* 批量获取用户(优化后)
*/
public List<User> batchGetUsers(List<Long> userIds) {
// 1. 构建所有key
List<String> keys = userIds.stream()
.map(id -> "user:" + id)
.collect(Collectors.toList());
// 2. Pipeline批量查询
List<String> jsons = pipelineService.batchGet(keys);
// 3. 解析结果
List<User> users = new ArrayList<>();
List<Long> missIds = new ArrayList<>();
for (int i = 0; i < jsons.size(); i++) {
String json = jsons.get(i);
if (json != null) {
users.add(JSON.parseObject(json, User.class));
} else {
// 记录缓存未命中的ID
missIds.add(userIds.get(i));
}
}
// 4. 批量查询数据库(缓存未命中的)
if (!missIds.isEmpty()) {
List<User> dbUsers = userDao.findByIds(missIds);
users.addAll(dbUsers);
// 5. Pipeline批量回写缓存
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (User user : dbUsers) {
String key = "user:" + user.getId();
connection.setEx(key.getBytes(), 3600,
JSON.toJSONString(user).getBytes());
}
return null;
});
}
return users;
}
}
案例3:批量删除缓存
/**
* 批量删除(使用Pipeline)
*/
public void batchDelete(List<String> keys) {
if (keys.isEmpty()) {
return;
}
// 分批删除(每批1000个)
int batchSize = 1000;
for (int i = 0; i < keys.size(); i += batchSize) {
int end = Math.min(i + batchSize, keys.size());
List<String> batch = keys.subList(i, end);
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (String key : batch) {
connection.del(key.getBytes());
}
return null;
});
}
System.out.println("批量删除完成:" + keys.size() + " 个key");
}
🎯 第七问:Pipeline的限制
限制1:不支持回滚
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.set("key1".getBytes(), "value1".getBytes());
connection.set("key2".getBytes(), "value2".getBytes());
connection.incr("key1".getBytes()); // ← 错误!key1不是数字
connection.set("key3".getBytes(), "value3".getBytes());
return null;
});
// 结果:
// key1 = "value1" ✅ 设置成功
// key2 = "value2" ✅ 设置成功
// INCR失败 ❌ 失败
// key3 = "value3" ✅ 还是设置了
// 不会回滚!
限制2:占用内存
// Pipeline会将所有响应缓存在客户端内存
// 100万个命令的响应
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < 1000000; i++) {
connection.get(("key:" + i).getBytes());
}
return null;
});
// 客户端内存可能爆掉!
// 解决:分批Pipeline
限制3:不支持集群
哈吉米: "Redis Cluster中Pipeline有限制!"
// Cluster中,key可能在不同节点
// ❌ 可能失败:
List<String> keys = Arrays.asList("key1", "key2", "key3");
// key1在节点A,key2在节点B,key3在节点C
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (String key : keys) {
connection.get(key.getBytes());
}
return null;
});
// 可能报错:CROSSSLOT Keys in request don't hash to the same slot
// ✅ 解决:使用Hash Tag
List<String> keys = Arrays.asList("user:{123}:profile", "user:{123}:orders", "user:{123}:cart");
// {123}确保所有key在同一个slot
💡 最佳实践
1. 合理控制批量大小
/**
* 根据网络情况调整批量大小
*/
public class PipelineConfig {
// 同机房:可以大一点
private static final int BATCH_SIZE_LOCAL = 5000;
// 跨机房:小一点
private static final int BATCH_SIZE_REMOTE = 1000;
// 公网:更小
private static final int BATCH_SIZE_INTERNET = 500;
}
2. Pipeline + 异常处理
public List<String> batchGetSafe(List<String> keys) {
try {
return redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (String key : keys) {
connection.get(key.getBytes());
}
return null;
}).stream()
.map(obj -> obj == null ? null : new String((byte[]) obj))
.collect(Collectors.toList());
} catch (Exception e) {
log.error("Pipeline执行失败", e);
// 降级:循环单次查询
return keys.stream()
.map(key -> redisTemplate.opsForValue().get(key))
.collect(Collectors.toList());
}
}
3. 结合MGET优化
/**
* 智能批量获取
*/
public List<String> smartBatchGet(List<String> keys) {
if (keys.size() <= 100) {
// 少量key:直接用MGET(最快)
return redisTemplate.opsForValue().multiGet(keys);
} else {
// 大量key:分批Pipeline
return batchGetWithPipeline(keys, 1000);
}
}
💡 知识点总结
Pipeline核心要点
✅ Pipeline概念
- 批量发送命令
- 减少网络往返
- 性能提升10倍以上
✅ Pipeline原理
- 客户端缓存命令
- 一次性发送
- 一次性接收响应
- 减少RTT次数
✅ 性能对比
- 循环单次:n次RTT
- Pipeline:1次RTT
- 提升倍数 = 命令数量
✅ 注意事项
- 不保证原子性
- 控制批量大小(建议1000)
- 分批处理大数据量
- Cluster需要Hash Tag
✅ 使用场景
- 批量初始化缓存
- 批量查询数据
- 批量删除key
- 性能优化
✅ 对比其他方式
- MGET/MSET:更快,但功能单一
- Pipeline:灵活,不原子
- Lua脚本:原子,支持逻辑
记忆口诀
Pipeline批量操作,
减少往返是关键。
一次发送n命令,
一次接收n响应。
RTT时间是瓶颈,
网络往返要减少。
性能提升十倍多,
大数据量更明显。
不保证原子性,
需要原子用Lua。
控制批量大小好,
一千一批最合适。
🤔 常见面试题
Q1: 介绍一下Redis Pipeline?
A:
Pipeline(管道):
概念:
- 批量发送命令,减少网络往返次数
原理:
- 客户端缓存多个命令
- 一次性发送到Redis
- 一次性接收所有响应
性能:
- n个命令:n次RTT → 1次RTT
- 提升倍数 ≈ 命令数量
使用:
redisTemplate.executePipelined(callback);
注意:
- 不保证原子性
- 控制批量大小
- 分批处理大数据
Q2: Pipeline和MGET的区别?
A:
MGET:
- 原生批量命令
- 只能批量GET
- 原子操作
- 性能最好
Pipeline:
- 客户端批量技术
- 支持任意命令组合
- 不是原子操作
- 性能很好
选择:
- 同类型操作:MGET/MSET
- 不同类型操作:Pipeline
Q3: Pipeline和Lua脚本的区别?
A:
Pipeline:
- 批量发送命令
- 不保证原子性
- 命令可能被打断
- 不支持逻辑判断
Lua脚本:
- 脚本整体原子执行
- 保证原子性
- 不会被打断
- 支持逻辑判断(if/else)
使用场景:
- Pipeline:批量操作,不需要原子性
- Lua脚本:需要原子性或逻辑判断
性能:
- Pipeline:稍快(纯批量)
- Lua脚本:稍慢(有脚本解析)
💬 写在最后
从网络往返到批量操作,我们深入学习了Redis Pipeline:
- ⚡ 理解了Pipeline的性能提升原理
- 📊 掌握了Pipeline的使用方法
- ⚠️ 学会了Pipeline的注意事项
- 💻 完成了多个实战优化案例
这篇文章,希望能让你的Redis性能优化上一个台阶!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流