Redis使用pipeLine批量获取数据加快接口响应速度 

本文探讨了Redis在处理批量数据操作时的性能瓶颈,并介绍了如何利用Pipeline技术显著提升效率,尤其是在连续执行多个命令的场景中。通过实例代码展示了使用RedisTemplate的multiGet方法和Pipeline进行批量读写的具体实现。

 

 

RedisClientRedisMultiGet

一、背景

  • 需求:

    redis通过tcp来对外提供服务,client通过socket连接发起请求,每个请求在命令发出后会阻塞等待redis服务器进行处理,处理完毕后将结果返回给client。

    其实和一个http的服务器类似,一问一答,请求一次给一次响应。而这个过程在排除掉redis服务本身做复杂操作时的耗时的话,可以看到最耗时的就是这个网络传输过程。每一个命令都对应了发送、接收两个网络传输,假如一个流程需要0.1秒,那么一秒最多只能处理10个请求,将严重制约redis的性能。

    在很多场景下,我们要完成一个业务,可能会对redis做连续的多个操作,譬如库存减一、订单加一、余额扣减等等,这有很多个步骤是需要依次连续执行的。

  • 潜在隐患:这样的场景,网络传输的耗时将是限制redis处理量的主要瓶颈。循环key,获取value,可能会造成连接池的连接数增多,连接的创建和摧毁,消耗性能
  • 解决方法:

    可以引入pipeline了,pipeline管道就是解决执行大量命令时、会产生大量同学次数而导致延迟的技术。

    其实原理很简单,pipeline就是把所有的命令一次发过去,避免频繁的发送、接收带来的网络开销,redis在打包接收到一堆命令后,依次执行,然后把结果再打包返回给客户端。

    根据项目中的缓存数据结构的实际情况,数据结构为string类型的,使用RedisTemplate的multiGet方法;数据结构为hash,使用Pipeline(管道),组合命令,批量操作redis。

二、操作

  1. RedisTemplate的multiGet的操作

    • 针对数据结构为String类型

    • 示例代码

List<String> keys = new ArrayList<>();
for (Book e : booklist) {
   String key = generateKey.getKey(e);
   keys.add(key);
}
List<Serializable> resultStr = template.opsForValue().multiGet(

    2.RedisTemplate的Pipeline使用

    为什么Pipelining这么快?    

    先看看原来的多条命令,是如何执行的:    

    Redis Client->>Redis Server: 发送第1个命令

    Redis Server->>Redis Client: 响应第1个命令

    Redis Client->>Redis Server: 发送第2个命令

    Redis Server->>Redis Client: 响应第2个命令

    Redis Client->>Redis Server: 发送第n个命令

    Redis Server->>Redis Client: 响应第n个命令

 Pipeling机制是怎样的呢:
    Redis Client->>Redis Server: 发送第1个命令(缓存在Redis Client,未即时发送)
    Redis Client->>Redis Server: 发送第2个命令(缓存在Redis Client,未即时发送)
    Redis Client->>Redis Server: 发送第n个命令(缓存在Redis Client,未即时发送)
    Redis Client->>Redis Server: 发送累积的命令
    Redis Server->>Redis Client: 响应第1、2、n个命令

  • 示例代码

package cn.chinotan.controller;

import cn.chinotan.service.RedisService;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @program: test
 * @description: redis批量数据测试
 * @author: xingcheng
 * @create: 2019-03-16 16:26
 **/
@RestController
@RequestMapping("/redisBatch")
@Log
public class RedisBatchController {

    @Autowired
    StringRedisTemplate redisTemplate;
    
    @Autowired
    Map<String, RedisService> redisServiceMap;

    /**
     * VALUE缓存时间 3分钟
     */
    public static final Integer VALUE_TIME = 1;

    /**
     * 测试列表长度
     */
    public static final Integer SIZE = 100000;

    @GetMapping(value = "/test/{model}")
    public Object hello(@PathVariable("model") String model) {
        List<Map<String, String>> saveList = new ArrayList<>(SIZE);
        List<String> keyList = new ArrayList<>(SIZE);
        for (int i = 0; i < SIZE; i++) {
            Map<String, String> objectObjectMap = new HashMap<>();
            String key = String.valueOf(i);
            objectObjectMap.put("key", key);
            StringBuilder sb = new StringBuilder();
            objectObjectMap.put("value", sb.append("value").append(i).toString());
            saveList.add(objectObjectMap);
            // 记录全部key
            keyList.add(key);
        }
        
        // 获取对应的实现
        RedisService redisService = redisServiceMap.get(model);
        
        long saveStart = System.currentTimeMillis();
        redisService.batchInsert(saveList, TimeUnit.MINUTES, VALUE_TIME);
        long saveEnd = System.currentTimeMillis();
        log.info("插入耗时:" + (saveEnd - saveStart) + " ms");
        // 批量获取
        long getStart = System.currentTimeMillis();
        List<String> valueList = redisService.batchGet(keyList);
        long getEnd = System.currentTimeMillis();
        log.info("获取耗时:" + (getEnd - getStart) + " ms");
        return valueList;
    }
}
package cn.chinotan.controller;

import cn.chinotan.service.RedisService;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @program: test
 * @description: redis批量数据测试
 * @author: xingcheng
 * @create: 2019-03-16 16:26
 **/
@RestController
@RequestMapping("/redisBatch")
@Log
public class RedisBatchController {

    @Autowired
    StringRedisTemplate redisTemplate;
    
    @Autowired
    Map<String, RedisService> redisServiceMap;

    /**
     * VALUE缓存时间 3分钟
     */
    public static final Integer VALUE_TIME = 1;

    /**
     * 测试列表长度
     */
    public static final Integer SIZE = 100000;

    @GetMapping(value = "/test/{model}")
    public Object hello(@PathVariable("model") String model) {
        List<Map<String, String>> saveList = new ArrayList<>(SIZE);
        List<String> keyList = new ArrayList<>(SIZE);
        for (int i = 0; i < SIZE; i++) {
            Map<String, String> objectObjectMap = new HashMap<>();
            String key = String.valueOf(i);
            objectObjectMap.put("key", key);
            StringBuilder sb = new StringBuilder();
            objectObjectMap.put("value", sb.append("value").append(i).toString());
            saveList.add(objectObjectMap);
            // 记录全部key
            keyList.add(key);
        }
        
        // 获取对应的实现
        RedisService redisService = redisServiceMap.get(model);
        
        long saveStart = System.currentTimeMillis();
        redisService.batchInsert(saveList, TimeUnit.MINUTES, VALUE_TIME);
        long saveEnd = System.currentTimeMillis();
        log.info("插入耗时:" + (saveEnd - saveStart) + " ms");
        // 批量获取
        long getStart = System.currentTimeMillis();
        List<String> valueList = redisService.batchGet(keyList);
        long getEnd = System.currentTimeMillis();
        log.info("获取耗时:" + (getEnd - getStart) + " ms");
        return valueList;
    }
}
package cn.chinotan.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @program: test
 * @description: redis管道操作
 * @author: xingcheng
 * @create: 2019-03-16 16:47
 **/
@Service("pipe")
public class RedisPipelineService implements RedisService {

    @Autowired
    StringRedisTemplate redisTemplate;

    @Override
    public void batchInsert(List<Map<String, String>> saveList, TimeUnit unit, int timeout) {
        /* 插入多条数据 */
        redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
                for (Map<String, String> needSave : saveList) {
                    redisTemplate.opsForValue().set(needSave.get("key"), needSave.get("value"), timeout,unit);
                }
                return null;
            }
        });
    }

    @Override
    public List<String> batchGet(List<String> keyList) {
        /* 批量获取多条数据 */
        List<Object> objects = redisTemplate.executePipelined(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection redisConnection) throws DataAccessException {
                StringRedisConnection stringRedisConnection = (StringRedisConnection) redisConnection;
                for (String key : keyList) {
                    stringRedisConnection.get(key);
                }
                return null;
            }
        });

        List<String> collect = objects.stream().map(val -> String.valueOf(val)).collect(Collectors.toList());

        return collect;
    }
}
package cn.chinotan.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @program: test
 * @description: redis普通遍历操作
 * @author: xingcheng
 * @create: 2019-03-16 16:47
 **/
@Service("generic")
public class RedisGenericService implements RedisService {

    @Autowired
    StringRedisTemplate redisTemplate;
    
    @Override
    public void batchInsert(List<Map<String, String>> saveList, TimeUnit unit, int timeout) {
        for (Map<String, String> needSave : saveList) {
            redisTemplate.opsForValue().set(needSave.get("key"), needSave.get("value"), timeout,unit);
        }
    }

    @Override
    public List<String> batchGet(List<String> keyList) {
        List<String> values = new ArrayList<>(keyList.size());
        for (String key : keyList) {
            String value = redisTemplate.opsForValue().get(key);
            values.add(value);
        }
        return values;
    }
}

测试结果:

 

 

 

 

可以看到性能提升了20倍之多

基于其特性,它有两个明显的局限性:

  • 鉴于Pipepining发送命令的特性,Redis服务器是以队列来存储准备执行的命令,而队列是存放在有限的内存中的,所以不宜一次性发送过多的命令。如果需要大量的命令,可分批进行,效率不会相差太远滴,总好过内存溢出嘛~~
  • 由于pipeline的原理是收集需执行的命令,到最后才一次性执行。所以无法在中途立即查得数据的结果(需待pipelining完毕后才能查得结果),这样会使得无法立即查得数据进行条件判断(比如判断是非继续插入记录)。
<think>首先,用户的问题是:在RuoYi Plus前端中实现定时查询Redis数据的方案设计。我需要基于系统级指令来构建回答。 关键指令: - 所有行内数学表达式用$...$,但这个问题可能不涉及数学,所以可能不需要。 - 独立公式用$$...$$,但同样,可能不适用。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾添加引用标识,例如[^1]。 - 回答结构清晰,逐步解决问题。 - 参考提供的引用:引用[1]关于Redis批量操作,引用[2]关于返回List对象集合。这些是参考,不是直接相关,但可以借鉴思路。 用户的问题:设计一个在RuoYi Plus前端中使用定时器查询Redis数据的方案。 RuoYi Plus是一个基于Spring Boot和Vue.js的后台管理系统框架。所以,前端是Vue.js,后端是Spring Boot。 方案设计: 1. **前端部分(Vue.js)**:使用定时器(如setInterval)来定期发送请求到后端API。 2. **后端部分(Spring Boot)**:提供一个API来查询Redis数据。 3. **Redis操作**:后端使用RedisTemplate或类似工具查询Redis。 4. **定时器管理**:确保定时器在组件销毁时清除,避免内存泄漏。 5. **性能考虑**:参考引用[1],避免批量操作过大,影响Redis性能。但这里是查询,不是批量存取,所以可能不需要,但可以提到优化查询频率和大小。 6. **数据返回**:参考引用[2],返回结构化的数据,如JSON对象,包含在data属性中。 步骤: - 前端:在Vue组件中,使用setInterval调用API方法。 - 后端:创建Controller方法,使用RedisTemplate查询数据。 - 数据传输:定义DTO或VO来封装返回数据。 - 错误处理:添加超时、错误回调等。 - 优化:考虑查询频率、数据量大小,避免对Redis造成压力。 生成相关问题:在回答末尾,添加相关问题,如系统指令所示。 回答结构: 1. **引言**:简要介绍方案。 2. **步骤分解**: - 前端实现 - 后端实现 - Redis查询 - 定时器管理 3. **代码示例**:提供简单代码片段(如Vue和Spring Boot代码)。 4. **注意事项**:性能、错误处理等。 5. **相关问题**:添加3-5个相关问题。 引用:由于是设计,可能没有直接引用,但可以参考提供的引用[1]和[2]来添加一些建议。例如,在讨论查询大小时,提到引用[1]的批量大小建议。 最终回答: - 使用中文。 - 结构清晰。 - 添加相关问题。</think>### RuoYi Plus 前端定时查询 Redis 数据方案设计 在 RuoYi Plus 框架中(基于 Vue.js 前端和 Spring Boot 后端),实现定时查询 Redis 数据的方案需要从前端定时器、后端 API 设计和 Redis 操作三部分入手。以下方案设计确保高效性、可靠性和可维护性,同时参考性能优化建议(如引用[1]中提到的批量操作限制)。方案分为五个步骤,逐步实现。 #### 1. **方案概述** - **目标**:前端定期轮询 Redis 数据(如缓存的状态信息),并更新 UI。 - **核心组件**: - 前端:Vue.js 组件,使用 `setInterval` 实现定时器。 - 后端:Spring Boot Controller,提供查询 API。 - Redis:通过 Spring Data Redis 操作。 - **数据流**: 1. 前端定时触发 API 请求。 2. 后端查询 Redis 并返回结构化数据(参考引用[2]的返回格式)。 3. 前端处理响应并渲染。 - **性能考虑**: - 查询频率不宜过高(建议 5-30 秒),避免 Redis 压力(Redis 单线程模型,高并发查询可能阻塞其他命令[^1])。 - 查询数据量应精简,避免大 Key 或复杂操作。 #### 2. **前端实现(Vue.js 部分)** 在 RuoYi Plus 的 Vue 组件中,使用定时器调用 API。关键点: - **定时器管理**:在 `mounted` 钩子启动定时器,在 `beforeDestroy` 钩子清除定时器,防止内存泄漏。 - **API 调用**:使用 Axios 发送 GET 请求到后端 API。 - **数据处理**:接收后端返回的 JSON 数据(格式如 `{ "data": [...] }`),更新组件状态。 - **错误处理**:添加超时和重试机制。 **代码示例**: ```vue <template> <div> <el-table :data="redisData"> <!-- 表格渲染 Redis 数据,例如 Key-Value 列表 --> </el-table> </div> </template> <script> import axios from 'axios'; export default { data() { return { redisData: [], // 存储从 Redis 查询的数据 timer: null, // 定时器对象 interval: 10000 // 查询间隔,10秒(可根据需求调整) }; }, mounted() { this.startTimer(); // 组件挂载时启动定时器 }, beforeDestroy() { this.clearTimer(); // 组件销毁前清除定时器 }, methods: { startTimer() { this.timer = setInterval(() => { this.fetchRedisData(); }, this.interval); }, clearTimer() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }, async fetchRedisData() { try { const response = await axios.get('/api/redis/data'); // 调用后端 API if (response.data && response.data.data) { this.redisData = response.data.data; // 更新数据,格式如:[{ key: "foo", value: "bar" }, ...] } } catch (error) { console.error('查询 Redis 失败:', error); // 可选:重试或显示错误消息 } } } }; </script> ``` #### 3. **后端实现(Spring Boot 部分)** 后端提供 API 查询 Redis,确保数据安全性和效率: - **API 设计**:创建 RESTful 接口,返回统一响应体(如 `Result` 对象,包含 `data` 属性)。 - **Redis 操作**:使用 `RedisTemplate` 查询数据,避免复杂操作。 - **性能优化**: - 查询数据量小:一次查询 Key 数量控制在 50-100 个以内(参考引用[1]的批量建议)。 - 使用 Pipeline 或 MGET 优化批量查询(但需测试 Benchmark 以确定最优值[^1])。 **代码示例**: ```java // RedisController.java @RestController @RequestMapping("/api/redis") public class RedisController { @Autowired private RedisTemplate<String, Object> redisTemplate; @GetMapping("/data") public Result getRedisData() { // 示例:查询特定 Key 列表(实际 Key 可从配置或参数获取) List<String> keys = Arrays.asList("status:1", "status:2"); Map<String, Object> data = new HashMap<>(); // 使用 MGET 批量查询(控制 Key 数量) List<Object> values = redisTemplate.opsForValue().multiGet(keys); if (values != null) { for (int i = 0; i < keys.size(); i++) { data.put(keys.get(i), values.get(i)); } } // 返回结构化数据,参考引用[2]格式 return Result.success(data); } } // Result.java(统一响应体) public class Result<T> { private int code; private String msg; private T data; // 构造方法省略... } ``` #### 4. **Redis 查询优化与注意事项** - **查询频率**:前端定时器间隔不宜过短,建议初始值 10 秒,通过 Benchmark 测试调整。 - **数据量控制**:一次查询只获取必要数据(如只查业务关键 Key),避免返回大对象。 - **错误处理**: - 前端:添加请求超时(如 Axios 的 `timeout` 设置)。 - 后端:捕获 Redis 异常(如 `RedisConnectionFailureException`),返回错误码。 - **安全性**:API 添加权限验证(如 RuoYi Plus 的 `@PreAuthorize` 注解)。 - **扩展性**:如果数据量大,考虑分页查询或增量更新。 #### 5. **性能测试与调优** - **Benchmark 测试**:在真实环境模拟高并发,测量不同查询频率和 Key 数量对 Redis 的影响(参考引用[1],一批 Key 不超过 100 个)。 - **监控**:使用 RuoYi Plus 内置的监控工具(如 Druid)观察 API 响应时间。 - **备选方案**:如果定时轮询压力大,可改用 WebSocket 推送(但需额外复杂度)。 通过以上方案,RuoYi Plus 前端能高效、可靠地定时查询 Redis 数据,同时避免性能瓶颈。实际部署时,建议从小规模测试开始,逐步优化参数。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值