Redis 性能优化实战:Pipeline 和 multiGet

📅 2026/7/17 14:52:43
Redis 性能优化实战:Pipeline 和 multiGet
Redis 性能优化实战Pipeline 和 multiGet循环里调redisTemplate.opsForValue().get(key)——每次都是独立的一次网络往返。100 个 key 就是 100 次网络 IO99 次纯浪费在 TCP 握手和等待上。Redis 自己一毫秒就查完了但你花了 100 毫秒在路上。Pipeline 把一堆命令打包发过去一口气执行multiGet 把多个 GET 合并成一条命令——两种手段干的同一件事砍掉多余的往返。没有 Pipeline 的时候循环 GET 有多慢// 经典反例循环 GET——100 个 key 100 次 Redis 请求for(Stringkey:keys){StringvalueredisTemplate.opsForValue().get(key);// 每次都是独立的网络往返// ...}RedisJavaRedisJava... 100 次往返 ...GET user:1value1GET user:2value2GET user:3value3GET user:100value100Redis 单线程执行一次 GET 不到 1ms。但网络往返每次 1ms100 个 key 累计 100ms。耗时不在 Redis 身上——全花在来回的网络上。方案一multiGet——合并成一条命令一次往返查完multiGet把多个 GET 合并成一条 Redis 命令MGET一次网络往返拿回所有结果。RedisJavaRedisJava一口气查完 100 个 keyMGET user:1 user:2 user:3 ... user:100[value1, value2, value3, ..., value100]从 100 次往返变成 1 次。耗时从 100ms 降到 ~2ms。代码// 构建 Redis Key 列表ListStringredisKeysuserIds.stream().map(RedisKeyConstants::buildUserInfoKey).toList();// 一次 multiGet查完所有 keyListObjectredisValuesredisTemplate.opsForValue().multiGet(redisKeys);// 过滤掉不存在的 key值为 nullredisValuesredisValues.stream().filter(Objects::nonNull).toList();实战常用批量查完缺的再走数据库不是所有 key 都命中了缓存对没命中的部分降级查 DB// 先从 Redis 批量查ListObjectredisValuesredisTemplate.opsForValue().multiGet(redisKeys);// 把命中的转成结果集ListUserVOresultredisValues.stream().filter(Objects::nonNull).map(v-JsonUtils.parseObject(v.toString(),UserVO.class)).toList();// 全命中了——直接返回if(result.size()userIds.size()){returnresult;}// 没全命中——找出缺失的 userId查 DBListLongmissedIdsuserIds.stream().filter(id-result.stream().noneMatch(u-u.getId().equals(id))).toList();ListUserVOdbResultsuserMapper.selectByIds(missedIds);result.addAll(dbResults);// 异步把 DB 查到的回种到 RedisthreadPool.submit(()-{for(UserVOuser:dbResults){StringkeybuildUserInfoKey(user.getId());longexpire24*3600ThreadLocalRandom.current().nextInt(24*3600);redisTemplate.opsForValue().set(key,JsonUtils.toJsonString(user),expire,TimeUnit.SECONDS);}});returnresult;关键点过期时间加了随机值。如果不加随机值100 个 key 都在同一秒过期下一秒全打在 DB 上——这就是缓存雪崩。方案二Pipeline——多条不同类型的命令打包一起执行multiGet只能干一件事批量 GET。如果你需要混合操作——比如批量 SET 100 个不同 value或者 SET EXPIRE 一起multiGet 就管不了了。Pipeline 不限制命令类型你可以把任意数量的 GET、SET、DEL、EXPIRE……打包成一个批次一次网络往返发给 Redis。RedisJavaRedisJava一口气执行完 100 条命令SET user:1 {json1} EX 86400SET user:2 {json2} EX 86400SET user:3 {json3} EX 86400...SET user:100 {json100} EX 86400[OK, OK, OK, ..., OK]代码// 异步线程将用户信息批量同步到 Redis 中threadPoolTaskExecutor.submit(()-{MapLong,UserVOmapdbResults.stream().collect(Collectors.toMap(UserVO::getId,u-u));// 执行 Pipeline——一批 SET 命令打包发送redisTemplate.executePipelined(newSessionCallback(){OverridepublicObjectexecute(RedisOperationsoperations){for(UserDOuser:userList){StringkeyRedisKeyConstants.buildUserInfoKey(user.getId());StringvalueJsonUtils.toJsonString(map.get(user.getId()));longexpire24*3600RandomUtil.randomInt(24*3600);// 这里的 SET 不会立刻发到 Redis——攒在 Pipeline 里operations.opsForValue().set(key,value,expire,TimeUnit.SECONDS);}returnnull;// Pipeline 里的返回值不关心}});});关键细节Pipeline 里的命令不会立即发送。executePipelined里所有的operations.xxx()调用只是把命令加入缓冲区execute方法结束后这批命令一次性发给 Redis。Pipeline 不保证原子性。跟 Lua 脚本不一样——Pipeline 只是打包发送Redis 接收后逐条执行中间可能被其他客户端插一条命令。需要原子性还是得用 Lua。返回值为 null。Pipeline 模式下每条命令的结果可以不收集return null因为我们只关心写进去没有不关心返回值。如果想知道每一条的执行结果可以收集但不常用。三种方式对比循环 GETmultiGetPipeline网络往返N 次N key 数量1 次1 次支持的操作所有类型仅 GETMGET所有类型GET/SET/DEL/EXPIRE……原子性单条 GET 原子不保证不保证适用场景查 1~2 个 key只读批量查多个 key读写混合批量操作日常使用查多用 multiGet写多用 Pipeline。