一文看分布式锁
为什么会存在分布式锁?
经典场景-扣库存,多人去同时购买一件商品,首先会查询判断是否有剩余,如果有进行购买并扣减库存,没有提示库存不足。假如现在仅存有一件商品,3人同时购买,三个线程同时执行方法,第一人通过了库存>0的校验,此时线程阻塞,第二人通过校验后抢先购买,此时库存已经为0,第一人继续执行扣减,库存就为-1,也就是【超卖】。
首先会想到加锁的方式解决,使用synchronized或者ReentrantLock解决,但如果多个服务器同时对一个共享资源操作时,多个服务器的内存是不共享的,加锁只能锁住当前服务器的进程。但在分布式系统下,例如使用nginx负载均衡,请求会发送到不同的tomcat容器。
1、基于关系型数据库
(1)lock表
基于主键或者唯一索引,一个线程尝试获取锁时往表中插入一条数据,插入成功则表示获取锁成功,获取锁依赖于数据库,架构简单,但是一旦出现宕机等问题,锁就无法释放,需要设置一个定时任务清理数据。并且获取不到锁的线程,需要一直等待,轮询查询什么时候可获取锁。
(2)悲观版本
锁数据提前初始化,抢锁时,使用select ...... for update; 数据库的悲观锁可以自动阻塞其他等待锁的线程,实现锁等待的功能,如果持有锁的节点宕机,数据库事务会自动回滚,锁自然释放。
这种锁可能存在两个问题:
第一个是MySQL可能会对查询做优化,对于小表可能采用表锁代替行锁,表中不同的锁之间就变成串行互斥的关系;
第二个是使用悲观锁需要开启事务,需要一直占用数据库的连接,如果锁过多,则对数据库造成压力。
2、基于Redis
(1)setnx+uuid+finally
setnx命令,只有key不存在时,才能添加成功,达到加锁的目的
void reduceStock() {//uidString uuid = UUID.randomUUID().toString();// 获取锁///Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, "1");// 设置锁过期时间//redisTemplate.expire("lock",30, TimeUnit.SECONDS);// 解决原子性问题boolean result = redisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);if (lock) {try{int num = stockMapper.selectNum();if(num > 0) {//扣减库存stockMapper.reduceStock();}else{log.info("库存不足");}} finally {//校验是否是当前线程的锁String lockValue = redisTemplate.opsForValue().get("lock");if(lockValue.equals(uuid)) {redisTemplate.delete("lock"); } }} else {log.info("未获取锁");}
}
- 在线程A获取锁之后,如果出现宕机或者代码报错异常,锁不会释放,需要在finally中释放锁。
- 仍存在一个问题,如果在执行释放锁逻辑的时候服务器宕机,释放锁失败,在重启服务器后,加锁的数据又被恢复,就出现死锁的问题。需要再redis中添加锁的过期时间。
- 那么又会出现,在获取锁和设置时间之间,如果存在宕机问题,时间会失败,也就是不满足原子性。通过setnx可以同时满足。(Lua脚本,后续阐述)
- 此时又会出现,在高并发的情况下,如果锁设置30s,线程A获取到锁,但方法执行了35s,线程A的锁已经过期,线程B重新获取锁并开始执行方法执行到5s时,线程A执行finally释放锁,导致线程A将线程B的锁释放。可以设置一个uuid表示当前线程的锁。
(2)redisson
void reduceStock() {RLock lock = redissonClient.getLock("lock");if (lock.tryLock()) {try {int stockNum = stockMapper.selectNum();if (stockNum > 0) {stockMapper.reduceStock();} else {log.info("库存不足");}} finally {lock.unlock();}} else {log.info("未获取锁");}
}
- 使用:通过getLock方法获取到RLock对象,tryLock或lock()来进行加锁(Lua脚本);
- 底层:reids的key是锁的名称,这个key对应的值是一个HASH结构,HASH的key是持有锁的客户端ID+线程ID,value初始化为1表示锁的重入次数,当其他线程去获取锁时,使用redis提的的subcribe特性等待redis的key的变动,方式轮询造成的系统消耗资源。
- 看门狗:当前持有锁的线程在redission客户端初始化一个watch dog线程,定时刷新key的过期时间,默认是30s,监听主线程是否还在执行,如果还在执行通过LUA脚本每10s给锁续期30秒。这个值可以config自定义。
- 【注意】加锁时,如果使用 tryLock(long t1,long t2, TimeUnit unit) 或 lock(long t1,long t2, TimeUnit unit) 第二个参数不是-1,则看门狗无法生效;
LUA脚本为什么能保证原子性呢?
因为redis是单线程的,当redis执行lua脚本时,lua脚本将一系列操作封装成一个命令,redis会把lua作为一个整体任务,加入到一个队列,单线程执行任务会按照队列的顺序依次执行,在执行时lua是不会被其他线程请求打断的,从而保证原子性。
优点:
减少网络开销,一次请求和接受一次响应;直接在redis上执行无需解析和转换;
缺点:
新的语言;需要单独存储和管理,需要维护;占用redis资源和时间;
源码解析
首先找到这个方法的实现
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {long threadId = Thread.currentThread().getId();//【核心方法】,尝试去获取锁,返回null时表示获取成功Long ttl = tryAcquire(-1, leaseTime, unit, threadId);if (ttl == null) {return;}//通过线程id去订阅锁CompletableFuture<RedissonLockEntry> future = subscribe(threadId);pubSub.timeout(future);RedissonLockEntry entry;if (interruptibly) {entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {//锁自旋while (true) {//尝试获取锁,获取成功后,break跳出循环ttl = tryAcquire(-1, leaseTime, unit, threadId);if (ttl == null) {break;}//获取锁成功,并且获取到锁的线程需要等待一段时间ttlif (ttl >= 0) {try {entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}//如果发生中断,根据 interruptibly 参数判断是否重新尝试获取许可entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {//获取锁成功,但获取到锁的线程无需等待,可以直接执行后续操作if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {//取消订阅unsubscribe(entry, threadId);}
// get(lockAsync(leaseTime, unit));}
首先看核心代码:
Lua脚本分析:
刷新时间:
这里为什么要使用ConCurrentHashMap呢?
锁的过期时间刷新任务需要在不同的节点之间共享,确保在一个节点续约锁的时候,其他节点也能够知道锁的状态,让不同的节点能够访问和更新相同的数据结构,保证一致性。其中putIfAbsent
也是原子的,防止并发问题。
看门狗:
(3) RedLock
redission解决了锁续期的问题,但是在redis集群中,主从复制是有延时性的。例如,当线程A获取锁成功后,主节点保存了锁信息,当主节点还未同步到从节点锁信息时,主节点宕机,从节点切换为主节点后,线程的锁就丢失了。
而使用RedLock解决这个问题,就是认为每台redis都是独立的主节点,在加锁时,会记录开始加速的时间,以及加锁成功后的时间。
例如:5台redis服务器
- 客户端获取当前毫秒级的时间戳,设置超时时间ttl=5
- 向5个redis服务发起请求,保证全局唯一的value请求key锁
- 如果存在3个(一半以上)那么就是获取锁成功,否则失败
- 如果失败,或者超过ttl超过5,则向所有的redis服务发出解锁请求
- 获取锁失败后,在 随机时间后重试获取锁,同时重试需要限制次数(随机时间是防止过多的客户端尝试去获取,但孩子有一台能获取到,导致大批出现失败的问题)
【注意】向redis服务建立网络连接时,要设置一个超时时间,避免redis服务宕机,客户端仍在等待,官方建议5-50毫秒之间。
如果所有节点同时宕机,怎么办?参考
延迟重启:reids同步到磁盘方式默认1次/s,在redis崩溃后,等待ttl之后再重启。
ttl时间后全部锁都过期,不会对现有的锁造成影响,但在ttl时间内是宕机状态,影响性能和使用。
(4)基于zookeeper
后续学到,继续更新
相关文章:

一文看分布式锁
为什么会存在分布式锁? 经典场景-扣库存,多人去同时购买一件商品,首先会查询判断是否有剩余,如果有进行购买并扣减库存,没有提示库存不足。假如现在仅存有一件商品,3人同时购买,三个线程同时执…...

Jenkins自动化部署一个Maven项目
Jenkins自动化部署 提示:本教程基于CentOS Linux 7系统下进行 Jenkins的安装 1. 下载安装jdk11 官网下载地址:https://www.oracle.com/cn/java/technologies/javase/jdk11-archive-downloads.html 本文档教程选择的是jdk-11.0.20_linux-x64_bin.tar.g…...

K8S1.23.5部署(此前1.17版本步骤囊括)及问题记录
应版本需求,升级容器版本为1.23.5 kubernetes组件 一个kubernetes集群主要由控制节点(master)与工作节点(node)组成,每个节点上需要安装不同的组件。 master控制节点:负责整个集群的管理。 …...

基于java web的中小型人力资源管理系统
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 目录…...
Python学习笔记--Python关键字yield
原文:http://stackoverflow.com/questions/231767/the-python-yield-keyword-explained 注:这是一篇 stackoverflow 上一个火爆帖子的译文 问题 Python 关键字 yield 的作用是什么?用来干什么的? 比如,我正在试图理解下面的代码: def node._get_child_candidates(self,…...
CF 850 C Arpa and a game with Mojtaba(爆搜优化SG)
CF 850 C. Arpa and a game with Mojtaba(爆搜优化SG) Problem - C - Codeforces Arpa and a game with Mojtaba - 洛谷 思路:显然对于每一种质因子来说操作都是独立的 , 因此可以考虑对于每一种质因子求当前质因子的SG &#…...

kafka分布式安装部署
1.集群规划 2.集群部署 官方下载地址:http://kafka.apache.org/downloads.html (1)上传并解压安装包 [zhangflink9wmwtivvjuibcd2e package]$ tar -zxvf kafka_2.12-3.3.1.tgz -C ../software/(2)修改解压后的文件…...

[云原生2.] Kurbernetes资源管理 ---- (陈述式资源管理方式)
文章目录 1. K8s管理资源的方法类别1.1 陈述式资源管理方式1.2 声明式资源管理方式1.3 GUI式资源管理方法 2. 陈述式资源管理方式2.1 命令行工具 ---- Kubelet2.1.1 简介2.1.2 特性2.1.3 kubelet拓展命令2.1.4 kubectl基本语法2.1.5 Kubectl工具的自动补全 2.2 k8s Service 的类…...

java:IDEA中的Scratches and Consoles
背景 IntelliJ IDEA中的Scratches and Consoles是一种临时的文件编辑环境,用于写一些文本内容或者代码片段。 其中,Scratch files拥有完整的运行和debug功能,这些文件需要指定编程语言类型并且指定后缀。 举例:调接口 可以看到…...

华为 Mate 60 Pro 拆解:陆制零件比率上升至47% | 百能云芯
近日,日经新闻联合研究公司Fomalhaut Techno Solutions对华为 Mate 60 Pro 进行了拆解,揭示了这款于8月发布的新型智能手机的成本结构。拆解结果显示,该手机的国产零部件比例达到了47%,相较于三年前的 Mate 40 Pro,提高…...

ZBrush 2024(三维数字雕刻软件)
ZBrush是一款Mac数字雕刻软件,它具有以下功能: 雕刻工具:ZBrush的雕刻工具非常强大,可以让用户在3D模型上进行雕刻,就像使用传统雕塑工具一样。高精度模型创建:ZBrush可以创建高精度的3D模型,适…...

wpf devexpress 排序、分组、过滤数据
这个教程示范在GridControl如何排序数据,分组数据给一个行创建一个过滤。这个教程基于前一个教程。 排序数据 可以使用GridControl 排序数据。这个例子如下过滤数据对于Order Date 和 Customer Id 行: 1、对于Order Date 和 Customer Id 行指定Colum…...
使用Badboy录制生成 JMeter 脚本
JMeter是一款在国外非常流行和受欢迎的开源性能测试工具,像LoadRunner 一样,它也提供了一个利用本地Proxy Server(代理服务器)来录制生成测试脚本的功能,但是这个功能并不好用。所以在本文中介绍一个更为常用的方法——…...

V10 桌面版、服务器版系统加固
V10 桌面版、服务器版系统加固 一、 文档说明 本文档中涉及的加固方法主要包括:密码策略配置、防火墙规 则配置、禁用高风险服务等。 二、 V10 桌面版系统加固 2.1 密码策略配置 密码策略包括密码老化控制策略和密码复杂度策略。密码老化 控制策略需要配置/etc…...

mtgsig1.2简单分析
{"a1": "1.2", # 加密版本"a2": new Date().valueOf() - serverTimeDiff, # 加密过程中用到的时间戳. 这次服主变坏了, 时间戳需要减去一个 serverTimeDiff(见a3) ! "a3": "这是把xxx信息加密后提交给服务器, 服主…...

场景交互与场景漫游-osgGA库(5)
osgGA库 osgGA库是OSG的一个附加的工具库,它为用户提供各种事件处理及操作处理。通过osgGA库读者可以像控制Windows窗口一样来处理各种事件 osgGA的事件处理器主要由两大部分组成,即事件适配器和动作适配器。osgGA:GUIEventHandler类主要提供了窗口系统的…...
Leetcode -1
Leetcode Leetcode -521.最长特殊序列Leetcode - 541.反转字符串Ⅱ Leetcode -521.最长特殊序列 题目:给你两个字符串 a 和 b,请返回 这两个字符串中 最长的特殊序列的长度。如果不存在,则返回 - 1 。 「最长特殊序列」 定义如下࿱…...

系列四、JVM的内存结构【本地接口(Native Interface)】
一、组成 本地接口由本地方法栈(Native Method Stack)、本地方法接口(Native Interface)、本地方法库组成。 二、本地接口的作用 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C程序,Jav…...

大型语言模型中的幻觉研究综述:原理、分类、挑战和未决问题11.15+11.16+11.17
大型语言模型中的幻觉研究综述:原理、分类、挑战和未决问题11.15 摘要1 引言2 定义2.1 LLM2.3 大语言模型中的幻觉 3 幻觉的原因3.1 数据的幻觉3.1.1 有缺陷的数据源3.1.2 较差的数据利用率3.1.3 摘要 3.2 来自训练的幻觉3.2.1训练前的幻觉3.2.2来自对齐的幻觉3.2.3…...
redis悲观锁和乐观锁
redis悲观锁 Redis加锁命令分有INCR、SETNX、SET 一、INCR锁 key不存在时,key的值会先被初始化为0,其它用户在执行INCR操作进行加一, 如果返回的数大于1,说明这个锁正在被使用当中,通常用在同时只能有一个人可以操作某…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...

相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

多模态大语言模型arxiv论文略读(108)
CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题:CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者:Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...

FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...

基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...

android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...