Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Lua脚本实现Redis原子操作

用Lua脚本实现Redis原子操作的示例

作者:Cloud_.

本文主要介绍了用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脚本缓存问题

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,影响性能。

解决:

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中,结合RedisTemplateDefaultRedisScript,能够高效集成Lua脚本。开发时需注意参数传递、集群兼容性和异常处理,避免踩坑。

到此这篇关于用Lua脚本实现Redis原子操作的示例的文章就介绍到这了,更多相关Lua脚本实现Redis原子操作内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文