分布式锁的实现方式

news2024/11/22 16:22:47

文章目录

  • 一、分布式锁概述
    • 1.1 为什么需要分布式锁
    • 1.2 概述分布式锁
    • 1.3 分布式锁的特性
    • 1.4 分布式锁的类型
    • 1.5 实现重点
  • 二、Mysql数据库实现分布式锁
    • 2.1 表结构
    • 2.2 加锁
    • 2.3 解锁
    • 2.4 锁超时
    • 2.5 实现重入锁
  • 三、Redis实现分布式锁
    • 3.1 加锁
    • 3.2 解锁
    • 3.3 锁超时
    • 3.4 redlock的容错性问题
  • 四、总结

一、分布式锁概述

1.1 为什么需要分布式锁

通常我们说的锁,指的是单机单进程中多线程环境下,当多个线程同时访问共享资源时,会引发并发访问的问题,可能导致数据不一致或竞态条件。通过使用锁,可以确保一次只有一个线程能够访问共享资源,从而避免这些问题。

通常使用的锁有:
1)互斥锁
2)自旋锁
3)信号量
4)读写锁
5)原子变量以及内存屏障
6)条件变量

那么,如果是多个进程相互竞争一个资源,如何保证资源只会被一个操作者持有呢?

在分布式系统中,一个应用部署在多台机器当中,在某些场景下,为了保证数据一致性,要求在同一时刻,同一任务只在一个节点上运行,即保证某个行为在同一时刻只能被一个线程执行;在单机单进程多线程环境,通过锁很容易做到,比如mutex、spinlock、信号量等;而在多机多进程环境中,此时就需要分布式锁来解决了。可以理解为在分布式场景中实现互斥类型的锁。
在这里插入图片描述
S1~S4是分布在不同机器或不同网段的节点。每个节点在执行某项操作前需要先获取锁,只有成功拿到锁才能执行后续的操作。

1.2 概述分布式锁

1)分布式锁是什么类型的锁?
分布式锁是在分布式场景中实现互斥类型的锁。
分布式:运行的节点可能在不同的机器或者网段当中,节点间通过socket进行通信
互斥类型:同一时刻只允许一个执行体进入临界资源

2)分布式锁解决了什么问题?
解决分布式事务中的隔离性问题。在分布式场景中,同时只允许一个节点执行某类任务。

3)分布式锁的存储
分布式锁本身也是一种资源,在分布式场景中,通过网络交互的方式,不同机器的实体都可以访问锁资源。可以将锁资源保存在MySQL后者Redis中。

4)分布式锁的行为
分布式锁的行为分别加锁和解锁。加锁和解锁本质上是一次网络交互行为,某个实例加锁成功,其他实例便加锁失败。并且,加锁和解锁的对象必须是同一个,除了因为网络异常而造成的锁超时情况。

1.3 分布式锁的特性

1)互斥性
同时只允许一个持锁对象进入临界资源;其他待持锁对象要么等待,要么轮询检测是否能获取锁。需要记录持有锁对象(加锁对象和解锁对象必须为同一个)方便判定锁被谁占有了。加锁的时候需要打上该对象的标记,解锁的时候取消标记。

2)锁超时
允许持锁对象持锁最长时间;如果持锁对象宕机,需要外力解除锁定,方便其他持锁对象获取锁。
在单进程的多线程场景下,资源和行为是同生共死的关系,程序宕机会自动释放所有资源和行为。而在分布式场景中有比较大的差别,锁资源和行为是分离的,通过网络交互操作锁,要考虑到锁资源宕机和行为实体宕机的情况如何释放资源和解除行为。
比如行为实体宕机了,如何释放锁?如果不能释放锁则其他的实体将一直等待;所以,需要锁超时机制,设置操作时长的最大值,超时释放锁。再比如锁资源宕机的情况。

3)可用性
锁存储位置若宕机,可能引发整个系统不可用。因此需要有备份存储位置和切换备份存储的机制,从而确保服务可用;
实现上有两种方式:
a)计算型:不存储数据只完成行为,比如网关只计算分发请求,不存储数据。此类实现方式是开多个备份点。
b)存储型:存储数据或者资源。此类实现方式是准备多个备份点,存储超过半数以上的备份节点;并且具有切换功能,以解决宕机问题。

4)容错性
若锁存储位置宕机,恰好锁丢失的话,是否能正确处理。
具体来说,就是一个实体行为申请了锁,此时锁资源宕机,切换到了备份资源。此时需要备份资源具有一致性,即确保数据在系统中的不同副本之间保持同步和一致的状态。
实现方式是通过一致性算法,比如raft一致性算法。
比如redlock的实现:开奇数个进程,写锁的时候,写入进程半数以上成功的返回获取锁成功,否则失败。

1.4 分布式锁的类型

1)重入锁和非重入锁
两者的区别是:是否允许持锁对象再次获取锁
对于一个进程多线程环境下,就是递归锁和非递归锁

2)公平锁和非公平锁
公平锁:通过排队来实现,对应的是互斥锁
非公平锁:不间断尝试获取锁来实现,对应的是自旋锁

对于互斥锁,当一个线程尝试获取一个已经被其他线程占有的互斥锁时,该线程会在用户态自旋等待一小段时间。当超过一定时间阈值后,会转入内核态,进入阻塞状态。该线程会被放入阻塞队列中,并引起线程切换。当互斥锁的持有者释放锁时,操作系统会唤醒等待中的线程,从阻塞队列中取出一个线程加入就绪队列,等待CPU调度。

对于自旋锁,当一个线程尝试获取一个已经被其他线程占有的自旋锁时。如果是单处理器,该线程会在一个循环中忙等待,空转CPU直到锁被释放。如果是多处理器系统中,该线程会在循环中忙等待一定时间,如果锁还不可用,则会调用sched_yield(),引起线程切换。自旋等待的线程会切换至非运行状态,并被放入就绪队列,等待CPU调度。

在就绪队列中的好处是当有CPU核心空闲时会从就绪队列中取出任务执行;可以注意到互斥锁是先加入阻塞队列再进入就绪队列。

1.5 实现重点

1)锁是一种资源,需要存储。同时要保证可用性,避免锁失效。
2)互斥语义:给锁打标记。加锁对象和解锁对象必须为同一个,解锁时需要判断当前持有锁的对象是否是自己。
3)加锁和解锁行为是网络通信,需要考虑锁超时(超时时间要远大于一次网络交互的时间)。
4)如何获知持锁对象的释放锁的行为:a)主动探寻(非公平锁);b)被动通知,有广播通知(非公平锁),有排队单独通知(公平锁)。
5)是否支持同一持锁对象多次加锁,即重入锁与非重入锁。

二、Mysql数据库实现分布式锁

MySQL是关系型数据库,通过表存储数据。不同业务类型的锁放置表中不同的行。
主要利用 MySQL 唯一键的唯一性约束来实现互斥性;

2.1 表结构

DROP TABLE IF EXISTS `dislock`;
CREATE TABLE `dislock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `lock_type` varchar(64) NOT NULL COMMENT '锁类型',
  `owner_id` varchar(255) NOT NULL COMMENT '持锁对象',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_lock_type` (`lock_type`) 
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT
CHARSET=utf8 COMMENT='分布式锁表';
  • id,不同类型的锁 主键id不断自增。
  • lock_type,锁的类型用来描述不同业务类型的锁。实现互斥。
  • owner_id,持锁对象,允许谁来解锁,其他对象不能解锁;另一个作用是避免重复加锁。
  • update_time,具体操作锁的时间,主要用于解决锁超时问题。
  • 唯一索引是指一列中不存在重复字段的行,即字段唯一。
  • 主键是非空唯一索引。

根据唯一索引的约束实现互斥,即lock_type在一个表中不会出现两个相同的lock_type。唯一键是确保字段在表中是唯一的。

在这里插入图片描述

2.2 加锁

假设S1加锁成功,也就是往表dislock中成功插入一条记录。其中:act_lock是具体的锁,ad2daf3是S1的id。

INSERT INTO dislock (`lock_type`, `owner_id`) VALUES ('act_lock', 'ad2daf3');

2.3 解锁

假设S1解锁,也就是从表中删除对应的记录。要表明解什么锁lock_type,谁来解owner_id,如果id不一致解锁失败。

DELETE FROM dislock WHERE `lock_type` = 'act_lock' AND `owner_id` = 'ad2daf3';

Mysql在解锁之后没有主动通知的功能。其他申请锁的实例只能在获取锁失败之后,休眠一会再主动轮询,看是否能加锁。

// 连续分布式锁的使用案例
while (1) {
    CLock my_lock;
    bool flag = dlm->ContinueLock("foo", 14000, my_lock);
    if (flag) {
        printf("获取成功, Acquired by client name:%s, res:%s, vttl:%d\n",
               my_lock.m_val, my_lock.m_resource, my_lock.m_validityTime);
        // do resource job
        sleep(10);
    } else {
        printf("获取失败, lock not acquired, name:%s\n", my_lock.m_val);
        sleep(rand() % 3);
    }
}

2.4 锁超时

锁超时是在MySQL中有超进程,利用定时器实现定时检测表,用当前时间减去update_time,如果超过最大持锁时间,就主动删除这条记录(释放锁)。

2.5 实现重入锁

在表结构中加一个count字段,加锁count加一,解锁count减一;当count等于0时删除数据(解锁)。

优点:
1)简单易用:使用MySQL作为分布式锁的实现方式相对简单,无需引入额外的组件或服务。

2)数据持久性:MySQL作为关系型数据库,具备数据持久性的特点,分布式锁的状态可以被持久化存储,即使系统重启也能保持锁的状态。

3)可靠性:MySQL提供了ACID事务特性,可以确保分布式锁的可靠性和一致性。

缺点:
1)效率不高,需要另起一个线程检测锁超时;并且MySQL是一种关系型数据库,对于高并发的场景可能存在性能瓶颈,因为每次获取或释放锁都需要与数据库进行交互。
2)锁释放不能主动通知,只能通过主动探寻解决;
3)还需额外实现锁失效的问题,解锁失败,其他线程将无法获得锁;
4)单点故障:如果使用单个MySQL实例作为分布式锁的中心节点,当该节点发生故障时,整个分布式锁系统将失效。

三、Redis实现分布式锁

redis是
1)内存数据库:Redis主要将数据存储在内存中,因此它可以被称为内存数据库。相比传统的磁盘存储数据库,Redis在读取和写入数据时具有更快的速度和更低的延迟。

2)数据结构数据库:Redis不仅仅是简单的键值对存储,它还提供了多种数据结构的支持,例如字符串、哈希表、列表、集合和有序集合等。这使得开发人员可以根据实际需求选择合适的数据结构,并在应用程序中使用这些数据结构来实现更复杂的功能。

3)键值数据库:Redis以键值对的形式存储数据。每个键都是唯一的,并且与一个值关联。这使得Redis非常适合用作分布式缓存、会话存储和数据存储等场景,其中快速查找和存储数据是关键。

3.1 加锁

redis的set命令中,可以把key存放锁的类型,value存放持锁对象;为了实现锁的互斥性,使用NX参数(NX就是not exist的缩写)。

zxm@ubuntu:~$ redis-cli
// key:act_lock   uuid:111  NX 表示只有当key不存在时,该命令执行成功,否则失败
127.0.0.1:6379> set act_lock 111 NX 
OK
// 因为key:act_lock 已经存在,所以加锁失败
127.0.0.1:6379> set act_lock 222 NX
(nil)
// 解锁act_lock
127.0.0.1:6379> del act_lock
(integer) 1
// 因为key:act_lock 已经解除,所以加锁成功
127.0.0.1:6379> set act_lock 222 NX
OK

真正加锁操作一般不会使用上面的,因为需要考虑重入锁。可以考虑使用hash的数据结构。

--[[
  KEYS[1]     lock_name
  KEYS[2]     lock_channel_name
  ARGV[1]     lock_time (ms)
  ARGV[2]     uuid
]]
if redis.call('exists', KEYS[1]) == 0 then
  redis.call('hset', KEYS[1], ARGV[2], 1)
  redis.call('pexpire', KEYS[1], ARGV[1])
  return
end
-- 若支持锁重入,将注释去掉
-- if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
--   redis.call('hincrby', KEYS[1], ARGV[2], 1)
--   redis.call('pexpire', KEYS[1], ARGV[1])
--   return
-- end
redis.call("subscribe", KEYS[2])
return redis.call('pttl', KEYS[1])

这段代码是一个 Redis Lua 脚本片段,根据给定的键(KEYS)和参数(ARGV),检查键是否存在,如果不存在则设置字段的初始值为 1,并为键设置过期时间。
这段代码的功能如下:
1)使用 Redis 的 exists 命令检查键 KEYS[1] 是否存在。如果结果为 0,表示键不存在。
2)如果键不存在,则使用 Redis 的 hset 命令将键 KEYS[1] 中的字段 ARGV[2] 的值设置为 1。
3)接着,使用 Redis 的 pexpire 命令为键 KEYS[1] 设置过期时间,过期时间由参数 ARGV[1] 指定。pexpire 命令以毫秒为单位设置过期时间。
4)最后,代码执行结束,函数返回。

3.2 解锁

先用get命令获取持锁对象的value,与自己对比。如果相等才调用del解锁。

get act_lock
if (val == uuid)
{
	/ 解锁
	del act_lock ;
}

注意上述三个操作是应当是原子操作

--[[
  KEYS[1]     lock_name
KEYS[2] uuid
]]
local uuid = redis.call("get", KEYS[1])
if uuid == KEYS[2] then
  redis.call("del", KEYS[1])
end

3.3 锁超时

redis的set命令中有EX和PX参数,设置超时时间。
EX就是Expire的缩写,这个以秒为单位。
PX是pExpire,这个是以毫秒为单位。

set act_lock 123 NX EX 10
# 或者
set act_lock 123 NX PX 10000

可以使用ttl命令查看剩余时间。

ttl act_lock

锁超时设定一定要远远大于网络交互时间。

3.4 redlock的容错性问题

一般redis有5个备份结点(进程),分别在不同的机器当中R1 - R5。比如S1申请锁资源,主要R1 - R5中,半数以上的节点回复确认,那么就获取锁成功。这样可以保证数据的一致性。
在这里插入图片描述

redis集群主从复制采用的是异步复制的方式,存在主从数据不一致问题,数据丢失意味着锁丢失;为了解决这个问题使用redlock方法,执行加锁时向所有的节点都执行相同语句,有半数以上(N/2 + 1)写成功就表示加锁成功。
(1)加锁,需要对每个进程执行加锁操作,超过半数以上成功才能说明加锁成功。
(2)解锁,需要对每个进程执行解锁操作,超过半数以上成功才能说明解锁成功。

四、总结

redis是效率最高的分布式锁;etcd是完备性最高的分布式锁;MySQl是效率最低的、最不完备的分布式锁。

三者比较:
1)完备性:etcd > redis >mysql
2)性能: redis > etcd > mysql
3)选择:三者都有选etcd。只有MySQL,那么就使用MySQL实现分布式锁。不要为了实现分布式锁引入redis等中间件

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

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

相关文章

设计模式-简单工厂模式

文章目录 简单工厂设计模式什么是简单工厂?为什么使用简单工厂工厂模式代码实现简单工厂优缺点优点: 简单工厂设计模式 学习视频 什么是简单工厂? 简单工厂模式属于类的创建型模式,又叫做静态工厂方法模式。通过专门定义一个类来负责创建其他类的实…

GitLab CICD Day 08 - 环境变量

1.局部/全局环境变量 stages:- testing # stage编排- build- deployvariables:global_var: "全部变量" #全部变量build_image:stage: buildvariables: #局部环境变量my_name: "局部环境变量" tags:- shell script:- …

14款奔驰R400升级ACC自适应巡航系统,增加您的行车安全性

有的时候你是否厌倦了不停的刹车、加油?是不是讨厌急刹车,为掌握不好车距而烦恼?如果是这样,那么就升级奔驰原厂ACC自适应式巡航控制系统,带排队自动辅助和行车距离警报功能,感受现代科技带给你的舒适安全和…

window.getComputedStyle

遇见一个问题,一个元素样式用的 固定定位。但是 top 属性没有在 元素树🌲种显示。js获取不到。这个方法可以获取到 style 里面设置的样式

OI中好用的技巧——c++快读快写

在写一些题时&#xff0c;发现不对读写进行处理会 TLE or WA 此时就需要降低读写的时间 c读写速度 &#xff1a; 普通写法 < scanf和printf写法 ≈ 缓存禁用写法 < 字符处理写法 普通写法&#xff08;读写最慢&#xff09; #include<iostream> using namespace…

车载调频发射机-德阳广播电台应用机关车队车载电台移动解说系统

车载调频发射机-德阳广播电台应用机关车队车载电台移动解说系统 北京海特伟业科技任洪卓发布于2023年7月13日 一、车载调频发射机-机关车队车载电台移动解说系统用户需求 机关车队主要承担领导干部公务用车以及重要会议、公务接待、调研应急等公务用车的服务保障工作。为认真…

前段开发概述

目录 网站概述 网站&#xff08;web site&#xff09;&#xff1a; 网页概述 网站开发流程&#xff1a; HTML基本概念 开发工具的选择 网站概述 网站&#xff08;web site&#xff09;&#xff1a; 按照一定规则&#xff0c;使用HTML超文本标记语言等工具制作的。用于展示…

FPGA——静态数码管

文章目录 一、实验环境二、实验原理三、实验任务四、实验过程4.1 time_count模块4.2 seg_led_static模块4.3 top_seg_led_static模块4.4 引脚配置 五、仿真5.1 仿真代码5.2 仿真结果 六、实验结果七、总结 一、实验环境 quartus 18.1 modelsim vscode Cyclone IV开发板 二、实…

大坝安全监测中需要做好检查监测

大坝安全监测是人们了解大坝运行状态和安全状况的有效手段和方法。它的目的主要是了解大坝安全状况及其发展态势&#xff0c;是一个包括由获取各种环境、水文、结构、安全信息到经过识别、计算、判断等步骤&#xff0c;最终给出一个大坝安全 程度的全过程。 此过程包括&#xf…

阿里云无影云电脑和服务器有啥区别?

阿里云无影云电脑和云服务器有什么区别&#xff1f;云电脑是作为个人或企业办公电脑使用&#xff0c;云服务器是对外提供24小时高可用服务&#xff0c;云电脑是桌面服务&#xff0c;云服务器是提供背后的计算服务&#xff0c;阿里云百科分享阿里云无影云电脑和云服务器的区别&a…

互联网医院源码|互联网医院系统源码|互联网医院诊疗系统

互联网医院系统开发可以提供许多好用的功能&#xff0c;以下是一些常见的功能&#xff1a;   在线挂号预约&#xff1a;用户可以通过系统在线选择医生、科室和就诊时间&#xff0c;并进行挂号预约&#xff0c;避免了传统排队等候的麻烦。   问诊咨询&#xff1a;用户可以通…

Fiddler 抓包工具 手机抓包配置

1. 下载Fiddler 工具阿里云盘分享 2. 安装后进行设置 Tools -->Options 这些设置完后开始手机WLAN 设置 1. 打开手机的“设置” ->“WLAN”&#xff0c;找到你要连接的网络&#xff0c;在上面长按&#xff0c;然后选择“修改网络”&#xff0c;弹出网络设置对话框&…

探索Web自动化测试工具的特点

Web应用程序的快速发展使得自动化测试在软件开发生命周期中变得至关重要。Web自动化测试工具为开发人员和测试人员提供了一种高效、准确且可重复的方法来验证Web应用程序的功能和稳定性&#xff0c;我们一起来探索Web自动化测试工具的特点。 1.多浏览器兼容性&#xff1a; Web自…

Notes中使用邮件合并功能

大家好&#xff0c;才是真的好。 很久很久以前&#xff0c;就实现了Notes客户机的邮件合并功能&#xff0c;老实说&#xff0c;早得我都忘记当时是如何实现的了。 对了&#xff0c;我记起来一点&#xff0c;就是Excel 2003和之前的版本&#xff0c;还可以导出为Lotus 1-2-3格…

< 每日算法 - JavaScript解析:跳跃游戏 Ⅰ/ Ⅱ - 贪心 >

每日算法 - JavaScript解析&#xff1a;跳跃游戏 Ⅰ/ Ⅱ - 贪心 跳跃游戏 Ⅰ① 任务描述&#xff1a;> 示例一> 示例二 ② 题意解析③ 解决方案&#xff1a; 跳跃游戏 Ⅱ① 任务描述&#xff1a;> 示例一> 示例二 ② 题意解析③ 解决方案 往期内容 &#x1f4a8; 跳…

09_SPI-Flash 页写实验

09_SPI-Flash 页写实验 1. 实验目标2. 操作时序3. 模块框图3.1 顶层模块3.2 页写模块 4. 波形图5. RTL5.1 flash_pp_ctrl5.2 spi_flash_pp 6. Testbench6.1 tb_flash_pp_ctrl6.2 tb_spi_flash_pp 1. 实验目标 使用页写指令&#xff0c;向 Flash 中写入 N 字节数据&#xff0c;…

4.postgresql--rollup,grouping sets,cube

PostgreSQL ROLLUP 是group by 的子句&#xff0c;是生成多个分组集合的快捷功能。与Cube子句的差异是&#xff0c;rollup 不生成基于特定列所有可能的分组集合&#xff0c;生成分组集合为其子集。 ROLLUP假设输入列之间存在层次结构&#xff0c;从而生成有意义的所有分组集合…

PyQt5+Python制作的位图字体生成工具

前言 本篇在讲什么 Pyqt5制作的Fnt字体创建工具 本篇需要什么 对Python语法有简单认知 依赖Python3.7环境 依赖Pycharm编辑器 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理论&#xff0c;快速上手 提供全流程的源码内容 ★提高阅读体验★ &#x1f449…

HW5300V3-ISCSI存储运维,看这一篇就够了03——HOST

HOST-创建主机01 1、选择“资源分配”→“主机” 2、选择“主机”→"创建"→"手动创建" 3、设置host属性&#xff0c;下一步 4、配置启动器。如已创建了启动器&#xff0c;选择对应的&#xff0c;下一步。如未创建可直接”下一步“后面创建再关联&#xff…

语义分割混淆矩阵、 mIoU、mPA计算

一、操作 需要会调试代码的人自己改&#xff0c;小白直接运行会出错 这是我从自己的大文件里摘取的一部分代码&#xff0c;可以运行&#xff0c;只是要改的文件地址path比较多&#xff0c;遇到双引号“”的地址注意一下&#xff0c;不然地址不对容易出错 把 calculate.py和 u…