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

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

背景

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

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

scheduler-client

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

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

scheduler-web

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

scheduler-trigger

该节点包含两个角色:masterslave

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

分布式锁

从图中可以看到,scheduler-trigger分为了两个角色masterslave,那么角色是怎么确定的呢?就是通过去抢锁,谁抢到锁,谁就是 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 怎么去执行业务逻辑,那又是另一件事情了,后面再单独说。

相关推荐
大白要努力!6 小时前
MySQL 8.0 + Navicat 完整操作指南
数据库·mysql
云絮.7 小时前
数据库操作
数据库·mysql·算法·oracle
设计师小聂!9 小时前
宝塔 Linux 面板保姆级教程
linux·mysql·开源·运维开发
Tong Z10 小时前
Mysql DDL中的ALGORITHM
数据库·mysql
minji...14 小时前
MySQL数据库 (七) MySQL表的基本查询(上),insert、replace、select、where、order by
数据库·mysql·select·replace·insert·order by·where
咖啡星人k14 小时前
MonkeyCode 开源协作指南:如何让分布式团队高效使用AI编程
分布式·开源·ai编程·monkeycode
阿坤带你走近大数据14 小时前
如何保证kafka中的数据一致性
分布式·kafka
凯源智能14 小时前
高寒地区分布式光伏箱变测控系统落地实战
分布式·箱变测控·光伏箱变测控装置·箱变监控系统
逆境不可逃15 小时前
深入理解 SingleFlight:从单机到分布式的请求合并方案全解析
分布式·wpf
阿坤带你走近大数据15 小时前
Kafka中的分区概念
分布式·kafka