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. 参数传递错误
- 问题:
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脚本。开发时需注意参数传递、集群兼容性和异常处理,避免踩坑。