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

ops/2024/12/19 19:29:08/

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{@Resourceprivate JdbcTemplate jdbcTemplate;@Overridepublic 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;}}@Overridepublic 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 {@Resourceprivate UnionKeyLockImpl unionKeyLock;@Testvoid 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.ppmy.cn/ops/143271.html

相关文章

一、LRU缓存

LRU缓存 1.LRU缓存介绍2.LRU缓存实现3.LRU缓存总结3.1 LRU 缓存的应用3.2 LRU 缓存的优缺点 1.LRU缓存介绍 LRU是Least Recently Used 的缩写&#xff0c;意为“最近最少使用”。它是一种常见的缓存淘汰策略&#xff0c;用于在缓存容量有限时&#xff0c;决定哪些数据需要被删…

mysql,创建数据库和用户授权核心语句

一.库操作1.创建库create database if not exists 库名 default 字符集 default 校对规则2.删除库drop database if exists 库名3.修改库的,字符集,校对规则alter databse 库名 default 字符集 default 校对规则4.查看当前使用的库seclect databse();5.查看库show databases;…

MTU MSS

目录 一、MTU\MSS是什么二、为什么三次握手协商MSS 一、MTU\MSS是什么 MTU : Maximum Transmission Unit,即最大传输单元&#xff0c;表示数据链路层可以传输的最大数据包&#xff08;不包含帧首部和尾部&#xff09;。 MSS : Maximum Segment Size,即最大报文段长度。MSS是TC…

最新ubuntu20.04安装docker流畅教程

最新ubuntu20.04安装docker流畅教程 使用清华镜像源 //编辑/etc/apt/sources.list # 默认注释了源码镜像以提高 apt update 速度&#xff0c;如有需要可自行取消注释 deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse # deb-sr…

MySQL学习之DML操作

目录 插入 删除 修改 数据库事务 事务的特征&#xff08;ACID原则&#xff09; 原子性 一致性 隔离性 持久性 事务隔离级别 读未提交 读已提交 可重复读 序列化 脏读 虚读 幻读 插入 insert into 表名 values(); 要求插入数据的数量&#xff0c;类型要和定义…

虚拟现实喷漆训练解决方案,为喷漆行业提供全新高效的培训方式

虚拟现实喷漆训练方案为喷漆操作员的培训与评估提供创新途径。此方案不仅能导入数据&#xff0c;还能定制专属的培训环境&#xff0c;从而大幅降低培训时间、材料及人力等资源消耗所带来的成本压力。 虚拟现实控制器与带触觉执行器的喷枪的组合&#xff0c;更是将操作的真实感提…

修改ubuntu apt 源及apt 使用

视频教程:修改ubuntu apt 源和apt 使用方法_哔哩哔哩_bilibili 1 修改apt源 1.1 获取阿里云ubuntu apt 源 https://developer.aliyun.com/mirror/ubuntu?spma2c6h.13651102.0.0.3e221b11mqqLBC 1.2 修改apt 源 vim /etc/apt/sources.list deb https://mirrors.aliyun.com/ub…

es build 使用配置详解:快速、可扩展的 JavaScript 打包器

引言 es build 是一个快速、可扩展的 JavaScript 打包器和压缩器&#xff0c;它的目标是成为最快的打包器。它使用 Go 编写&#xff0c;可以在几乎瞬间内完成大多数项目的构建。在本文中&#xff0c;我们将深入了解 es build&#xff0c;并探讨其如何实现如此出色的性能。 什…