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

news2025/1/12 15:59:18

前言

分布式锁相信大家都有用过,常见的分布式锁实现方式例如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() {
      @Override
      public 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() {
            @Override
            public 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() {
            @Override
            public 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.coloradmin.cn/o/1361600.html

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

相关文章

21.串的处理

题目 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);String str sc.nextLine();char[] c str.toCharArray();int n c.length;StringBuilder st new StringBuilder();int i 0;while(i<n)…

G4周:CGAN,手势生成

本文为&#x1f517;365天深度学习训练营 中的学习记录博客 原作者&#xff1a;K同学啊|接辅导、项目定制 我的环境&#xff1a; 1.语言&#xff1a;python3.7 2.编译器&#xff1a;pycharm 3.深度学习框架Pytorch 1.8.0cu111 一、CGAN介绍 条件生成对抗网络&#xff08;…

【Golang】Json 无法表示 float64 类型的 NaN 以及 Inf 导致的 panic

【Golang】Json 无法表示 float64 类型的 NaN 以及 Inf 导致的 panic 原因 golang 服务出现了 panic&#xff0c;根据 panic 打印出的堆栈找到了问题代码&#xff0c;看上去原因是&#xff1a;json 序列化时&#xff0c;遇到了无法序列化的内容 [panic]: json: unsupported …

微信小程序——调节手机屏幕亮度案例分享

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

Elasticsearch基本操作之索引操作

本文说下Elasticsearch基本操作之索引操作 文章目录 概述创建索引创建索引示例重复创建索引示例 查看索引查看所有索引查看单个索引 删除索引删除索引 概述 由于是使用命令来操作Elasticsearch&#xff0c;可以使用kibana&#xff0c;postman和apifox等工具 我使用了apifox来执…

python-查漏补缺笔记-更新中

包导入时__init__.py中命令的执行顺序和sys.modules变化 ref: https://edu.csdn.net/skill/practice/python-3-6/164 在有父包和子包的情况下&#xff0c;父包中的“ __ init__.py”语句会在子包的“ __ init__.py”语句之前执行&#xff0c;然后按下列顺序执行导入子包和模块…

7款实用的SQLite数据库可视化管理工具

前言 俗话说得好“工欲善其事&#xff0c;必先利其器”&#xff0c;合理的选择和使用可视化的管理工具可以降低技术入门和使用门槛。今天推荐7款实用的SQLite数据库可视化管理工具(GUI)&#xff0c;帮助大家更好的管理SQLite数据库。 什么是SQLite&#xff1f; SQLite是一个…

[C#]使用onnxruntime部署yolov8-onnx印章检测

【官方框架地址】 https://github.com/ultralytics/ultralytics.git 【算法介绍】 YOLOv8是目标检测领域中的一种先进算法&#xff0c;它是YOLO&#xff08;You Only Look Once&#xff09;系列算法的最新发展。YOLO算法以其高效和实时的性能而著名&#xff0c;而YOLOv8则进一…

Flume基础知识(九):Flume 企业开发案例之复制和多路复用

1&#xff09;案例需求 使用 Flume-1 监控文件变动&#xff0c;Flume-1 将变动内容传递给 Flume-2&#xff0c;Flume-2 负责存储 到 HDFS。同时 Flume-1 将变动内容传递给 Flume-3&#xff0c;Flume-3 负责输出到 Local FileSystem。 2&#xff09;需求分析&#xff1a; 3&…

【十】【C语言\动态规划】376. 摆动序列、673. 最长递增子序列的个数、646. 最长数对链,三道题目深度解析

动态规划 动态规划就像是解决问题的一种策略&#xff0c;它可以帮助我们更高效地找到问题的解决方案。这个策略的核心思想就是将问题分解为一系列的小问题&#xff0c;并将每个小问题的解保存起来。这样&#xff0c;当我们需要解决原始问题的时候&#xff0c;我们就可以直接利…

用Redis实现全局唯一ID

全局唯一ID 如果使用数据库自增ID就存在一些问题&#xff1a; id的规律性太明显受表数据量的限制 全局ID生成器&#xff0c;是一种在分布式系统下用来生成全局唯一ID的工具&#xff0c;一般要满足下列特性&#xff1a; 唯一性高可用递增性安全性高性能 为了增加ID的安全性…

Linux第15步_安装FTP客户端

安装完FTP服务器后&#xff0c;还需要安装FTP客户端&#xff0c;才可以实现Ubuntu系统和Windows系统进行文件互传。 1、在STM32MP157开发板A盘基础资料\03软件中&#xff0c;找到“FileZilla_3.51.0_win64-setup.exe”&#xff0c;双击它&#xff0c;就可以安装。 2、点击“I …

How can I be sure that I am pulling a trusted image from docker?

1、Error response from daemon: manifest for jenkins:latest not found: manifest unknown: manifest unknown 2、Error response from daemon: pull access denied for nacos, repository does not exist or may require ‘docker login’: denied: requested access to th…

云服务器ECS搭建个人项目

一、登录云服务器ECS 在ECS实例的操作列中点击远程连接云服务器ECS&#xff0c;点击实例最右侧的远程连接按钮&#xff0c;并立即登录后会跳转至Workbench的登录页面。但是第一次进去不知道密码&#xff1f;可以重置密码 登录后可以看到如下页面&#xff0c;说明已经成功登录到…

开源项目 | 完整部署流程、一款开源人人可用的开源数据可视化分析工具

&#x1f4da; 项目介绍 在互联网数据大爆炸的这几年&#xff0c;各类数据处理、数据可视化的需求使得 GitHub 上诞生了一大批高质量的 BI 工具。 借助这些 BI 工具&#xff0c;我们能够大幅提升数据分析效率、生成更高质量的项目报告&#xff0c;让用户通过直观的数据看到结…

Spring Boot依赖版本声明

链接 官网 Spring Boot文档官网&#xff1a;​​​​​​https://docs.spring.io/spring-boot/docs/https://docs.spring.io/spring-boot/docs/ Spring Boot 2.0.7.RELEASE Spring Boot 2.0.7.RELEASE reference相关&#xff1a;https://docs.spring.io/spring-boot/docs/2.…

大学生搜题软件,未来可期吗?

作为一家专注于软件开发的公司《智创有术》&#xff0c;我们致力于为客户提供创新、高效和可靠的解决方案。通过多年的经验和专业知识&#xff0c;我们已经在行业内建立了良好的声誉&#xff0c;并赢得了客户的信任和支持。 支持各种源码&#xff0c;网站搭建&#xff0c;APP&a…

为什么说UUID是唯一的?

在数字时代&#xff0c;我们需要一种能够唯一标识各种实体的方法。通用唯一标识符&#xff08;UUID&#xff09;正是为满足这一需求而诞生的。本文将从多个方面介绍UUID&#xff0c;探讨它为何成为通用唯一标识符&#xff0c;以及为什么说UUID是唯一的。 UUID/GUID生成器 | 一…

基于多反应堆的高并发服务器【C/C++/Reactor】(中)在EventLoop中处理被激活的文件描述符的事件

文件描述符处理与回调函数 一、主要概念 反应堆模型&#xff1a;一种处理系统事件或网络事件的模型&#xff0c;当文件描述符被激活时&#xff0c;可以检测到文件描述符&#xff1a;在操作系统中&#xff0c;用于标识打开的文件、套接字等的一种数据类型 处理激活的文件描述符…

RK3568驱动指南|第九篇 设备模型-第111章 platform总线注册驱动流程实例分析实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…