先讲一下为什么使用分布式锁:

在传统的单体应用中,我们可以使用Java并发处理相关的API(如ReentrantLock或synchronized)来实现对共享资源的互斥控制,确保在高并发情况下同一时间只有一个线程能够执行特定方法。然而,随着业务的发展,单体应用逐渐演化为分布式系统,多线程、多进程分布在不同机器上,这导致了原有的单机部署下的并发控制策略失效。为了解决这一问题,我们需要引入一种跨JVM的互斥机制来管理共享资源的访问,这就是分布式锁所要解决的核心问题。

Lua介绍

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

为什么要用Lua呢

Redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性。

在以下场景中:

  1. 当 事务1执行删除操作时,查询到的锁值确实相等。
  2. 在 事务1执行删除操作之前,锁的过期时间刚好到达,导致 Redis 自动释放了该锁。
  3. 事务2获取了这个已被释放的锁。
  4. 当 事务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脚本实现分布式锁的更多相关文章

  1. 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁

    spring boot基于redis的LUA脚本 实现分布式锁[都是基于redis单点下] 一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁 1.pom.xml &l ...

  2. redis集群+JedisCluster+lua脚本实现分布式锁(转)

    https://blog.csdn.net/qq_20597727/article/details/85235602 在这片文章中,使用Jedis clien进行lua脚本的相关操作,同时也使用一部分 ...

  3. Redis学习笔记(三)使用Lua脚本实现分布式锁

    Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行. 使用Lua脚本的好处如下: 1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放 ...

  4. .Net Core使用分布式缓存Redis:Lua脚本

    一.前言 运行环境window,redis版本3.2.1.此处暂不对Lua进行详细讲解,只从Redis的方面讲解. 二.Redis的Lua脚本 在Redis的2.6版本推出了脚本功能,允许开发者使用L ...

  5. Redis结合Lua脚本实现高并发原子性操作

    从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚 ...

  6. Redis中是如何实现分布式锁的?

    分布式锁常见的三种实现方式: 数据库乐观锁: 基于Redis的分布式锁: 基于ZooKeeper的分布式锁. 本地面试考点是,你对Redis使用熟悉吗?Redis中是如何实现分布式锁的. 要点 Red ...

  7. Redis的“假事务”与分布式锁

    关注公众号:CoderBuff,回复"redis"获取<Redis5.x入门教程>完整版PDF. <Redis5.x入门教程>目录 第一章 · 准备工作 第 ...

  8. python使用redis实现协同控制的分布式锁

    python使用redis实现协同控制的分布式锁 上午的时候,有个腾讯的朋友问我,关于用zookeeper分布式锁的设计,他的需求其实很简单,就是节点之间的协同合作. 我以前用redis写过一个网络锁 ...

  9. 要想用活Redis,Lua脚本是绕不过去的坎

    前言 Redis 当中提供了许多重要的高级特性,比如发布与订阅,Lua 脚本等.Redis 当中也提供了自增的原子命令,但是假如我们需要同时执行好几个命令的同时又想让这些命令保持原子性,该怎么办呢?这 ...

  10. 快速入门Redis调用Lua脚本及使用场景介绍

    Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储.大多数开发人员可能听说过redis可以运行 Lua 脚本,但是可能不知道redis在什么情况下需要使用到Lua脚本. 一.阅读本文 ...

随机推荐

  1. Linux上面Shell简单进行数值计算的办法

    1. 自己简单写了一个脚本 来计算 一个服务进程启动的耗时, 精度要求不高 10秒上下就可以. 在程序执行之前和之后的处理 在最前面设置一句time1=`date +%s`在末尾一句设置time2=` ...

  2. node使用nodemailer发送邮件

    安装模块 npm install nodemailer 代码 const nodemailer = require('nodemailer'); // 查找到有关QQ邮箱的相关信息在 /node_mo ...

  3. React类组件中事件绑定this指向的三种方式

    有状态组件和无状态组件 函数组件又叫做无状态组件,类组件又叫做有状态组件. 状态又叫做数据 函数组件没有自己的状态,只负责静态页面的展示. 我们可以理解为纯ui展示.() 类组件有自己的状态,扶着更新 ...

  4. 【K哥爬虫普法】孤注一掷的爬虫er,究竟还要误入歧途多远?

    我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...

  5. 在K8S中,Pod生命周期包含哪些?

    在Kubernetes(简称K8s)中,Pod的生命周期经历了一系列状态变化.以下是Pod可能处于的一些主要状态: Pending: 当创建一个Pod时,它首先会进入Pending状态.这个状态下,K ...

  6. 开启中文智能之旅:探秘超乎想象的 Llama2-Chinese 大模型世界

    "开启中文智能之旅:探秘超乎想象的 Llama2-Chinese 大模型世界" 1.国内Llama2最新下载地址 本仓库中的代码示例主要是基于Hugging Face版本参数进行调 ...

  7. 【4】jupyter notebook快速入门、以及常用快捷键使用

    相关文章: [1]Anaconda安装超简洁教程,瞬间学会! [2]Anaconda下:ipython文件的打开方式,Jupyter Notebook中运行.py文件,快速打开ipython文件的方法 ...

  8. 5.4 Windows驱动开发:内核通过PEB取进程参数

    PEB结构(Process Envirorment Block Structure)其中文名是进程环境块信息,进程环境块内部包含了进程运行的详细参数信息,每一个进程在运行后都会存在一个特有的PEB结构 ...

  9. 4.4 EAT Hook 挂钩技术

    EAT(Export Address Table)用于修改动态链接库(DLL)中导出函数的调用.与IAT Hook不同,EAT Hook是在DLL自身中进行钩子操作,而不是修改应用程序的导入表.它的原 ...

  10. SpringCloud-Config配置中心搭建保姆级教程

    一.分布式配置中⼼ 在使⽤微服务架构开发的项⽬中,每个服务都有⾃⼰的配置⽂件(application.yml),如果将每个服务的配置⽂件直接写在对应的服务中,存在以下问题: 1. 服务开发完成之后,需 ...