【📕分布式锁通关指南 03】通过Lua脚本保证redis操作的原子性

简介: 本文介绍了如何通过Lua脚本在Redis中实现分布式锁的原子性操作,避免并发问题。首先讲解了Lua脚本的基本概念及其在Redis中的使用方法,包括通过`eval`指令执行Lua脚本和通过`script load`指令缓存脚本。接着详细展示了如何用Lua脚本实现加锁、解锁及可重入锁的功能,确保同一线程可以多次获取锁而不发生死锁。最后,通过代码示例演示了如何在实际业务中调用这些Lua脚本,确保锁操作的原子性和安全性。

引言

在02篇的小结中,为大家指出了我们处理锁误删的代码中存在的问题,但其实只要使用redis来做分布式锁,如果你不能把操作一步完成,不管什么场景可能或多或少都会出问题。所以引出了本篇的内容。在03篇中,我会为大家讲解如何通过Lua脚本来保持redis指令的原子性,从而避免并发问题。友情提示:本篇其实算番外篇,如果你对Lua脚本不感兴趣可以直接跳过,毕竟在实际业务里应该没多少人会自己去手撸,后续篇章中会讲解到其他中间件更为成熟可靠的方案。

什么是Lua脚本

官方介绍它是一种轻量小巧的脚本语言,设计的目的是为了嵌入应用程序,从而为应用程序提供灵活的扩展性和定制功能。redis支持嵌入Lua脚本,因此可以很方便地使用。安装过程很简单,本文不做赘述,直接移步官网下载即可,如遇到问题,善用一下你的搜索引擎即可。

1.png

Lua脚本在redis中的使用

我们并不讲解Lua的基础语法,能看这篇文章的肯定都是起码掌握一门甚至多门编程语言的人,所有我们直接上手实战,看看它是如何在redis中使用的。

通过eval指令执行Lua脚本

redis里提供了eval指令,让用户可以输入Lua脚本并执行,接下来,让我们以此来实现一个简单的set指令,如下:

2.png

大致说明下,redis.call就是执行redis指令,里面的参数分别是具体的指令和key、value的占位符,括号外的1表示1个键,name和cc则分别是key和value的实际值。通过get命令查看结果,如下:

3.png

通过script load指令缓存lua脚本

上面通过eval指令可以实现输入并执行lua脚本,那么如果相同的脚本每次都要重新输入就很麻烦,因此,我们可以通过script load指令来将脚本缓存下来,如下:

4.png

执行后它会返回一个校验和,我们就把这个校验和当做脚本的id吧,然后我们就可以通过evalsha命令通过这个校验和来执行对应的脚本命令,如下:

5.png

利用Lua脚本实现建议加锁和解锁

既然我们已经学会了Lua脚本的基础用法,现在让我们用Lua脚本来分别实现加锁和解锁的逻辑,这里可以创建一个lock.lua文件来进行编写。

简易锁加锁

if redis.call('EXISTS', KEYS[1]) == 0 then
    redis.call('SET', KEYS[1], ARGV[1])
    redis.call('EXPIRE', KEYS[1], ARGV[2])
    return 1
else
    return 0
end

逻辑看起来非常的简单,就是判断一下当前是否存在这把锁,如果存在,则加锁失败;如果不存在,就set一个锁,并且给一个过期时间。脚本编写好后,就可以用eval来执行了,如下:

redis-cli eval "if redis.call('EXISTS', KEYS[1]) == 0 then redis.call('SET', KEYS[1], ARGV[1]); redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1; else return 0; end" 1 lockName uuid 3000

6.png

简易锁解锁

if (redis.call('EXISTS', KEYS[1]) == 0) then
    return 0;
end
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    redis.call('DEL', KEYS[1])
    return 1;
else
    return 0;
end

逻辑也是相当的简单,就是先判断锁是否存在,如果存在,再比较的value的uuid是否一致,如果一致,则删除锁。执行如下:

7.png

在代码中使用lua脚本

脚本编写完成后,我们就需要在代码中进行调用,这里我们对原来的加锁和解锁方法进行改造,代码如下:

    @Override
    public void lock(TimeUnit timeUnit, Long expireTime) {
   

        while (true) {
   
            // 使用Lua脚本进行加锁
            String luaScript = "if(redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('expire', KEYS[1], ARGV[2]) return 1; else return 0; end";
            Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
                    Collections.singletonList(this.lockName), uuid, expireTime.toString());
            if (result != null && result.equals(1L)) {
   
                break;
            }

            try {
   
                Thread.sleep(100);
            } catch (InterruptedException e) {
   
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void unlock() {
   
        // 使用Lua脚本进行解锁
        String luaScript = "if (redis.call('EXISTS', KEYS[1]) == 0) then return 0; end if (redis.call('GET', KEYS[1]) == ARGV[1]) then redis.call('DEL', KEYS[1]) return 1; else return 0; end";
        stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
                Collections.singletonList(this.lockName), uuid);
    }

它的使用也是非常简单,把脚本声明之后,直接通过execute方法进行传参调用即可。通过Lua脚本的加解锁主要是将原本使用redis的多步操作合并成了一步来保证了操作的原子性。

利用Lua脚本实现锁的可重入

在前文中,我们讲到分布式锁具备的几个特性中有提到可重入性,这个特性对于分布式锁的实现至关重要。首先,我们明确下它的定义-在同一个线程中,同一个锁可以被多次获取而不会发生死锁。假设方法A调用了需要相同锁的方法B或者本身就是递归的,当不可重入时,那么第二次获取锁的时候就会被阻塞,从而发生死锁。而观察下我们前面写的Lua脚本显然不足以支持可重入,因此我们就需要改造。

8.png

那么实现可重入的关键就是:获取了多少把锁就得解锁的时候解多少把,这里需要保持最终一致性。所以我们这里的实现思路就需要用到redis的一种数据结构-hash,代码如下:

if (redis.call('EXISTS', KEYS[1]) == 0) then
    redis.call('HINCRBY', KEYS[1], ARGV[1], 1)
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1;
end
if (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1) then
    redis.call('HINCRBY', KEYS[1], ARGV[1], 1)
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1;
else
    return 0;
end

这里的实现逻辑也是很简单:先判断锁是否存在,如果不存在,直接加锁,重入次数设置为1以及加过期时间;如果存在,则比较uuid是否是本线程,如果是,那么可重入次数+1,并且给锁加一个过期时间,如果不是,那么就加锁失败。那么我们的解锁逻辑也就很清晰了,如下:

if (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0) then
    return 0;
end
local lockCount = redis.call('HINCRBY', KEYS[1], ARGV[1], -1)
if (lockCount > 0) then
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1;
else
    redis.call('DEL', KEYS[1])
end

这里的实现逻辑是:首先判断当前持有锁的线程是不是本线程,不是的话,就不需要释放了。如果是,就对重入次数减1,减1之后判断值是否大于0,如果大于还持有锁,就设置一个新的过期时间,如果不大于0,就可以删除锁了。这里我们执行一下脚本看看效果,如下:

9.png

这里我们测试先加了三次锁,然后执行一次解锁,再次查看,可以看到锁只剩两次了,至此,可重入锁成功实现。

小结

本期带领大家简单学习了如何通过Lua脚本来保证锁的原子性,进而保证了我们锁的安全性。在下期的04篇中将会给大家继续完善我们的redis分布式锁,因为目前看似很完美,但其实还存在细节问题,那么我们下一期见。

目录
相关文章
|
3月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
274 2
|
3月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
195 6
|
4月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
8月前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
767 0
分布式爬虫框架Scrapy-Redis实战指南
|
2月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
163 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
2月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
4月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
171 8
|
6月前
|
数据采集 存储 NoSQL
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
350 67
|
5月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
1359 7
|
6月前
|
缓存 监控 NoSQL
Redis设计与实现——分布式Redis
Redis Sentinel 和 Cluster 是 Redis 高可用与分布式架构的核心组件。Sentinel 提供主从故障检测与自动切换,通过主观/客观下线判断及 Raft 算法选举领导者完成故障转移,但存在数据一致性和复杂度问题。Cluster 支持数据分片和水平扩展,基于哈希槽分配数据,具备自动故障转移和节点发现机制,适合大规模高并发场景。复制机制包括全量同步和部分同步,通过复制积压缓冲区优化同步效率,但仍面临延迟和资源消耗挑战。两者各有优劣,需根据业务需求选择合适方案。

热门文章

最新文章