在开发壁纸时,使用到了NestJS中的定时任务,但在发布生产后,却出现了数据重复问题。反复排查之后,最终发现问题的罪魁祸首是多进程环境导致的并发问题。在定时任务场景中,每个单独的服务进程都将执行一次定时任务,导致重复爬取。且因为各进程的执行时间差在毫秒级别,根本无法使用ID重复校验机制。
因此,只能通过加锁,或者其它类似的机制解决多进程并发问题。而加锁可以是在controller的action层面,也可以是在数据库层面,显然,数据库层面的代价是不小的(复杂性、性能),因此,只能考虑在controller层面解决。
然而,考虑到场景的特殊性,即,并不需要发挥多进程的并发优势,只需其中任何一个进程执行成功即可,因此,可以通过某种负载均衡的机制(算法)去调度执行定时任务的进程,也就规避了多进程带来的并发问题。实际上,这不就是典型的“负载均衡”算法的应用吗?
在友人的提示下,考虑采用某种一致性哈希算法进行任务调度,其推荐的算法是murmurhash
。代码如下:
typescriptCopy code- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
const isCluster = this.configService.get('env.isCluster');
if (isCluster) {
const hash = murmurhash(data[0][0].urlBase + '_' + data[1][0].urlBase, 20160124);
const worker = (hash % getWorkerCount()) + 1;
if (worker !== (cluster as any).worker.id) {
return;
}
this.logger.infoRaw(`Task is served by worker: ${worker}`);
}
const isCluster = this.configService.get('env.isCluster');
if (isCluster) {
const hash = murmurhash(data[0][0].urlBase + '_' + data[1][0].urlBase, 20160124);
const worker = (hash % getWorkerCount()) + 1;
if (worker !== (cluster as any).worker.id) {
return;
}
this.logger.infoRaw(`Task is served by worker: ${worker}`);
}
其中:
- 先根据环境判断是否开启集群模式,仅在开启的情况下进行判断;
- 调用
murmurhash
生成一致性哈希; - 获取进程数,对其取余,再同进程id进行判断,选择执行任务的具体进程。
如此,便解决了集群模式下的定时任务并发执行问题。
但,考虑到定时任务可能执行失败,因此,还需要在适当的时机“重启”定时任务,并输出日志:
typescriptCopy code- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
try {
...
} catch (e) {
await this.exceptionService.handleException(e);
// if exists, delete first
const retryTaskName = 'retryFetchWallpapers';
if (this.scheduler.doesExist('timeout', retryTaskName)) {
this.scheduler.deleteTimeout(retryTaskName);
}
// retry after 1 hour
const timeout = setTimeout(this.fetchWallpapers.bind(this), 60 * 60 * 1000);
this.scheduler.addTimeout(retryTaskName, timeout);
}
try {
...
} catch (e) {
await this.exceptionService.handleException(e);
// if exists, delete first
const retryTaskName = 'retryFetchWallpapers';
if (this.scheduler.doesExist('timeout', retryTaskName)) {
this.scheduler.deleteTimeout(retryTaskName);
}
// retry after 1 hour
const timeout = setTimeout(this.fetchWallpapers.bind(this), 60 * 60 * 1000);
this.scheduler.addTimeout(retryTaskName, timeout);
}