一、分布式锁的必要性

在单体应用时代,我们使用ReentrantLocksynchronized就能解决线程安全问题。但当系统拆分为分布式架构后(目前大多数公司应该不会只是单体应用了),跨进程的共享资源竞争就成了必须要解决的问题。

分布式锁由此应运而生,但是必须解决三大核心问题:

  1. 竞态条件:多人操作共享资源,顺序不可控
  2. 锁失效:锁自动过期但业务未执行完,其他客户端抢占资源 / 加锁成功但未设置过期时间,服务宕机导致死锁
  3. 锁误删:客户端A释放了客户端B持有的锁。

二、核心实现解析(附源码)

2.1 原子性加锁

local lockKey = KEYS[1]              -- 锁的键名,如"order_lock_123"
local lockSecret = ARGV[1] -- 锁的唯一标识(建议UUID)
local expireTime = tonumber(ARGV[2]) -- 过期时间(单位:秒) -- 参数有效性校验
if not expireTime or expireTime <= 0 then
return "0" -- 参数非法直接返回失败
end -- 原子操作:SET lockKey lockSecret NX EX expireTime
local result = redis.call("set", lockKey, lockSecret, "NX", "EX", expireTime)
return result and "1" or "0" -- 成功返回"1",失败返回"0"

设计思路:

  • value使用客户端唯一标识(推荐SnowflakeID)
  • 参数校验:防止传入非法过期时间
  • 原子性:单命令完成"判断+设置+过期"操作

2.2 看门狗续期机制

local lockKey = KEYS[1]              -- 锁的键名
local lockSecret = ARGV[1] -- 锁标识
local expireTime = tonumber(ARGV[2]) -- 新的过期时间 -- 参数校验
if not expireTime or expireTime <= 0 then
return "0"
end -- 获取当前锁的值
local storedSecret = redis.call("get", lockKey) -- 续期逻辑
if storedSecret == lockSecret then
-- 值匹配则延长过期时间
local result = redis.call("expire", lockKey, expireTime)
return result == 1 and "1" or "0" -- 续期成功返回"1"
else
-- 锁不存在或值不匹配
return "0"
end
// 定时续约线程
watchdogExecutor.scheduleAtFixedRate(() -> {
locks.entrySet().removeIf(entry -> entry.getValue().isCancelled());
for (Entry<String, Lock> entry : locks.entrySet()) {
if (!entry.getValue().isCancelled()) {
String result = redisTemplate.execute(RENEWAL_SCRIPT,
Collections.singletonList(key),
lock.value, "30");
if ("0".equals(result)) lock.cancel();
}
}
}, 0, 10, TimeUnit.SECONDS);

设计思路:

  • 续期间隔=过期时间/3(如30s过期则10s续期)
  • 异步线程池需单独配置
  • 双重校验锁状态(内存标记+Redis实际值)

2.3 安全释放锁

local lockKey = KEYS[1]       -- 锁的键名
local lockSecret = ARGV[1] -- 要释放的锁标识 -- 获取当前锁的值
local storedSecret = redis.call("get", lockKey) -- 校验锁归属
if storedSecret == lockSecret then
-- 值匹配则删除Key
return redis.call("del", lockKey) == 1 and "1" or "0"
else
-- 值不匹配
return "0"
end

设计思路:

  • 校验value避免误删其他线程的锁

三、源码

package org.example.tao.util;

import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript; import javax.annotation.PreDestroy;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; public class RedisUtils { static class Lock {
private final String value;
private volatile boolean isCancelled = false; public Lock(String value) {
this.value = value;
} public boolean isCancelled() {
return isCancelled;
} public void cancel() {
isCancelled = true;
}
} private static final String LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2]) -- 动态过期时间\n" + "if not expireTime or expireTime <= 0 then\n" + " return \"0\"\n" + "end\n" + "local result = redis.call(\"set\", lockKey, lockSecret, \"NX\", \"EX\", expireTime)\n" + "return result and \"1\" or \"0\"";
private static final String RELEASE_LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local storedSecret = redis.call(\"get\", lockKey)\n" + "if storedSecret == lockSecret then\n" + " return redis.call(\"del\", lockKey) == 1 and \"1\" or \"0\"\n" + "else\n" + " return \"0\"\n" + "end";
private static final String RENEWAL_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2])\n" + "if not expireTime or expireTime <= 0 then\n" + " return \"0\"\n" + "end\n" + "local storedSecret = redis.call(\"get\", lockKey)\n" + "if storedSecret == lockSecret then\n" + " local result = redis.call(\"expire\", lockKey, expireTime)\n" + " return result == 1 and \"1\" or \"0\"\n" + "else\n" + " return \"0\"\n" + "end"; private final String defaultExpireTime = "30";
private final RedisTemplate<String, String> redisTemplate;
private final Map<String, Lock> locks = new ConcurrentHashMap<>();
private final ScheduledExecutorService watchdogExecutor = Executors.newScheduledThreadPool(1); public RedisUtils(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
watchdogExecutor.scheduleAtFixedRate(() -> {
try {
System.out.println("watchdogExecutor 执行中... locks => " + JSON.toJSONString(locks));
locks.entrySet().removeIf(entry -> entry.getValue().isCancelled());
for (Map.Entry<String, Lock> entry : locks.entrySet()) {
String key = entry.getKey();
Lock lock = entry.getValue();
if (!lock.isCancelled()) {
RedisScript<String> redisScript = RedisScript.of(RENEWAL_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), lock.value, defaultExpireTime);
if (Objects.equals(result, "0")) {
lock.cancel(); // 移除已经释放的锁
}
}
}
} catch (Exception e) {
System.err.println("看门狗任务执行失败: " + e.getMessage());
}
}, 0, 10, TimeUnit.SECONDS);
} public boolean acquireLock(String key, String value) {
RedisScript<String> redisScript = RedisScript.of(LOCK_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, defaultExpireTime);
if (Objects.equals(result, "1")) {
locks.put(key, new Lock(value));
return true;
}
return false;
} public boolean acquireLockWithRetry(String key, String value, int maxRetries, long retryIntervalMillis) {
int retryCount = 0;
while (retryCount < maxRetries) {
boolean result = this.acquireLock(key, value);
if (result) {
locks.put(key, new Lock(value));
return true;
}
retryCount++;
try {
Thread.sleep(retryIntervalMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
} public boolean releaseLock(String key, String value) {
RedisScript<String> redisScript = RedisScript.of(RELEASE_LOCK_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
if (Objects.equals(result, "1")) {
Lock lock = locks.get(key);
if (lock != null) {
lock.cancel();
}
return true;
}
return false;
} @PreDestroy
public void shutdown() {
watchdogExecutor.shutdown();
try {
if (!watchdogExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
watchdogExecutor.shutdownNow();
}
} catch (InterruptedException e) {
watchdogExecutor.shutdownNow();
}
}
}

四、如何使用

4.1 配置类

@Configuration
public class AppConfig { @Resource
private RedisTemplate<String, String> redisTemplate; @Bean
public RedisUtils init() {
return new RedisUtils(redisTemplate);
} }

4.2 使用

@RestController
@RequestMapping("/users")
public class UserController {
@Resource
private RedisTemplate<String, String> redisTemplate; @PostMapping("/test2")
public Boolean test2(@RequestBody Map<String, String> map) {
boolean res;
if (Objects.equals(map.get("lockFlag"), "true")) {
res = redisUtils.acquireLock(map.get("key"), map.get("value"));
} else {
res = redisUtils.releaseLock(map.get("key"), map.get("value"));
}
return res;
} }

后记

还是免责声明,仅供学习参考

学习高可靠Redis分布式锁实现思路的更多相关文章

  1. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

  2. 高并发redis分布式锁

    1.方法一 2方法二

  3. 基于redis实现可靠的分布式锁

    什么是锁 今天要谈的是如何在分布式环境下实现一个全局锁,在开始之前先说说非分布式下的锁: 单机 – 单进程程序使用互斥锁mutex,解决多个线程之间的同步问题 单机 – 多进程程序使用信号量sem,解 ...

  4. Redis 当成数据库在使用和可靠的分布式锁,Redlock 真的可行么?

    怎样做可靠的分布式锁,Redlock 真的可行么? https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html ...

  5. redis整理:常用命令,雪崩击穿穿透原因及方案,分布式锁实现思路,分布式锁redission(更新中)

    redis个人整理笔记 reids常见数据结构 基本类型 String: 普通key-value Hash: 类似hashMap List: 双向链表 Set: 不可重复 SortedSet: 不可重 ...

  6. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  7. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  8. 面试官:你真的了解Redis分布式锁吗?

    什么是分布式锁 说到Redis,我们第一想到的功能就是可以缓存数据,除此之外,Redis因为单进程.性能高的特点,它还经常被用于做分布式锁. 锁我们都知道,在程序中的作用就是同步工具,保证共享资源在同 ...

  9. Redis分布式锁 (图解-秒懂-史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  10. Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!(转)

    基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本篇文章主要是基于我们实际项目中因为redis分布式锁造成的事故分析及解决方案.我们项目中的抢购订单采用的是分布式锁来解决的,有一次,运营做了一 ...

随机推荐

  1. Docker封装Java环境镜像(Alpine+OpenJDK)

    在给Java程序封装镜像时,使用的基础镜像动辄上百M,还需要每次部署的时候挂载时区等问题,不如自己封装一个镜像,供之后使用. 这里使用Alpine Linux(3.9) 安装OpenJDK 1.8及部 ...

  2. java正则表达式(find()和 matches())

    1.find()方法是部分匹配,是查找输入串中与模式匹配的子串,如果该匹配的串有组还可以使用group()函数. matches()是全部匹配,是将整个输入串与模式匹配,如果要验证一个输入的数据是否为 ...

  3. Qt/C++编写物联网组件/支持modbus/rtu/tcp/udp/websocket/mqtt/多线程采集

    一.功能特点 支持多种协议,包括Modbus_Rtu_Com/Modbus_Rtu_Tcp/Modbus_Rtu_Udp/Modbus_Rtu_Web/Modbus_Tcp/Modbus_Udp/Mo ...

  4. 基于Xxl-Job,dataX设计的数据同步和可视化任务编排工具

    使用vue3对xxl-job进行重构,并集成datax工具实现不同数据源的数据同步,支持glue模式,并新增存储过程调用,api任务调度和可视化任务编排,支持单任务-单任务串并联,单任务-任务集串并联 ...

  5. eclipse中汉字横着的问题解决

    最近在eclipse中加上中午注释,出现那种汉字是横着的情况,解决方案如下: 方法一: 同一种字体有两种显示方式,比如Fixedsys Excelsior 3.01和@Fixedsys Excelsi ...

  6. 百度公共IM系统的Andriod端IM SDK组件架构设计与技术实现

    本文由百度技术团队分享,引用自百度Geek说,原题"百度Android IM SDK组件能力建设及应用",本文进行了排版和内容优化. 1.引言 移动互联网时代,随着社交媒体.移动支 ...

  7. 关于vue加element-ui上传文件获取文件的sha256的值

    首先使用element的上传文件的组件 安装依赖crypto-js npm i crypto-js <el-upload class="upload-demo" drag : ...

  8. (十).NET6.0 搭建基于Quartz组件的定时调度任务

    1.添加Quartz定时器组件 2.新建类库项目Wsk.Core.QuartzNet,并且引用包类库项目.然后新建一个中间调度类,叫QuartzMiddleJob 3.新建一个Job工厂类,叫YsqJ ...

  9. Solution -「Gym 101630J」Journey from Petersburg to Moscow

    \(\mathscr{Description}\)   Link.   给定含有 \(n\) 个点 \(m\) 条边的带权无向图,一条路径的长度定义为其中前 \(k\) 大的边权和,求 \(1\) 到 ...

  10. linux:搭建Drupal

    了解 Drupal 是使用 PHP 语言编写的开源内容管理框架(CMF),由内容管理系统(CMS)及 PHP 开发框架(Framework)共同构成.Drupal 具备强大的定制化开发能力,您可使用 ...