Redis加Lua脚本实现分布式锁
先讲一下为什么使用分布式锁:
在传统的单体应用中,我们可以使用Java并发处理相关的API(如ReentrantLock或synchronized)来实现对共享资源的互斥控制,确保在高并发情况下同一时间只有一个线程能够执行特定方法。然而,随着业务的发展,单体应用逐渐演化为分布式系统,多线程、多进程分布在不同机器上,这导致了原有的单机部署下的并发控制策略失效。为了解决这一问题,我们需要引入一种跨JVM的互斥机制来管理共享资源的访问,这就是分布式锁所要解决的核心问题。
Lua介绍
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
为什么要用Lua呢
Redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性。
在以下场景中:
- 当 事务1执行删除操作时,查询到的锁值确实相等。
- 在 事务1执行删除操作之前,锁的过期时间刚好到达,导致 Redis 自动释放了该锁。
- 事务2获取了这个已被释放的锁。
- 当 事务1执行删除操作时,会意外地删除掉 事务2持有的锁。
上面的删除情况也无法保证原子性,只能通过lua脚本实现
如果redis客户端通过lua脚本把3个命令一次性发送给redis服务器,那么这三个指令就不会被其他客户端指令打断。Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。
Lua脚本命令
在Redis中需要通过eval命令执行lua脚本
EVAL script numkeys key [key ...] arg [arg ...]
script:lua脚本字符串,这段Lua脚本不需要(也不应该)定义函数。
numkeys:lua脚本中KEYS数组的大小
key [key ...]:KEYS数组中的元素
arg [arg ...]:ARGV数组中的元素
案列1:动态传参
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 8 10 30 40 50 60 70
# 输出:8 10 60 70
EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 10 20
# 输出:0
EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 20 10
# 输出:1
案列2:执行redis类库方法
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 bbb 20

可重入性
可重入性是指一个线程在持有锁的情况下,可以多次获取同一个锁而不会发生死锁或阻塞的特性。在可重入锁中,线程可以重复获取已经持有的锁,每次获取都会增加一个计数器,直到计数器归零时才会真正释放锁。
下面是一个示例代码来说明可重入性:
public synchronized void a() {
b();
}
public synchronized void b() {
// pass
}
假设线程X在方法a中获取了锁后,继续执行方法b。如果这是一个不可重入的锁,线程X在执行b方法时将会被阻塞,因为它已经持有了该锁并且无法再次获取。这种情况下,线程X必须等待自己释放锁后才能再次争抢该锁。
而对于可重入性的情况,当线程X持有了该锁后,在遇到加锁方法时会直接将加锁次数加1,并继续执行方法逻辑。当退出加锁方法时,加锁次数再减1。只有当加锁次数归零时,该线程才会真正释放该锁。
因此,可重入性的最大特点就是计数器的存在,用于统计加锁的次数。在分布式环境中实现可重入分布式锁时也需要考虑如何正确统计和管理加锁次数。
加锁脚本
Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用 lua 脚本判断逻辑。
if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1)
then
redis.call('hincrby', KEYS[1], ARGV[1], 1);
redis.call('expire', KEYS[1], ARGV[2]);
return 1;
else
return 0;
end
假设值为:KEYS:[lock], ARGV[uuid, expire]
如果锁不存在或者这是自己的锁,就通过hincrby(不存在就新增并加1,存在就加1)获取锁或者锁次数加1。
解锁脚本
-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 nil 代表 自己的锁已不存在,在尝试解其他线程的锁,解锁失败
-- 如果为 0 代表 可重入次数被减 1
-- 如果为 1 代表 该可重入 key 解锁成功
if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then
return 0;
else
redis.call('del', KEYS[1]);
return 1;
end;
如果锁不存在直接返回null,如果锁存在就对数量进行减一,如果减到等于0 就直接删除此锁
自动续期
有可能代码没执行完毕,锁就到期了。基于上面这种情况需要对锁进行续期。使用定时器加lua脚本进行对锁续期
if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
redis.call('expire', KEYS[1], ARGV[2]);
return 1;
else
return 0;
end
Java代码实现

考虑到分布式锁可能使用多种方式实现,比如Redis、mysql、zookeeper,所以暂时做成一个工厂类,按需使用。
以下是完整代码:
public class DistributedRedisLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;
private String uuid;
private long expire = 30;
public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid = uuid + ":" + Thread.currentThread().getId();
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 加锁方法
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1){
this.expire = unit.toSeconds(time);
}
String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
Thread.sleep(50);
}
// 加锁成功,返回之前,开启定时器自动续期
this.renewExpire();
return true;
}
/**
* 解锁方法
*/
@Override
public void unlock() {
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
"then " +
" return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
if (flag == null){
throw new IllegalMonitorStateException("this lock doesn't belong to you!");
}
}
@Override
public Condition newCondition() {
return null;
}
private void renewExpire(){
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
renewExpire();
}
}
}, this.expire * 1000 / 3);
}
}
DistributedLockClient
@Component
public class DistributedLockClient {
@Autowired
private StringRedisTemplate redisTemplate;
private String uuid;
public DistributedLockClient() {
this.uuid = UUID.randomUUID().toString();
}
public DistributedRedisLock getRedisLock(String lockName){
return new DistributedRedisLock(redisTemplate, lockName, uuid);
}
}
使用及测试:
在业务代码中使用:
public void deduct() {
DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lock");
redisLock.lock();
try {
// 1. 查询库存信息
String stock = redisTemplate.opsForValue().get("stock").toString();
// 2. 判断库存是否充足
if (stock != null && stock.length() != 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
// 3.扣减库存
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
} finally {
redisLock.unlock();
}
}
测试可重入性:

Redis加Lua脚本实现分布式锁的更多相关文章
- 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁
spring boot基于redis的LUA脚本 实现分布式锁[都是基于redis单点下] 一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁 1.pom.xml &l ...
- redis集群+JedisCluster+lua脚本实现分布式锁(转)
https://blog.csdn.net/qq_20597727/article/details/85235602 在这片文章中,使用Jedis clien进行lua脚本的相关操作,同时也使用一部分 ...
- Redis学习笔记(三)使用Lua脚本实现分布式锁
Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行. 使用Lua脚本的好处如下: 1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放 ...
- .Net Core使用分布式缓存Redis:Lua脚本
一.前言 运行环境window,redis版本3.2.1.此处暂不对Lua进行详细讲解,只从Redis的方面讲解. 二.Redis的Lua脚本 在Redis的2.6版本推出了脚本功能,允许开发者使用L ...
- Redis结合Lua脚本实现高并发原子性操作
从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚 ...
- Redis中是如何实现分布式锁的?
分布式锁常见的三种实现方式: 数据库乐观锁: 基于Redis的分布式锁: 基于ZooKeeper的分布式锁. 本地面试考点是,你对Redis使用熟悉吗?Redis中是如何实现分布式锁的. 要点 Red ...
- Redis的“假事务”与分布式锁
关注公众号:CoderBuff,回复"redis"获取<Redis5.x入门教程>完整版PDF. <Redis5.x入门教程>目录 第一章 · 准备工作 第 ...
- python使用redis实现协同控制的分布式锁
python使用redis实现协同控制的分布式锁 上午的时候,有个腾讯的朋友问我,关于用zookeeper分布式锁的设计,他的需求其实很简单,就是节点之间的协同合作. 我以前用redis写过一个网络锁 ...
- 要想用活Redis,Lua脚本是绕不过去的坎
前言 Redis 当中提供了许多重要的高级特性,比如发布与订阅,Lua 脚本等.Redis 当中也提供了自增的原子命令,但是假如我们需要同时执行好几个命令的同时又想让这些命令保持原子性,该怎么办呢?这 ...
- 快速入门Redis调用Lua脚本及使用场景介绍
Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储.大多数开发人员可能听说过redis可以运行 Lua 脚本,但是可能不知道redis在什么情况下需要使用到Lua脚本. 一.阅读本文 ...
随机推荐
- CentOS8 安装Oracle19c RPM的办法
1. 下载相应的rpm包 我这边使用的主要有: -rw-r--r-- 1 root root 19112 Apr 5 15:13 compat-libcap1-1.10-7.el7.x86_64.rp ...
- 文心一言 VS 讯飞星火 VS chatgpt (187)-- 算法导论14.1 4题
四.用go语言,写出一个递归过程 OS-KEY-RANK(T,k),以一棵顺序统计树T和一个关键字k作为输入,要求返回 k 在由 T 表示的动态集合中的秩.假设 T 的所有关键字都不相同. 文心一言, ...
- 【VictoriaMetrics的vmbackupmanager】这个一年卖 2 万美元的功能,我做出来了
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 1.背景 在可观测领域的 metrics 解决方案中,Vi ...
- interface{}类型 + fmt.Sprintf() 导致栈逃逸
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 对部分代码进行了栈逃逸检查: go build -gcfl ...
- 无法下载安装文件,请检查internet连接
win10 vs2019下提示visual studio installer无法下载安装文件,请检查internet连接 1.打开"网络和Internet设置",更改适配器 ...
- windowsbat命令大全
Bat文件的创建及其命令大全 一.bat文件的创建 新建txt文本文件 向文本文件中输入命令 保存并修改文本文件后缀为.bat 双击保存后的bat文件,运行 二.bat命令大全 echo 和 @ @ ...
- P7036 [NWRRC2016] Folding
题目简述 有两个矩形,大小分别是 \(W \times Y\) 和 $ w \times y$.现在我们要通过折叠将两个矩阵变成一样. 思路 part1 已知一条边折叠一次会变成 \(\frac{x} ...
- CMake出错的处理
在windows上使用cmake来c++的程序,遇到一个问题 问题排查 试过在电脑上单独使用gcc是可以编译成功的,那么就可能是IDE集成的问题了 IDE的编译工具链从mingw换成vs,编译通过 让 ...
- win10 局域网共享文件创建方法
win10 局域网共享文件创建方法 1.先在桌面文件夹,我命名为"xxxx",然后将文件放在该文件里. 2.右击共享文件夹,找到属性选项,点击"属性".再点击& ...
- 6.7 Windows驱动开发:内核枚举LoadImage映像回调
在笔者之前的文章<内核特征码搜索函数封装>中我们封装实现了特征码定位功能,本章将继续使用该功能,本次我们需要枚举内核LoadImage映像回调,在Win64环境下我们可以设置一个LoadI ...