分布式锁(Redis)

一、序言

本文和大家聊聊分布式锁以及常见的解决方案。

二、什么是分布式锁

未命名文件 (4).png
假设一个场景:一个库存服务部署在上面三台机器上,数据库里有 100 件库存,现有 300 个客户同时下单。并且这 300 个客户均摊到上面的三台机器上(即三台机器上分别有 100 个客户)。如果库存服务采取的是传统的进程锁或线程锁,我们会发现三台机器上在检测库存时都能满足(因为每台机器有 100 个客户,刚好满足 100 件库存)。此时会出现只有 100 件库存,却卖出了 300 件的现象(即超卖现象)。
未命名文件 (6).png
为了解决上述在分布式环境中存在的问题,我们需要使用分布式锁。分布式锁是一种在分布式系统中实现线程或进程同步访问共享资源的机制。它的主要目标是在分布式环境下,确保在同一时间只有一个线程或进程可以访问特定的资源

三、分布式锁方案

分布式锁的实现方式主要有三种:

  1. 基于数据库的分布式锁:这种方案主要是在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引。想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。这种方案的缺点包括数据库单点问题、没有锁超时机制、不可重入、非公平锁、非阻塞锁等。
  2. 基于 Redis 的分布式锁:Redis 的分布式锁主要是通过 SETNX 和 EXPIRE 命令来实现的。SETNX 可以用来尝试获取锁。EXPIRE 命令用来设置锁的超时时间,防止死锁。此外,还有基于 Redlock 算法的 Redisson 分布式锁。
  3. 基于 Zookeeper 的分布式锁:Zookeeper 是一个开源的分布式协调服务,它提供了一种高效且可靠的分布式锁实现机制。Zookeeper 的分布式锁主要是通过临时顺序节点和使用 watch 机制来实现。

四、Redis 分布式锁

4.1 Redis 分布式锁实现方式

Redis 分布式锁的实现通常基于 Redis 的原子性操作(比如 SETNX、EXPIRE、DEL 等),主要思想是通过在Redis 中设置一个特定的键值对来表示锁的状态,当某个节点需要获取锁时,会尝试在 Redis 中设置这个键值对,如果设置成功,则获取到锁,可以执行相应的操作;如果设置失败,则表示锁已经被其他节点持有,当前节点需要等待或重试。

public class RedisLock {private Jedis jedis;private String lockKey;// 构造器public RedisLock(Jedis jedis) {this.jedis = jedis;this.lockKey = "lock";}// 获取锁public boolean tryLock() {// 使用 set key value NX 命令尝试获取锁String result = jedis.set(lockKey, "1", SetParams.setParams().nx());return "OK".equals(result);}// 释放锁public void unLock() {// 释放锁,即删除对应的键jedis.del(lockKey);}
}

4.2 Redis 分布式锁过期

在之前,我们利用 set key value nx 这个互斥命令实现了最基本的分布式锁。但是,现在有一个问题:如果有一个业务在获取锁之后,由于未知原因发生了业务阻塞或者在业务完成之后忘记了释放锁,这将会导致当前业务会永久性的持有该锁。为了解决 Redis 分布式锁无法释放的问题,我们采用给锁设置超时时间:

public class RedisLock {private Jedis jedis;private String lockKey;// 构造器public RedisLock(Jedis jedis) {this.jedis = jedis;this.lockKey = "lock";}// 获取锁public boolean tryLock() {// 使用 set key value NX EX seconds 命令尝试获取锁, 并设置过期时间String result = jedis.set(lockKey, "1", SetParams.setParams().nx().ex(5));return "OK".equals(result);}// 释放锁public void unLock() {// 释放锁,即删除对应的键jedis.del(lockKey);}
}

在上面的代码中,我们利用 set key value nx ex seconds 命令给锁设定了超时时间解决了 Redis 分布式锁被占用而无法释放的问题(设定了超时时间,就算发生了业务阻塞,锁最终也会被释放)。

4.3 Redis 分布式锁误解锁

上面的 Redis 分布式锁引入了超时机制后会带来一个问题。我们先假设一个场景:

  1. 业务 A 获取到锁之后发生了业务阻塞,锁被超时释放了。
  2. 业务 B 正常获取到锁执行业务。此时,业务 A 恢复执行,并在执行完成后释放掉了锁(此时锁是属于业务 B 的)
  3. 业务 C 争抢到锁,但是业务 B 与业务 C 是互斥的此时就会导致并发问题(业务 B 与业务 C 是互斥的,但是同时在执行)。
public class RedisLock {private Jedis jedis;private String lockKey;// 构造器public RedisLock(Jedis jedis, String lockKey) {this.jedis = jedis;// lockKey 不再为固定值this.lockKey = lockKey;}// 获取锁public boolean tryLock() {// 使用 set key value NX EX seconds 命令尝试获取锁, 并设置过期时间String result = jedis.set(lockKey, "1", SetParams.setParams().nx().ex(5));return "OK".equals(result);}// 释放锁public void unLock() {// 释放锁,即删除对应的键jedis.del(lockKey);}
}

为了解决分布式锁误解锁的问题,Redis 分布式锁的 key 不再为一个固定值。业务 A 有自己的 lockKey,业务 B 与业务 C 有相同的 lockKey。此时,业务 A 只能释放自己的锁,业务 B 与业务 C 拥有相同的 lockKey,当业务 B 没有释放锁时,业务 C 是无法获取到锁的,从而保证了业务 B 与业务 C 的互斥。

4.4 Redis 分布式锁续约

在 Redis 分布式锁误解锁的例子中,我们似乎使用不同的 lockKey 解决了误解锁的问题。但是当我们再深入思考一下会发现还有一个问题。我们现假设:

  1. 首先,业务 B 获取到锁之后发生了业务阻塞,锁被超时释放了。
  2. 然后,业务 C 争抢到锁,由于业务 B 与业务 C 是互斥的此时就会导致并发问题。

每一个业务的执行时间大抵是不尽相同的。在之前的例子中我们使用 jedis.set(lockKey, "1", SetParams.setParams().nx().ex(5))将锁的的释放时间设置成了 5 秒,如果业务能够在 5 秒内执行完成倒是没什么问题,若发生了业务阻塞或业务执行时间大于我们设定的过期时间呢?
针对以上的问题,我们通常采用一种称为 Watch Dog(看门狗)的机制去解决。

public class RedisLock {private final Jedis jedis;private final String lockKey;private final ScheduledExecutorService executorService;// 构造器public RedisLock(Jedis jedis, String lockKey) {this.jedis = jedis;this.lockKey = lockKey;this.executorService = Executors.newSingleThreadScheduledExecutor();}// 尝试获取锁@SneakyThrowspublic boolean tryLock(long leaseTime, TimeUnit timeUnit)  {String result = jedis.set(lockKey, "1", SetParams.setParams().nx().px(timeUnit.toMillis(leaseTime)));if ("OK".equals(result)) {// 获取锁成功时自动启动 watch dogstartWatchdog(leaseTime, timeUnit);return true;}return false;}// 释放锁public void unlock() {jedis.del(lockKey);stopWatchdog(); // 释放锁时停止 watch dog}// 启动 Watchdogpublic void startWatchdog(long leaseTime, TimeUnit timeUnit) {long leaseTimeMillis = timeUnit.toMillis(leaseTime);// 续期检测时间间隔为租约时间的 1/3long checkIntervalMillis = leaseTimeMillis / 3;executorService.scheduleAtFixedRate(() -> {long ttl = jedis.pttl(lockKey);if (ttl > 0) {// 续约锁jedis.pexpire(lockKey, leaseTimeMillis);} else {// 锁过期后停止 watch dogstopWatchdog();}// 周期性执行任务}, checkIntervalMillis, checkIntervalMillis, TimeUnit.MILLISECONDS);}// 停止 Watchdogpublic void stopWatchdog() {executorService.shutdown();}
}

在上面的代码中采用了 Watch Dog 机制周期性的去给锁续期,在业务完成之后,调用 unlock() 方法便可释放锁,并且停止 Watch Dog。

4.5 Redis 分布式锁重试

现在的 Redis 分布式锁已经解决了一部分问题,但是我们假设一个场景:

  1. 有三个业务(业务 A,业务 B,业务 C)同时争抢锁,业务 A 首先抢到了锁
  2. 业务 A 的执行时间很短,业务 B 与业务 C 此时应该如何处理

现有两种处理方式:

  1. 业务 B 与业务 C 直接返回失败信息
  2. 业务 B 与业务 C 自动重试争抢锁

在高并发的场景下,第一种方式业务 B 与业务 C 获取到锁的成功率会很小(因为第一次没抢到就返回失败信息了),第二种方式显然会更高(业务 B 与业务 C 会重试获取锁,如果在重试时锁空闲了便能获取到)。为了系统的稳定性和可靠性我们通常会采用第二种方式。

public class RedisLock {private final Jedis jedis;private final String lockKey;private final ScheduledExecutorService executorService;// 构造器public RedisLock(Jedis jedis, String lockKey) {this.jedis = jedis;this.lockKey = lockKey;this.executorService = Executors.newSingleThreadScheduledExecutor();}// 获取锁@SneakyThrowspublic boolean tryLock(long waitTime, long leaseTime, TimeUnit timeUnit)  {long start = System.currentTimeMillis();long end = start + timeUnit.toMillis(waitTime);// while 循环进行锁重试while (System.currentTimeMillis() < end) {String result = jedis.set(lockKey, "1", SetParams.setParams().nx().px(timeUnit.toMillis(leaseTime)));if ("OK".equals(result)) {// 获取锁成功时自动启动 watch dogstartWatchdog(leaseTime, timeUnit);return true;}// 尝试等待一段时间再重试TimeUnit.MILLISECONDS.sleep(100);}return false;}// 释放锁public void unlock() {jedis.del(lockKey);stopWatchdog(); // 释放锁时停止 watch dog}// 启动 Watchdogpublic void startWatchdog(long leaseTime, TimeUnit timeUnit) {long leaseTimeMillis = timeUnit.toMillis(leaseTime);// 续期检测时间间隔为租约时间的 1/3long checkIntervalMillis = leaseTimeMillis / 3;executorService.scheduleAtFixedRate(() -> {long ttl = jedis.pttl(lockKey);if (ttl > 0) {// 续约锁jedis.pexpire(lockKey, leaseTimeMillis);} else {// 锁过期后停止 watch dogstopWatchdog();}}, checkIntervalMillis, checkIntervalMillis, TimeUnit.MILLISECONDS);}// 停止 Watchdogpublic void stopWatchdog() {executorService.shutdown();}
}

五、Redis 分布式锁的问题

5.1 如何实现可重入分布式锁

之前我们基于 set key value 实现的分布式锁,但是这样的锁是不可重入的。如果我们想实现可重入的分布式锁可以基于 Hash 类型,采用 hset key field value 这样的命令实现(重入一次 value 自增 +1)。

5.2 锁过期与锁续约的冲突

锁过期是为了防止锁一直被占用无法释放,锁续约是为了防止锁被提前释放。如果锁无限续约那么锁设置过期时间就无意义了,所以锁在续约时需要一些兜底方案(例如:有一个最大的续约时间)。除此之外,应该在设置锁过期时间和锁续约时间时充分考虑业务的执行时间,从而尽可能提前避免一些问题。

5.3 锁重试为什么需要等待

在我们设计锁重试时有这么一行代码 TimeUnit.MILLISECONDS.sleep(100)(即休眠 100 ms)。为什么需要休眠呢?现假设一个场景:

  1. 业务 A 与业务 B 同时争抢锁
  2. 业务 A 先抢到了锁,执行业务需要 1s

业务 A 既然执行时间需要 1s,如果业务 B 在重试的时候不休眠就会白白浪费系统资源。如果休眠 100 ms,最多会重试 10 次,这样很大程度上节省了系统资源。

往期推荐

  1. JDK 动态代理
  2. ThreadLocal
  3. HashMap 源码分析(三)
  4. Spring 三级缓存
  5. RBAC 权限设计(二)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/1751.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React 19 带来了 JSX 运行时的重要更新

在 React 的发展历程中&#xff0c;JSX 运行时一直扮演着重要的角色。在以前的的版本&#xff0c;JSX 运行时会克隆传入的 props 对象&#xff0c;这背后有着两大原因。 历史原因 React 保留了一些特殊的 prop 名称&#xff0c;如 key 和在 React 19 之前的 ref。这些 prop 并…

SpringBoot整合Swagger3生成接口文档

一&#xff1a;前言 Swagger 是一个 RESTful API 的开源框架&#xff0c;它的主要目的是帮助开发者设计、构建、文档化和测试 Web API。Swagger 的核心思想是通过定义和描述 API 的规范、结构和交互方式&#xff0c;以提高 API 的可读性、可靠性和易用性&#xff0c;同时降低 …

深度图转点云

一、理论分析 二、其他分析 1、相机内参 相机内参主要是四个参数fx,fy,u0,v0。要明白相机内参就是相机内部参数&#xff0c;是参考像素坐标系而言&#xff0c;有了这个前提&#xff0c;这四个参数也就很好理解了。 &#xff08;1&#xff09;首先&#xff0c;。其中F是相机的…

Oracle中的 plsql语法

01-plsql 为什么要plsql 复杂的业务逻辑 可以使用 编程语言实现 sql无法实现 plsql也可以实现复杂的业务逻辑 为不直接使用编程语言 而是学习plsql plsql会比直接使用 编程语言 速度更快 基本语法&#xff1a; [declare --声明变量 变量名 变量类型 ] begin --代码逻辑 …

Springboot Gateway 报错Failed to resolve “bogon”的原因及解决办法

一、问题出现原因及初步分析 今天遇到一个奇怪的错误&#xff0c;一个一直正确运行的微服务后台&#xff0c;突然无法访问&#xff0c;如何重启都会报错。 想到近期有人在服务器上安装过其它服务&#xff0c;因此&#xff0c;考虑可能是配置问题&#xff0c;可配置问题修复后…

1.基于Springboot对SpringEvent初步封装

一&#xff1a;前置知识 Spring Event是Spring框架提供的一种事件机制&#xff0c;用于处理组件之间的通信。在复杂的系统中&#xff0c;模块或组件之间的通信是必不可少的。Spring Event可以用于以下场景&#xff1a; 1.系统间解耦&#xff1a;模块或组件之间通过事件进行通…

账号安全基本措施1

一、系统账号清理 1.1 将用户设置为无法登录 useradd -s /sbin/nologin lisi shell类型设置为/sbin/nologin用户将无法使用bash或其他shell来登录系统。 1.2 锁定用户。passwd -l 用户名 正常情况下是可以送普通用户切换到其他普通用户的 当锁定密码后passwd -l lisi就用普…

LeetCode:组合求和III之回溯法

题目 题目链接 找出所有相加之和为 n 的 k 个数的组合&#xff0c;且满足下列条件&#xff1a;只使用数字1到9 每个数字 最多使用一次 返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次&#xff0c;组合可以以任何顺序返回。题目图解 ** ** cpp代码 class …

AI预测体彩排列3第2套算法实战化测试第1弹2024年4月22日第1次测试

从今天开始&#xff0c;开始新一轮的测试&#xff0c;本轮测试&#xff0c;以6码为基础&#xff0c;同步测试杀号情况&#xff0c;争取杀至4-5码。经过计算&#xff0c;假如5码命中&#xff0c;即每期125注&#xff0c;投入250元&#xff0c;十期共计2500元&#xff0c;则命中率…

牛客NC233 加起来和为目标值的组合(四)【中等 DFS C++、Java、Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/7a64b6a6cf2e4e88a0a73af0a967a82b 解法 dfs参考答案C class Solution {public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可*** param nums int整型…

日本二次元团建国内院线:一周一部,占据36.2%票房

从《你想活出怎样的人生》开始&#xff0c;到《哈尔的移动城堡》结束&#xff0c;日本动画正在占据国内院线的整个4月份档期。 包括《数码宝贝02&#xff1a;最初的召唤》、《间谍过家家 代号&#xff1a;白》多部作品在内&#xff0c;整个国内四月份院线日本动画平均一周上映…

【Linux实践室】Linux高级用户管理实战指南:Linux用户与用户组编辑操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;Linux查看用户属性命令2.1.1 &#x1f47b;…

数据结构PT1——线性表/链表

1&#xff1a;顺序存储实现(数组实现) Data&#xff1a; a1 a2 .....ai ai1 .... an .... typedef struct LNode *List; //指向LNode的指针&#xff0c;这是typedef的&#xff0c;你可以随时声明&#xff0c;而不加typedef只是创建一个 struct LNode{ //结构体成员ElementT…

【前端】掌握按钮的显示与隐藏

目录 一、前言二、实现显示与隐藏的四种方式1、CSS属性控制2、JavaScript控制3、Vue.js响应式控制 三、需求背景四、代码实现1、定义变量2、绑定事件3、监听选中的学生4、返回return 五、整体效果1、没有勾选学生2、已勾选学生 六、设置按钮显示与隐藏的优势利弊1、优势2、劣势…

MySQL慢查询怎么办?需要关注Explain的哪些关键字?

目录 1-引言&#xff1a;什么是慢查询1-1 慢查询定义1-2 为什么排查慢查询 2-核心&#xff1a;慢查询排查2-1 慢查询定位2-2 慢查询解决2-2-1 Explain 排查慢查询2-2-2 Explain 重点关键字 3-总结&#xff1a;慢查询知识点小结 1-引言&#xff1a;什么是慢查询 1-1 慢查询定义…

C# 字面量null对于引用类型变量✓和值类型变量×

编译器让相同的字符串字面量共享堆中的同一内存位置以节约内存。 在C#中&#xff0c;字面量&#xff08;literal&#xff09;是指直接表示固定值的符号&#xff0c;比如数字、字符串或者布尔值。而关键字&#xff08;keyword&#xff09;则是由编程语言定义的具有特殊含义的标…

羊大师解读,春季羊奶VS夏季羊奶

羊大师解读&#xff0c;春季羊奶VS夏季羊奶 夏季的羊奶和春季的羊奶在营养成分上并没有本质的区别&#xff0c;都含有丰富的蛋白质、矿物质和维生素等。然而&#xff0c;由于季节和气候的变化&#xff0c;人们饮用羊奶的需求和效果可能会有所不同。 在夏季&#xff0c;天气炎热…

C语言 | Leetcode C语言题解之第35题搜索插入位置

题目&#xff1a; 题解&#xff1a; int searchInsert(int* nums, int numsSize, int target) {int left 0, right numsSize - 1, ans numsSize;while (left < right) {int mid ((right - left) >> 1) left;if (target < nums[mid]) {ans mid;right mid - …

使用vue3+ts+vite从零开始搭建bolg(二)

二、全局变量 2.1element-ui集成 pnpm i element-plus pnpm i element-plus element-plus/icons-vue main.ts配置文件 import ElementPlus from element-plus import element-plus/dist/index.css //ts-ignore import zhCn from element-plus/dist/locale/zh-cn.mjsapp.use…