当前位置: 首页 > 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 {@Beanpublic 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 thenredis.call('expire', key, expire)return 1 -- 加锁成功
    elsereturn 0 -- 加锁失败
    end
    
  • 解锁脚本 unlock.lua

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

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

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

    @Service
    public class RedisLockService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate 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脚本。开发时需注意参数传递集群兼容性异常处理,避免踩坑。


http://www.mrgr.cn/news/94593.html

相关文章:

  • Qt 控件概述 QWdiget
  • Java数据结构第二十三期:Map与Set的高效应用之道(二)
  • A SURVEY ON POST-TRAINING OF LARGE LANGUAGE MODELS——大型语言模型的训练后优化综述——第2部分
  • 从0开始搭建微服务架构特别篇SpringCloud网关聚合knife4j
  • C语言【内存函数】详解加模拟实现
  • 【大模型基础_毛玉仁】2.4 基于 Encoder-Decoder 架构的大语言模型
  • Ansible 自动化运维
  • 路由器与防火墙配置命令
  • (done) 梳理 xv6-lab-2023 fs.img 生成过程,以及 xv6 磁盘结构
  • python速通小笔记-------1.容器
  • pytest 框架学习总结
  • 论Linux进程间通信
  • 高德地图猎鹰服务调用指南(Java后端)
  • Flutter三棵树是什么,为什么这么设计
  • 使用kubeadm方式以及使用第三方工具sealos搭建K8S集群
  • synchronized与 Java内置锁(未写完)
  • 嵌入式八股C语言---面向对象篇
  • 【DeepSeek应用】DeepSeek模型本地化部署方案及Python实现
  • 备赛蓝桥杯-Python-Day1-基础语法回顾
  • Java 学习记录:基础到进阶之路(二)