Redis分布式锁的实现(Redission)

写在前面

本人在学习Redis过程中学习到分布式锁时太多困惑和疑难杂点 需要总结梳理思路 以下思路都是最简单最基本的思路 主要用到Redission工具类 会涉及到看门狗机制等 本文内容部分引自Javaguide,小林coding等热门八股 用于个人学习用途

分布式锁介绍

对于单机多线程来说 通常用ReetrantLock类 synchronized关键字这类JDK自带的本地锁来控制一个JVM进程内多线程对本地共享资源的访问

图解引自JavaGuide:

 线程访问共享资源是 互斥 的!也就是同一时刻只有单线程可获取本地锁访问共享资源

在分布式系统下 不同服务或者客户端通常运行在多个独立的JVM进程若多个JVM进程共享同份资源 使用本地锁就没办法实现资源的互斥访问了 为了解决这种问题 分布式锁 诞生了

举例子:下单服务一共部署 3 份 都对外提供服务 用户下订单之前后台要查库存 为了防止超卖 就需要加锁来实现对检查库存操作的同步访问 由于订单服务位于不同JVM进程中 本地锁在这种情况下就无法正常工作  因此要用到分布式锁 才能解决多线程不在同个JVM进程下也能获取同把锁 劲儿实现共享资源的互斥访问

图解引自JavaGuide:

可以看出来两者区别就在于 分布式锁就是联系起不同的JVM进程并保证所有JVM都可以对共享资源进行同步操作 基本的要求还是大体一致的 同样 独立进程里的线程访问共享资源是互斥的 某一时刻只有单线程可获取到分布式锁访问共享资源

分布式锁最基本要满足:

1.互斥:任何一个时刻 锁只能被一个线程持有(保证原子操作以免造成数据不一致问题)

2.高可用:锁的服务是高可用的 即使客户端释放锁的代码逻辑出现问题 锁最后还是会被释放 不影响其他线程中进程对共享资源的访问(一定可以释放锁 无论是异常还是出错 锁的效率很高)

3.可重入:一个节点获取了锁以后还可以再次获取锁(多次使用客户端一个功能 增删改)

实现分布式锁:

Redis 或 ZooKeeper实现分布式锁 以Redis为例

how to 实现?首先 从分布式锁的定义出发 无论是本地锁还是分布式锁 都有共性--“互斥”

在Redis里 setnx命令可以实现互斥 setnx(set if Not eXist)对应Java里的setIfAbsent方法 如果key不存在 才会设置key的值 若key存在就什么都不做

> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0

释放锁 用del删除key就行

> DEL lockKey
(integer) 1

但是这里有很多可能 在自学过程中 想到过若在当前线程已经获取锁的情况下

如果使用多个命令(如 setnx 和 expire设置过期时间),可能会出现:

1.成功获取锁 但是设置过期时间失败 导致锁无法正常释放

2.释放锁时 误删其他线程的锁(如锁已过期,但当前线程误删了新线程的锁)

为了解决特殊情况 需要保证数据前后操作一致性 也就是原子性 最常见的就是用lua脚本通过key对应的value(唯一值)来判断

lua脚本基于c语言 保证了解锁操作的原子性 因为Redis在执行lua脚本时可以用原子性的方式执行 从而保证锁释放操作的原子性

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

图解引自JavaGuide: 

 这样做有两个好处 用value判断是否为对应线程的锁 是则释放 否则error 防止误删其他锁

同时 用lua脚本保持原子操作 确保锁释放不会被其他线程打断

技术往往都是双刃剑 同样这种锁也有一些问题 要是程序遇到问题 比如说释放锁的逻辑突然挂掉可能导致锁无法正常释放 进而造成共享资源无法被其他进程或者线程访问 所以 为了避免这种问题 需要给锁设置一个过期时间

分布式锁设置过期时间

为了避免这种因外部原因或者突然断电异常 可以给这个key(这个分布式锁)设置一个过期时间

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK

 lockKey:加锁的锁名 uniqueValue 能够唯一标识锁的随机字符串(可以用UUID)

NX:只有当lockKey对应的key值不存在时才能set成功

EX:过期时间设置(秒为单位) EX 3 表示这个锁有三秒的自动过期时间 与EX对应的是PX(毫秒)两个都是过期时间设置

一定需要保证设置指定key的值和过期时间是原子操作!!!!!!否则锁无法被释放

但是我怎么知道要设置多长过期时间 我写完这个文章我难道要给自己设置个过期时间 到九点前我写完了 那我网页还挂着为啥不关机 九点之前没写完 我电脑直接关机了我怎么写

对于这种分布式锁也是一样的道理 一般正常的操作都是毫秒级别 过期时间小于线程对共享资源的操作时间 就回出现锁提前过期的问题 进而导致分布式锁直接失效 过期时间设置过长又会浪费性能

要是能自动设置就好了!

如何实现优雅的续期....?

总会有人想到的 这里就引入Redis里现成的方案 Redission

Redission自动续期机制

Redission就可以做到自动续期机制 其底层实现上 使用了看门狗机制(Watch Dog)

在共享资源的线程操作还未完成的情况下 看门狗会一直延长过期时间(默认情况下存活30s

每10s更新一次过期时间)进而保证锁不会因为超时而被释放

多好的看门狗 多希望自己学习也是这

图解引自JavaGuide: 

 看门狗名字的由来于 getLockWatchdogTimeout() 方法,此方法返回看门狗给锁续期的过期时间(默认30s)

//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {this.lockWatchdogTimeout = lockWatchdogTimeout;return this;
}
public long getLockWatchdogTimeout() {return lockWatchdogTimeout;
}

方法内用lua脚本保证操作原子性 renewExpiration方法包含主要逻辑 

private void renewExpiration() {//......Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//......// 异步续期,基于 Lua 脚本CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) {// 无法续期log.error("Can't update lock " + getRawName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// 递归调用实现续期renewExpiration();} else {// 取消续期cancelExpirationRenewal(null);}});}// 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);}

JavaGuide的源码分析很到位 我之前做过看门狗机制的笔记 比较凌乱 基本上是这样的流程

有ttl和time(ttl 锁有效时间 time获取锁的等待时间) 需要确保业务正常运行释放锁  不能因为阻塞异常释放锁 引出看门狗机制

这里的ee对象封装了当前线程id与当前定时的任务(该定时任务里用lua脚本对redis有效期定时更新 在看门狗机制30s的情况下 每30/3 = 10秒后会重启任务 通过内部递归重复调用方法执行)

Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +"return 0;",Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));
}

 这里的renewExpirationAsync方法实现了续期 用lua脚本主要是保证操作原子性

JavaGuide里用Redission分布式 可重入锁RLock来说明如何使用Redission实现分布式锁

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);

注意 不设置锁过期时间时 才会出发看门狗自动续期机制

如何实现可重入锁?

什么是可重入锁?

单线程可以多次获取同一把锁 例如一个线程在执行一个带锁的方法 在方法中又调用了另一个同样需要相同锁的方法 则改成可以直接执行调用的方法 即可重入 相同线程多次进方法内的锁 

Java中 synchronized(JVM内置监视器锁)与ReentrantLock(Lock实现类锁)都是可重入锁

不可重入的分布式锁基本可以满足绝大部分场景 特殊情况下还是要用可重入分布式锁

可重入分布式锁实现核心思路线程在获取锁时判断是否为自己的锁 如果是 就不用重新获取 

所以 可以给每个锁关联一个可重入计数器 和 一个 占有他的线程(给线程上标识)当可重入计数器

大于0时 则锁被占有 需要判断占有该所的线程和请求获取锁的线程是否一致(是否有相同标识)

实际上 总有人替你负重前行 Redission内部内置了多种类型的锁比如可重入锁(ReentrantLock)自旋锁(SpinLock) 公平锁(Fair Lock)多重锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)

要是都用一个Redis服务 Redis爆炸了怎么办?所以一般都是集群服务 所以..

集群模式下如何实现分布式锁?如何保证分布式锁可靠性?

Redis集群数据同步到每个节点 会是异步还是同步? 一定是异步操作 同步太慢 那如果Redis主节点获取到锁后 在未能同步到其他节点时主服务宕机了.. 此时 新的Redis主节点还是能获取锁  变成新老大 多个应用服务还是可以同时获取锁

图解引自JavaGuide:

针对这种突然崩溃的情况 还是有人替我负重前行 用RedLock(红锁)来解决

RedLock算法思想 是 让客户端向Redis集群中多个独立的Redis实例依次请求申请加锁 若客户端能和多半数实例成功完成加锁操作 那么就宏观上认为:客户端成功获得分布式锁 否则加锁失败

即使部分Redis节点出现问题 只要保证Redis集群中有多半数Redis节点可用 分布式锁服务就ok

RedLock是直接操作Redis节点的 并不是通过Redis集群操作的 这样才可以避免Redis集群主从切换导致锁丢失问题

贴上锁优化时的问题

哨兵会让从节点变为主节点 但是原锁就会失效  可以在新主节点进新锁 会引发线程安全问题

以及从开始到现在我遇到的分布式锁的优缺点和优化过程 今天又看到RedLock

基本上到这里也是个人项目的内容

面试拷打:如何在项目中实现分布式锁?采用了什么方法?

Redission实现分布式锁..后续后面补上

参考资料

[1]Redisson: https://github.com/redisson/redisson

[2]redisson-3.17.6: https://github.com/redisson/redisson/releases/tag/redisson-3.17.6

[3]Redlock 算法: https://redis.io/topics/distlock

[4]How to do distributed locking - Martin Kleppmann - 2016: https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

[5]JavaGuide:原文链接:支付宝一面:如何基于Redis实现分布式锁?

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

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

相关文章

【愚公系列】《Python网络爬虫从入门到精通》038-SQLite数据库

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

【开源免费】基于SpringBoot+Vue.JS网络海鲜市场系统(JAVA毕业设计)

本文项目编号 T 222 &#xff0c;文末自助获取源码 \color{red}{T222&#xff0c;文末自助获取源码} T222&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

JDK17安装方法/如何安装JDK17/环境变量配置

双击安装 我们这里安装jdk17 然后更改jdk目录 jdk环境变量配置 右键——>此电脑——>高级系统设置——>环境变量 当前用户的环境变量&#xff0c;另外有一个系统变量。 一般我们配置系统环境变量 一、配置JAVA_HOME 这里我们配置一个JAVA_HOME 我这里先前的jdk…

python集合set的常用方法

目录 集合的定义 集合的基础操作 多个集合之间的操作 集合的for循环 集合的定义 集合的基础操作 集合.add(元素) 添加新元素 集合.pop() 从集合中随机取出一个元素 集合.clear() 清空集合 集合.remove(元素) 移除元素 #定义集合,集合自动去重了 set1{"春"…

看得见摸得着的AI:具身智能

“如果Siri有手有脚&#xff0c;你的生活会变成什么样&#xff1f;” 想象一下&#xff1a; • 你家的扫地机器人不再横冲直撞&#xff0c;而是像猫咪一样轻巧绕过桌脚 • 手机里的语音助手能“摸”到你发烧的额头&#xff0c;主动帮你叫医生 • 工厂里的机械臂会边干活边学习&…

ES、OAS、ERP、电子政务、企业信息化(高软35)

系列文章目录 ES、OAS、ERP、电子政务、企业信息化 文章目录 系列文章目录前言一、专家系统&#xff08;ES&#xff09;二、办公自动化系统&#xff08;OAS&#xff09;三、企业资源规划&#xff08;ERP&#xff09;四、典型信息系统架构模型1.政府信息化和电子政务2.企业信息…

【算法学习之路】4.简单数论(4)

简单数论&#xff08;4&#xff09; 前言三.高精度1.什么是高精度2.解决办法 精度乘除法一.精度乘法1.数据的存储2.步骤3.例题&#xff1a;高精度乘法 二.精度除法1.例子2.步骤3.例题&#xff1a;高精度除法 前言 我会将一些常用的算法以及对应的题单给写完&#xff0c;形成一套…

Linux 动静态库和_make_进度条(一)

文章目录 一、如何理解条件编译二、动静态库1. 理论2. 实践3. 解决普通用户的sudo问题4. 技术上理解库 三、make和make_file 一、如何理解条件编译 1. gcc code.c -o code -DM 命令行级别的宏定义预处理的本质就是修改编辑我们的文本代码 头文件展开到源文件中去注释宏替换条…

基于springboot+vue的拖恒ERP-物资管理

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

Leetcode-853. Car Fleet [C++][Java]

目录 一、题目描述 二、解题思路 Leetcode-853. Car Fleethttps://leetcode.com/problems/car-fleet/description/ 一、题目描述 There are n cars at given miles away from the starting mile 0, traveling to reach the mile target. You are given two integer array …

Vue核心知识:动态路由实现完整方案

在Vue中实现动态路由&#xff0c;并结合后端接口和数据库表设计&#xff0c;是一个复杂的项目&#xff0c;需要多个技术栈和步骤的配合。以下将详细描述整个实现过程&#xff0c;包括数据库设计、后端接口设计、前端路由配置以及如何实现动态路由的功能。 目录 一、需求分析二…

CMU15445(2023fall) Project #4 - Concurrency Control踩坑历程

把树木磨成月亮最亮时的样子&#xff0c; 就能让它更快地滚下山坡&#xff0c; 有时会比骑马还快。 完整代码见&#xff1a; SnowLegend-star/CMU15445-2023fall: Having Conquered the Loftiest Peak, We Stand But a Step Away from Victory in This Stage. With unwavering…

天疱疮是一种慢性、严重的皮肤疾病

天疱疮是一种慢性、严重的皮肤疾病&#xff0c;对患者的日常生活带来很大的困扰。为了更好地预防天疱疮的发生&#xff0c;我们需要了解其成因及传播途径&#xff0c;并采取相应的预防措施。以下是关于天疱疮预防的小知识。 一、了解天疱疮 天疱疮是一种自身免疫性疾病&#…

神经网络代码入门解析

神经网络代码入门解析 import torch import matplotlib.pyplot as pltimport randomdef create_data(w, b, data_num): # 数据生成x torch.normal(0, 1, (data_num, len(w)))y torch.matmul(x, w) b # 矩阵相乘再加bnoise torch.normal(0, 0.01, y.shape) # 为y添加噪声…

解决android studio(ladybug版本) gradle的一些task突然消失了

今天不知道干了啥&#xff0c;AS&#xff08;ladybug版本&#xff09;右边gradle的task有些不见了&#xff0c;研究了半天解决了&#xff0c;这里记录下&#xff1a; 操作&#xff1a; File -->Settings-->Experimental--> 取消选项“Enable support for multi-vari…

软件测试之白盒测试知识总结

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 概念与定义 白盒测试&#xff1a;侧重于系统或部件内部机制的测试&#xff0c;类型分为分支测试&#xff08;判定节点测试&#xff09;、路径测试、语句测试…

Unity中动态切换光照贴图的方法

关键代码&#xff1a;LightmapSettings.lightmaps lightmapDatas; LightmapData中操作三张图&#xff1a;lightmapColor,lightmapDir,以及一张ShadowMap 这里只操作前两张&#xff1a; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI;public cl…

LLC谐振变换器恒压恒流双竞争闭环simulink仿真

1.模型简介 本仿真模型基于MATLAB/Simulink&#xff08;版本MATLAB 2017Ra&#xff09;软件。建议采用matlab2017 Ra及以上版本打开。&#xff08;若需要其他版本可联系代为转换&#xff09;针对全桥LLC拓扑&#xff0c;利用Matlab软件搭建模型&#xff0c;分别对轻载&#xf…

网络变压器的主要电性参数与测试方法(2)

Hqst盈盛&#xff08;华强盛&#xff09;电子导读&#xff1a;网络变压器的主要电性参数与测试方法&#xff08;2&#xff09;.. 今天我们继续来看看网络变压器的2个主要电性参数与它的测试方法&#xff1a; 1. 线圈间分布电容Cp:线圈间杂散静电容 测试条件:100KHz/0.1…

前端正则表达式完全指南:从入门到实战

文章目录 第一章&#xff1a;正则表达式基础概念1.1 什么是正则表达式1.2 正则表达式工作原理1.3 基础示例演示 第二章&#xff1a;正则表达式核心语法2.1 元字符大全表2.2 量词系统详解2.3 字符集合与排除 第三章&#xff1a;前端常用正则模式3.1 表单验证类3.1.1 邮箱验证3.1…