深入探索分布式任务调度框架:MySQL实现高效锁机制

本文主要介绍项目中怎么使用 MySQL 实现分布式锁的

背景

假如我们现在要做一个高性能、可扩展的分布式任务调度框架,要怎么设计呢?下面是我之前自己设计的一个架构图。

为了方便后续的分布式锁的设计,我们大致描述下各个角色都做了哪些事情(这不是本篇文章的重点)

scheduler-client

做为一个 sdk,植入到各业务方系统,功能如下:

  • 定期注册心跳信息
  • 暴露 http 服务端口,监听 handler 请求

scheduler-web

与前端页面交互,保存任务调度信息

scheduler-trigger

该节点包含两个角色:master 和 slave

  • master:读任务表,分配任务
  • slave:按策略执行任务,调用 scheduler-client 的接口

分布式锁

从图中可以看到,scheduler-trigger分为了两个角色master 和 slave,那么角色是怎么确定的呢?就是通过去抢锁,谁抢到锁,谁就是 master。

所以,重点就在于该怎么设计抢锁这个动作?

锁表设计

sql 复制代码
CREATE TABLE ` lock ` (
  ` id ` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  ` namespace ` varchar(128)  NOT NULL DEFAULT '' COMMENT '锁名',
  ` owner ` varchar(128) DEFAULT NULL COMMENT '资源所有者(ip地址)',
  ` version ` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
  ` create_time ` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  ` update_time ` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (` id `) USING BTREE,
  KEY ` idx_namespace ` (` namespace `) USING BTREE
) ENGINE = InnoDB  COMMENT = '锁信息表'

这就是基于 MySQL 的分布式锁表设计,其中namespace代表锁的名称,owner代表该锁被哪个 ip 节点所持有。

初始化锁

在系统启动的时候,我们需要去监听启动事件,然后去初始化锁信息。

java 复制代码
@Component
public class LockerHandler implements ApplicationListener<ApplicationEvent> {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        try {
            if (event instanceof ApplicationStartedEvent) {
                //初始化锁
            }
        } catch (Exception e) {}
    }
}

初始化锁的逻辑就是insert 一条锁信息,只不过此时锁还未被任何 owner 所持有。

sql 复制代码
INSERT INTO lock(`namespace`,`version`,create_time, update_time) VALUE ('master',1,NOW(),NOW())

抢锁&锁续约

在系统启动几秒后,有一个定时任务,每秒钟执行一次。

java 复制代码
@Scheduled(initialDelay = 5000,fixedDelay = 1000)
public void lock() {
    try {
        LockTypeEnum lt = lock("master", InetTool.LOCAL_IP, 3);
        /**
        这里面是抢锁之后的业务逻辑,暂且不表
        **/
        return;
    } catch (Exception e) {  
    }
}


@Override
public LockTypeEnum lock(String namespace, String owner, int ttl) {
    //查询锁
    LockDO lock = this.lockDao.get(namespace);
    //如果锁过期或者无人持有锁,则触发DB抢锁
    if (lock.isExpired(ttl)) {
        return this.lockDao.lock(namespace, owner, lock.getVersion()) ? LockTypeEnum.PROMOTED : LockTypeEnum.UNKNOWN;
    }
    //如果锁未过期,且持有者是自己,则续约
    if (owner.equals(lock.getOwner())) {
        return this.lockDao.keepAlive(namespace, owner, ttl) ? LockTypeEnum.STAY : LockTypeEnum.LEAVE;
    }
    return LockTypeEnum.UNKNOWN;
}

下面来详细解剖下里面的代码逻辑

查询锁

通过namespace去查询锁信息

sql 复制代码
SELECT *,
(UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(update_time)) AS remaining
FROM lock
WHERE namespace = #{namespace}
LIMIT 1

remaining 代表当前时间与上一次keepAlive更新时间的差值,可用来判断锁是否过期。

判断触发抢锁的条件
java 复制代码
//触发抢锁的条件 1.锁过期 2.无人持有锁
public boolean isExpired(int ttl) {
    return remaining - ttl > 0 || owner == null;
}

remaining > ttl 代表已经超过 3秒没有更新了,满足该条件或owner 为空,会触发抢锁。

抢锁
sql 复制代码
UPDATE lock
SET
    update_time = NOW(),
    owner = #{owner},
    `version` = `version` + 1
WHERE `namespace` = #{namespace} 
AND `version` = #{version}

抢锁涉及到并发操作,所以采用了版本号的方式来保证安全性

锁续约

如果锁未过期且持有者是自己,触发锁续约

sql 复制代码
UPDATE lock
SET
update_time = NOW()
WHERE namespace = #{namespace} 
AND owner = #{owner} 
AND update_time >= DATE_SUB(NOW(),INTERVAL 3 SECOND)

此处的where判断条件也再次校验了,update_time >= 当前时间 - 3秒,代表锁还没有过期。

总结

以上就是基于 MySQL 实现锁机制的流程,至于抢到锁后的 master 和 slaver 怎么去执行业务逻辑,那又是另一件事情了,后面再单独说。

相关推荐
这个DBA有点耶2 小时前
函数索引的“完全一致”有多苛刻:写法、字符集、类型,差一处就失效
数据库·mysql·代码规范
星恒随风2 小时前
MySQL 从零入门(三):表的约束详解
linux·服务器·数据库·笔记·mysql
Alluxio2 小时前
Alluxio + OCI:为 AI 工作负载实现亚毫秒访问延迟与可扩展吞吐
运维·数据库·人工智能·分布式·缓存·ai·语言模型
阿卡好可爱8 小时前
Lua 热更新不生效:改完文件线上仍是旧逻辑,package.loaded 与 upvalue 重绑定详解
数据结构·分布式·后端·架构·lua
杨凯凡9 小时前
MySQL 死锁怎么排查?InnoDB 锁等待与 Spring Boot 整事务重试实战
数据库·spring boot·mysql
Decay__9 小时前
01_SQLServer 体系架构全解 —— 实例进程、关系引擎、SQLOS 与事务日志深入
运维·数据库·sql·mysql·架构
瑞雪兆丰年兮10 小时前
JavaWeb-day07-MySQL(DQL&多表设计)
java·开发语言·数据库·sql·mysql·javaweb·web
FungLeo17 小时前
成为全栈·基础补充·数据库迁移与事务:数据结构怎样安全演进
mysql·postgresql·sqlite·数据一致性·数据库事务·数据库迁移·成为全栈
aiks_yuan1 天前
MySQL GROUP BY 很慢怎么优化:加了索引反而从 1 秒变 7 秒,Using temporary 该不该消(MySQL 8.2 实测 400 万行)
mysql
数据库小学妹1 天前
MySQL大字段治理:行溢出机制、覆盖索引与拆表
数据库·mysql·性能优化