用Lua脚本实现Redis原子操作

ops/2025/3/17 0:42:23/
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

    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

    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.ppmy.cn/ops/166357.html

相关文章

SpringBoot 开启配置绑定:@EnableConfigurationProperties

文章目录 EnableConfigurationProperties 开启单个配置属性绑定EnableConfigurationProperties 开启多个配置属性绑定EnableConfigurationProperties 的应用场景 EnableConfigurationProperties 是 SpringBoot 在 org.springframework.boot.context.properties 包下提供的一个…

OpenCV实现图像特征提取与匹配

‌一、特征检测与描述子提取‌ ‌选择特征检测器‌ 常用算法包括&#xff1a; ‌ORB‌&#xff1a;一种高效的替代SIFT和SURF的算法&#xff0c;主要用于移动机器人和增强现实等领域。适合实时应用&#xff0c;结合FAST关键点与BRIEF描述子‌。‌SIFT&#xff08;尺度不变特征变…

王者荣耀道具页面爬虫(json格式数据)

首先这个和英雄页面是不一样的&#xff0c;英雄页面的图片链接是直接放在源代码里面的&#xff0c;直接就可以请求到&#xff0c;但是这个源代码里面是没有的 虽然在检查页面能够搜索到&#xff0c;但是应该是动态加载的&#xff0c;源码中搜不到该链接 然后就去看看是不是某…

mac安装mysql之后报错zsh: command not found: mysql !

在Mac上安装MySQL后&#xff0c;如果终端中找不到mysql命令&#xff0c;通常是 因为MySQL的命令行工具&#xff08;如mysql客户端&#xff09;没有被正确地添加到你的环境变量中。 检查 MySQL 是否已安装 ps -ef|grep mysql查看到路径在 /usr/local/mysql/bin 查看 .bash_pro…

【SpringMVC】常用注解:@ModelAttribute

1.作用 该注解是在SpringMVC4.3版本后新加入的。它可以修饰方法和参数。出现在方法上&#xff0c;表示当前方法会在控制器的方法之前执行。它可以修饰 没有返回值的方法&#xff0c;也可以修饰没有返回值的方法。它修饰参数&#xff0c;获取指定 的数据给参数赋值。 当表单提…

多线程到底重不重要?

我们先说一下为什么要讲多线程和高并发&#xff1f; 原因是&#xff0c;你想拿到一个更高的薪水&#xff0c;在面试的时候呈现出了两个方向的现象&#xff1a; 第一个是上天 项目经验高并发 缓存 大流量 大数据量的架构设计 第二个是入地 各种基础算法&#xff0c;各种基础…

3ds Max 导入到 After Effects 还原摄像机要注意事项--deepseek

我&#xff1a;dp我这有两个脚本分别是syn软件相机导出到max的和syn软件相机导出到ae的&#xff0c;你能看出差别来吗&#xff1f;如果我想把max里的相机导入到ae里&#xff0c;保持原来的位置方向&#xff0c;该怎么做 dp&#xff1a;从这两个脚本可以看出&#xff0c;3ds Ma…

【漫话机器学习系列】134.基于半径的最近邻分类器(Radius-Based Nearest Neighbor Classifier)

在机器学习中&#xff0c;最近邻&#xff08;Nearest Neighbor&#xff09;算法是一种基本的分类方法&#xff0c;它主要依赖于计算点之间的距离来进行分类。最常见的最近邻算法是 k-最近邻&#xff08;k-Nearest Neighbors, k-NN&#xff09;&#xff0c;它通过选取距离目标点…