用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. 参数传递错误
- 问题:
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(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("解锁失败:锁已过期或非持有者");
}
}
调试与优化建议
-
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脚本。开发时需注意参数传递、集群兼容性和异常处理,避免踩坑。