本文已参与「新人创作礼」活动,一起开启掘金创作之路。
在微服务架构的应用中,为了保障业务高可用,通常会一个服务部署多机部署。
但要想在微服务中处理一些任务的时候(比如:定时统计,定时扫描,钉钉提醒等),首先面临的一个问题就是如何在多服务下保障任务只执行一次的问题。该如何实现呢?
第一种方式,分布式任务调度服务
在服务中开一个任务执行的API接口,通过分布式任务调度服务来发起调用,实现目的。
一个成熟的微服务体系里面,一定是需要一个分布式任务调度服务或平台的。xxl-job是一个不错的选择。
优点是一次性解决所有问题 缺点是1. 增加了复杂性;2. 对于小规模的微服务或团队来说,属于大马拉小车
第二种方式,选主
逻辑很简单就是个服务实例争抢注册同一路径地址,通常集成zookeeper实现。
轻量级的微服务架构中通常又没有zookeeper集群,为了一个任务调度,去搭建一套zookeeper集群,显然项目经理是不会同意的。
那该怎么办呢?
下面是通过Redis(如果没有redis也可以使用数据库代替)实现的一个简单易用的选主模块。
- AppConfig.java:全局配置类
- MasterReactor.java:选主反应堆
- BusinessScheduler.java:业务触发器
逻辑是,每个服务启动都会生成一个唯一的服务实例ID,通过守护进程定时发送心跳到Redis,抢先注册的服务实例ID为Master,记录到服务的全局配置类中。任务模块识别是否为Master,判断是否执行业务。
示例中一个服务实例ID被标记Master,后面任务就只会在这一个服务中执行,属于固定模式。
但如果一个任务调用频率比较高,只在一个机器上执行,会导致这个服务负载过重,如把选主改为随机模式 或 轮训模式,该怎么办呢?
AppConfig.java
/**
* 全局配置类
*/
@Getter
@Setter
@Configuration
@RefreshScope
public class AppConfig {
@Value("${light.master:false}")
private Boolean master;
}
MasterReactor.java
/**
* 选主反应堆
*/
@Slf4j
@Component("masterReactor")
public class MasterReactor implements DisposableBean {
private String serviceInstanceId;
private static final Long beatPeriod = 20L;
private ScheduledExecutorService executorService;
private static final String KEY_SERVICE_NAME_MASTER = "light_master";
private volatile boolean running = true;
@Autowired
private AppConfig appConfig;
@Autowired
private RedisTemplate redisTemplate;
public MasterReactor() {
executorService = new ScheduledThreadPoolExecutor(UtilAndComs.DEFAULT_CLIENT_BEAT_THREAD_COUNT, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.light.beat.sender");
return thread;
}
});
// 生成唯一服务实例ID
serviceInstanceId = "light:" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
executorService.schedule(new MasterReactor.BeatTask(), beatPeriod, TimeUnit.SECONDS);
}
class BeatTask implements Runnable {
@Override
public void run() {
if (!running) {
return;
}
try {
// 选主
Object masterServiceInstanceId = redisTemplate.opsForValue().get(KEY_SERVICE_NAME_MASTER);
if (masterServiceInstanceId != null) {
if (serviceInstanceId.equals(masterServiceInstanceId)) {
redisTemplate.opsForValue().set(KEY_SERVICE_NAME_MASTER, serviceInstanceId, beatPeriod * 3);
appConfig.setMaster(true);
} else {
appConfig.setMaster(false);
}
} else {
Boolean lock = redisTemplate.opsForValue().setIfAbsent(KEY_SERVICE_NAME_MASTER, serviceInstanceId, beatPeriod * 3, TimeUnit.SECONDS);
if (lock) {
appConfig.setMaster(true);
} else {
appConfig.setMaster(false);
}
}
} catch (Exception e) {
log.error("Exception[MasterReactor]", e);
}
executorService.schedule(new MasterReactorT.BeatTask(), beatPeriod, TimeUnit.SECONDS);
}
}
@Override
public void destroy() {
running = false;
}
}
BusinessScheduler.java
/**
* 定时业务触发器
*/
@Slf4j
@Component("businessScheduler")
public class BusinessScheduler implements DisposableBean, Runnable {
private volatile boolean running = true;
private Thread thread;
ScanScheduler() {
this.thread = new Thread(this);
this.thread.start();
}
@SneakyThrows
@Override
public void run() {
while (running) {
try {
if (appConfig.getMaster()) {
// 业务逻辑
}
Thread.sleep(60000);
} catch (Exception e) {
log.info("Exception[BusinessScheduler]", e);
}
}
}
@Override
public void destroy() {
running = false;
}
}