【MySQL】优雅的使用MySQL实现分布式锁

news2024/12/19 7:13:24

MySQL实现分布式锁

  • 引言
  • 二、基于唯一索引
    • 2.1、实现思路
    • 2.2、代码实现
    • 2.3、 测试代码
    • 2.4、小结
  • 三、基于悲观锁
    • 3.1 、实现思路
    • 3.2、代码实现
    • 3.3、测试代码
    • 3.4、小结
  • 四、基于乐观锁
    • 4.1 、实现思路
    • 4.2 、代码实现
    • 4.3 、测试代码
    • 4.4、小结
  • 总结

引言

在文章《Redis实现分布式锁详细方法》详细的讲解了Redis实现分布式锁的过程,如果项目中没有引用Redis也可以基于数据库来实现一个简单的分布式锁了,基于数据库实现分布式锁主要有三种方式,基于数据库唯一索引、基于数据库悲观锁和基于数据库乐观锁,接下来将详细介绍这三种方式实现的具体步骤。

二、基于唯一索引

2.1、实现思路

我们知道数据库表中的唯一索引可以确保一张表中相同数据只能插入一次,基于这条规则我们可以创建一张表,然后给锁名字段创建一个唯一索引,当并发插入时如果插入成功就获取到锁,插入失败就未获取到锁,释放锁就是把数据这条数据删除。

创建union_key_lock表:

CREATE TABLE `union_key_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `lock_name` varchar(255) NOT NULL DEFAULT '',
  `expire_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '锁过期时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_key_name` (`lock_name`) USING BTREE
) ENGINE=InnoDB  COMMENT='唯一键实现分布式锁'

union_key_lock表中将锁名字段lock_name添加唯一索引,expire_at为锁过期时间,可以在Mysql中或者项目中添加定时任务删除expire_at<now()的数据,防止代码出现异常未及时释放锁导致死锁。

2.2、代码实现

基于数据库唯一索引代码实现起来是非常简单的,有两个方法,第一个方法是lock(),接收一个锁名参数和锁超时时间参数,第二个方法是unLock()释放锁方法:

import cn.hutool.core.date.DateUtil;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * @author hanson.huang
 * @version V1.0
 * @ClassName UnionKeyLockImpl
 * @date 2024/12/18 16:57
 **/
@Service
public class UnionKeyLockImpl implements UnionKeyLock{

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public Boolean lock(String lockName, Integer second) {
        try {
            String sql = String.format("insert into union_key_lock (lock_name, expire_at) value ('%s','%s')", lockName, DateUtil.formatLocalDateTime(LocalDateTime.now().plusSeconds(second)));
            jdbcTemplate.execute(sql);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public void unLock(String lockName) {
        String sql = String.format("delete from union_key_lock where lock_name='%s';", lockName);
        jdbcTemplate.execute(sql);
    }

}

2.3、 测试代码

下面代码使用并行流(IntStream.range(1, 5).parallel())来模拟多个线程并发执行某个操作。对同一个锁名 “Hanson” 进行加锁和解锁操作:

import com.hanson.java.base.mysqllock.UnionKeyLockImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.stream.IntStream;

/**
 * @author hanson.huang
 * @version V1.0
 * @ClassName UnionKeyLockTest
 * @date 2024/12/18 17:00
 **/
@Slf4j
@SpringBootTest
public class MySQLLockTest {

    @Resource
    private UnionKeyLockImpl unionKeyLock;

    @Test
    void test_union_key_lock() {
        String lockName = "Hanson";
        IntStream.range(1, 5).parallel().forEach(x -> {
            try {
                if (unionKeyLock.lock(lockName, 5)) {
                    log.info("get lock success");
                } else {
                    log.warn("get lock error");
                }
            } finally {
                unionKeyLock.unLock(lockName);
            }
        });
    }
}

2.4、小结

基于数据库的分布式锁优点包括实现简单、事务支持、无需额外组件和持久化特性。缺点则包括性能较低、锁粒度受限、死锁风险、资源开销较大以及锁释放问题。

优点:

  • 实现简单:基于数据库唯一索引。
  • 事务支持:与业务操作同事务,保一致性。
  • 无需额外组件:适用于已有数据库系统。
  • 持久化:锁信息数据库存储,系统崩溃后仍存在。

缺点:

  • 性能较低:相比内存级锁,SQL操作性能低。
  • 锁粒度受限:以表或行为单位,易竞争。
  • 死锁风险:需严格事务管理。
  • 资源开销:频繁获取锁增数据库负载。
  • 锁释放问题:异常未释放需额外机制处理。

三、基于悲观锁

3.1 、实现思路

基于数据库悲观锁实现分布式锁依赖于数据库的行级锁机制,通过SELECT ... FOR UPDATE等操作显式地锁定数据库中的某一行,来达到获取分布式锁的目的。在这种方式下,其他事务在尝试修改这行数据时会被阻塞,直到锁被释放。

创建一张锁表,记录需要锁定的资源:

CREATE TABLE `select_for_update_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `lock_name` varchar(255) NOT NULL DEFAULT '',
  `lock_status` int(255) NOT NULL DEFAULT '0' COMMENT '0--正常 1--被锁',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_key_name` (`lock_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COMMENT='悲观锁'

当获取锁时使用FOR UPDATE阻塞其它查询,任务执行完成后COMMIT提交事务后自动释放锁,在调用锁之前要将锁名信息添加到表中。

BEGIN;
SELECT * FROM select_for_update_lock WHERE lock_name = 'my_lock' AND lock_status = 0 FOR UPDATE;
...执行任务
COMMIT;

3.2、代码实现

基于数据库悲观锁使用分布式锁代码也是非常简单的,只有一个方法:

@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private PlatformTransactionManager platformTransactionManager;
public void lock(String lockName, Runnable runnable)  {
    // 定义事务
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    // 开启事务
    TransactionStatus status = platformTransactionManager.getTransaction(def);
    try {
        // 尝试获取锁
        jdbcTemplate.queryForObject("SELECT lock_name FROM select_for_update_lock WHERE lock_name = ? FOR UPDATE", String.class, lockName);
        runnable.run();;
    } catch (Exception e) {
        // 出现异常时回滚事务
        platformTransactionManager.rollback(status);
        throw e;
    }finally {
        // 提交事务,释放锁
        platformTransactionManager.commit(status);
    }
}

在代码lock()方法中使用PlatformTransactionManager手动开启使用,在finally中手动提交事务

3.3、测试代码

@Resource
private SelectForUpdateLockImpl selectForUpdateLock;

@Test
void testSelectForUpdateLock() {
    String lockName = "Hanson";
    IntStream.range(1, 10).parallel().forEach(x -> {
        try {
            selectForUpdateLock.lock(lockName, () -> {
                log.info("get {} lock success", lockName);
            });
        } catch (Exception e) {
            log.error("get {} lock error", lockName);
        }
    });
}

在xxl-job中,作者就是通过mysql悲观锁实现分布式锁,从而避免多个服务器同时调度任务,附上源码:

在这里插入图片描述

3.4、小结

基于数据库悲观锁的分布式锁有以下优缺点:

优点:

  • 实现简单:利用数据库行级锁机制,无需引入其他分布式锁组件。
  • 事务支持:悲观锁与数据库事务结合紧密,能保证业务逻辑的原子性。
  • 一致性强:依赖数据库锁机制,保证了高并发下数据的一致性。

缺点:

  • 性能瓶颈:数据库行锁在高并发时可能成为性能瓶颈,导致数据库连接阻塞。
  • 可用性受限:数据库故障或网络问题会影响锁的释放,降低系统可用性。
  • 死锁风险:多事务复杂操作下可能产生死锁,需要精心设计锁策略。
  • 锁粒度粗:行级锁可能导致锁竞争激烈,影响性能。
  • 资源开销大:长期占用数据库资源,可能导致锁等待和连接池资源耗尽。

四、基于乐观锁

4.1 、实现思路

基于数据库的乐观锁实现分布式锁通常利用唯一索引或版本号机制来确保在高并发场景下的锁定操作。乐观锁适合在冲突较少的场景中使用,依赖于更新时的数据状态一致性判断。以下是一个基于数据库乐观锁的分布式锁实现示例。创建一张optimistic_lock表:

CREATE TABLE `optimistic_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `lock_name` varchar(50) DEFAULT NULL,
  `expire_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '锁过期时间',
  `lock_status` int(255) NOT NULL DEFAULT '0' COMMENT '0--正常 1--被锁',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uidx_lock_name` (`lock_name`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET=utf8 COMMENT='乐观锁实现分布式锁'

在锁名字段上增加唯一索引,其实现思路是通过数据库的更新数据是否成功能判断是否获取到锁,所以我们要提前将锁名任务添加到表中,expire_at为锁过期时间,防止未及时释放导致死锁,这里可以通过定时任务删除过期的锁。

4.2 、代码实现

基于数据库乐观锁实现分布锁主要有两个方法:

@Resource
private JdbcTemplate jdbcTemplate;

public boolean lock(String lockName) {
    try {
        String sql = String.format("update optimistic_lock set lock_status=1, expire_at = NOW() + INTERVAL 1 MINUTE where lock_name ='%s' and lock_status = 0 ;", lockName);
        return jdbcTemplate.update(sql) == 1;
    } catch (Exception e) {
        return false;
    }
}

public void unLock(String lockName) {
    String sql = String.format("update optimistic_lock set lock_status=0 ,expire_at=now() where lock_name='%s' ;", lockName);
    jdbcTemplate.update(sql);
}

4.3 、测试代码

@Resource
private OptimisticLock optimisticLock;

@Test
void testOptimisticLock() {
    String lockName = "Hanson";
    IntStream.range(1, 10).parallel().forEach(x -> {
        try {
            if (optimisticLock.lock(lockName)) {
                log.info("get lock success");
            } else {
                log.warn("get lock error");
            }
        } finally {
            optimisticLock.unLock(lockName);
        }
    });
}

4.4、小结

基于数据库乐观锁的分布式锁具有以下优缺点:

优点:

  • 实现简单:易于理解和实现,可以直接利用现有数据库,无需额外分布式中间件。
  • 数据库天然一致性:利用数据库的事务和一致性机制,保证并发场景下的数据一致性。
  • 适用于小规模系统:对于低并发系统,乐观锁可以有效满足需求,避免引入复杂中间件。

缺点:

  • 性能瓶颈:数据库不适合处理高并发锁操作,频繁的读写操作会给数据库带来压力。
  • 冲突处理复杂:乐观锁在冲突时需要重试,可能导致操作延迟。
  • 锁粒度问题:基于记录的锁粒度较粗,可能导致资源争用。
  • 不适合高并发场景:高并发下冲突率增加,重试操作影响性能和响应时间。
  • 数据库单点问题:依赖单个数据库节点可能导致单点故障。
  • 锁过期处理复杂:数据库锁缺乏自动过期机制,可能导致操作阻塞。

总结

基于数据库唯一索引、悲观锁、乐观锁实现分布式锁的适用场景可以总结如下:

基于数据库唯一索引的分布式锁

  • 适用场景:低并发、简单锁定操作、短时间锁持有、无需自动超时机制。
  • 典型场景:任务调度、确保资源独占访问。

基于数据库悲观锁的分布式锁:

  • 适用场景:高冲突、长业务操作、资源一致性要求高。
  • 典型场景:金融交易、订单状态更新。

基于数据库乐观锁的分布式锁:

  • 适用场景:低冲突高并发、短时间锁持有、允许重试获取锁。
  • 典型场景:订单扣库存、数据表版本更新、用户抽奖等。

创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️

在这里插入图片描述

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

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

相关文章

Elasticsearch:使用 Open Crawler 和 semantic text 进行语义搜索

作者&#xff1a;来自 Elastic Jeff Vestal 了解如何使用开放爬虫与 semantic text 字段结合来轻松抓取网站并使其可进行语义搜索。 Elastic Open Crawler 演练 我们在这里要做什么&#xff1f; Elastic Open Crawler 是 Elastic 托管爬虫的后继者。 Semantic text 是 Elasti…

健康养生:拥抱生活的艺术

健康养生&#xff1a;拥抱生活的艺术 在快节奏的现代生活中&#xff0c;健康已成为我们最宝贵的财富。健康养生&#xff0c;不仅仅是一种生活方式的选择&#xff0c;更是一种对待生活的态度&#xff0c;它关乎于如何在日常中寻找到平衡&#xff0c;让身心得以滋养&#xff0c;…

零基础开始学习鸿蒙开发-交友软件页面设计

目录 1.找一张网图&#xff0c;确定大致页面设计 2.页面布局代码详细介绍 3.完整的代码如下 4.最终的运行效果如下图所示 5.总结 1.找一张网图&#xff0c;确定大致页面设计 2.页面布局代码详细介绍 2.1 顶部文字与搜索框布局&#xff0c;在顶部采用行Row组件布局&#xf…

大数据之Hbase环境安装

Hbase软件版本下载地址&#xff1a; http://mirror.bit.edu.cn/apache/hbase/ 1. 集群环境 Master 172.16.11.97 Slave1 172.16.11.98 Slave2 172.16.11.99 2. 下载软件包 #Master wget http://archive.apache.org/dist/hbase/0.98.24/hbase-0.98.24-hadoop1-bin.tar.gz…

【Java服务端开发】深入理解Java中的Server 层的详细分析

目录 1. 什么是服务端&#xff08;Server&#xff09;层&#xff1f; 2. 设计 Server 层的基本原则 2.1 单一职责原则 2.2 面向接口编程 2.3 事务管理 3. 基于 Spring 的 Server 层实现 3.1 示例&#xff1a;创建一个简单的订单服务 3.2 编写 OrderService 3.3 编写 O…

JAVA:代理模式(Proxy Pattern)的技术指南

1、简述 代理模式(Proxy Pattern)是一种结构型设计模式,用于为其他对象提供一种代理,以控制对这个对象的访问。通过代理模式,我们可以在不修改目标对象代码的情况下扩展功能,满足特定的需求。 设计模式样例:https://gitee.com/lhdxhl/design-pattern-example.git 2、什…

XXE练习

pikachu-XXE靶场 1.POC:攻击测试 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe "a">]> <foo>&xxe;</foo> 2.EXP:查看文件 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SY…

Leetcode打卡:形成目标字符串需要的最少字符串数II

执行结果&#xff1a;通过 题目&#xff1a;3292 形成目标字符串需要的最少字符串数II 给你一个字符串数组 words 和一个字符串 target。 如果字符串 x 是 words 中 任意 字符串的 前缀 &#xff0c;则认为 x 是一个 有效 字符串。 现计划通过 连接 有效字符串形成 targ…

【蓝桥杯】49362.《视频相关度计算》

视频相关性计算 问题描述 小蓝作为异世界最大流媒体网站 LanTube 的高级算法工程师&#xff0c;他想要实现更加精准的视频推荐服务来满足用户的喜好。 其中&#xff0c;**“视频的相关性”**是一个重要指标&#xff0c;它代表了两个视频 A 到 B 的关联程度&#xff0c;记作 f…

ASP.NET|日常开发中数据集合详解

ASP.NET&#xff5c;日常开发中数据集合详解 前言一、数组&#xff08;Array&#xff09;1.1 定义和基本概念1.2 数组的操作 二、列表&#xff08;List<T>&#xff09;2.1 特点和优势2.2 常用操作 三、字典&#xff08;Dictionary<K, V>&#xff09;3.1 概念和用途…

如何将多张图片合并为一个pdf?多张图片合并成一个PDF文件的方法

如何将多张图片合并为一个pdf&#xff1f;当我们需要将多张图片合并为一个PDF文件时&#xff0c;通常是因为我们希望将这些图片整理成一个统一的文档&#xff0c;方便查看、分享或打印。无论是工作中需要提交的报告、学生们需要整理的作业&#xff0c;还是个人收藏的照片、旅行…

【html网页页面013】html+css制作节日主题圣诞节网页含视频、留言表单(独创首发-5页面附效果及源码)

节日主题圣诞节网页制作 &#x1f964;1、写在前面&#x1f367;2、涉及知识&#x1f333;3、网页效果完整效果(5页)&#xff1a;代码目录结构&#xff1a;page1、首页page2、庆祝page3、影响page4、起源page5、留言板 &#x1f308;4、网页源码4.1 html4.2 CSS4.3 源码获取圣诞…

直播预告 | 蓝卓生态说,解锁supOS在化工领域的无限可能

生态是蓝卓生命力的体现&#xff0c;为全方位赋能生态伙伴使用supOS并从中获益&#xff0c;蓝卓打造生态说系列栏目&#xff0c;通过生态沙龙、直播对话、案例剖析、产品解读等&#xff0c;持续展现“12N”的智能工厂创新路径&#xff0c;加速推进工业数字化转型。 嘉宾介绍 朱…

java对子网掩码的转换

一般的子网掩码展示为点分十进制形式&#xff0c;如&#xff1a;255.255.255.0&#xff0c;但有时因为业务需要&#xff0c;我们需要转换成对应的数字&#xff0c;以及数字转成点分十进制&#xff0c;所以整理了java的方法可以进行两者的互相转换 1、点分十进制转数字 public…

亚马逊-用表格创建多变体商品

引言 当我们使用“月亮树选品软件”找到一款不错的产品时&#xff0c;我们会延续这个产品的优点&#xff0c;并对其进行改良。改良之后&#xff0c;我们需要将产品上架到亚马逊平台&#xff0c;以测试这个产品的市场表现。然而&#xff0c;许多亚马逊卖家觉得上传多变体商品这…

使用C#在目录层次结构中搜索文件以查找目标字符串

例程以递归方式搜索目录层次结构中的文件以查找目标字符串。它可以搜索几乎任何类型的文件&#xff0c;即使它不包含 Windows 理解的文本。例如&#xff0c;它可以搜索 DLL 和可执行文件以查看它们是否恰好包含字符串。 下面的代码中显示的ListFiles 方法完成了大部分工作。 …

【深度学习总结】使用PDF构建RAG:结合Langchain和通义千问

【深度学习总结】使用PDF构建RAG&#xff1a;结合Langchain和通义千问 使用平台&#xff1a;趋动云&#xff0c;注册送算力 前言 在大型语言模型&#xff08;LLMs&#xff09;应用领域&#xff0c;我们面临着大量挑战&#xff0c;从特定领域知识的匮乏到信息准确性的窘境&am…

P8772 [蓝桥杯 2022 省 A] 求和

题目描述&#xff1a; 解题思路&#xff1a; 首先这题我们可以直接用两个for循环嵌套来控制两个变量来求值&#xff0c;但是这样做时间复杂度高。这里我们用到了一个前缀和差的方法。通过for循环变量第一个变量&#xff0c;用和差的方法的到第二个量&#xff0c;这样就只用了一…

Flux Tools 结构简析

Flux Tools 结构简析 BFL 这次一共发布了 Canny、Depth、Redux、Fill 四个 Tools 模型系列&#xff0c;分别对应我们熟悉的 ControlNets、Image Variation&#xff08;IP Adapter&#xff09;和 Inpainting 三种图片条件控制方法。虽然实现功能是相同的&#xff0c;但是其具体…

什么是芯片电阻

有人把Chip Resistor翻译成“芯片电阻”&#xff0c;我觉得翻译成“贴片电阻”或“片状电阻”更合适。有些厂商也称之为”电阻片”&#xff0c;英文写作Resistor Chip。比如&#xff1a;Thick film resistor chips&#xff08;厚膜电阻片&#xff09;、Thin film resistor chip…