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

用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脚本。开发时需注意参数传递集群兼容性异常处理,避免踩坑。


http://www.kler.cn/a/587751.html

相关文章:

  • Google Filament 渲染引擎(2)-Backend 核心类介绍
  • C语言实现冒泡排序,超详解
  • Scheme语言的区块链
  • 【后端开发面试题】每日 3 题(十一)
  • Spring Boot实战:MySQL与Redis数据一致性深度解析与代码实战
  • 一.ffmpeg打开麦克风,录制音频并重采样
  • 地理信息系统(ArcGIS)在水文水资源及水环境中的应用:实践技术与案例分析深度剖析
  • 深入解析 TCP/IP 协议:架构、原理与应用
  • kmp报错→Cannot find skiko-windows-x64.dll.sha256
  • C++ 中的 RTTI(Run-Time Type Information,运行时类型识别)
  • 虚拟电商-数据库分库分表(二)
  • 16.使用读写包操作Excel文件:XlsxWriter 包
  • 中科院自动化所人形机器人研究进展:全面综述与展望
  • Git LFS (Large File Storage) 简介
  • Linux中Gdb调试工具常用指令大全
  • Vue.js 项目部署全解析:从开发到上线的关键旅程题
  • Elasticsearch集群与日志系统实战部署指南
  • JVM内存结构笔记03-方法区
  • Mysql查看执行计划、explain关键字详解(超详细)
  • linux在 Ubuntu 系统中设置服务器时间