如何设计分布式锁?

1. 为什么需要使用分布式锁?

在实际项目中,经常会遇到多个客户端对同一个资源或数据进行访问,为了避免并发访问带来错误,就会对该资源或数据加一把锁,只允许获得锁的客户端进行操作。
总结来说,分布式锁是解决分布式系统访问共享资源时,避免并发访问共享资源带来错误而采取的解决方法;其实现思路是,通过为共享资源加锁,让各个访问互斥,保证并发访问的安全性以及写入操作唯一性,并保持数据一致;根据锁的用途还可以细分为以下2类:

  • 允许多个客户端操作共享资源
    这种情况下,对共享资源的操作需要满足幂等性,保证无论操作多少次都不会出现不同结果。在这里使用锁,是为了避免重复操作共享资源从而提高效率。比如:服务A对数据库获取某个数据,并缓存到redis中;希望将数据从数据库提取—>业务处理—>缓存到redis中,这一过程只有一个操作,后续请求该数据时,均从redis中获取,就可以给该项操作加锁操作。
  • 只允许一个客户端操作共享资源
    这种情况下,对共享资源的操作一般是非幂等性操作。在该情况下,如果出现多个客户端操作共享资源,就可能出现数据不一致或数据丢失等情况。比如:请求A更新数据x.cnt为x.cnt+1,请求B更新数据x.cnt为x.cnt+1,两个请求同时发生,希望x.cnt为x.cnt+2,最终结果可能是x.cnt为x.cnt+1,导致了数据不一致。

2. 分布式锁有哪些实现方式?

在分布式系统中,常用来实现分布式锁的方式有:基于数据库和分布式缓存 2种方式。基于数据库实现,主要有数据库的事务功能和乐观锁两种方式;基于分布式缓存实现,主要有基于内存实现和中间件实现两种方式,其中基于中间件实现有redis、zookeeper和etcd等实现。

2.1 基于关系型数据库MySql实现

2.1.1 基于数据库的事务功能

实现思路:先查询数据库是否存在记录,为防止幻读取通过数据库行锁select for update锁住这行数据,然后将查询和插入的SQL在同一个事务中提交。
以订单表为例:

select if from order where order_id=xx for update

那么存在如下情况:
在这里插入图片描述
对于上述中事务1和事务2,希望要么全部成功要么全部失败;但实际上会根据数据库设置的事务隔离级别决定,不同的隔离级别会出现不同的结果。
数据库系统提供了4种事务隔离级别:

隔离级别脏读(Dirty Read)不可重复读(Non Repeatable Read)幻读(Phantom Read)
读未提交yesyesyes
读已提交-yesyes
可重复读--yes
可串行化---

数据库的隔离级别越高其系统的并发性能越差。当设置较低事务隔离级别时,容易出现数据不一致情况;当设置隔离级别太高时,处理时长增加,会降低系统可用性。适合对数据一致性要求不高的场景。

2.1.2 基于乐观锁实现

实现思路:在表结构中增加version字段标识数据版本,更新过程中对版本号进行比较,版本号一致,则成功执行,反之,更新失败。
以订单表为例:

# Select 时获取version值
select amount, old_version from order where order_id = xxx;
# update 的时候检查ver值是否与第2步中获取相同的值
update order set version=old_version+1, amount = yyy where order_id =xxx and ver=old_ver

2.3 基于分布式缓存实现

基于缓存的分布式锁,可以避免大量请求直接访问数据库,提高系统的响应能力。主要将分布式锁存在在内存中,提高了读写速度。引入中间件,将分布式锁设置到中间件。
基于中间件实现分布式锁
有如下优点:性能高效、实现方便、避免单点故障
同时存在如下一些问题需要解决:

  • 可用性:无论何时都要保证锁服务的可用性;避免服务端单点部署,当节点故障时,锁服务无法提供服务,导致使用锁服务的客户端无法工作。
  • 死锁:避免锁一直存在;客户端一定可以获取锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达。
  • 脑裂:集群同步时产生的数据不一致,导致新的进程有可能拿到锁,但之前的进程以为自己还有锁,那么就出现两个进程拿到了同一个锁的问题。
    常见的实现分布式锁的中间件有:redis、etcd、zookeeper等。

3. 基于缓存实现分布式锁

3.1 基于Redis实现分布式锁

实现思路:

  1. 加锁:当键值不存在时,对键值设置操作并返回成功,否则返回失败;
Set lock_key unique_value NX PX 10000;
# lock_key: key键值
# unique_value: 客户端生产的唯一标识
# NX代表只在lock_key不存在时,才对lock_key进行设置操作
# PX 10000表示设置lock_key的过期时间为10s,这是为了避免客户端发生异常无法释放锁
  1. 解锁:删除lock_key,通过删除键值对释放锁,释放后其他线程可以通过设置setnx获取锁;
  2. 超时:设置lock_key的超时时间,保证锁在没有显示释放的情况,锁也可以自动释放,这样避免了资源被永远锁住(死锁情况)。
    注意事项
  3. Redis 2.6.12之前的版本中,加锁和设置超时是2步操作,需要注意原子性操作;这里可以选择使用redis 2.6.12之后版本或使用LUA脚本,保证原子性操作。
  4. del误删:客户端A执行时间超过设置锁的超时时间,导致锁被释放,客户端B获取锁,然后出现客户端A的任务结束,客户端A删除客户端B设置的锁。
    在这里插入图片描述
    为避免这种情况,可以为客户端B设置唯一ID,释放锁时先匹配ID是否一致,然后再删除key。但这种情况无法解决客户端A锁被释放问题。
  5. 超时解锁导致并发:在上图中存在A客户端A和B同时操作共享资源的情况;为解决该情况,需要合理设置超时时间,但过长的超时时间会增加客户端B获取锁时间;因此,可以通过基于续约设置超时时间;在服务内部,设计监听任务执行情况的程序,当锁快超时但任务未结束时,给锁增加时间。
  6. redis集群下的分布式锁:在集群中,需要解决锁存在于多个节点的问题。

架构设计层面上Redis怎么解决集群情况下分布式锁的可靠性问题

redis官方设计了分布式算法Redlock。RedLock 算法旨在解决单个 Redis 实例作为分布式锁时可能出现的单点故障问题,通过在多个独立运行的 Redis 实例上同时获取锁的方式来提高锁服务的可用性和安全性。
实现思路:RedLock是对集群的每个节点进行加锁,如果大多数节点(N/2+1)加锁成功,则会认为加锁成功。这样即使集群中有某个节点挂掉了,因为大部分集群节点都加锁成功了,所以分布式锁仍然可以使用。
工作流程

  • 客户端向多个独立的Redis实例尝试获取锁,设置锁的过期时间非常短;
  • 如果客户端能在大部分节点上成功获取锁,并且花费的时间小于过期时间的一半,那么认为客户端成功获取到了分布式锁;
  • 当客户端完成对受保护资源的操作后,它需要向所有曾获取锁的Redis实例释放锁;
  • 若在释放锁的过程中,客户端因无法完成,由于设置了锁的过期时间,锁最终会自动过期释放,避免了死锁。
    存在问题
  • 性能问题:RedLock需要等待大多数节点返回之后,才能加锁成功,而这个过程中可能会因为网络问题,或节点超时的问题,影响加锁的耗时;
  • 并发安全性问题:当客户端加锁时,如果遇到GC可能会导致加锁失效(还有其他类似节点中某一个节点失效等问题),但GC后误认为加锁成功的安全事务,例如:
    • 客户端A请求3个节点进行加锁
    • 在节点回复处理之前,客户端A进入GC阶段(存在STW,全局停顿)
    • 因为加锁时间太长,锁失效
    • 客户端B请求加锁(和客户端A请求的是同一把锁),加锁成功
    • 客户端A GC完成,继续处理前面节点的消息,误以为加锁成功
    • 此时客户端B和客户端A同时加锁成功,出现并发安全性问题
  • redis集群内节点的时间要保持一致:若节点时间不一致会存在时间差导致加锁失败
  • Other:Distributed Locks with Redis
    在这里插入图片描述

Go实现redis分布式锁

3.2 基于Zookeeper实现分布式锁

Zookeeper分布式锁的实现,主要利用了Zookeeper的临时顺序节点特点、监听机制,保证锁的公平性、可重入。
大致思想是:1)请求排队,先来先得;2)上一个任务结束后释放锁并通知下一个排队对象;3)当前排队对象判断是否满足某个规则(获取锁的规则),若是则获取锁。具体实现原理如下

  • ZooKeeper的每一个节点,都是一个天然的顺序发号器
    在每个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面,会加上一个次序编号,而这个生成的次序编号,是在上一个生成次序编号基础上+1
  • ZooKeeper节点的递增有序性,可以确保锁的公平
    一个Zookeeper分布式锁,首先需要创建一个节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程,都在这个节点下创建个临时顺序节点。由于ZK节点,是按照创建的次序,依次递增的。为了确保公平,可以简单的规定:编号最小的那个节点,表示获得了锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。
  • ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效
    每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,就处于等待通知的状态。等待前一个ZNode删除事件通知,收到上一个Znode删除事件时,判断自己是否为当前最小的序号,若是则获取锁。
    使用ZooKeeper实现分布式锁的算法,主要有以下几个步骤
  • 一把分布式锁通常使用一个Znode节点表示;如果锁对应的Znode节点不存在,首先创建Znode节点。这里假设为“/test/lock”,代表了一把需要创建的分布式锁;
  • 抢占锁的所有客户端,使用锁的Znode节点的子节点列表来表示;如果某个客户端需要占用锁,则在“/test/lock”下创建一个临时有序的子节点;(这里,所有临时有序子节点,尽量共用一个有意义的子节点前缀。)
  • 判定客户端是否占有锁。客户端创建子节点后,需要进行判断:自己创建的子节点,是否为当前子节点列表中序号最小的子节点;如果是,则认为加锁成功;如果不是,则监听前一个Znode子节点变更消息,等待前一个节点释放锁;
  • 监听上一个节点变更通知,判断自己是否为当前子节点列表中序号最小的节点,若是则加锁成功;否则继续监听,直到获取锁;
  • 获取锁后,处理业务逻辑,完成之后,删除对应的子节点,完成锁释放工作。
    存在的问题
  • 并发安全问题:zookeeper若长时间检测不到客户端的心跳时,会认为session过期,那么由该session创建的所有ephemeral类型的znode节点会被自动删除,这时就会出现如下的问题:
    在这里插入图片描述
    如上图所示,客户端A发生GC停顿的时候,zookeeper检测不到心跳,也是有可能出现多个客户端同时操作共享资源的情形。
  • 性能方面:每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。在Zookeeper 中创建和删除节点只能通过 Leader 服务器来执行,然后 Leader 服务器还需要将数据同步到所有的 Follower 机器上,这样频繁的网络通信,性能的短板是非常突出的;zookeeper是一个XP系统,在网络分区情况下,系统优先保证一致性,可能牺牲可用性。
    Go 使用zookeeper实现分布式锁

3.3 基于ETCD实现分布式锁

ETCD支持以下功能,并依赖这些功能来实现分布式锁:

  • Lease机制:即租约机制(TTL,Time to Live),Etcd可以为存储的key-value对设置租约,当租约到期,key-value将失效删除;同时也支持续约续期(KeepAlive)
  • Revision机制:每个Key带有一个Revision属性值,Etcd每进行一次事务对应的全局Revision值都会加一,因此每个key对应的Revision属性值是全局唯一的。通过比较Revision的大小可以知道进行写操作的顺序。在实现分布式锁时,多个程序同时抢锁,根据Revison值大小依次获得锁,可以避免惊群效应,实习公平锁
  • Prefix机制:即前缀机制(或目录机制)。可以根据前缀(目录)获取该目录下所有key及对应的属性(包括key、value及revision等)
  • Watch机制:即监听机制,Watch机制支持Warch某个固定的Key,也支持Watch一个目录前缀(前缀机制),当被warch的key活目录发生变化,客户端将收到通知
    实现分布式锁的步骤:
  • 假设分布式锁的Name为/root/lockname,用来控制某个共享资源,concurency会自动将其转换为目录形式:/root/lockname
  • 客户端A连接Etcd,创建一个租约Leaseid_A,并设置TTL,以/root/lockname为前缀创建全局唯一的key,该key的组织形式为/root/lockname/{leaseid_A},客户端A将此key绑定租约写入Etcd,同时调用TXN事务查询写入情况和具有相同前缀/root/lockname/的Revision的排序情况
  • 客户端A判断自己是否获得锁,以前缀/root/lockname/读取keyValue列表(keyValue中带有key对应的Revison),判断自己Key是否为当前列表中最小的,如果是则认为获得锁;否则阻塞监听列表中前一个Revison比自己小的Key删除事件,一旦监听到删除事件或因租约失效而删除的事件,则自己获得锁
  • 执行业务逻辑,操作共享资源
  • 释放分布式锁:业务逻辑执行完或异常退出时,删除锁;
  • 当客户端持有锁期间,其他客户端只能等待,为避免等待期间租约失效,客户端需要创建一个定时任务进行续约操作;若持有锁期间客户端崩溃,心跳停止,key将因租约到期而被删除,从而锁被释放,避免死锁

Go 基于Etcd实现分布式锁

总结

在分布式系统中,常见的实现分布式锁方式是基于数据库、基于分布式缓存方式等;其中基于数据库方式主要依赖于数据库的事务功能或表结构的实现乐观锁,基于分布式缓存方式常用的有redis、etcd、zookeeper等;redis实现分布式锁性能最好、zookeeper和etcd实现可靠性更好;其中zookeeper和etcd实现方式比较类似,但etcd在通讯、锁设置上性能较zookeeper更好。

参考资料

为什么需要分布式锁?(Redis分布式锁)
mysql事务隔离
Distributed Locks with Redis
Go实现redis分布式锁
Zookeeper 分布式锁 – 图解 – 秒懂
浅谈分布式锁:安全与性能的取舍之道
Zookeeper分布式锁
Redis分布式锁
为什么分布式要有分布式锁!
Etcd 应用开发之分布式锁

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

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

相关文章

新能源汽车空调系统的四个工作过程

汽车空调制冷系统组成 1.汽车空调制冷系统组成 以R134a为制冷剂的汽车空调制冷系统主要包括压缩机、电磁离合器、冷凝器、 散热风扇、储液于燥器、膨胀阀、蒸发器、鼓风机、制冷连接管路、高低压检测 连接接头、调节与控制装置等组成。 汽车空调的四个过程 1压缩过程 传统车…

金融数据的pandas模块应用

金融数据的pandas模块应用 数据链接:https://pan.baidu.com/s/1VMh8-4IeCUYXB9p3rL45qw 提取码:c6ys 1. 导入所需基础库 import pandas as pd import matplotlib.pyplot as plt from pylab import mpl mpl.rcParams[font.sans-serif][FangSong] mpl.rcP…

JAVA.1.新建项目

1.代码结构 2.如何创建项目 1.创建工程 至此,我们创建了我们的第一个工程 2.创建模块 可见已经有了p28的一个模块,删掉了再添加 展开src 3.创建包 4.新建类 5.编写代码 package demo1;public class Hello {public static void main(String[] args) {Sys…

华为od机试真题:火星符号运算(Python)

题目描述 已知火星人使用的运算符号为 #和$ 其与地球人的等价公式如下 x#y2*x3*y4 x$y3*xy2x y是无符号整数。地球人公式按照c语言规则进行计算。火星人公式中,# 号的优先级高于 $ ,相同的运算符,按从左往右的顺序计算 现有一段火星人的字符串报文&a…

基于Centos7搭建rsyslog服务器

一、配置rsyslog可接收日志 1、准备新的Centos7环境 2、部署lnmp环境 # 安装扩展源 wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo# 安装扩展源 yum install nginx -y# 安装nginx yum install -y php php-devel php-fpm php-mysql php-co…

UNiapp 微信小程序渐变不生效

开始用的一直是这个,调试一直没问题,但是重新启动就没生效,经查询这个不适合小程序使用:不适合没生效 background-image:linear-gradient(to right, #33f38d8a,#6dd5ed00); 正确使用下面这个: 生效,适合…

【TensorRT】Yolov5-DeepSORT 目标跟踪

Yolov5-DeepSORT-TensorRT 本项目是 Yolo-DeepSORT 的 C 实现,使用 TensorRT 进行推理 🚀🚀🚀 开源地址:Yolov5_DeepSORT_TensorRT,求 star⭐ ~ 引言 ⚡ 推理速度可达25-30FPS,可以落地部署&…

LeetCode-day20-2850. 将石头分散到网格图的最少移动次数

LeetCode-day20-2850. 将石头分散到网格图的最少移动次数 题目描述示例示例1:示例2: 思路代码 题目描述 给你一个大小为 3 * 3 ,下标从 0 开始的二维整数矩阵 grid ,分别表示每一个格子里石头的数目。网格图中总共恰好有 9 个石头…

5.java操作RabbitMQ-简单队列

1.引入依赖 <!--rabbitmq依赖客户端--> <dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId> </dependency> 操作文件的依赖 <!--操作文件流的一个依赖--> <dependency><groupId>c…

如何在 Mac 上下载安装植物大战僵尸杂交版? 最新版本 2.2 详细安装运行教程问题详解

植物大战僵尸杂交版已经更新至2.2了&#xff0c;但作者只支持 Windows、手机等版本并没有支持 MAC 版本&#xff0c;最近搞到了一个最新的杂交 2.2 版本的可以在 Macbook 上安装运行的移植安装包&#xff0c;试了一下非常完美能够正常在 MAC 上安装运行&#xff0c;看图&#x…

Pytest测试框架的基本使用

目录 安装教程 Pytest命名约束 创建测试用例 执行测试用例 生成测试报告 参数化测试 pytest框架 pytest是目前非常成熟且功能齐全的一个测试框架&#xff0c;能够进行简单的单元测试和复杂的功能测试。还可以结合selenium/appnium进行自动化测试&#xff0c;或结合reques…

加拿大上市药品查询-加拿大药品数据库

在加拿大&#xff0c;药品的安全性、有效性和质量是受到严格监管的。根据《食品药品法案》的规定&#xff0c;所有药品制造商必须提供充分的科学证据&#xff0c;证明其产品的安全性和有效性。为此&#xff0c;加拿大卫生部建立了一个全面的药品数据库 &#xff08;DPD) &#…

【C++】类和对象——默认成员函数(下)

目录 前言拷贝构造1.概念2.特征3.总结 赋值重载运算符重载赋值运算符重载探讨传引用返回和传值返回的区别 const成员取地址及const取地址操作符重载 前言 上一讲我们已经说了关于C的默认成员函数中的两个——构造和析构函数。所谓默认成员函数也就是&#xff1a;用户没有显示定…

你的Type-c接口有几颗牙齿

C 口为啥不能混用 想想 C 口当年推出时给我们画的饼&#xff0c;“正反都能插&#xff0c;而且充电、传数据、连显示器等等&#xff0c;什么活都能干”&#xff0c;而实现这一切的前提全靠 C 口里面的 24 根针脚 这 24 根真叫呈中心对称分布&#xff0c;这种设计使得插头可以以…

iPhone手机上备忘录怎么设置字数显示

在日常生活和工作中&#xff0c;我经常会使用iPhone的备忘录功能来记录一些重要的想法、待办事项或临时笔记。备忘录的便捷性让我可以随时捕捉灵感&#xff0c;但有时候&#xff0c;我也会苦恼于不知道自己记录了多少内容&#xff0c;尤其是在需要控制字数的时候。 想象一下&a…

机器学习 | 深入理解激活函数

什么是激活函数&#xff1f; 在人工神经网络中&#xff0c;节点的激活函数定义了该节点或神经元对于给定输入或一组输入的输出。然后&#xff0c;将此输出用作下一个节点的输入&#xff0c;依此类推&#xff0c;直到找到原始问题的所需解决方案。 它将结果值映射到所需的范围…

【功能】DOTween动画插件使用

一、下载安装DOTween插件&#xff0c;下载地址&#xff1a;DOTween - Asset Store (unity.com) 使用 Free免费版本即可&#xff0c;导入成功后&#xff0c;Project视图中会出现 DOTween 文件夹 二、使用案例 需求1&#xff1a;控制材质球中的某个属性值&#xff0c;实现美术需…

SQL执行流程、SQL执行计划、SQL优化

select查询语句 select查询语句中join连接是如何工作的&#xff1f; 1、INNER JOIN 返回两个表中的匹配行。 2、LEFT JOIN 返回左表中的所有记录以及右表中的匹配记录。 3、RIGHT JOIN 返回右表中的所有记录以及左表中的匹配记录。 4、FULL OUTER JOIN 返回左侧或右侧表中有匹…

二维码如何用来存储图片?扫码看图有哪些好处

现在通过二维码来分享图片是一种很常见的方法&#xff0c;二维码可以承载大量的图片内容&#xff0c;从而节省对图片空间容量的占用&#xff0c;并且将图片放入二维码中便于分享让图片传递变得更加方便快捷&#xff0c;那么图片生成二维码具体该怎么操作呢&#xff1f;通过下面…

MySQL----初始数据类型

前言 一、tinyint 范围&#xff1a;-128-----127 在MySQL中&#xff0c;整型可以指定是有符号的和无符号的&#xff0c;默认是有符号的。可以通过UNSIGNED来说明某个字段是无符号的。如果我们向mysqlt特定的类型中插入不合法的数据&#xff0c;Mysq一般会直接拦截&#xff0c…