分布式锁总结

2023-05-16

乐观锁
在select的时候不会加锁,是基于程序实现的,所以不会存在死锁的情况。
适用于读多写少的场景(写的并发量相对不高),可以提高系统的吞吐量。
因为如果写多的话,乐观锁会有很大机率更新失败,需要不断的自旋执行查找和更新操作。
自旋的时候会一直占用CPU,会耗费大量的CPU资源。

悲观锁
在select的时候就会加锁,采用先加锁后处理的模式,虽然保证了数据处理的安全性,但也会阻塞其他线程的写操作。
悲观锁适用于写多读少的场景,因为拿不到锁的线程,会将线程挂起,交出CPU资源,可以把CPU给其他线程使用,提高了CPU的利用率。

锁分类:
悲观锁:具有强烈的独占和排他特性,在整个数据处理过程中,将数据处于锁定状态。适合于写比较多,会阻塞读操作。
乐观锁:采取了更加宽松的加锁机制,大多是基于数据版本( Version )及时间戳来实现。。适合于读比较多,不会阻塞读

独占锁、互斥锁、排他锁:保证在任一时刻,只能被一个线程独占排他持有。synchronized、ReentrantLock
共享锁:可同时被多个线程共享持有。CountDownLatch到计数器、Semaphore信号量

可重入锁:又名递归锁。同一个线程在外层方法获取锁的时候,在进入内层方法时会自动获取锁。
不可重入锁:

公平锁:有优先级的锁,先来先得,谁先申请锁就先获取到锁
非公平锁:无优先级的锁,后来者也有机会先获取到锁

自旋锁:当线程尝试获取锁失败时(锁已经被其它线程占用了),无限循环重试尝试获取锁
阻塞锁:当线程尝试获取锁失败时,线程进入阻塞状态,直到接收信号后被唤醒。在竞争激烈情况下,性能较高

读锁:共享锁
写锁:独占排他锁

偏向锁:一直被一个线程所访问,那么该线程会自动获取锁
轻量级锁(CAS):当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,
	其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候(10次),
	还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
以上其实是synchronized的锁升级过程
	

表级锁:对整张表加锁,加锁快开销小,不会出现死锁,但并发度低,会增加锁冲突的概率
行级锁:是mysql粒度最小的锁,只针对操作行,可大大减少锁冲突概率,并发度高,但加锁慢,开销大,会出现死锁

具体锁实现:
jvm:
ReentrantLock悲观的独占的可重入的可公平可不公平锁
synchronized悲观的独占的可重入的非公平锁
无锁 --> 偏向锁(同一个线程再次获取锁) --> 轻量级锁(自旋) --> 重量级锁

mysql:
	select ... for update:悲观的独占的
	select ... lock in share mode

jvm:ReentrantLock + synchronized
1.单个jvm实例 单机
2.必须单例
3.与事务并存问题
总之,不适合于保证数据库数据可靠性

mysql:
1.直接更新时判断。在更新中判断库存是否大于0 update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0 ;
解决jvm锁多例模式锁失效问题 及 事务共存问题
锁范围控制:条件字段必须创建索引;查询条件必须具体的值
同一个商品有多个库存时,无法解决。
无法记录库存变化前后的状态
2.悲观锁:select … for update
库存操作要统一:不能有的操作是select … for update 而有的操作是普通的select
死锁风险:多条记录时,加锁顺序要一致
阻塞及性能问题
3.乐观锁:version 或者 时间戳(CAS思想)
ABA问题
失败需要重试,高并发情况下性能不高
读写分离情况下导致乐观锁不可靠

zookeeper
客户端:ZooKeeper原生客户端、ZkClient、Curator
前两个客户端参照:https://blog.csdn.net/qq_42349306/article/details/118209298

读操作和设置监听事件之间是有原子性的

阻塞公平锁:
	1.接收到请求时,在/locks节点下创建一个临时序列化节点
	2.判断自己是不是/locks节点下最下的节点:是则获取到锁,不是则监听前一个节点
	3.获取到锁,处理完业务逻辑后,通过delete删除当前节点释放锁。监听当前节点的下一个节点收到通知,重复第二步。

Curator分布式锁源码解读:https://blog.csdn.net/qq_41432730/article/details/123389670

分布式锁的场景
缓存击穿
库存超卖
防重复提交

分布式锁的实现

如果没有添加到暂存区:
git checkout – 文件名
git checkout .
如果已经添加到暂存区,要先执行:
git rm --cached 文件名
或者:git reset HEAD file

git reset --hard HEAD

ReentrantLock底层原理:
AQS --> Sync --> NonfairAsyc
–> FairAsync
加锁:lock --> nonfairAsyc.lock() --> AQS.acquire(1) --> NonfairAsyc.tryAcquire(1) --> Sync.nonfairTryAcquire(1)
1.CAS尝试获取锁:如果state的值为0,则获取锁成功,并记录排他有锁线程是当前线程(再次尝试获取锁)
2.判断当前线程是否是排他有锁线程,如果是则state的值加1
3.获取锁失败,则入队

解锁:
	1.判断当前线程是否是排他有锁线程,不是则报错
	2.对state的值减1,判断减1后的值是否为0,为0则释放锁
	3.不为0则释放锁结束

总结

判断是否自己的锁,如果是自己的锁,执行删除操作。
if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end

if redis.call(‘get’, KEYS[1]) == ARGV[1]
then
return redis.call(‘del’, KEYS[1])
else
return 0
end

key: lock
arg: uuid

可重入锁加锁流程:ReentrantLock.lock() --> NonfairSync.lock() --> AQS.acquire(1) --> NonfairSync.tryAcquire(1) --> Sync.nonfairTryAcquire(1)
1.CAS获取锁,如果没有线程占用锁(state==0),加锁成功并记录当前线程是有锁线程(两次)
2.如果state的值不为0,说明锁已经被占用。则判断当前线程是否是有锁线程,如果是则重入(state + 1)
3.否则加锁失败,入队等待

可重入锁解锁流程:ReentrantLock.unlock() --> AQS.release(1) --> Sync.tryRelease(1)
1.判断当前线程是否是有锁线程,不是则抛出异常
2.对state的值减1之后,判断state的值是否为0,为0则解锁成功,返回true
3.如果减1后的值不为0,则返回false

参照ReentrantLock中的非公平可重入锁实现分布式可重入锁:hash + lua脚本
加锁:
1.判断锁是否存在(exists),则直接获取锁 hset key field value
2.如果锁存在则判断是否自己的锁(hexists),如果是自己的锁则重入:hincrby key field increment
3.否则重试:递归 循环

	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
	
	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
	
	key: lock
	arg: uuid 30

解锁:
	1.判断自己的锁是否存在(hexists),不存在则返回nil
	2.如果自己的锁存在,则减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del)并返回1 
	3.不为0,返回0
	
	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

	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

	key: lock
	arg: uuid

自动续期:定时任务(时间驱动 Timer定时器) + lua脚本
判断自己的锁是否存在(hexists),如果存在则重置过期时间
if redis.call(‘hexists’, KEYS[1], ARGV[1]) == 1 then return redis.call(‘expire’, KEYS[1], ARGV[2]) else return 0 end

if redis.call('hexists', KEYS[1], ARGV[1]) == 1
then
	return redis.call('expire', KEYS[1], ARGV[2])
else 
	return 0
end

key: lock
arg: uuid 30

基于redis实现分布式锁:
特征:
1.独占排他:setnx
2.防死锁:
redis客户端程序获取到锁之后,立马宕机。给锁添加过期时间
不可重入:可重入
3.防误删:
先判断是否自己的锁才能删除
4.原子性:
加锁和过期时间之间
判断和释放锁之间
5.可重入性:hash + lua脚本
6.自动续期:Timer定时器 + lua脚本

锁操作:
1.加锁:
1.setnx:独占排他 死锁、不可重入、原子性
2.set k v ex 30 nx:独占排他、死锁 不可重入
3.hash + lua脚本:可重入锁
1.判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby)并设置过期时间(expire)
2.如果锁被占用,则判断是否当前线程占用的,如果是则重入(hincrby)并重置过期时间(expire)
3.否则获取锁失败,将来代码中重试
4.Timer定时器 + lua脚本:实现锁的自动续期

2.解锁 
	1.del:导致误删
	2.先判断再删除同时保证原子性:lua脚本
	3.hash + lua脚本:可重入 
		1.判断当前线程的锁是否存在,不存在则返回nil,将来抛出异常
		2.存在则直接减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1
		3.不为0,则返回0

3.重试:递归 循环 

redisson:redis的java客户端,分布式锁
玩法:
1.引入依赖
2.java配置类:RedissonConfig
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress(“redis://ip:port”);
return Redisson.create(config);
}
3.代码使用:
可重入锁RLock对象:CompletableFuture + lua脚本 + hash
RLock lock = this.redissonClient.getLock(“xxx”);
lock.lock()/unlock()

		公平锁:
			RLock lock = this.redissonClient.getFairLock("xxx");
			lock.lock()/unlock()
			
		联锁 和 红锁:
		
		读写锁:
			RReadWriteLock rwLock = this.redissonClient.getReadWriteLock("xxx");
			rwLock.readLock().lock()/unlock();
			rwLock.writeLock().lock()/unlock();
			
		信号量:
			RSemaphore semaphore = this.redissonClient.getSemaphore("xxx");
			semaphore.trySetPermits(3);
			semaphore.acquire()/release();
			
		闭锁:
			RCountDownLatch cdl = this.redissonClient.getCountDownLatch("xxx");
			cdl.trySetCount(6);
			cdl.await()/countDowntch();

zookeeper分布式锁:
1.介绍了zk
2.zk下载及安装
3.指令:
ls /
get /zookeeper
create /aa “test”
delete /aa
set /aa “test1”
4.znode节点类型:
永久节点:create /path content
临时节点:create -e /path content 。只要客户端程序断开链接自动删除
永久序列化节点:create -s /path content
临时序列化节点:create -s -e /path content
5.节点的事件监听:一次性
1.节点创建:NodeCreated
stat -w /xx
2.节点删除:NodeDeleted
stat -w /xx
3.节点数据变化:NodeDataChanged
get -w /xx
4.子节点变化:NodeChildrenChanged
ls -w /xx
6.java客户端:官方提供 ZkClient Curator

7.分布式锁:
	1.独占排他:znode节点不可重复 自旋锁
	2.阻塞锁:临时序列化节点 
		1.所有请求要求获取锁时,给每一个请求创建临时序列化节点
		2.获取当前节点的前置节点,如果前置节点为空,则获取锁成功,否则监听前置节点
		3.获取锁成功之后执行业务操作,然后释放当前节点的锁
	3.可重入:同一线程已经获取过该锁的情况下,可重入
		1.在节点的内容中记录服务器、线程以及重入信息
		2.ThreadLocal:线程的局部变量,线程私有
	4.公平锁:有序列
	
	1.独占排他互斥使用 节点不重复
	2.防死锁: 
		客户端程序获取到锁之后服务器立马宕机。临时节点:一旦客户端服务器宕机,链接就会关闭,此时zk心跳检测不到客户端程序,删除对应的临时节点。
		不可重入:可重入锁 
	3.防误删:给每一个请求线程创建一个唯一的序列化节点。
	4.原子性:
		创建节点 删除节点 查询及监听 具备原子性
	5.可重入:ThreadLocal实现 节点数据 ConcurrentHashMap
	6.自动续期:没有过期时间 也就不需要自动续期
	7.单点故障:zk一般都是集群部署
	8.zk集群:偏向于一致性集群
		
8.Curator:Netflix贡献给Apache
	Curator-framework:zk的底层做了一些封装。
	Curator-recipes:典型应用场景做了一些封装,分布式锁
	
	1.InterProcessMutex:类似于ReentrantLock可重入锁 分布式版本
		public InterProcessMutex(CuratorFramework client, String path)
		public void acquire()
		public void release()
	
		InterProcessMutex
			basePath:初始化锁时指定的节点路径
			internals:LockInternals对象,加锁 解锁
			ConcurrentMap<Thread, LockData> threadData:记录了重入信息
			class LockData {
				Thread lockPath lockCount
			}
			
		
		LockInternals
			maxLeases:租约,值为1
			basePath:初始化锁时指定的节点路径
			path:basePath + "/lock-"
			
		加锁:InterProcessMutex.acquire() --> InterProcessMutex.internalLock() --> 	
				LockInternals.attemptLock()
			
	2.InterProcessSemaphoreMutex:不可重入锁
		
	3.InterProcessReadWriteMutex:可重入的读写锁
		读读可以并发的
		读写不可以并发
		写写不可以并发
		写锁在释放之前会阻塞请求线程,而读锁是不会的。
		
	4.InterProcessMultiLock:联锁  redisson中的联锁对象
	
	5.InterProcessSemaphoreV2:信号量,限流
	
	
	6.共享计数器:CountDownLatch
		ShareCount
		DistributedAtomicNumber:
			DistributedAtomicLong
			DistributedAtomicInteger

基于MySQL关系型数据库实现:唯一键索引
redis:基于Key唯一性
zk:基于znode节点唯一性

思路:
	1.加锁:INSERT INTO tb_lock(lock_name) values ('lock') 执行成功代表获取锁成功
	2.释放锁:获取锁成功的请求执行业务操作,执行完成之后通过delete删除对应记录
	3.重试:递归

1.独占排他互斥使用  唯一键索引 
2.防死锁: 
	客户端程序获取到锁之后,客户端程序的服务器宕机。给锁记录添加一个获取锁时间列。 
		额外的定时器检查获取锁的系统时间和当前系统时间的差值是否超过了阈值。
	不可重入:可重入 记录服务信息 及 线程信息 重入次数
3.防误删: 借助于id的唯一性防止误删
4.原子性:一个写操作   还可以借助于mysql悲观锁
5.可重入:
6.自动续期:服务器内的定时器重置获取锁的系统时间
7.单机故障,搭建mysql主备
8.集群情况下锁机制失效问题。

9.阻塞锁:

总结:
1.简易程序:mysql > redis(lua脚本) > zk
2.性能:redis > zk > mysql
3.可靠性:zk > redis = mysql
追求极致性能:redis
追求可靠性:zk
简单玩一下,实现独占排他,对性能 对可靠性要求都不高的情况下,选择mysql分布式锁。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

分布式锁总结 的相关文章

  • VirtualBox中如何释放鼠标

    相信在使用VritualBox的小伙伴中 xff0c 有发现他释放鼠标的方式和VMware不一样 xff0c 其实也很简单 xff0c 并且VritualBox也有说明释放的方式 按键盘右边的Ctrl键就可以
  • c++中sizeof()的详细用法

    sizeof 定义 sizeof是一个操作符 xff08 operator xff09 其作用是返回一个对象或类型所占的内存字节数 sizeof 语法 sizeof有三种语法形式 xff1a 1 sizeof object sizeof 对
  • Linux下如何查找特定的文件

    使用 find命令 语法 xff1a find path options params 作用 xff1a 在指定目录下查找文件 常用用法示例 xff1a find name file java xff1a 精确查找文件 xff08 在根目录
  • Ubuntu16.04使用教程(一)——安装

    Ubuntu16 04使用教程 xff08 一 xff09 安装 前言 最近我同学需要用Ubuntu16 04做项目 xff0c 可是她之前从没用过 xff0c 于是我在这里写一些我常用的简单的操作吧 本节将教你如何在你的Windows电脑
  • Linux环境下QT开发(三)——QT编程基础

    Linux环境下QT开发 xff08 三 xff09 QT编程基础 在上一篇文章中 xff0c 我们完成了第一个程序 HelloWorld 在这篇文章中 xff0c 我将介绍一些QT编程基础知识 xff0c 让我们由此进入QT的世界 xff
  • Kali学习笔记(三)——Kali2020.1安装从入门到入土

    Kali学习笔记 xff08 三 xff09 Kali2020 1安装从入门到入土 我的笔记本是神舟战神Z7M KP7GT xff0c 配置如下 xff1a 项目配置CPU酷睿i7内存8GB显卡1050Ti固态硬盘128GB机械硬盘1TB
  • VP9编码(1)-- 简介

    VP9编码 xff08 1 xff09 简介 VP9是谷歌基于VP8发布的新版视频编解码标准 xff0c 主要对标H265 H264标准 youtube目前采用VP9标准 本系列博客为博主学习笔记 xff0c 翻译自 vp9 bitstre
  • c++学习之路

    3 19 内存分区模型 内存四区意义 不同区域存放的数据 xff0c 赋予不同的生命周期 给我们更大的灵活编程 程序exe 运行前分为 代码区和全局区 xff1b 运行后分为 栈区和堆区 1 代码区 存放CPU执行的机器指令 存放函数体的二
  • RTSP协议(2)——摘要(RFC2326)

    RTSP协议 xff08 2 xff09 摘要 实时流协议 xff08 RTSP xff09 是一种应用级协议 xff0c 用于控制实时数据的传输 RTSP提供了一个可扩展的框架 xff0c 以实现实时数据 xff08 如音频和视频 xff
  • 简道云-第3章-表单

    title 简道云 第3章 表单 date 2022 06 11 09 07 10 tags 简道云 categories 简道云 简道云 第3章 表单 背景介绍 简道云三个基本项目表单 流程以及仪表 关于它们的介绍可以参照官方文档表单 v
  • 搭建Hexo博客-第2章-Hexo基本用法

    搭建Hexo博客 第2章 Hexo基本用法 搭建Hexo博客 第2章 Hexo基本用法 搭建Hexo博客 第2章 Hexo基本用法 大家好 xff0c 在上一篇文章中 xff0c 我们学习了 Git GitHub 和 Coding 的基本用
  • 群晖-第5章-Docker安装gitlab

    群晖 第5章 Docker安装gitlab 群晖安装gitlab可以只在内网访问 xff0c 也就不需要前面几章的内容 xff0c 但是我这个是通过外网HTTPS访问的 参考 xff1a Nas码农篇 xff1a 群晖Docker安装Git
  • 小应用记账本-第1章-需求分析

    小应用记账本 第1章 需求分析 这个专题是开发一个小应用 记账本的记录 一 项目背景 我从21年开始用挖财记账 xff0c 挖财记账最初可以免费导出账本的 xff0c 但是后来变成了会员收费功能 xff0c 到23年年初 xff0c 有一次
  • 小应用记账本-第2章-数据库设计

    小应用记账本 第2章 数据库设计 在上一章 小应用记账本 第1章 需求分析 已经罗列了我们需要的功能 xff0c 因为很简单 xff0c 所以这一章就来设计数据库吧 Account表 xff1a 账户表 字段名类型说明取值idint账户id
  • Ae项目中,GPU加速不能使用了。视频预览显示应不少于2个帧或多帧,以及VR效果不能使用GPU加速!

    首先 xff0c 如果百度上的其他方法没有帮助到你 我这里还有另外一种方法 点击文件菜单下的项目设置 xff0c 看是否如图所示 如果可以的话 xff0c 点个关注支持一下 xff1a https space bilibili com 73
  • CCF A类会议或期刊----近两年对比学习相关论文

    工具代码位置 期待您的PR 会议 期刊论文sigmod2021Explaining Black Box Algorithms Using Probabilistic Contrastive Counterfactuals neurips20
  • 您还在为您公司的网络限制摸不了鱼在苦恼难过吗?难受的话请您看完:解决公司内网限制 Nginx反向代理访问外网

    1 nginx安装 xff08 在这里就不赘述了 xff09 2 修改nginx conf xff08 直接复制可用 xff09 worker processes 4 error log var logs nginx log info 日志
  • 切换系统默认语言后,Eclipse代码自动补全快捷键失效。

    今天心血来潮 xff0c 把Win10的默认语言切换成了英文 xff0c 想锻炼下英语 然后在打开Eclipse敲代码的时候尴尬了 xff0c 平时输入syso后按alt 43 能自动补全 xff0c 现在怎么不管用了 xff01 之后找到
  • 虚拟机(vmware)ubuntu16.04的扩容

    2018 11 13 版本 xff1a VMware Workstation 14 Pro ubuntu 16 04 有两种扩容方法 xff1a 1 创建一块新的虚拟硬盘 xff1b 2 直接扩展根分区的大小 这里呢 xff0c 我们只考虑
  • github如何绑定自己的域名(无须备案)

    现在常用的在阿里云购买的域名或者在腾讯云购买的域名 下面以腾讯云购买的域名为列子详细介绍一下如何用github绑定自己域名 本文前提是你已经搭建好了博客 xff0c 已经可以通过类似 xff1a https nyy 2017 github

随机推荐