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

前言

分布式锁相信大家都有用过,常见的分布式锁实现方式例如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.rhkb.cn/news/231366.html

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

相关文章

ChatGPT怎么帮我上班的

1.解放生产力 1&#xff09;标准格式&#xff0c;完美输出。GPT对于公文等具有一定标准格式的文件&#xff0c;可以进行完美仿写&#xff0c;随随便便以假乱真那都是小菜一碟&#xff0c;这对于经常要开展规范成文的人来说&#xff0c;简直就是个福音&#xff0c;只要前期调教…

pyqt5用qtdesign设计页面时,去掉页面的空白界面、边框和标题栏

前言 Windows默认的标题栏有时候自己觉得不太美观&#xff0c;就想自己设计一个&#xff0c;然后把默认的去掉&#xff0c;并且把长方形的边框和多余的空表界面去掉&#xff0c;就是下图中圈出来的区域&#xff1a; 去掉之后的效果如图&#xff1a; 这样我们就可以自定义窗…

面试算法89:房屋偷盗

题目 输入一个数组表示某条街道上的一排房屋内财产的数量。如果这条街道上相邻的两幢房屋被盗就会自动触发报警系统。请计算小偷在这条街道上最多能偷取到多少财产。例如&#xff0c;街道上5幢房屋内的财产用数组[2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;3]表示…

php ext-sodium 拓展安装 linux+windows

php编译安装(linux)&#xff0c;可以参考&#xff1a;php编译安装 一、windows soduim源码包自带&#xff0c;直接修改php.ini&#xff0c;取消extensionsodium注释即可 二、linux 1.安装依赖 apt-get install libsodium-dev2.进入源码目录 这里写自己的源码目录 cd /us…

大数据规模存储的几个核心问题

文章目录 三个关键问题RAID&#xff08;独立磁盘冗余阵列&#xff09;RAID是如何解决关于存储的三个关键问题&#xff1f;水平伸缩 大规模数据存储都需要解决几个核心问题&#xff0c;这些问题都是什么呢&#xff1f; 三个关键问题 1.数据存储容量的问题 既然大数据要解决的…

Windows系统中Wireshark抓包工具的安装使用

在使用Windows服务器时&#xff0c;如果我们发现网络流量异常或存在异常的外发数据包行为&#xff0c;我们可以利用抓包工具来捕获网络流量包&#xff0c;并对这些流量包进行特征分析&#xff0c;以查看其来源和目的地。通过这些信息&#xff0c;我们可以进一步诊断问题。 以下…

人生重开模拟器

前言&#xff1a; 人生重开模拟器是前段时间非常火的一个小游戏&#xff0c;接下来我们将一起学习使用c语言写一个简易版的人生重开模拟器。 网页版游戏&#xff1a; 人生重开模拟器 (ytecn.com) 1.实现一个简化版的人生重开模拟器 &#xff08;1&#xff09; 游戏开始的时…

如何快速定位php程序运行慢的地方

1 slow log日志 查看slowlog日志位置 编辑php-fpm.conf文件&#xff0c;更改或增加两行内容 slowlog /data/logs/php-slow.log request_slowlog_timeout 2 说明&#xff1a;slowlog定义日志路径和名字&#xff0c;request_slowlog_timeout定义超时时间&#xff0c;单位…

[足式机器人]Part2 Dr. CAN学习笔记-自动控制原理Ch1-6根轨迹Root locus

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-自动控制原理Ch1-6根轨迹Root locus 1. 根的作用2. 手绘技巧3. 分离点/汇合点&根轨迹的几何性质 1. 根的作用 G ( s ) s 3 s 2 2 s 4 G\left( s \right) \frac{s3}{s^22s4} G(s)s22s4s3​…

线性代数 --- 为什么LU分解中L矩阵的行列式一定等于(+-)1?

以下是关于下三角矩阵L的行列式一定等于-1的一些说明 证明&#xff1a;在LU分解中&#xff0c;下三角矩阵L的行列式一定是. 在证明之前&#xff0c;我这里先补充几条关于行列式的性质&#xff1a; 性质1&#xff1a;对于三角矩阵而言&#xff0c;不论是上三角矩阵还是下三角矩…

<六>Python的字符串切片及常见操作

字符串的表示 在Python里&#xff0c;可以使用一对单引号、一对双引号或者一对三个双引号、一对三个单引号表示字符串。 a "Im Tom" # 一对双引号 b Tom said:"I am Tom" # 一对单引号c Tom said:"I\m Tom" # 转义字符d Tom said:"…

C++矩阵例题分析(3):螺旋矩阵

一、审题 时间限制&#xff1a;1000ms 内存限制&#xff1a;256MB 各平台平均AC率&#xff1a;14.89% 题目描述 输出一个n*n大小的螺旋矩阵。 螺旋矩阵的样子&#xff1a; 输入描述 共一行&#xff0c;一个正整数n&#xff0c;表示矩阵变长的长度…

静态网页设计——电影角(HTML+CSS+JavaScript)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 使用技术&#xff1a;HTMLCSSJS 主要内容&#xff1a;本网页主要利用HTML语言编写&#xff0c;简要介绍世界上一些主要国家&#xff0c;例如&#xff0c;中&#xff0c;…

STM32通用定时器-输入捕获-脉冲计数

一、知识点 编码器   两相编码器&#xff08;正交编码器&#xff09;&#xff1a;两相编码器由 A 相和 B 相组成&#xff0c;相位差为 90 度。当旋转方向为顺时针时&#xff0c;A 相先变化&#xff0c;然后 B 相变化&#xff1b;当旋转方向为逆时针时&#xff0c;B 相先变化…

前端页面锚点跳转

一&#xff0c;页面 二&#xff0c;获取需要跳转的标签class或者id 三&#xff0c;调用跳转方法 如果你的标签有唯一的ID&#xff0c;那么用getElementById方法更好 点击即可跳转锚点

DDIA 第十一章:流处理

本文是《数据密集型应用系统设计》&#xff08;DDIA&#xff09;的读书笔记&#xff0c;一共十二章&#xff0c;我已经全部阅读并且整理完毕。 采用一问一答的形式&#xff0c;并且用列表形式整理了原文。 笔记的内容大概是原文的 1/5 ~ 1/3&#xff0c;所以你如果没有很多时间…

Python爬虫实战技巧:如何在爬取过程中动态切换代理IP

目录 前言 第一步&#xff1a;获取代理IP列表 第二步&#xff1a;测试代理IP的可用性 第三步&#xff1a;动态切换代理IP 总结 前言 在进行爬虫开发的过程中&#xff0c;有时候需要使用代理IP来访问目标网站&#xff0c;以避免被封IP或者降低访问频率的限制。本文将介绍如…

gem5学习(8):创建一个简单的缓存对象--Creating a simple cache object

目录 一、SimpleCache SimObject 二、Implementing the SimpleCache 1、getSlavePort() 2、handleRequest() 3、AccessEvent() 4、accessTiming() &#xff08;1&#xff09;缓存命中&#xff1a;sendResponse() &#xff08;2&#xff09;缓存未命中&#xff1a; 三、…

数据分析-25-电商用户行为可视化分析

文章目录 0. 数据代码获取1. 项目介绍1.1 分析背景1.2 分析目的1.3 分析思路 2. 数据清洗2.1 加载必要的库2.2 读取数据2.3 统计缺失值2.4 处理数据a. 删除重复值b. 转换时间格式c. 提取日期和时间d. 转换数据类型 3. 分析内容3.1 用户活跃规律a. 日均pv与uvb. 日新增pv、uv趋势…

Linux 修改主机名称并通过主机名称访问服务器

一、命令提示符简介 当我们打开终端的时候&#xff0c;我们要输入命令的左边就是命令提示符&#xff0c;如下图&#xff0c;接下来介绍下他们分别代表什么含义 1、root 和 xhf 表示的是当前登录的用户名称。 2、node2 表示的当前的主机名称。 3、~ 表示的是当前的目录 4、# 表示…