MySQL做分布式锁

news2024/11/24 14:01:33

分布式锁mysql实现方式
方式1:唯一索引

  • 创建锁表,内部存在字段表示资源名及资源描述,同一资源名使用数据库唯一性限制。
  • 多个进程同时往数据库锁表中写入对某个资源的占有记录,当某个进程成功写入时则表示其获取锁成功
  • 其他进程由于资源字段唯一性限制插入失败陷入自旋并且失败重试。
  • 当执行完业务后持有该锁的进程则删除该表内的记录,此时回到步骤一。
    在这里插入图片描述
    表数据
create table `database_lock`(
	`id` BIGINT NOT NULL AUTO_INCREMENT,
	`resource` INT NOT NULL COMMENT '锁资源',
	`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
	PRIMARY KEY (`id`),
	UNIQUE KEY `resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

db.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/distribute_lock?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
user=root
password=123456
  • PropertiesReader
@Slf4j
public class PropertiesReader {

    // Properties缓存文件
    private static final Map<String, Properties> propertiesCache = new HashMap<String, Properties>();

    public static Properties getProperties(String propertiesName) throws IOException {
        if (propertiesCache.containsKey(propertiesName)) {
            return propertiesCache.get(propertiesName);
        }
        loadProperties(propertiesName);
        return propertiesCache.get(propertiesName);
    }

    private synchronized static void loadProperties(String propertiesName) throws IOException {
        FileReader fileReader = null;
        try {
            // 创建Properties集合类
            Properties pro = new Properties();
            // 获取src路径下的文件--->ClassLoader类加载器
            ClassLoader classLoader = PropertiesReader.class.getClassLoader();
            URL resource = classLoader.getResource(propertiesName);
            // 获取配置路径
            String path = resource.getPath();
            // 读取文件
            fileReader = new FileReader(path);
            // 加载文件
            pro.load(fileReader);
            // 初始化
            propertiesCache.put(propertiesName, pro);
        } catch (IOException e) {
            log.error("读取Properties文件失败,Properties名为:" + propertiesName);
            throw e;
        } finally {
            try {
                if (fileReader != null) {
                    fileReader.close();
                }
            } catch (IOException e) {
                log.error("fileReader关闭失败!", e);
            }
        }
    }
}
  • JDBCUtils
@Slf4j
public class JDBCUtils {
    private static String url;
    private static String user;
    private static String password;

    static {
        //读取文件,获取值
        try {
            Properties properties = PropertiesReader.getProperties("db.properties");
            url = properties.getProperty("url");
            user = properties.getProperty("user");
            password = properties.getProperty("password");
            String driver = properties.getProperty("driver");
            //4.注册驱动
            Class.forName(driver);
        } catch (IOException | ClassNotFoundException e) {
            log.error("初始化jdbc连接失败!", e);
        }
    }

    /**
     * 获取连接
     * @return 连接对象
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }

    /**
     * 释放资源
     * @param rs
     * @param st
     * @param conn
     */
    public static void close(ResultSet rs, Statement st, Connection conn) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (st != null) {
            try {
                st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

数据库操作类

/**
 * MySQL 锁操作类(加锁+释放锁)
 */
@Slf4j
public class MySQLDistributedLockService {

    private static Connection connection;
    private static Statement statement;
    private static ResultSet resultSet;

    static{
        try {
            connection = JDBCUtils.getConnection();
            statement = connection.createStatement();
            resultSet = null;
        } catch (SQLException e) {
            log.error("数据库连接失败!");
        }
    }

    /**
     * 锁表 - 获取锁
     * @param resource      资源
     * @param description   锁描述
     * @return  是否操作成功
     */
    public static boolean tryLock(int resource,String description){
        String sql = "insert into database_lock (resource,description) values (" + resource + ", '" + description + "');";
        //获取数据库连接
        try {
            int stat = statement.executeUpdate(sql);
            return stat == 1;
        } catch (SQLException e) {
            return false;
        }
    }
    /**
     * 锁表-释放锁
     * @return
     */
    public static boolean releaseLock(int resource) throws SQLException {
        String sql = "delete from database_lock where resource = " + resource;
        //获取数据库连接
        int stat = statement.executeUpdate(sql);
        return stat == 1;
    }
    /**
     * 关闭连接
     */
    public static void close(){
        log.info("当前线程: " + ManagementFactory.getRuntimeMXBean().getName().split("@")[0] +
                ",关闭了数据库连接!");
        JDBCUtils.close(resultSet,statement,connection);
    }
}

LockTable

/**
 * mysql分布式锁
 *      执行流程: 多进程抢占数据库某个资源,然后执行业务,执行完释放资源
 *      锁机制: 单一进程获取锁时,则其他进程提交失败
 */
@Slf4j
public class LockTable extends Thread {
    @Override
    public void run() {
        super.run();
        //获取Java虚拟机的进程ID
        String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        try{
            while(true){
                log.info("当前进程PID:" + pid + ",尝试获取锁资源!");
                if(MySQLDistributedLockService.tryLock(1,"测试锁")){
                    log.info("当前进程PID:" + pid + ",获取锁资源成功!");
                    //sleep模拟业务处理过程
                    log.info("开始处理业务!");
                    Thread.sleep(10*1000);
                    log.info("业务处理完成!");
                    MySQLDistributedLockService.releaseLock(1);
                    log.info("当前进程PID: " + pid + ",释放了锁资源!");
                    break;
                }else{
                    log.info("当前进程PID: " + pid + ",获取锁资源失败!");
                    Thread.sleep(2000);
                }
            }
        }catch (Exception e){
            log.error("抢占锁发生错误!",e);
        }finally {
            MySQLDistributedLockService.close();
        }
    }
    // 程序入口
    public static void main(String[] args) {
        new LockTable().start();
    }
}

测试
运行时开启并行执行选项,每次运行三个或三个以上进程. Allow parallel run 运行并行执行
image.png
image.png
注意事项:

  • 该锁为非阻塞的
  • 当某进程持有锁并且挂死时候会造成资源一直不释放的情况,造成死锁,因此需要维护一个定时清理任务去清理持有过久的锁
  • 要注意数据库的单点问题,最好设置备库,进一步提高可靠性
  • 该锁为非可重入锁,如果要设置成可重入锁需要添加数据库字段记录持有该锁的设备信息以及加锁次数

方式二:基于乐观锁

  • 每次执行业务前首先进行数据库查询,查询当前的需要修改的资源值(或版本号)。
  • 进行资源的修改操作,并且修改前进行资源(或版本号)的比对操作,比较此时数据库中的值是否和上一步查询结果相同。
  • 查询结果相同则修改对应资源值,不同则回到第一步。
    image.png

例子:数据库中设定某商品基本信息(名为外科口罩,数量为10),多进程对该商品进行抢购,当商品数量为0时结束抢购。
在这里插入图片描述
代码实现

    /**
     * 乐观锁-获取资源
     * @param id 资源ID
     * @return result
     */
    public static ResultSet getGoodCount(int id) throws SQLException {

        String sql = "select  * from  database_lock_2 where id = " + id;

        //查询数据
        resultSet = statement.executeQuery(sql);
        return resultSet;
    }


    /**
     * 乐观锁-修改资源
     * @param id        资源ID
     * @param goodCount 资源
     * @return  修改状态
     */
    public static boolean setGoodCount(int id, int goodCount) throws SQLException {

        String sql = "update database_lock_2 set good_count = good_count - 1 where id =" + id +"  and good_count = " + goodCount;

        int stat = statement.executeUpdate(sql);
        return stat == 1;
    }

    /**
     * 乐观锁-开启事务自动提交
     */
    public static void AutoCommit(){
        try {
            connection.setAutoCommit(true);
        } catch (SQLException e) {
            log.error("开启自动提交!",e);
        }
    }

OptimisticLock测试类

/**
 * mysql分布式锁-乐观锁
 *  执行流程: 多进程抢购同一商品,每次抢购成功商品数量-1,商品数据量为0时退出
 *  锁机制: 单一进程获取锁时,则其他进程提交失败
 */
@Slf4j
public class OptimisticLock extends Thread{

    @Override
    public void run() {
        super.run();
        String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];

        ResultSet resultSet = null;
        String goodName = null;
        int goodCount = 0;
        try {
            while(true){
                log.info("当前线程:" + pid + ",开始抢购商品!");
                //获取当前商品信息
                resultSet = MySQLDistributedLockService.getGoodCount(1);
                while (resultSet.next()){
                    goodName = resultSet.getString("good_name");
                    goodCount = resultSet.getInt("good_count");
                }
                log.info("获取库存成功,当前商品名为:" + goodName + ",当前库存剩余量为:" + goodCount);
                //模拟执行业务操作
                Thread.sleep(2*3000);
                if(0 == goodCount){
                    log.info("抢购失败,当前库存为0! ");
                    break;
                }
                //修改库存信息,库存量-1
                if(MySQLDistributedLockService.setGoodCount(1,goodCount)){
                    log.info("当前线程:" + pid + " 抢购商品:" + goodName + "成功,剩余库存为:" + (goodCount -1));
                    //模拟延迟,防止锁每次被同一进程获取
                    Thread.sleep(2 * 1000);
                }else{
                    log.error("抢购商品:" + goodName +"失败,商品数量已被修改");
                }
            }
        }catch (Exception e){
            log.error("抢购商品发生错误!",e);
        }finally {
            if(resultSet != null){
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                    log.error("关闭Result失败!" , e);
                }
            }
            MySQLDistributedLockService.close();
        }
    }

    public static void main(String[] args) {
        new OptimisticLock().start();
    }
}

代码测试
开启三个进程,查看执行情况
image.png
image.png
image.png
注意事项:

  • 该锁为非阻塞的
  • 该锁对于业务具有侵入式,如果设置版本号校验则增加的额外的字段,增加了数据库冗余
  • 当并发量过高时会有大量请求访问数据库的某行记录,对数据库造成很大的写压力
  • 因此乐观锁适用于并发量不高,并且写操作不频繁的场景

方式三:悲观锁实现方式(利用事务加上行/表锁)
在这里插入图片描述
实现思路

  • 关闭jdbc连接自动commit属性
  • 每次执行业务前先使用查询语句后接for update表示锁定该行数据(注意查询条件如果未命中主键或索引,此时将会从行锁变为表锁)
  • 执行业务流程修改表资源
  • 执行commit操作

代码实现
MySQLDistributedLockService


    /**
     * 悲观锁-获取资源
     * @param id 资源ID
     * @return result
     */
    public static ResultSet getGoodCount2(int id) throws SQLException {
        String sql = "select  * from  database_lock_2 where id = " + id + "for update";
        //查询数据
        resultSet = statement.executeQuery(sql);
        return resultSet;
    }

    /**
     * 悲观锁-修改资源
     * @param id        资源ID
     * @return  修改状态
     */
    public static boolean setGoodCount2(int id) throws SQLException {

        String sql = "update database_lock_2 set good_count = good_count - 1 where id =" + id;
        int stat = statement.executeUpdate(sql);
        return stat == 1;
    }

    /**
     * 悲观锁-关闭事务自动提交
     */
    public static void closeAutoCommit(){
        try {
            connection.setAutoCommit(false);
        } catch (SQLException e) {
            log.error("关闭自动提交失败!",e);
        }
    }

    /**
     * 悲观锁-提交事务
     */
    public static void commit(String pid,String goodName,int goodCount) throws SQLException {
        connection.commit();
        log.info("当前线程:" + pid + "抢购商品: " + goodName + "成功,剩余库存为:" + (goodCount-1));
    }

    /**
     * 悲观锁-回滚
     */
    public static void rollBack() throws SQLException {
        connection.rollback();
    }

PessimisticLock

/**
 * mysql 分布式锁-悲观锁
 *     执行流程:多个进程抢占同一个商品,执行业务完毕则通过connection.commit() 释放锁
 *     锁机制:单一进程获取锁时,则其他进程将阻塞等待
 */
@Slf4j
public class PessimisticLock extends Thread {
    @Override
    public void run() {
        super.run();
        ResultSet resultSet = null;
        String goodName = null;
        int goodCount = 0;
        String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        //关闭自动提交
        MySQLDistributedLockService.closeAutoCommit();
        try{
            while(true){
                log.info("当前线程:" + pid + "");
                //获取库存
                resultSet = MySQLDistributedLockService.getGoodCount2(1);
                while (resultSet.next()) {
                    goodName = resultSet.getString("good_name");
                    goodCount = resultSet.getInt("good_count");
                }
                log.info("获取库存成功,当前商品名称为:" + goodName + ",当前库存剩余量为:" + goodCount);
                // 模拟执行业务事件
                Thread.sleep(2 * 1000);
                if (0 == goodCount) {
                    log.info("抢购失败,当前库存为0!");
                    break;
                }
                // 抢购商品
                if (MySQLDistributedLockService.setGoodCount2(1)) {
                    // 模拟延时,防止锁每次被同一进程获取
                    MySQLDistributedLockService.commit(pid, goodName, goodCount);
                    Thread.sleep(2 * 1000);
                } else {
                    log.error("抢购商品:" + goodName + "失败!");
                }
            }
        }catch (Exception e){
            //抢购失败
            log.error("抢购商品发生错误!",e);
            try {
                MySQLDistributedLockService.rollBack();
            } catch (SQLException ex) {
                log.error("回滚失败! ",e);
            }
        }finally {
            if(resultSet != null){
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    log.error("Result关闭失败!",e);
                }
            }
            MySQLDistributedLockService.close();
        }
    }

    public static void main(String[] args) {
        new PessimisticLock().start();
    }
}

测试结果
image.png
image.png
image.png
注意事项:

  • 该锁为阻塞锁
  • 每次请求存在额外加锁的开销
  • 在并发量很高的情况下会造成系统中存在大量阻塞的请求,影响系统的可用性
  • 因此悲观锁适用于并发量不高,读操作不频繁的写场景

总结:

  • 在实际使用中,由于受到性能以及稳定性约束,对于关系型数据库实现的分布式锁一般很少被用到。但是对于一些并发量不高、系统仅提供给内部人员使用的单一业务场景可以考虑使用关系型数据库分布式锁,因为其复杂度较低,可靠性也能够得到保证。

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

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

相关文章

iOS——Block two

Block 的实质究竟是什么呢&#xff1f;类型&#xff1f;变量&#xff1f;还是什么黑科技&#xff1f; Blocks 是 带有局部变量的匿名函数 Blocks 由 OC 转 C 源码方法 在项目中添加 blocks.m 文件&#xff0c;并写好 block 的相关代码。打开「终端」&#xff0c;执行 cd XX…

Windows 使用 Linux 子系统,轻轻松松安装多个linux

Windows Subsystem for Linux WSL 简称WSL,是一个在Windows 10\11上能够运行原生Linux二进制可执行文件&#xff08;ELF格式&#xff09;的兼容层。它是由微软与Canonical公司合作开发&#xff0c;其目标是使纯正的Ubuntu、Debian等映像能下载和解压到用户的本地计算机&#…

【计算机网络】NAT及Bridge介绍

OSI七层模型 七层模型介绍及举例 为通过网络将人类可读信息通过网络从一台设备传输到另一台设备&#xff0c;必须在发送设备沿 OSI 模型的七层结构向下传输数据&#xff0c;然后在接收端沿七层结构向上传输数据。 数据在 OSI 模型中如何流动 库珀先生想给帕尔梅女士发一封电…

Vue + el-upload阿里云文件上传进度条功能,阿里云文件上传切片进度条功能实现,el-upload文件上传真实进度条

el-upload阿里云文件上传进度条功能&#xff0c;进度条切片功能&#xff0c;做一个记录&#xff0c;此功能查询过其他文档&#xff0c;最终都还是实现不了&#xff08;这是真实进度条&#xff0c;就是根据文件上传过程中文件切片实现的&#xff09; 效果&#xff1a; <t…

电脑黑屏按什么键恢复?掌握这3个按键,轻松解决!

“电脑怎么会突然就黑屏呢&#xff1f;我看综艺看得好好的莫名其妙电脑就黑屏了&#xff0c;怎么按都没有用。电脑黑屏应该怎么解决呢&#xff1f;大佬快帮帮我&#xff01;” 对于电脑黑屏的问题&#xff0c;很多小伙伴应该都不陌生吧。在实际使用电脑的操作中&#xff0c;或许…

干货 | 三款民宿App会员评测,你都用过吗?

为了留存用户&#xff0c;许多旅游住宿类APP都开通了会员权益&#xff0c;对老用户来说&#xff0c;会员等级的提升会为他们带来更多的权益及优惠&#xff0c;那么问题来了&#xff0c;到底哪家的会员优惠更多呢&#xff1f; 根据第三方统计的下载数据来看&#xff0c;APP STO…

云计算与大数据领域新指南 | 《揭秘云计算与大数据》助您驾驭数字化浪潮!

日前&#xff0c;《揭秘云计算与大数据》正式上市。这本由国际知名的技术专家撰写的书籍&#xff0c;将带领读者深入了解云计算和大数据领域的技术前沿和应用趋势&#xff0c;为读者呈现一个全面而深入的视角。 随着信息技术的飞速发展&#xff0c;云计算和大数据作为两大前沿…

iOS——Block回调

先跟着我实现最简单的 Block 回调传参的使用&#xff0c;如果你能举一反三&#xff0c;基本上可以满足了 OC 中的开发需求。已经实现的同学可以跳到下一节。 首先解释一下我们例子要实现什么功能&#xff08;其实是烂大街又最形象的例子&#xff09;&#xff1a; 有两个视图控…

自问自答----WEB篇

目录 1、https和http协议的端口 2、http协议的版本 3、linux中查看报文的方法 3.1 curl www.baidu.com -v 3.2 wget --debug www.baidu.com 4、http有哪些请求方法 5、http的请求头 6、http响应头​编辑 7、状态码有哪些 8、uri和url 9、静态&#xff1f;动态&#x…

第九章 HL7 架构和可用工具 - 查看批量消息

文章目录 第九章 HL7 架构和可用工具 - 查看批量消息查看批量消息Class类有关 HL7 消息类别的详细信息TypeCategoryName 第九章 HL7 架构和可用工具 - 查看批量消息 查看批量消息 如果消息是一组批处理格式的 HL7 消息&#xff0c;而不是单个 HL7 消息&#xff0c;则 HL7 文档…

三星书画联展:三位艺术家开启国风艺术之旅

7月22日&#xff0c;由广州白云区文联、白云区工商联主办的“三星书画联展”&#xff0c;在源美术馆正式开展。本次书画展展出的艺术种类丰富&#xff0c;油画、国画、彩墨画、书法等作品异彩纷呈。广东省政协原副主席、农工党省委书画院名誉院长马光瑜&#xff0c;意大利艺术研…

小程序安全性加固:如何保护用户数据和防止恶意攻击

第一章&#xff1a;引言 在当今数字化时代&#xff0c;移动应用程序的使用已经成为人们日常生活中的重要组成部分。小程序作为一种轻量级的应用程序形式&#xff0c;受到了广泛的欢迎。然而&#xff0c;随着小程序的流行&#xff0c;安全性问题也日益凸显。用户数据泄露和恶意攻…

SpringBoot项目修改中静态资源,只需刷新页面无需重启项目(附赠—热加载)

初衷 &#x1f4a2;初衷&#x1f4a2; 因为一遍遍修改并重启项目觉得很麻烦&#xff0c;所以刚开始就自己给项目配置了热加载&#xff0c;但奈何代码更新还是慢&#xff0c;还不如我重启一遍项目的速度&#xff0c;所以放弃了自己上网找到的热加载配置。直到我debugger前端代码…

星戈瑞 | DSPE-PEG-CY5荧光成像技术在生物医学研究中的重要性

DSPE-PEG-CY5荧光成像技术在生物医学研究中具有重要性&#xff0c;其在成像和标记方面的优势为生物学和医学领域提供了强大的工具。以下是DSPE-PEG-CY5荧光成像技术在生物医学研究中的重要性&#xff1a; 1. 非侵入性成像&#xff1a;DSPE-PEG-CY5荧光成像技术是一种非侵入性的…

面试必考精华版Leetcode2095. 删除链表的中间节点

题目&#xff1a; 代码&#xff08;首刷看解析 day22&#xff09;&#xff1a; class Solution { public:ListNode* deleteMiddle(ListNode* head) {if(head->nextnullptr) return nullptr;ListNode *righthead;ListNode *lefthead;ListNode *NodeBeforeLeft;while(right!n…

【NLP】训练chatglm2的评价指标BLEU,ROUGE

衡量模型输出的描述语句的准确性&#xff0c;因为衡量的对象是一个个的自然语言文本&#xff0c;所以通常会选择自然语言处理领域的相关评价指标。这些指标原先都是用来度量机器翻译结果质量的&#xff0c;并且被证明可以很好的反映待评测语句的准确性&#xff0c;主要包含4种&…

IDEA中maven项目失效,pom.xml文件橙色/橘色

IDEA中maven项目失效&#xff0c;pom.xml文件橙色/橘色 IDEA中Maven项目失效 IDEA中创建的maven项目中的文件夹都变成普通格式&#xff0c;pom.xml变成橙色 右键点击橙色的pom.xml文件&#xff0c;选择add as maven project maven项目开始重新导入相应依赖&#xff0c;恢复…

智驾「完整数据闭环」受困,谁能蹚出一条最佳路线?

城市NOA&#xff0c;标志着自动驾驶商业化的一座里程碑&#xff0c;也意味着智能汽车下半场的开端。 自2023年上海车展以来&#xff0c;有关城市NOA的路线之争逐渐明晰&#xff0c;“重感知&#xff0b;轻地图”、借助纯感知和融合感知路线、以及BEVTransformer模型的智能驾驶…

为什么有的时候Socks5比HTTP爬虫IP更快?

作为一名长期从事爬虫行业动态IP解决方案服务商&#xff0c;我发现经常有客户疑惑&#xff1a;为什么有的时候Socks5代理IP比HTTP代理IP更快&#xff1f;其实&#xff0c;这个是很常见的一种现象。今天&#xff0c;我就来分析一下为什么Socks5代理IP有时比HTTP代理IP更快&#…

基于django社区物品转让平台-计算机毕设 附源码40507

django社区物品转让平台 目 录 摘 要 Abstract 第1章 前 言 1.1 研究背景 1.2 研究现状 1.3 系统开发目标 第2章 技术与原理 2.1 开发技术说明 2.2 B/S体系工作原理 2.3 Django框架介绍 6 2.4 MySQL数据库 7 2.5 Ajax应用 8 第3章 需求分析 9 3.1 需…