用Lua脚本实现Redis原子操作的示例
目录
- 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. 参数传递错误
问题:KEYS
和ARGV
数量或类型不匹配,导致脚本执行失败。
解决:明确区分参数类型:
// 正确传参示例 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内置命令(如
SETNX
、EXPIRE
)。
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中,结合RedisTemplate
和DefaultRedisScript
,能够高效集成Lua脚本。开发时需注意参数传递、集群兼容性和异常处理,避免踩坑。
到此这篇关于用Lua脚本实现Redis原子操作的示例的文章就介绍到这了,更多相关Lua脚本实现Redis原子操作内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论