基于关系型数据mysql实现分布式锁介绍
前言
本文主要用于介绍常规分布式锁的使用及其原理,在主篇中进行了常规分布式锁的扫盲介绍,在子篇中介绍了现主流分布式锁框架的源码以及自写学习demo解析。
全部代码及介绍:https://gitee.com/FWEM/distributed-lock
文章主要分为以下两个部分:
1. 基于表记录实现
1、基本实现思路:
- 创建锁表,内部存在字段表示资源名及资源描述,同一资源名使用数据库唯一性限制。
- 多个进程同时往数据库锁表中写入对某个资源的占有记录,当某个进程成功写入时则表示其获取锁成功。
- 其他进程由于资源字段唯一性限制插入失败陷入自旋并且失败重试。
- 当执行完业务后持有该锁的进程则删除该表内的记录,此时回到步骤一。
2、基本流程
3、样例
描述: 多个进程同时往表中插入记录(锁资源为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='数据库分布式锁表';
核心代码
启动类
import lombok.extern.slf4j.Slf4j;
import pers.zifeng.distributed.lock.mysql.service.MysqlDistributedLockService;
import java.lang.management.ManagementFactory;
/**
* @author: zf
* @date: 2021/05/14 17:21:06
* @version: 1.0.0
* @description: mysql分布式锁-锁表
* 执行流程: 多进程抢占数据库中某个资源然后执行业务,执行完毕释放资源
* 锁机制: 单一进程获取锁时则其他进程提交失败
*/
@Slf4j
public class LockTable extends Thread {
@Override
public void run() {
super.run();
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
try {
while (true) {
log.info("当前线程:" + pid + ",尝试获取锁资源!");
if (MysqlDistributedLockService.tryLock(1, "测试锁")) {
log.info("当前线程:" + pid + ",获取锁资源成功!");
// 这里用sleep模拟业务处理过程
log.info("开始处理业务!");
Thread.sleep(10 * 1000);
log.info("业务处理完成!");
MysqlDistributedLockService.releaseLock(1);
log.info("当前线程:" + pid + ",释放了锁资源!");
break;
} else {
log.info("当前线程:" + pid + ",获取锁资源失败!");
Thread.sleep(2000);
}
}
} catch (Exception e) {
log.error("抢占锁发生错误!", e);
} finally {
MysqlDistributedLockService.close();
}
}
// 程序入口
public static void main(String[] args) {
new LockTable().start();
}
}
数据库操作
/**
* 锁表-获取锁
*
* @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 (Exception e) {
return false;
}
}
/**
* 锁表-释放锁
*
* @param resource 资源
* @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;
}
三个进程执行情况
注意事项:
- 该锁为非阻塞的
- 当某进程持有锁并且挂死时候会造成资源一直不释放的情况,造成死锁,因此需要维护一个定时清理任务去清理持有过久的锁
- 要注意数据库的单点问题,最好设置备库,进一步提高可靠性
- 该锁为非可重入锁,如果要设置成可重入锁需要添加数据库字段记录持有该锁的设备信息以及加锁次数
2. 乐观锁
1、基本实现思路:
- 每次执行业务前首先进行数据库查询,查询当前的需要修改的资源值(或版本号)
- 进行资源的修改操作,并且修改前进行资源(或版本号)的比对操作,比较此时数据库中的值是否和上一步查询结果相同
- 查询结果相同则修改对应资源值,不同则回到第一步
2、基本流程
3、样例
描述: 数据库中设定某商品基本信息(名为外科口罩,数量为10),多进程对该商品进行抢购,当商品数量为0时结束抢购。
创建数据库表
# 创建数据库表
create table `database_lock_2`(
`id` BIGINT NOT NULL AUTO_INCREMENT,
`good_name` VARCHAR(256) NOT NULL DEFAULT "" COMMENT '商品名称',
`good_count` INT NOT NULL COMMENT '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表2';
# 插入原始数据
insert into database_lock_2 (good_name,good_count) values ('医用口罩',10);
核心代码
三个进程执行情况
注意事项:
- 该锁为非阻塞的
- 该锁对于业务具有侵入式,如果设置版本号校验则增加的额外的字段,增加了数据库冗余
- 当并发量过高时会有大量请求访问数据库的某行记录,对数据库造成很大的写压力
- 因此乐观锁适用于并发量不高,并且写操作不频繁的场景
3. 悲观锁
1、基本实现思路:
- 关闭jdbc连接自动commit属性
- 每次执行业务前先使用查询语句后接for update表示锁定该行数据(注意查询条件如果未命中主键或索引,此时将会从行锁变为表锁)
- 执行业务流程修改表资源
- 执行commit操作
2、基本流程
3、样例
描述: 数据库中设定某商品基本信息(名为外科口罩,数量为10),多进程对该商品进行抢购,当商品数量为0时结束抢购。
创建数据库表
# 创建数据库表
create table `database_lock_2`(
`id` BIGINT NOT NULL AUTO_INCREMENT,
`good_name` VARCHAR(256) NOT NULL DEFAULT "" COMMENT '商品名称',
`good_count` INT NOT NULL COMMENT '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表2';
# 插入原始数据
insert into database_lock_2 (good_name,good_count) values ('医用口罩',10);
核心代码
三个进程执行情况
注意事项:
- 该锁为阻塞锁
- 每次请求存在额外加锁的开销
- 在并发量很高的情况下会造成系统中存在大量阻塞的请求,影响系统的可用性
- 因此悲观锁适用于并发量不高,读操作不频繁的写场景
4.总结
在实际使用中,由于受到性能以及稳定性约束,对于关系型数据库实现的分布式锁一般很少被用到。但是对于一些并发量不高、系统仅提供给内部人员使用的单一业务场景可以考虑使用关系型数据库分布式锁,因为其复杂度较低,可靠性也能够得到保证。