Redis之Lua脚本与分布式锁改造

Redis之Lua脚本与分布式锁改造

一、Lua脚本解决多条命令原子性问题

1.1 为什么需要Lua脚本

在分布式系统中,当需要执行多个Redis命令时,传统方式存在两个核心问题:

问题一:非原子性操作

java 复制代码
// 传统释放锁方式存在竞态条件
String id = redisTemplate.opsForValue().get("lock:order");
if (id.equals(currentThreadId)) {
    redisTemplate.delete("lock:order");
}

问题二:网络开销大

每次Redis命令都需要一次网络往返(RTT),多个命令会累积大量网络延迟。

1.2 Lua脚本的三大优势

  1. 原子性保证:Redis将整个Lua脚本作为一个整体执行,执行期间不会响应其他客户端请求,确保脚本内所有命令要么全部成功,要么全部失败。
  2. 减少网络开销:多个命令封装为一个脚本,只需一次网络传输,在高并发场景下性能提升显著。
  3. 复杂逻辑封装:支持条件判断、循环等编程特性,实现原生命令无法完成的复杂业务逻辑。

1.3 Lua脚本基础语法

核心API

  • redis.call():执行Redis命令,出错时抛出异常并停止脚本
  • redis.pcall():执行Redis命令,出错时返回错误对象而不抛出异常

参数传递机制

  • KEYS数组:存储所有被操作的Redis键名(必须显式声明)
  • ARGV数组:存储非键名参数

二、分布式锁的原子性问题

2.1 传统分布式锁的问题

误删锁场景

  1. 线程A持有锁,执行业务逻辑
  2. 锁超时自动释放
  3. 线程B获取锁
  4. 线程A恢复执行,删除锁(误删线程B的锁)
  5. 线程C获取锁,导致并发执行

根本原因:判断锁标识和删除锁是两个独立的Redis操作,中间可能被其他线程插入。

2.2 Lua脚本改造方案

释放锁的Lua脚本

java 复制代码
-- KEYS[1]: 锁的key
-- ARGV[1]: 当前线程标识

-- 获取锁中的线程标识
local id = redis.call('get', KEYS[1])

-- 比较线程标识与锁中的标识是否一致
if (id == ARGV[1]) then
    -- 释放锁
    return redis.call('del', KEYS[1])
end

-- 不一致则返回0
return 0

简化版脚本

java 复制代码
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    return redis.call('del', KEYS[1])
end
return 0

Java代码实现

java 复制代码
@Component
public class SimpleRedisLock implements Ilock {
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true);
    
    // 加载Lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    
    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }
    
    @Override
    public void unlock() {
        // 调用Lua脚本释放锁
        stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId()
        );
    }
}

三、Lua脚本原子性原理

3.1 底层实现机制

单线程模型:Redis采用单线程事件循环模型,所有命令按FIFO顺序执行。当执行Lua脚本时,Redis会阻塞其他客户端请求,直到脚本执行完成。

脚本级事务封装

  • 全有全无执行:脚本内所有命令要么全部成功,要么全部失败
  • 数据锁定机制:执行期间自动锁定脚本操作的所有key,防止其他客户端修改

执行流程

  1. 客户端通过EVAL命令提交脚本
  2. Redis计算脚本的SHA1哈希用于缓存
  3. 设置CLIENT_LUA标志,阻塞事件循环
  4. 执行Lua脚本中的Redis命令
  5. 清除CLIENT_LUA标志,恢复事件循环

3.2 与MULTI/EXEC事务对比

特性 Lua脚本 MULTI/EXEC事务
原子性范围 整个脚本 事务块内命令
错误处理 脚本级错误导致全部回滚 单个命令错误不影响后续
性能开销 较低(单次网络往返) 较高(命令队列维护)

四、总结

通过Lua脚本改造分布式锁,我们实现了:

  1. 原子性保证:将判断锁标识和删除锁封装为一个原子操作
  2. 性能优化:减少网络往返次数,提升系统吞吐量
  3. 安全性提升:避免误删其他线程的锁,确保数据一致性
  4. 可扩展性:支持可重入、锁续期等高级特性

在实际项目中,建议优先使用Redisson等成熟框架,它们已经封装了完整的分布式锁功能,包括看门狗机制、可重入锁等特性,能够更好地满足生产环境需求。

相关推荐
钱多多_qdd3 小时前
mini-spring基础篇:IoC(十一):Aware接口
java·spring
AM越.3 小时前
Java设计模式超详解——抽象工厂设计模式(含UML图)
java·设计模式·uml
嵌入式小能手3 小时前
飞凌嵌入式ElfBoard-文件I/O的深入学习之文件锁
java·服务器·学习
JavaBoy_XJ3 小时前
Redis在 Spring Boot 项目中的完整配置指南
数据库·spring boot·redis·redis配置
Predestination王瀞潞3 小时前
Java EE开发技术 (报错解决 请求的资源[/Bank/$%7BpageContext.request.contextPath%7D/login]不可用)
java·java-ee
Sahadev_3 小时前
GitHub 一周热门项目速览 | 2025年12月08日
java·大数据·人工智能·安全·ai作画
明月出天山_3 小时前
【金融科技理论与实践】常见知识点汇总——北大软微期末考复习
分布式·科技·金融·区块链·智能合约
Jaxson Lin3 小时前
Java编程进阶:打造专属于你的背单词软件V1.0
java·开发语言