自己用Java寫了個可視化的定時任務工具,取名sundial(日晷),定時任務的原理就是spring schedule;分布式鎖基于zookeeper實作,客戶端采用Netflix開源的Curator,JDK用的17,最新的長期支持版本(LTS),資料庫是MySQL,同樣是最新的MySQL8,
所謂分布式鎖,即獨立于整個分布式環境之外的全域且唯一的鎖的添加、釋放的機制,簡單的分布式鎖可由資料庫實作,比如MySQL,但其性能顯然不如基于記憶體的redis、zookeeper快,且不支持鎖超時,公平鎖功能,用redis也可以做,redis是基于記憶體且支持持久化的鍵值對資料庫,6.0版本之后更是支持多執行緒,加上傳統技能IO多路復用及底層采用的跳表等高性能的資料結構,性能更上一層樓,redis加鎖本質上就是呼叫其set命令來對同一key設定鍵值對,value的話可以用當前執行緒的執行緒id,在解鎖時對value做校驗以避免釋放其他執行緒的鎖,再給鍵值對設定一個過期時間以實作鎖超時功能,但有可能到了過期時間,持有鎖的執行緒還沒執行完成,這時鎖已被釋放,其他執行緒獲取了鎖,再對同一共享資源進行操作,就會出現bug,而且執行緒安全問題相關的bug難以排查,可以用Redisson搭配Lua腳本來實作鎖超時功能,基于其watchdog,每隔一定時間,默認為30秒,如果某客戶端持有鎖超過30秒,watchdog就會每隔10秒再把key的過期時間再設為30秒,這樣,某執行緒執行的慢,一直持有鎖,其他執行緒也不會獲取到當前執行緒持有的鎖,
而zookeeper實作分布式鎖,就是利用其臨時有序節點特性,在某一指定目錄下創建節點,并判斷節點序號是否為當前目錄下最小,若是,則視為創建鎖成功,否則,就對前一個節點添加一個監聽事件,如果鎖釋放,會通知后一個節點,后一個節點再判斷自己序號是否最小,最小就獲得鎖,這里的邏輯類似JUC里的AQS佇列同步器的公平鎖模式,AQS里有個雙向鏈表,持有鎖的執行緒在頭節點,其他的等著搶鎖的執行緒就在連在后面,持有鎖的執行緒釋放鎖后會通知后一個節點的執行緒來競爭鎖,
囿于篇幅,下面只介紹主要的類和資料庫設計,查看詳細代碼,請移步GitHub,GitHub - riveryue/sundial,這個分支updateCronActivrImmediately,
使用時需要在任務類上加上自定義注解SundialTask,注解的name屬性在整個工程是需保持唯一,
下圖為示例,執行緒休眠5秒是為了模擬真實業務系統中的定時任務,測驗分布式環境下同一任務只被執行一次,如果不休眠5秒,就不會存在鎖競爭,導致每臺服務器上都會執行一遍定時任務,造成一種zook分布式鎖不生效的假象,
package sundial;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import sundial.annotation.SundialTask;
import java.util.Date;
/**
* @author yao
*/
@Component
@Slf4j
public class Task2 implements SundialExecute {
@SundialTask(name = "test2")
@Override
public void execute() {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
log.error("error in schedule ", e);
}
log.info("task2 {}", new Date());
log.info("schedule execute successfully");
}
}
下圖為上述注解的原始碼:
package sundial;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import sundial.annotation.SundialTask;
import sundial.config.CuratorFrameworkConfig;
import sundial.constant.TaskStatus;
import sundial.dto.TaskConfDTO;
import sundial.service.TaskConfService;
import sundial.utils.SpringUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* @author yao
*/
public interface SundialExecute extends Runnable {
void execute();
/**
* valid status of job if available
*/
@Override
default void run() {
TaskConfService taskConfService = SpringUtils.getBean(TaskConfService.class);
Method[] declaredMethods = this.getClass().getDeclaredMethods();
String annoVal = StringUtils.EMPTY;
for (Method declaredMethod : declaredMethods) {
boolean annotationPresent = declaredMethod.isAnnotationPresent(SundialTask.class);
if (annotationPresent) {
SundialTask methodAnno = declaredMethod.getAnnotation(SundialTask.class);
annoVal = methodAnno.name();
}
}
TaskConfDTO taskConfDTO = taskConfService.queryByTaskName(annoVal);
if (taskConfDTO != null && taskConfDTO.getStatus().equals(TaskStatus.DISABLE)) {
return;
}
String ZK_LOCK_PATH = "/distributeLock";
CuratorFramework client = SpringUtils.getBean(CuratorFrameworkConfig.class).curatorFramework();
final InterProcessMutex mutex = new InterProcessMutex(client, ZK_LOCK_PATH);
try {
//獲取鎖資源
boolean flag = mutex.acquire(1, TimeUnit.SECONDS);
if (flag) {
execute();
}
} catch (Exception e) {
} finally {
try {
mutex.release();
} catch (Exception e) {
}
}
}
}
這是資料庫表設計:
CREATE database if NOT EXISTS `sundial` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `sundial`;
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for task_conf
-- ----------------------------
DROP TABLE IF EXISTS `task_conf`;
CREATE TABLE `task_conf` (
`id` int NOT NULL AUTO_INCREMENT,
`task_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`status` tinyint DEFAULT NULL COMMENT '1 available, 2 unavailable',
`cron` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uni_task_name` (`task_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of task_conf
-- ----------------------------
INSERT INTO `task_conf` VALUES ('1', 'test1', '1', '*/4 * * * * ?');
INSERT INTO `task_conf` VALUES ('2', 'test2', '1', '*/9 * * * * ?');
下面為前端頁面,畢竟不是專業前端,所以做的丑,但菜就是菜,菜就是原罪,以后再改改,

我的MySQL和zookeeper裝在本地,然后虛擬機就裝個jdk,MySQL客戶端,資料庫安裝在本地物理機,所以虛擬機里沒必要裝完整的MySQL服務,虛擬機里起三個服務以模擬分布式環境,
啟動zookeeper,


虛擬機里啟動定時任務服務,只貼了一臺機器的截圖,

在頁面上啟用定時任務2,

虛擬機里可以看到只有一臺機器打出了任務2的日志,



自此,定時任務只在集群中的一臺機器上執行,且執行成功,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/421426.html
標籤:其他
上一篇:Hive表權限控制
