dolphinscheduler 使用mysql实现分布式锁能力分析

284 阅读2分钟

dolphinschedule(ds) 除了使用zookeeper 还用etcd和mysql 实现了服务注册 分布式锁的功能,这里通过源码角度来看ds是如何通过mysql实现分布式锁的

1. 分布式锁实现


DROP TABLE IF EXISTS `t_ds_jdbc_registry_lock`;
CREATE TABLE `t_ds_jdbc_registry_lock`
(
    `id`               bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
    `lock_key`         varchar(256) NOT NULL COMMENT 'lock path',
    `lock_owner`       varchar(256) NOT NULL COMMENT 'the lock owner, ip_processId',
    `last_term`        bigint       NOT NULL COMMENT 'last term time',
    `last_update_time` timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'last update time',
    `create_time`      timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
    PRIMARY KEY (`id`),
    unique (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

这里是通过t_ds_jdbc_registry_lock这张表来实现的分布式锁的能力

2. JdbcRegistry

@Component
@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "jdbc")
@Slf4j
public class JdbcRegistry implements Registry {
......
}

从这里可以看出在application.yml 配置了registry.typejdbc时,会注册这个bean,spring容器起来后就会调用如下方法

@PostConstruct
public void start() {
    log.info("Starting Jdbc Registry...");
    // start a jdbc connect check
    ephemeralDateManager.start();
    subscribeDataManager.start();
    registryLockManager.start();
    log.info("Started Jdbc Registry...");
}

对于分布式锁管理,对应的是registryLockManager

3. registryLockManager

public void start() {
    lockTermUpdateThreadPool.scheduleWithFixedDelay(
            new LockTermRefreshTask(lockHoldMap, jdbcOperator),
            registryProperties.getTermRefreshInterval().toMillis(),
            registryProperties.getTermRefreshInterval().toMillis(),
            TimeUnit.MILLISECONDS);
}

这里启动了一个定时任务,执行LockTermRefreshTask

4. LockTermRefreshTask

public void run() {
    try {
        if (lockHoldMap.isEmpty()) {
            return;
        }
        List<Long> lockIds = lockHoldMap.values()
                .stream()
                .map(JdbcRegistryLock::getId)
                .collect(Collectors.toList());
        if (!jdbcOperator.updateLockTerm(lockIds)) {
            log.warn("Update the lock: {} term failed.", lockIds);
        }
        jdbcOperator.clearExpireLock();
    } catch (Exception e) {
        log.error("Update lock term error", e);
    }
}

从这里可以看出,是把自己当前申请到的lock按两秒一次的频率刷上一遍 jdbcOperator.updateLockTerm(lockIds)

public boolean updateLockTerm(List<Long> lockIds) {
    if (CollectionUtils.isEmpty(lockIds)) {
        return true;
    }
    return jdbcRegistryLockMapper.updateTermByIds(lockIds, System.currentTimeMillis()) > 0;
}

更新他的last_term字段

5. 上面是维护锁,再看申请锁

public boolean acquireLock(String key) {
    try {
        registryLockManager.acquireLock(key);
        return true;
    } catch (RegistryException e) {
        throw e;
    } catch (Exception e) {
        throw new RegistryException(String.format("Acquire lock: %s error", key), e);
    }
}

6. registryLockManager.acquireLock

public void acquireLock(String lockKey) throws RegistryException {
    // maybe we can use the computeIf absent
    lockHoldMap.computeIfAbsent(lockKey, key -> {
        JdbcRegistryLock jdbcRegistryLock;
        try {
            while ((jdbcRegistryLock = jdbcOperator.tryToAcquireLock(lockKey)) == null) {
                log.debug("Acquire the lock {} failed try again", key);
                // acquire failed, wait and try again
                ThreadUtils.sleep(JdbcRegistryConstant.LOCK_ACQUIRE_INTERVAL);
            }
        } catch (SQLException e) {
            throw new RegistryException("Acquire the lock error", e);
        }
        return jdbcRegistryLock;
    });
}

调用jdbcOperator.tryToAcquireLock去操作库

public JdbcRegistryLock tryToAcquireLock(String key) throws SQLException {
    JdbcRegistryLock jdbcRegistryLock = JdbcRegistryLock.builder()
            .lockKey(key)
            .lockOwner(JdbcRegistryConstant.LOCK_OWNER)
            .lastTerm(System.currentTimeMillis())
            .build();
    try {
        jdbcRegistryLockMapper.insert(jdbcRegistryLock);
        return jdbcRegistryLock;
    } catch (Exception e) {
        if (e instanceof SQLIntegrityConstraintViolationException) {
            return null;
        }
        throw e;
    }
}

这里利用 mysql unique key的特性,如果已经有这个key了,那么会返回null,进入sleep循环等待,来实现AcquireLock的操作

7. releaseLock

public void releaseLock(String lockKey) {
    JdbcRegistryLock jdbcRegistryLock = lockHoldMap.get(lockKey);
    if (jdbcRegistryLock != null) {
        try {
            // the lock is unExit
            jdbcOperator.releaseLock(jdbcRegistryLock.getId());
            lockHoldMap.remove(lockKey);
        } catch (SQLException e) {
            throw new RegistryException(String.format("Release lock: %s error", lockKey), e);
        }
    }
}

通过删除表中所数据和清除lockHoldMap中的数据实现释放key对应的锁