用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脚本
以分布式锁为例,实现加锁和解锁的原子操作:
加锁脚本
lock.lualocal 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.lualocal 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脚本。开发时需注意参数传递、集群兼容性和异常处理,避免踩坑。到此这篇关于用Lua脚本实现Redis原子操作的示例的文章就介绍到这了,更多相关Lua脚本实现Redis原子操作内容请搜索电脑手机教程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持电脑手机教程网!