Redis Pipeline批量操作深度解析:性能提升10倍的秘密武器!

难度:⭐⭐⭐ | 适合人群:想优化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性能优化上一个台阶!

如果这篇文章对你有帮助,请:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 🔄 转发分享
  • 💬 评论交流