分布式锁的三种实现方式:Redis、基于数据库和Zookeeper

news2024/11/24 19:21:44

分布式锁的实现

  • 操作共享资源:例如操作数据库中的唯一用户数据、订单系统、优惠券系统、积分系统等,这些系统需要修改用户数据,而多个系统可能同时修改同一份数据,这时就需要使用分布式锁来控制访问,防止数据不一致。
  • 在电商系统中,如果多个用户同时购买同一商品,可能会出现超卖现象。通过使用分布式锁,可以确保在同一时间只有一个用户能够进行购买操作,从而避免库存超卖的问题。‌
  • 防止重复调用第三方接口:在分布式系统中,如果多个节点同时调用同一个第三方接口,可能会导致接口调用失败或数据错误。使用分布式锁可以确保在同一时间只有一个节点进行接口调用,避免重复调用问题。

转自:https://lingkang.top/archives/lock333

Redis

当客户端需要获取锁时,向Redis发送SETNX命令,如果返回1,说明客户端获得了锁;如果返回0,则说明锁已被其他客户端占用。当客户端释放锁时,使用DEL命令删除对应的键即可。

1、Maven中添加依赖

<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-core</artifactId>
  <version>5.8.27</version>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.9.3</version>
</dependency>

2、启动好Redis

在这里插入图片描述

window可以在这里下载一个:https://gitee.com/lingkang_top/redis-window

3、编写java代码

package redis;

import cn.hutool.core.thread.ThreadUtil;
import redis.clients.jedis.Jedis;

import java.util.ArrayList;
import java.util.List;

/**
 * @author lingkang
 * @create by 2024/7/25 15:28
 */
public class Demo01 {
    /**
     * 假设是库存
     */
    private static int number = 100;

    public static void main(String[] args) {
        // 提前初始化好redis连接
        List<Jedis> redis = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Jedis jedis = new Jedis("localhost", 6379);
            redis.add(jedis);
        }

        // 操作前先清空锁
        redis.get(0).del("lock");

        // 假设有10个线程进行锁操作
        for (int i = 0; i < 10; i++) {
            final Jedis jedis = redis.get(i);
            new Thread(() -> {
                // 每隔线程减扣 5次
                for (int j = 0; j < 5; j++)
                    dd(jedis);

                jedis.close();
            }, "t-" + i).start();
        }

        ThreadUtil.sleep(15000);
        // 应该输出 100-50=50
        System.out.println("库存: " + number);
    }

    /**
     * 业务处理
     */
    private static void dd(Jedis jedis) {
        for (; ; ) {
            long lock = jedis.setnx("lock", "1");
            if (lock == 1) {
                // 给key设置一个过期时间,防止死锁
                jedis.expire("lock", 20);
                // 获取到锁
                System.out.println("当前线程获得锁:" + Thread.currentThread().getName());
                // 进行减扣库存等一系列操作....
                number--;
                // 假设处理业务延迟一下
                ThreadUtil.sleep(200);

                // 处理完毕要移除锁
                jedis.del("lock");
                break;
            }
            // 等待一下
            ThreadUtil.sleep(200);
        }
    }
}

4、结果正确

在这里插入图片描述

基于数据库

基于数据库的分布式锁主要依赖于数据库的唯一索引或主键约束。具体实现时,当客户端需要获取锁时,向数据库中插入一条记录,该记录的唯一键表示锁。如果插入成功,说明客户端获得了锁;如果插入失败(如主键冲突),则说明锁已被其他客户端占用。当客户端释放锁时,删除该记录即可。

1、Maven中添加依赖

<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-core</artifactId>
  <version>5.8.27</version>
</dependency>
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <version>9.0.0</version>
</dependency>

2、添加表格

CREATE TABLE `mylock` (
  `id` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `create_time` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

3、编写java代码

package mysql;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.thread.ThreadUtil;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author lingkang
 * @create by 2024/7/25 16:58
 */
public class Demo01 {
    /**
     * 假设是库存
     */
    private static int number = 100;

    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");
        String URL = "jdbc:mysql://localhost:3306/mylock?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai";
        // 提前初始化好redis连接
        List<Connection> connectionList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Connection conn = DriverManager.getConnection(URL, "root", "123456");
            connectionList.add(conn);
        }

        // 开始前,先将表表的锁数据清理
        connectionList.get(0).prepareStatement("delete from mylock where id='1'").executeUpdate();

        // 假设有10个线程进行锁操作
        for (int i = 0; i < 10; i++) {
            final Connection conn = connectionList.get(i);
            new Thread(() -> {
                // 每隔线程减扣 5次
                for (int j = 0; j < 5; j++) {
                    try {
                        dd(conn);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }

                IoUtil.close(conn);
            }, "t-" + i).start();
        }

        ThreadUtil.sleep(15000);
        // 应该输出 100-50=50
        System.out.println("库存: " + number);
    }

    /**
     * 业务处理
     */
    private static void dd(Connection conn) throws Exception {
        for (; ; ) {
            int success = 0;
            try {
                PreparedStatement statement = conn.prepareStatement("insert into mylock(id,create_time) values('1',now())");
                success = statement.executeUpdate();
            } catch (Exception e) {
            }

            if (success == 1) {
                // 获取到锁
                System.out.println("当前线程获得锁:" + Thread.currentThread().getName());
                // 进行减扣库存等一系列操作....
                number--;
                // 假设处理业务延迟一下
                ThreadUtil.sleep(200);

                // 处理完毕要移除锁
                PreparedStatement statement = conn.prepareStatement("delete from mylock where id='1'");
                statement.executeUpdate();
                statement.close();
                break;
            } else {
                ResultSet query = conn.prepareStatement("select create_time from mylock where id='1'").executeQuery();
                if (query.next()) {
                    Date date = query.getDate(1);
                    // 防止死锁,超过20秒删除
                    if (date.getTime() + 20000L > System.currentTimeMillis()) {
                        conn.prepareStatement(
                                "delete from mylock where id='1' and create_time='" + date.getTime() + "'"
                        ).executeUpdate();
                        // 等待一下
                        ThreadUtil.sleep(1000);
                    }
                }
                query.close();
            }
            // 等待一下
            ThreadUtil.sleep(200);
        }
    }
}
4、执行结果正确

在这里插入图片描述

Zookeeper

使用zookeeper有多钟方案,如下:

  • 每个客户端在zookeeper的一个指定目录下创建一个临时节点,通过判断当前目录下的所有节点与自己的节点的顺序,就可以确定自己是否获取到锁。
  • 每个客户端在zookeeper的一个指定目录下创建一个有序节点,通过判断自己节点在所有子节点中的顺序,就可以确定自己是否获取到锁。

第一种方案与数据库类似

Zookeeper 方案一

每个客户端在zookeeper的一个指定目录下创建一个临时节点,通过判断当前目录下的所有节点与自己的节点的顺序,就可以确定自己是否获取到锁。

1、Maven中添加依赖

<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-core</artifactId>
  <version>5.8.27</version>
</dependency>
<dependency>
  <groupId>org.apache.zookeeper</groupId>
  <artifactId>zookeeper</artifactId>
  <version>3.9.2</version>
</dependency>

2、找个Zookeeper启动好

3、java实现代码

package zk;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.thread.ThreadUtil;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @author lingkang
 * @create by 2024/7/25 18:17
 */
public class Demo01 {

    /**
     * 假设是库存
     */
    private static int number = 100;
    private static final String lockPath = "/lock";

    public static void main(String[] args) throws Exception {
        // 提前初始化好redis连接
        List<ZooKeeper> zooKeeperList = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            ZooKeeper zooKeeper = new ZooKeeper("10.8.4.191:2181", 20000, null);
            // 用来做一个初始化调用
            zooKeeper.exists(lockPath, false);
            zooKeeperList.add(zooKeeper);
        }

        // 操作前先清空锁
        ZooKeeper zk = zooKeeperList.get(0);
        Stat exists = zk.exists(lockPath, false);
        if (exists != null) {
            zk.delete(lockPath, exists.getVersion());
        }

        // 假设有2个线程进行锁操作
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            final ZooKeeper zooKeeper = zooKeeperList.get(i);
            Thread thread = new Thread(() -> {
                // 每隔线程减扣 5次
                for (int j = 0; j < 5; j++) {
                    try {
                        dd(zooKeeper);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }

                IoUtil.close(zooKeeper);
                // 注意此线程id,此线程id用于模拟全局唯一业务处理id,实际开发可以用具体业务id替代,必须全局唯一
            }, "t-" + i);
            thread.start();
            threadList.add(thread);
        }

        for (Thread thread : threadList)
            thread.join();

        ThreadUtil.sleep(1000);
        // 应该输出 100-5*2 --> 90
        System.out.println("库存: " + number);
    }

    /**
     * 业务处理
     */
    private static void dd(ZooKeeper zooKeeper) throws Exception {
        String threadName = Thread.currentThread().getName();
        for (; ; ) {
            Stat stat = zooKeeper.exists(lockPath, false);
            if (stat != null) {
                byte[] data = zooKeeper.getData(lockPath, null, stat);
                if (!threadName.equals(new String(data))) {
                    // 说明已经被其他服务获取锁了,等待一下跳过此次锁创建
                    ThreadUtil.sleep(300);// 等待一下
                    continue;
                }
            }

            try {
                // 创建者将会获得锁,注意:注意此线程id,此线程id用于模拟全局唯一业务处理id,
                // 实际开发可以用具体业务id替代,必须全局唯一
                zooKeeper.create(lockPath, threadName.getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL);
            } catch (Exception e) {
                // 创建锁失败
            }
            // 等待一下
            ThreadUtil.sleep(200);
            stat = zooKeeper.exists(lockPath, false);
            // 如果是当前session创建的,就获得锁
            if (stat == null) {
                continue;
            } else {
                byte[] data = zooKeeper.getData(lockPath, null, stat);
                if (!threadName.equals(new String(data))) {
                    // 说明已经被其他服务获取锁了,等待一下跳过此次锁创建
                    ThreadUtil.sleep(200);// 等待一下
                    continue;
                }
            }

            // 获取到锁
            System.out.println("当前线程获得锁:" + threadName);
            // 进行减扣库存等一系列操作....
            number--;
            // 假设处理业务延迟一下
            ThreadUtil.sleep(200);

            // 处理完毕要移除锁
            zooKeeper.delete(lockPath, stat.getVersion());

            // 处理完成
            break;
        }
    }
}
4、执行结果正确

在这里插入图片描述

Zookeeper 方案二(推荐)

每个客户端在zookeeper的一个指定目录下创建一个有序节点,通过判断自己节点在所有子节点中的顺序,就可以确定自己是否获取到锁。

1、Maven中添加依赖

<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-core</artifactId>
  <version>5.8.27</version>
</dependency>
<!-- 由于我的zk版本是3.4.8 版本较低,所以使用低版本curator-recipes -->
<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-recipes</artifactId>
  <version>2.13.0</version>
</dependency>

2、找个Zookeeper启动好

v3.4.8

3、java实现代码

package zk;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.thread.ThreadUtil;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.data.Stat;

import java.util.ArrayList;
import java.util.List;

/**
 * @author lingkang
 * @create by 2024/7/26 10:43
 */
public class Demo02 {

    /**
     * 假设是库存
     */
    private static int number = 100;
    private static final String lockPath = "/lock";

    public static void main(String[] args) throws Exception {
        // 提前初始化好zk连接
        List<CuratorFramework> curatorFrameworks = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(2000, 3);
            CuratorFramework client = CuratorFrameworkFactory.newClient("10.8.4.191:2181", retryPolicy);
            client.start();
            curatorFrameworks.add(client);
        }

        // 操作前先清空锁
        CuratorFramework curator = curatorFrameworks.get(0);
        Stat stat = curator.checkExists().forPath(lockPath);
        if (stat != null)
            curator.delete().withVersion(stat.getVersion()).forPath(lockPath);

        // 假设有JVM进行锁操作
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            final CuratorFramework curatorFramework = curatorFrameworks.get(i);
            Thread thread = new Thread(() -> {
                // 每个线程减扣 5次
                for (int j = 0; j < 5; j++) {
                    try {
                        dd(curatorFramework);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }, "t-" + i);
            threadList.add(thread);
            thread.start();
        }
        for (Thread thread : threadList)
            thread.join();

        ThreadUtil.sleep(1000);
        for (CuratorFramework curatorFramework : curatorFrameworks)
            IoUtil.close(curatorFramework);
        System.out.println("-------------------------------------------------------------------");
        // 应该输出 100-5*2 --> 90
        System.out.println("库存: " + number);
    }

    /**
     * 业务处理
     */
    private static void dd(CuratorFramework curatorFramework) {
        String threadName = Thread.currentThread().getName();
        InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
        try {
            lock.acquire();

            // 获取到锁,执行业务逻辑
            System.out.println("当前线程获得锁:" + threadName);
            // 进行减扣库存等一系列操作....
            number--;
            // 假设处理业务延迟一下
            ThreadUtil.sleep(200);
        } catch (Exception e) {
            // 处理异常
            e.printStackTrace();
        } finally {
            try {
                lock.release();
                // 释放锁
            } catch (Exception e) {
                // 处理释放锁时的异常
                e.printStackTrace();
            }
        }
    }
}
4、执行结果正确

在这里插入图片描述

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

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

相关文章

一步步教你学会如何安装VMare虚拟机(流程参考图)

前言&#xff1a;一步步教你安装VMare虚拟机&#xff08;此版本为17.5。2版本&#xff09;。 1、安装 2、确认协议 3、选择位置存放 4、选择第二个 5、都不选。 6、都选提供便捷操作 7、点击许可证&#xff0c;将密钥输入&#xff08;可以在网络寻找自己版本的密钥&#xff…

学好C++之——函数重载、缺省参数、内联函数

函数重载、缺省参数、内联函数都是C不同于C语言的知识点&#xff0c;简单轻松&#xff0c;这里就放到一篇来讲—— 目录 1.缺省参数 1.1什么是缺省参数&#xff1f; 1.2为什么需要缺省参数&#xff1f; 1.3缺省参数的使用规则 2.函数重载 参数类型不同&#xff1a; 参数个…

错误代码0x80070035是什么情况?针对错误代码0x80070035的解决方法

错误代码 0x80070035 通常与网络连接和文件共享有关&#xff0c;表示“找不到网络路径”。这个问题可能由多种原因引起&#xff0c;包括网络设置不当、服务未启动、注册表配置错误等。今天这篇文章就和大家分享几种针对错误代码0x80070035的解决方法。 针对错误代码0x80070035问…

Linux权限维持篇

目录 SSH后门 &#xff08;1&#xff09;软链接sshd &#xff08;2&#xff09;SSH Key 生成公私钥 创建个authorized_keys文件来保存公钥 通过修改文件时间来隐藏authorized_keys &#xff08;3&#xff09;SSH Keylogger&#xff08;记录日志&#xff09; Linux的PA…

vue 给特定满足条件的表单数据添加背景颜色,组件的 row-class-name

1、:row-class-name"tableRowClassName" 可为表格每行根据后面的函数绑定class名 <!-- 列表框 --><div class"tableList"><el-table :data"teamModelListTable" style"width: 100%"selection-change"handleSele…

基于Python的哔哩哔哩国产动画排行数据分析系统

需要本项目的可以私信博主&#xff0c;提供完整的部署、讲解、文档、代码服务 随着经济社会的快速发展&#xff0c;中国影视产业迎来了蓬勃发展的契机&#xff0c;其中动漫产业发展尤为突出。中国拥有古老而又璀璨的文明&#xff0c;仅仅从中提取一部分就足以催生出大量精彩的…

数字图像处理和机器视觉中的常用特殊矩阵及MATLAB实现详解

一、前言 Matlab的名称来源于“矩阵实验室&#xff08;Matrix Laboratory&#xff09;”&#xff0c;其对矩阵的操作具有先天性的优势&#xff08;特别是相对于C语言的数组来说&#xff09;。在数字图像处理和机器视觉实践中&#xff0c;为了提高编程效率&#xff0c;MATLAB 提…

用Java手写jvm之实现查找class

写在前面 完成类加载器加载class的三阶段&#xff0c;加载&#xff0c;解析&#xff0c;初始化中的加载&#x1f600;&#x1f600;&#x1f600; 源码 。 jvm想要运行class&#xff0c;是根据类全限定名称来从特定的位置基于类加载器来查找的&#xff0c;分别如下&#xff1a;…

MySQL常见指令

MySQL中的数据类型 大致分为五种&#xff1a;数值&#xff0c;日期和时间&#xff0c;字符串&#xff0c;json&#xff0c;空间类型 每种类型也包括也一些不同的子类型&#xff0c;根据需要来选择。 如数值类型包括整数类型和浮点数类型 整数类型根据占用的存储空间的不同 又…

Javascript 沙漏图案(Hour-glass Pattern)

给定正整数 n&#xff0c;以沙漏形式打印数字模式。示例&#xff1a; 输入&#xff1a;rows_no 7 输出&#xff1a; 1 2 3 4 5 6 7 2 3 4 5 6 7 3 4 5 6 7 4 5 6 7 5 6 7 6 7 7 6 7 5 6 7 4 5 6 7 3 4 5 6 7 2 3 4 5 6 7 1 2 3 4 5 6…

指针的面试题

这里写目录标题 判断链表中是否有环描述代码检测链表中是否存在环链表中存在环想检测链表中是否存在环&#xff0c;而不需要找到环的入口 判断链表中是否有环 题目 描述 判断给定的链表中是否有环。如果有环则返回true&#xff0c;否则返回false。 数据范围&#xff1a;链表…

Java语言程序设计——篇九(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 内部类 概述内部类—内部类的分类成员内部类实战演练 局部内部类实战演练 匿名内部类实战演练 静态内部类实战演练 概述 内部类或嵌套类&#…

不支持jdk8的jenkins部署jdk8项目

1、背景 目前最新的jenkins必须基于jdk8以上&#xff0c;才能安装。jenkins最新的插件部分也不支持jdk8了。 2、全局工具配置 配置一个jdk8 配置一个jdk8以上的版本&#xff0c;如jdk17 3、部署maven项目 jdk17项目 可以直接使用maven插件&#xff0c;部署。 jdk8项目 由…

Zenario CMS 9.2 文件上传漏洞(CVE-2022-23043)

前言 CVE-2022-23043 是一个影响 Zenario CMS 9.2 的严重漏洞。该漏洞允许经过身份验证的管理员用户绕过文件上传限制。具体来说&#xff0c;管理员可以通过创建一个新的带有 ".phar" 扩展名的“文件/MIME 类型”&#xff0c;然后上传一个恶意文件。在上传过程中&am…

运维锅总详解NFS

NFS是什么&#xff1f;如何对NFS进行部署及优化&#xff1f;NFS工作流程是什么&#xff1f;NFS的性能及优缺点是什么&#xff1f;NFS发展历史又是怎样的&#xff1f;希望本文能帮您解答这些疑惑&#xff01; 一、NFS简介 NFS (Network File System) 是由 Sun Microsystems 在…

【最新】cudnn安装教程

最近换了新电脑需要重新安装cuda和cudnn&#xff0c;发现现在cudnn的安装比以前方便多了&#xff0c;直接在官网下载exe安装包一键运行即可。安装的时候注意cuda和cudnn的对应关系即可&#xff1a;【最新】cuda和cudnn和显卡驱动的对应关系-CSDN博客 访问cudnn下载链接cuDNN 9…

docker-compose 根据yaml拉取镜像出问题

在学习go微服务时&#xff0c;用docker-compose启动nacos以及对应的mysql时出现上面的问题&#xff0c; 使用的yaml如下 version: "3.8" services:nacos:image: nacos/nacos-server:${NACOS_VERSION}container_name: nacos-standalone-mysqlenv_file:- ../env/cust…

SpringCloud+Vue3多对多,多表联查

♥️作者&#xff1a;小宋1021 &#x1f935;‍♂️个人主页&#xff1a;小宋1021主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识&#xff0c;和大家一起努力呀&#xff01;&#xff01;&#xff01; &#x1f388;&#x1f388;加油&#xff01; 加油&#xff01…

c/c++的内存管理(超详细)

一、c/c的内存分布 这是操作系统中对于内存的划分&#xff1a; 我们重点掌握以下几个区域即可&#xff1a; 1.栈 (调用函数会建立栈帧) 2.堆(动态开辟的空间) 3.数据段(静态区)&#xff1a;存放静态变量以及全局变量 4.代码段 (常量区) 先来看看一个题目&#xff1a; int…

JDK的配置

安装好JDK后&#xff0c;配置三个环境变量 第一步&#xff0c;配置JAVA_HOME. 先找到JDK的安装目录&#xff0c;然后复制路径&#xff0c;在电脑的环境变量里增添变量名为JAVA_HOME,变量值为 C:\Program Files\Java\jdk1.8.0_192。&#xff08;具体根据你的JDK安装路径&…