Spring Boot项目中,Redis 如何同时执行多条命令

在 Spring Boot 项目中高效、合理地使用 Redis 同时执行多条命令,可以显著提升应用性能。下面我将为你介绍几种主要方式、它们的典型应用场景,以及如何在 Spring Boot 中实现。

首先,我们来通过一个表格快速了解这几种方式的特点和适用场景:

方式 原子性 主要优势 Spring Boot 中的典型应用场景
Pipeline (管道)​ 大幅提升批量操作效率,减少网络往返次数 缓存预热、批量数据导入导出、无依赖关系的批量查询
事务 (Transaction)​ 部分 命令队列化,保证连续执行(但不支持回滚) 简单的原子操作序列,如库存扣减、更新多个相关键
Lua 脚本 复杂逻辑原子执行,避免中间状态,性能高 分布式锁、秒杀、需要原子性的复杂业务逻辑

⚡ 1. Pipeline (管道)

Pipeline 允许客户端将多个命令打包后一次性发送给 Redis 服务器,服务器依次执行后再次将所有结果一次性返回给客户端。这能显著减少网络往返次数(RTT)​,从而大幅提升吞吐量,尤其在高延迟网络环境下效果明显。

Spring Boot 实现示例:​

kotlin 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class RedisPipelineService {

    private final StringRedisTemplate stringRedisTemplate;

    // 构造器注入
    public RedisPipelineService(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void batchSetKeys(List<String> keys, List<String> values) {
        // 使用 executePipelined 方法执行管道操作
        List<Object> results = stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (int i = 0; i < keys.size(); i++) {
                connection.set(keys.get(i).getBytes(), values.get(i).getBytes());
            }
            return null; // 回调中返回 null
        });
        // results 包含每个命令的执行结果
    }
}

使用场景:​

  • 缓存预热:应用启动时批量加载热点数据到 Redis。
  • 批量数据导入/导出:例如从数据库批量导入数据到 Redis,或从 Redis 批量获取数据进行处理。
  • 批量查询无关联数据:一次性获取多个不相关键的值,减少网络开销。

注意事项:​

  • Pipeline 中的命令不具备原子性
  • 建议单次 Pipeline 命令数控制在合理范围内(如几千条),避免服务器内存压力或客户端长时间阻塞。
  • 错误处理:某个命令失败不会影响 Pipeline 中其他命令的执行,需要在客户端解析结果列表时逐一检查。

🔄 2. 事务 (Transaction)

Redis 事务通过 MULTI, EXEC, DISCARD, WATCH等命令实现。它允许将多个命令放入一个队列,然后通过 EXEC命令原子性地顺序执行这些命令。

Spring Boot 实现示例:​

Spring Data Redis 提供了 SessionCallback接口来支持在同一个连接中执行多个操作,这对于事务至关重要。

typescript 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class RedisTransactionService {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisTransactionService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public List<Object> executeInTransaction() {
        // 使用 execute 方法并传递 SessionCallback 来执行事务
        SessionCallback<List<Object>> sessionCallback = new SessionCallback<>() {
            @Override
            public List<Object> execute(org.springframework.data.redis.core.RedisOperations operations) {
                operations.multi(); // 开启事务
                operations.opsForValue().set("key1", "value1");
                operations.opsForValue().increment("counter");
                operations.opsForSet().add("setKey", "member1");
                return operations.exec(); // 执行事务并返回结果
            }
        };
        return redisTemplate.execute(sessionCallback);
    }
}

使用场景:​

  • 简单的原子操作序列 :需要确保一系列命令连续执行,不被其他命令打断,但不要求所有命令必须全部成功(例如,扣减库存后增加销量)。
  • 结合 WATCH实现乐观锁:监控特定键,如秒杀场景中监控库存键,防止超卖。
typescript 复制代码
// 结合 WATCH 的乐观锁示例
public boolean watchAndExecute(String key, String expectedValue, String newValue) {
    return redisTemplate.execute(new SessionCallback<Boolean>() {
        @Override
        public Boolean execute(RedisOperations operations) {
            operations.watch(key); // 监视 key
            String currentValue = (String) operations.opsForValue().get(key);
            if (expectedValue.equals(currentValue)) {
                operations.multi();
                operations.opsForValue().set(key, newValue);
                List<Object> execResult = operations.exec(); // 如果 execResult 为空,表示事务执行失败(键被修改)
                return execResult != null && !execResult.isEmpty();
            }
            operations.unwatch();
            return false;
        }
    });
}

注意事项:​

  • Redis 事务不支持回滚 (Rollback)​。如果在执行过程中某个命令失败,已执行的命令不会回滚,后续命令仍会继续执行。

  • 错误类型​:

    • 入队错误 (如命令语法错误):在执行 EXEC前,Redis 可能会检查出错误并放弃整个事务。
    • 运行时错误 (如数据类型操作错误):在 EXEC后执行中发生的错误,Redis 会记录错误信息但不会中断事务执行。

📜 3. Lua 脚本

Redis 支持执行 ​Lua 脚本 。脚本中的所有命令会作为一个整体原子性地执行 ,期间不会被其他命令打断,是原子性最强的方案,同时还能减少网络往返。

Spring Boot 实现示例:​

RedisTemplate提供了 execute方法用于执行 Lua 脚本。

typescript 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;

@Service
public class RedisLuaService {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisLuaService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String useLuaScript() {
        // 定义 Lua 脚本字符串
        String luaScript = """
                local key1 = KEYS[1]
                local value1 = ARGV[1]
                redis.call('set', key1, value1)
                local value = redis.call('get', key1)
                redis.call('incr', 'counter')
                return value
                """;
        
        DefaultRedisScript<String> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(String.class); // 设置返回值类型

        // 执行脚本,传入 keys 和 args 数组
        String result = redisTemplate.execute(script, Arrays.asList("myKey"), "myValue");
        return result; // 返回脚本执行结果
    }
}

使用场景:​

  • 释放分布式锁​:确保判断锁标识和删除锁是一个原子操作。

    vbnet 复制代码
    -- KEYS[1] 是锁的key,ARGV[1]是当前持有者的标识
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
  • 秒杀/抢购​:判断库存和扣减库存需要原子性。

  • 复杂业务逻辑​:需要多个命令的中间结果进行逻辑判断。

注意事项:​

  • 脚本应尽量简单快速:执行 Lua 脚本会阻塞 Redis,长时间运行的脚本会影响性能。
  • 注意脚本的复用 :Redis 会缓存编译过的脚本,可以使用 EVALSHA通过脚本摘要来执行,减少带宽。DefaultRedisScript对象通常会被配置为单例,Spring 会智能地处理 EVALEVALSHA

💡 如何选择?

  • 追求极致性能,批量处理无关联命令 :选择 Pipeline
  • 需要保证一系列命令连续执行(简单原子性),且不介意无回滚 :选择事务(可配合 `WATCH**)。
  • 需要保证复杂操作原子性,或操作依赖于中间结果 :选择 Lua 脚本

⚠️ 集群环境特别注意

在 Redis Cluster 模式下,使用事务 (Transaction) 或 Lua 脚本时,​所有涉及的键必须在同一个哈希槽 (hash slot) 上 ,否则会报错。 可以通过 ​hash tag​ 确保不同的键分配到同一个槽。

相关推荐
召摇2 小时前
如何避免写垃圾代码:Java篇
java·后端·代码规范
vker2 小时前
第 1 天:单例模式(Singleton Pattern)—— 创建型模式
java·设计模式
我不是混子2 小时前
什么是内存泄漏?
java
程序员小假2 小时前
我们来说说当一个线程两次调用 start() 方法会出现什么情况?
java·后端
SimonKing3 小时前
Archery:开源、一站式的数据库 SQL 审核与运维平台
java·后端·程序员
Seven974 小时前
Redis常见性能问题
redis
皮皮林55114 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
卡尔特斯18 小时前
Android Kotlin 项目代理配置【详细步骤(可选)】
android·java·kotlin
白鲸开源18 小时前
Ubuntu 22 下 DolphinScheduler 3.x 伪集群部署实录
java·ubuntu·开源