模板模式实现分布式锁实战

前言

分布式锁相信大家都有用过,常见的分布式锁实现方式例如redis、zookeeper、数据库都可以实现,而我们代码中强引用这些分布式锁的代码,那么当我们以后想替换分布式锁的实现方式时,需要修改代码的成本会很高,于是我们需要借鉴一些设计模式思想来设计,下面我介绍下这三个分布式锁的实现逻辑以及我们项目中是怎么实现

实现方式

数据库实现

首先我们设计一张这样的表

CREATE TABLE `lock` (`key` varchar(128) C NOT NULL,`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`version` int(8) DEFAULT '1',PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT = '分布式锁表';

首先方法开启事务,当我们需要对某块业务上锁时,确定加锁的颗粒度设置key的值,执行插入语句,这里我们可以设计的全面一点,对比其他的分布式锁实现方式,貌似还缺了点什么,比如这个锁不可重入,无法设置超时时间 其实这些我们也可以解决, 首先可重入方面,version字段记录重入的次数

insert into table (`key`) values (#key#) on duplicate key `version` total = `version` + 1

key是唯一索引,执行完这个语句后,会在这一行添加行锁。这时候后续有两种可能性,如果是当前线程A重复插入锁,那么会更新version字段;如果是其他线程B想要插入这个锁,那么由于A持有的行锁还未释放,所以B会阻塞

超时方面,我们可以设置数据库的innodb_lock_wait_timeout参数来设置超时时间,默认50s,等待时间超时这个时间则会报错

由于insert语句在RR隔离级别会生成间隙锁,在并发较高的情况下会产生死锁的情况,所以建议在RC情况下使用

Redis实现

如果我们自己设计redis实现加锁的话,我们第一个想到的就是setNx语法,它的作用就是当key不存在的情况下,将key 的值设置为 value,同时返回1,如果key已存在,则不设置value,并且返回0

如果还要加上超时时间,那么还需要执行expire语法

setnx lock true
expire lock 10

使用这两个语法,会产生一个问题,就是这两个语法不是原子性的,当执行一个语法后,系统报错了,那么超时时间就无法设置,这样子这个key就无法过期

基于这个问题,Redis官方将这两个指令组合在了一起,解决Redis分布式锁原子性操作的问题

SET key value [EX 过期时间] NX
将key 的值设置为 value,同时返回1,如果key已存在,则不设置value,并且返回0;同时加上超时时间,这是一个原子性操作

由此可见,我们自己实现需要踩很多坑,市面上有成熟的redis实现的分布式锁框架redission,我们直接用就可以了,它帮我们把坑都踩了一遍,不用重复造轮子了,简单讲解下它的原理

首先讲一下它的键值kv结构

  • key为锁的key
  • value为一个hash对象 {key:线程id,value:重入次数}

value为什么这么设计?我们要解决两个问题

1、删除锁时,A线程把B线程持有的锁给删除了?为了解决这个问题,我们需要在锁中记录线程id,删除时就可以判断锁是否是当前线程持有的,一致才可以删除

2、实现可重入的逻辑,所以需要在锁中记录重入次数,每次重入次数+1

不同于上面SET key value [EX 过期时间] NX方式,redission的加锁逻辑是通过一段lua脚本来实现的,redis的lua脚本可以实现原子性的操作,下图是lua脚本

image-20240105170729529

解锁逻辑也是一样的,由于需要判断当前线程才能执行删除,所以也需要通过lua脚本来实现删除的逻辑

当我们设置了过期时间后,如果我们的业务执行时间超过了设定的过期时间,那么锁会提前删除,就会出现各种各样的问题。所以redission实现了一个watch dog逻辑。它是一个后台线程,每10s检查一次,将锁的过期时间延长

redis我们通过都会设置高可用,最常见的方案就是主从或者哨兵,但是它保证了高可用的同时,无法保证高一致性。这样子当redis的主节点挂了,从节点还没有同步到主节点的数据,就变成了主节点,那么锁就会发生丢失

redission提供了RedLock算法,通过使用多个Redis实例,各个实例之间没有主从关系,相互独立,超过一半节点加锁成功才算获取到锁。不过这种算法也不是一个完美的算法,多个实例加锁效率低,同时也会衍生出一些其他问题

Zookeeper实现

Zookeeper有很多种节点种类,其中有一种节点种类叫做临时顺序节点,这个节点有两个特性,首先是当客户端向Zookeeper添加了这个节点后,如果之后客户端挂了,那么这个临时节点会被删除,不会一直存在,其次是这个节点是有序递增的。这些特性很适合用来做分布式锁

加锁就是在Zookeeper上添加临时顺序节点,判断是否是最小节点,如果是最小节点,则无需排队直接执行。如果不是最小的,则往后加一个顺序节点,并且向前一个节点添加一个watch监听,线程阻塞等待排队

当前一个节点删除时,当前节点监听到删除事件并唤醒线程。这样子第一个通知第二个,第二个通知第三个,这种击鼓传花的方式可以避免羊群效应

羊群效应就是前一个节点释放锁后,所有节点被唤醒,这样会给服务器带来巨大压力

哪种更好?

这个问题,我觉得得根据我们的现实情况做判定,当我们的系统只有数据库,又不想依赖其他的中间件,那我们使用数据库实现的方方式就可以了,但是性能会很差,容易出现瓶颈

Redis和Zookeeper性能都比数据库好,这两者相比较而言,Redis作为分布式锁大家使用的会多一些,主要原因我想应该是Zookeeper的cp特性导致单leader节点易出现瓶颈,而redis如果出现瓶颈后弹性伸缩(增加节点)会很方便,所以性能更高

踩坑点

之前我们在代码中加入分布式锁时,碰到过一个坑,就是明明加入了分布式锁,但是没有"锁住"代码。这里有几种原因,我就不一一展开了,我们之前踩过的一个坑就是我们的方法加了事务@Transactional注解,同时方法里面的逻辑加了redis分布式锁,类似下面的代码。

@Transactional
private void test() {// 加锁// 查询 id =1的记录// 更新 id=1的记录// 释放锁
}

这时候,当线程A进入了方法,首先进入事务,加锁,然后执行方法里面的逻辑,释放锁,但是事务还未释放。这时候线程B也进入了这个方法,锁因为已经释放了,就直接进入方法逻辑了,但是线程A的事务此时还没有提交,所以线程B查询的id=1的记录是不对的。

这个是踩坑了,解决方法有两种,一种是把事务的方法放在加锁的逻辑里面;另外一种就是释放锁的逻辑改成监听spring 事务提交的事件,实现事务完成后再释放锁,我们最后也是这么改的

@Transactional
private void test() {// 加锁// 查询 id =1的记录// 更新 id=1的记录// unlockAfterTransaction方法
}private void unlockAfterTransaction(LockResult lockResult) {//事物完成后释放锁TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCompletion(int status) {super.afterCompletion(status);distLockSservice.unlock(lockResult);}});
}

实战

这里再重审下为什么使用模板模式,因为我们使用分布式锁的实现有多种,如果我们在业务代码中直接依赖某一种分布式锁的话,那么后续我们想替换分布式锁的实现会很麻烦,所有有依赖的类都得替换,所以我们使用模版模式,把分布式锁的实现以及加锁、释放锁的逻辑放到公共代码中,我们业务类只需要实现自己的业务逻辑即可,无需关心分布式锁的相关逻辑,调用方法即可

我们在1.8之前使用模版模式会比较繁琐,我们需要准备一个抽象类,定义一些公共的逻辑,然后子类继承这个抽象类来自定义不同的逻辑。下面我以redission分布式锁的实战为例

public abstract class AbstractLockService {RedissonClient redissonClient;public void lock(List<String> keyNames, Long timeout,Object... o) throws InterruptedException {// 加锁RLock[] locks = new RLock[keyNames.size()];for(int i = 0; i < keyNames.size(); ++i) {locks[i] = this.redissonClient.getLock(keyNames.get(i));}RLock lock = this.redissonClient.getMultiLock(locks);boolean success = lock.tryLock(timeout, TimeUnit.MILLISECONDS);// 加锁成功,走业务逻辑if (success) {this.doBusiness(o);}// 释放锁unlockAfterTransaction(lock);}// 业务逻辑abstract Object doBusiness(Object... o);private void unlockAfterTransaction(RLock lock) {//事物完成后释放锁TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCompletion(int status) {super.afterCompletion(status);lock.unlock();}});}
}

当我们使用1.8以上的JDK时,针对模板模式,做了很多优化。我们可以将实现方法作为函数式方法传入模版中

public class LockFunctionService {RedissonClient redissonClient;public void lock(List<String> keyNames, Long timeout, ILockCallback lockCallback) throws InterruptedException {// 加锁RLock[] locks = new RLock[keyNames.size()];for(int i = 0; i < keyNames.size(); ++i) {locks[i] = this.redissonClient.getLock(keyNames.get(i));}RLock lock = this.redissonClient.getMultiLock(locks);boolean success = lock.tryLock(timeout, TimeUnit.MILLISECONDS);// 加锁成功,走业务逻辑if (success) {lockCallback.callback();}// 释放锁unlockAfterTransaction(lock);}private void unlockAfterTransaction(RLock lock) {//事物完成后释放锁TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCompletion(int status) {super.afterCompletion(status);lock.unlock();}});}}interface ILockCallback<T> {T callback();
}

当我们调用时,比之前方便多了

public static void main(String[] args) throws InterruptedException {LockFunctionService lockFunctionService = new LockFunctionService();List<String> keys = Lists.newArrayList("lock_1");// 需要加锁执行释放lockFunctionService.lock(keys, 5L, () -> {System.out.println("执行业务逻辑");return null;});
}

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

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

相关文章

OpenCV图像处理|1.1 OpenCV介绍与环境搭建

1.1.1 介绍 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;开放源代码计算机视觉库&#xff0c;主要算法涉及图像处理、计算机视觉和机器学习相关方法。OpenCV 其实就是一堆 C 和 C语言的源代码文件&#xff0c;这些源代码文件中实现了许多常用的计算机视…

安装PyTorch及环境配置(应用于Python上的YOLO)

这个基本都是Bilibili网站里面叫“小手丫子”up的视频教程&#xff0c;此前自己需要装了好几次又卸载了好几次&#xff0c;现在根据视频教学整理出来自己所理解的文档。 注意事项 1.安装的pycharm版本和anaconda版本无要求。 2.运行pycharm尽量以管理员身份运行。 3.Cuda是独…

AWTK 开源串口屏开发(5) - MCU端 SDK 用法

AWTK 开源智能串口屏&#xff0c;不但开放了串口屏端全部源码&#xff0c;还提供了MCU 端 SDK&#xff0c;大大加快 MCU 软件的开发。本介绍一下 MCU 端 SDK 在不同平台上的用法。 完整示例可以参考下面的几个例子&#xff1a; 普通嵌入式系统 mcu/stm32/hmi_app/hmi_app.c 低…

docker +gitee+ jenkins +maven项目 (二)

文章目录 前言一、创建Maven项目二、常规配置1.gitee配置2.gitee仓库配置3.构建时操作4.构建后操作 总结 前言 上一篇文章介绍了Jenkins的环境配置和工具配置&#xff0c;这篇进行具体maven项目的配置 一、创建Maven项目 二、常规配置 1.gitee配置 在工具哪里配置好gitee后&…

设计模式② :交给子类

文章目录 一、前言二、Template Method 模式1. 介绍2. 应用3. 总结 三、Factory Method 模式1. 介绍2. 应用3. 总结 参考内容 一、前言 有时候不想动脑子&#xff0c;就懒得看源码又不像浪费时间所以会看看书&#xff0c;但是又记不住&#xff0c;所以决定开始写"抄书&qu…

JMeter之测试WebService接口

JMeter之测试WebService接口 1 背景2 目的3 介绍4 具体操作4.1 soapUI调用4.2 JMeter工具调用4.3 操作步骤流程4.3 重点 1 背景 WebService应用的范围是非常广&#xff0c;任何需要跨平台、跨系统进行数据交换和功能调用的场景都可以用此来实现&#xff0c;在实际的工作中也常常…

Unity中Shader的_Time精度问题

文章目录 前言一、U方向上优化二、V方向上优化在这里插入图片描述 三、最终代码1、效果2、Shader 前言 在Unity的Shader中&#xff0c;使用了_Time来达到UV的流动效果&#xff0c;普遍会出现一个问题。我们的UV值会随着时间一直增加&#xff08;uv值增加了&#xff0c;但是因为…

3元一平方公里的在线卫星影像

我们为大家分享了免费下载卫星影像的方法。 但让人遗憾的是&#xff0c;该影像的最高分辨率只有10米&#xff0c;需要更高清且比较新的卫星影像&#xff0c;看来还是得付费购买才比较靠谱。 自助选择区县范围 商业卫星影像主要面向企事业单位&#xff0c;一般来讲都比较贵&a…

Windows安装DolphinDB,配置单节点启动与GUI

1. 安装Java 首先&#xff0c;进入网址&#xff1a;jdk11 下载jdk-11.0.20_windows-x64_bin.exe&#xff0c;然后安装即可 安装完成后&#xff0c;打开命令提示符&#xff0c;输入&#xff1a; java javac如果这两个命令都出现一大堆东西&#xff0c;而不是找不到指令的提示的…

物联网云平台源码,Spring Cloud智慧工地源码,建筑施工智能化管理

智慧工地以物联网云平台为核心&#xff0c;基于智慧工地物联网云平台与现场多个子系统的互联&#xff0c;实现现场各类工况数据采集&#xff0c;存储、分析与应用。通过接入智慧工地物联网云平台的多个子系统板块&#xff0c;根据现场管理实际需求灵活组合&#xff0c;实现一体…

玩转Python:用Python处理文档,5个必备的库,特别实用,附代码

在Python中&#xff0c;有几个流行的库用于处理文档&#xff0c;包括解析、生成和操作文档内容。以下是一些常用的库及其简介和简单的代码示例&#xff1a; PyPDF2 - 用于处理PDF文件。 简介&#xff1a;PyPDF2是一个纯Python库&#xff0c;用于分割、合并、转换和提取PDF文件中…

Vary: Scaling up the Vision Vocabulary for Large Vision-Language Models

ABSTRACT 现代大规模视觉-语言模型&#xff08;LVLMs&#xff09;采用了相同的视觉词汇-CLIP&#xff0c;可以涵盖大多数常见的视觉任务。然而&#xff0c;对于一些需要密集和细粒度视觉感知的特殊视觉任务&#xff0c;例如文档级OCR或图表理解&#xff0c;尤其是在非英语环境…

卷积神经网络(CNN)、循环神经网络(RNN)和自注意力(self-attention)对比

考虑同一个的问题&#xff1a;将由个词元组成的序列映射到另一个长度相同的序列&#xff0c;其中的每个输入词元或输出词元由维向量表示。 我们将比较能够解决上述问题的三种常用方法&#xff1a;卷积神经网络&#xff08;CNN&#xff09;、循环神经网络&#xff08;RNN&#x…

Verifiable Credentials可验证证书 2023 终极指南

1. 引言 Dock公司为去中心化数字身份领域的先驱者&#xff0c;其自2017年以来&#xff0c;已知专注于构建前沿的可验证证书&#xff08;Verifiable Credentials&#xff09;技术。本文将阐述何为电子证书、电子证书工作原理、以及其对组合和个人的重要性。 伪造实物证书和数字…

单元测试、系统测试、集成测试知识总结

一、单元测试的概念 单元测试是对软件基本组成单元进行的测试&#xff0c;如函数或一个类的方法。当然这里的基本单元不仅仅指的是一个函数或者方法&#xff0c;有可能对应多个程序文件中的一组函数。 单元也具有一些基本的属性。比如&#xff1a;明确的功能、规格定义&#…

win10报错“api-ms-win-crt-string-l1-1-0.dll文件丢失,软件无法启动”,快速修复方法,亲测有效

api-ms-win-crt-string-l1-1-0.dll是Windows操作系统中的一个动态链接库文件&#xff0c;属于Microsoft C Runtime Library。它包含了Windows操作系统需要运行C程序的一些基本系统函数&#xff0c;比如字符串处理、内存分配等。 它的作用主要是提供一些基本的、用于支持C语言编…

算法每日一题: 被列覆盖的最多行数 | 二进制 - 状态压缩

大家好&#xff0c;我是星恒 今天的题目又是一道有关二进制的题目&#xff0c;有我们之前做的那道 参加考试的最大学生数的 感觉&#xff0c;哈哈&#xff0c;当然&#xff0c;比那道题简单多了&#xff0c;这道题感觉主要的考点就是二进制&#xff0c;大家可以好好总结一下这道…

04、Kafka ------ CMAK 各个功能的作用解释(Cluster、集群、Broker、位移主题、复制因子、领导者副本、主题)

目录 启动命令&#xff1a;CMAK的用法★ 在CMAK中添加 Cluster★ 在CMAK中查看指定集群★ 在CMAK中查看 Broker★ 位移主题★ 复制因子★ 领导者副本和追随者副本★ 查看主题 启动命令&#xff1a; 1、启动 zookeeper 服务器端 小黑窗输入命令&#xff1a; zkServer 2、启动 …

苹果电脑Markdown写作工具:ulysses mac软件介绍

ulysses for mac是一款Markdown写作工具&#xff0c;支持Markdown拼写检查、语音识别、iCloud同步、版本管理等功能&#xff0c;并且可以导出为 PDF、word、RTF、TXT、Markdown、HTML 和 ePub等文件格式。 ulysses for mac软件介绍 适用于Mac&#xff0c;iPad和iPhone的终极写…

试除法判定质数算法总结

知识概览 质数的定义 在大于1的整数中&#xff0c;如果只包含1和本身这两个约数&#xff0c;就被称为质数&#xff0c;或者叫素数。 质数的判定——试除法 暴力算法 时间复杂度 改进算法 时间复杂度 暴力算法&#xff1a;时间复杂度O(n) 算法模版 bool is_pr…