开发者

用Lua脚本实现Redis原子操作的示例

开发者 https://www.devze.com 2025-03-15 11:23 出处:网络 作者: Cloud_.
目录1. 环境准备2. 编写Lua脚本3. 加载并执行脚本开发中的常见问题与解决方案1. Lua脚本缓存问题2. 参数传递错误3. Redis集群兼容性4. 脚本性能问题5. 异常处理完整示例:分布式锁调试与优化建议总结1. 环境准备
目录
  • 1. 环境准备
  • 2. 编写Lua脚本
  • 3. 加载并执行脚本
  • 开发中的常见问题与解决方案
    • 1. Lua脚本缓存问题
    • 2. 参数传递错误
    • 3. Redis集群兼容性
    • 4. 脚本性能问题
    • 5. 异常处理
    • 完整示例:分布式锁
    • 调试与优化建议
  • 总结

    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脚本

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

    加锁脚本 lockCAGUKRtWf.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
    http://www.devze.com

    解锁脚本 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;
    
       http://www.devze.com 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(exp编程客栈ireSec)
        ) == 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("解锁失败:锁已过期或非持有者");
        }
    }
    

    调试与优化建议

    Redis CLI调试:

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

    日志配置:

    # application.properties
    logging.level.org.springframework.data.redis=DEBUG
    

    监控脚本执行时间:

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

    总结

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

    到此这篇关于用Lua脚本实现Redis原子操作的示例的文章就介绍到这了,更多相关Lua脚本实现Redis原子操作内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号