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

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、小结

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

优点:

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

缺点:

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

总结

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

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

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

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

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

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

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

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

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

生活小妙招之UE CaptureRT改

需求&#xff0c;四个不同的相机拍摄结果同屏分屏显示 一般的想法是四个Capture拍四张RT&#xff0c;然后最后在面片/UI上组合。这样的开销是创建4张RT&#xff0c;材质中采样4次RT。 以更省的角度&#xff0c;想要对以上流程做优化&#xff0c;4个相机拍摄是必须的&#xff…

1 JVM JDK JRE之间的区别以及使用字节码的好处

JDK jdk是编译java源文件成class文件的&#xff0c;我们使用javac命令把java源文件编译成class文件。 我们在java安装的目录下找到bin文件夹&#xff0c;如下图所示: 遵循着编译原理&#xff0c;把java源文件编译成JVM可识别的机器码。 其中还包括jar打包工具等。主要是针对…

【Unity功能集】TextureShop纹理工坊(二)图层(上)

项目源码&#xff1a;后期发布 索引 图层TextureLayer可见性激活性可编辑性绘画区域、绘画板绘画区域锚点导入图像 图层 在PS中&#xff0c;图层的概念贯穿始终&#xff08;了解PS图层&#xff09;&#xff0c;他可以称作PS最基础也是最强大的特性之一。 那么&#xff0c;在T…

贪心算法 part01

class Solution { public:int maxSubArray(vector<int>& nums) {int result INT32_MIN;int count 0;for (int i 0; i < nums.size(); i) {count nums[i];if (count > result) { // 取区间累计的最大值&#xff08;相当于不断确定最大子序终止位置&#xff…

二、FIFO缓存

FIFO缓存 1.FIFO缓存介绍2.FIFO缓存实现3.FIFO缓存总结 1.FIFO缓存介绍 FIFO&#xff08;First-In-First-Out&#xff09;缓存 是一种简单的缓存淘汰策略&#xff0c;它基于先进先出的原则来管理数据。当缓存达到容量限制并需要淘汰元素时&#xff0c;最先进入缓存的元素会被移…

王佩丰24节Excel学习笔记——第十四讲:日期函数

【以 Excel2010 系列学习&#xff0c;用 Office LTSC 专业增强版 2021 实践】 【本章小技巧】 掌握date()日期函数&#xff0c;配合年月日时分秒使用使用datedif()函数计算两个日期之前的差&#xff0c;重点记住参数三&#xff0c;差的值以哪种类型显示。使用weeknum/weekday,…

python--在服务器上面创建conda环境

今天刚开始使用服务器的时候使用上面的公共环境发现老师缺少模块&#xff0c; [guoyupingcins195 ~]$ conda --version Traceback (most recent call last): File "/home/miniconda3/bin/conda", line 12, in <module> from conda.cli import main Fil…

Trimble天宝三维激光扫描仪在建筑工程竣工测量中的应用【沪敖3D】

竣工测量是建筑项目竣工阶段的一个至关重要的环节&#xff0c;它为建筑工程的质量验收和成果核查提供了核心的参考依据。传统的竣工测量方法&#xff0c;如全站仪测量&#xff0c;主要依赖于现场人工操作&#xff0c;存在一些明显的局限性&#xff0c;例如作业时间长、工作量大…

SEO初学者-搜索引擎如何工作

搜索引擎基础搜索引擎是如何建立索引的搜索引擎如何对网页进行排名搜索引擎是如何个性化搜索结果的 搜索引擎的工作方式是使用网络爬虫抓取数十亿个页面。爬虫也称为蜘蛛或机器人&#xff0c;它们在网络上导航并跟踪链接以查找新页面。然后&#xff0c;这些页面会被添加到搜索引…

react中实现导出excel文件

react中实现导出excel文件 一、安装依赖二、实现导出功能三、自定义列标题四、设置列宽度五、样式优化1、安装扩展库2、设置样式3、扩展样式功能 在 React 项目中实现点击按钮后导出数据为 Excel 文件&#xff0c;可以使用 xlsx 和 file-saver 这两个库。 一、安装依赖 在项目…

Latex中表格添加底部文本注释并调整对齐

如何实现从第一个表到第三个表的转换&#xff0c; 其中主要涉及到两点&#xff1a; &#xff08;1&#xff09;底部脚注与表格自动对齐并缩进换行 &#xff08;2&#xff09;表格自适应页面宽度 底部脚注的对齐与换行缩进需要用到 \usepackage{threeparttable} \usepackage{…

MySQL基础 -----MySQL数据类型

目录 INT类型 tinyint类型 类型大小范围 测试tinyint类型数据 float类型 测试&#xff1a; 测试正常数据范围的数据 测试插入范围超过临界值的数据&#xff1a; 测试float类型的四舍五入 ​编辑 decimal类型 同样测试&#xff1a; 字符串类型 char类型 测试&…

【HarmonyOS NEXT】Web 组件的基础用法以及 H5 侧与原生侧的双向数据通讯

关键词&#xff1a;鸿蒙、ArkTs、Web组件、通讯、数据 官方文档Web组件用法介绍&#xff1a;文档中心 Web 组件加载沙箱中页面可参考我的另一篇文章&#xff1a;【HarmonyOS NEXT】 如何将rawfile中文件复制到沙箱中_鸿蒙rawfile 复制到沙箱-CSDN博客 目录 如何在鸿蒙应用中加…

ONES 功能上新|ONES Copilot、ONES Wiki 新功能一览

ONES Copilot 可基于工作项的标题、描述、属性信息&#xff0c;对工作项产生的动态和评论生成总结。 针对不同类型的工作项&#xff0c;总结输出的内容有对应的侧重点。 应用场景&#xff1a; 在一些流程步骤复杂、上下游参与成员角色丰富的场景中&#xff0c;工作项动态往往会…

使用qemu搭建armv7嵌入式开发环境

目录 目录 1 概述 2 环境准备 2.1 vexpress系列开发板介绍 2.2 安装工具 2.2.1 安装交叉工具链 2.2.2 安装qemu 2.2.3 安装其他工具 3 启动uboot 3.1 uboot下载与编译 3.1.1 下载 3.1.2 编译 3.2 使用qemu启动uboot 4 启动kernel 4.1 下载和编译kernel 4.1.1 下…

28.操作数据库

第三方库pymysql 使用安装命令 pip install pymysql 连接数据库、选择库、获取游标&#xff0c;执行创建表语句 from pymysql import Connection# 获取到mysql数据库连接对象 conn Connection(host"localhost", passwd"123456", user"root", …

docker(wsl)命令 帮助文档

WSL wsl使用教程 wsl -l -v 列出所有已安装的 Linux 发行版 wsl -t Ubuntu-22.04 --shutdown 关闭所有正在运行的WSL发行版。如果你只想关闭特定的发行版 wsl -d Ubuntu-22.04 登录到Ubuntu环境 wsl --list --running 查看正在wsl中运行的linux发行版 wsl --unregister (系统名…

JVM系列之内存区域

每日禅语 有一位年轻和尚&#xff0c;一心求道&#xff0c;多年苦修参禅&#xff0c;但一直没有开悟。有一天&#xff0c;他打听到深山中有一古寺&#xff0c;住持和尚修炼圆通&#xff0c;是得道高僧。于是&#xff0c;年轻和尚打点行装&#xff0c;跋山涉水&#xff0c;千辛万…

【ADS射频电路学习笔记】2.阻抗匹配电路设计

本节课学习smith圆图匹配 1.史密斯圆图各功能介绍 首先调出s参数的控件 并增加两个端口 调出smith chart matching的控件 连接好端口在ADS中&#xff0c;默认是从负载端&#xff08;term2&#xff09;向源端&#xff08;term1&#xff09;做匹配的。 调节s参数控件的的频率扫…

springcloud-gateway获取应用响应信息乱码

客户端通过springcloud gateway跳转访问tongweb上的应用&#xff0c;接口响应信息乱码。使用postman直接访问tongweb上的应用&#xff0c;响应信息显示正常。 用户gateway中自定义了实现GlobalFilter的Filter类&#xff0c;在该类中获取了上游应用接口的响应信息&#xff0c;直…