有的时候,我们需要确保当前只有一个进程实例在运行。特别是定时跑定时任务的时候。

unix/linux的cron可以配置定时任务,让它在指定的时间运行。但是,只要到任务指定的时 间,cron就会fork一个新的进程来执行任务,它不会保证前一个任务运行完成了,下一个任 务才开始。而有时我们却需要只能有一个实例运行。

在之前的golang定时任务cron的文章中介绍了使用golang的cron 包来控制只能有一个实例运行。也说了对于系统的cron不好控制。是不好控制,其实也是可 以控制的。

背景

这个是我以前的一个工作需求。需求是这样的,我们用spark定期计算,计算完了之后把数据 导出到mysql里。但是要提供数据给客户端使用,mysql不能支撑得了高并发,所以我们需要 把mysql的数据导出到cache中,可以是redis,或者其他的内存数据库(我们用了公司自己 的一个kv类数据库)。我们要做的就是跟spark的任务对齐时间表,比如spark每10分钟跑一次 任务,完了之后导出到mysql。然后我们要在spark导出数据之后,去把mysql的数据导出来。 但是,spark的任务完成之后,它通知不到我们。所以我们只能让工具比spark更短的时间周期 地去检查mysql有没有新数据了,有新数据。这个时期周期可以是5分钟或者更短些都可以。对 于linux系统的cron来说,最小的时间粒度是1分钟。而我们团队用到的语言主C++。所以我们 就要用C++写一个工具,然后配好cron去扫描mysql了。但是,上面也说了cron不会去帮你确 保只有一个任务实例在运行,因为我们不需要多个实例同时运行,如果扫描和导出数据慢的话 下一次任务又开启了,而如果我们没有做控制让这一次任务不要去导数据,它又开始执行导数 数据。如果cpu不够用的话,那么计算就会越来越慢,导致下一个任务又来了,就出现了雪崩 现象了。

所以这里我们要做的就是,当有一个任务实例在运行的时候,如果新的周期到了启动新的进程 我们就应该立刻终止它。

方法

首先我们要知道当前是否有进程实例在运行,方法其实有很多。

方法1:ps检查进程号

第一种可以用ps去检查是否有当前名字的进程在运行。这种方法比较简单,但是有一个问题 就是去做检查的时候不是原子性的,也就是说,当两个进程并发在检查,进程A检查到不存在, 进程B这时也检查到不存在,然后两个进程就会同时启动了。对于比较粗糙的检查简单的用是 没有问题的。所以放弃了这种方法

方法2:共享内存通信

第二种方法是用共享内存。进程启动时去共享内存去检查是否内存的值,如果为0,我们就把 它改成1,然后正常启动启动任务。如果是1,我们就退出进程。这种方法,对于共享内存的 操作,代码比较多,而且跟上面的方法一样,我们无法做到原子性。所以也放弃了这种方法

方法3:文件锁

第三种方法用文件独享锁。进程启动时,我们就用flock去给文件上锁,如果锁成功,则说明 当前没有进程实例在运行,我们的代码逻辑就可以正常往下走。如果锁失败,说明当前已经 有进程实例在运行,我们直接退出当前补全。当然也可以使用block锁,它会一直在内旋, 一直等当已经在运行的实例解锁了,当前进程就往下走。在进程任务完成之后,要记得解锁。 而且这种方法调用,操作系统会帮我们确保原子性,也就是说在第一个进程上锁的过程,它不 会被打断,这时第二个进程再来上锁,那么它就失败了。另外就是它的使用非常简单,代码 也非常少。最后我们采用了这种方法

下面是上锁的代码:

#include <stdio.h>
#include <sys/file.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int fd = open("./foo.lock", O_WRONLY|O_CREAT);
    if (fd < 0) 
    {
        printf("open foo.lock failed\n");
        return -1;
    }

    int err = flock(fd, LOCK_EX|LOCK_NB);
    if (err == -1)
    {
        printf("lock failed\n");
        return -2;
    }

    printf("lock success\n");

    // 做任务逻辑
    sleep(1);

    flock(fd, LOCK_UN);

    return 0;
}

这个例子很简单,进程启动时,去打开一个文件,然后用flock来对这个文件描述符上独享 锁,同时指定时nonblocking的,这样当锁不上的时候,就直接退出当前进程了。

flock介绍

flock使用非常简单,以下是它的原型:

#include <sys/file.h>
int flock(int fd, int operation);

fd就是一个文件描述符,我们可以通过open调用来获得一个。
operation是要做的动作,它可以是以下的值:
LOCK_SH,这是一个共享锁,也叫乐观锁和读锁。多个共享锁可以同时锁定。
LOCK_EX,这是一个独享锁,也叫悲观锁和写锁。一个文件只能被一个独享锁锁定。
LOCK_UN,这个是释放锁。对使用以上的锁进行释放。

另外,对于上锁操作,也就是LOCK_SHLOCK_EX可以使用位或操作符与LOCK_NB位或, 当上不了解的时候,不阻塞,而是直接返回-1。否则的话,就会一直在等待,一直到可以上 锁。

所以,在上面的例子中,进程启动时,我们使用flockLOCK_EX来设置独享锁,同时使用 了LOCK_NB来让它不阻塞,这样我们知道有一个进程在运行时,我们就可以直接退出当前 进程了。当进程运行结束的时候,我们再次调用flockLOCK_UN释放锁。这样下一个 进程启动时就可以成功上锁了。