当前位置: 首页 > news >正文

用Lua脚本实现Redis原子操作

1. 环境准备
  • 依赖:在pom.xml中添加Spring Data Redis:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 配置RedisTemplate

    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            return template;
        }
    }
    

2. 编写Lua脚本

以分布式锁为例,实现加锁和解锁的原子操作:

  • 加锁脚本 lock.lua

    local key = KEYS[1]
    local value = ARGV[1]
    local expire = ARGV[2]
    -- 如果key不存在则设置,并添加过期时间
    if redis.call('setnx', key, value) == 1 then
        redis.call('expire', key, expire)
        return 1 -- 加锁成功
    else
        return 0 -- 加锁失败
    end
    
  • 解锁脚本 unlock.lua

    local key = KEYS[1]
    local value = ARGV[1]
    -- 只有锁的值匹配时才删除
    if redis.call('get', key) == value then
        return redis.call('del', key)
    else
        return 0
    end
    

3. 加载并执行脚本
  • 定义脚本Bean

    @Configuration
    public class LuaScriptConfig {
        @Bean
        public DefaultRedisScript<Long> lockScript() {
            DefaultRedisScript<Long> script = new DefaultRedisScript<>();
            script.setLocation(new ClassPathResource("lock.lua"));
            script.setResultType(Long.class);
            return script;
        }
    }
    
  • 调用脚本

    @Service
    public class RedisLockService {
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        @Autowired
        private DefaultRedisScript<Long> lockScript;
    
        public boolean tryLock(String key, String value, int expireSec) {
            List<String> keys = Collections.singletonList(key);
            Long result = redisTemplate.execute(
                    lockScript,
                    keys,
                    value,
                    String.valueOf(expireSec)
            );
            return result != null && result == 1;
        }
    }
    

开发中的常见问题与解决方案
1. Lua脚本缓存问题
  • 问题:每次执行脚本会传输整个脚本内容,增加网络开销。
  • 解决:Redis会自动缓存脚本并返回SHA1值,Spring Data Redis的DefaultRedisScript会自动管理SHA1。确保脚本对象是单例,避免重复加载。

2. 参数传递错误
  • 问题KEYSARGV数量或类型不匹配,导致脚本执行失败。
  • 解决:明确区分参数类型:
    // 正确传参示例
    List<String> keys = Arrays.asList("key1", "key2"); // KEYS数组
    Object[] args = new Object[]{"arg1", "arg2"};      // ARGV数组
    

3. Redis集群兼容性
  • 问题:集群模式下,所有操作的Key必须位于同一slot。
  • 解决:使用{}定义hash tag,强制Key分配到同一节点:
    String key = "{user}:lock:" + userId; // 所有包含{user}的Key分配到同一节点
    

4. 脚本性能问题
  • 问题:复杂Lua脚本可能阻塞Redis,影响性能。
  • 解决
    • 避免在Lua中使用循环或复杂逻辑。
    • 优先使用Redis内置命令(如SETNXEXPIRE)。

5. 异常处理
  • 问题:脚本执行超时或返回非预期结果。
  • 解决:捕获异常并设计重试机制:
    public boolean tryLockWithRetry(String key, int maxRetry) {
        int retry = 0;
        while (retry < maxRetry) {
            if (tryLock(key, "value", 30)) {
                return true;
            }
            retry++;
            Thread.sleep(100); // 短暂等待
        }
        return false;
    }
    

完整示例:分布式锁
// 加锁
public boolean lock(String key, String value, int expireSec) {
    return redisTemplate.execute(
        lockScript,
        Collections.singletonList(key),
        value,
        String.valueOf(expireSec)
    ) == 1;
}

// 解锁
public void unlock(String key, String value) {
    Long result = redisTemplate.execute(
        unlockScript,
        Collections.singletonList(key),
        value
    );
    if (result == null || result == 0) {
        throw new RuntimeException("解锁失败:锁已过期或非持有者");
    }
}

调试与优化建议
  1. Redis CLI调试

    # 直接在Redis服务器测试脚本
    EVAL "return redis.call('setnx', KEYS[1], ARGV[1])" 1 mykey 123
    
  2. 日志配置

    # application.properties
    logging.level.org.springframework.data.redis=DEBUG
    
  3. 监控脚本执行时间

    # Redis慢查询日志
    slowlog-log-slower-than 5
    slowlog-max-len 128
    

总结

通过Lua脚本,可以轻松实现Redis复杂操作的原子性,解决高并发下的竞态条件问题。在Spring Boot中,结合RedisTemplateDefaultRedisScript,能够高效集成Lua脚本。开发时需注意参数传递集群兼容性异常处理,避免踩坑。

相关文章:

  • 【ARMv7汇编编程语言】
  • 用Scrum敏捷的视角看《哪吒2》的创作
  • 设计模式之组合模式:原理、实现与应用
  • Cursor+MCP,解锁AI更多可能!
  • 【eNSP实战】配置端口映射(NAT Server)
  • allWebPlugin中间件自动适应Web系统多层iframe嵌套
  • PostgreSQL技术内幕26:PG聚合算子实现分析
  • 【leetcode hot 100 101】对称二叉树
  • Vue开发者工具(VueDevtools)下载与安装
  • Huggingface科研代码学习(自定义模型开发)
  • SpringBoot 第一课(Ⅰ)--框架结构
  • 订单超时自动取消功能如何设计
  • 自然语言处理预训练模型的研究综述
  • C++——STL 常用的排序算法
  • C++使用ZeroMQ和MessagePack实现简单又轻量级的RPC框架
  • Spring是如何管理事务的
  • Blender-MCP服务源码2-依赖分析
  • 汽车感性负载-智能高边钳位能量计算
  • LeetCode 3110.字符串的分数:模拟(注意一个小细节)
  • 《基于机器学习(xgboost)的人体卡路里消耗预测系统》开题报告
  • MSCI中国指数5月调整:新增5只A股、1只港股
  • 阿坝州委书记徐芝文已任四川省政府党组成员
  • 安徽省委副秘书长、省委政研室主任余三元调任省社科院院长
  • 【社论】个人破产探索,要守住“诚实而不幸”的底线
  • 复旦大学与上海杨浦共建市东医院
  • 外交部:正确认识和对待历史是检验日本能否恪守和平发展承诺的重要标准