标签归档:注解

基于Redis的分布式锁的简易实现

本文讨论的分布式锁适用于集群节点程序的互斥执行,注意此处的“分布式”是指该锁的适用场景是分布式系统,而非锁本身是分布式的,关于锁本身的分布式设计可以参考Redis官方介绍:『Distributed locks with Redis』。

之前在『如何用Spring实现集群环境下的定时任务』一文中提到利用Redis的"GET/SET+TTL"机制实现集群场景下定时任务的互斥执行功能,这实际上就是本文实现的原型。该文描述的方法的问题在于GET和SET方法的组合并非原子操作,在多进程并行执行场景下可能有多个客户端获得锁,从而破坏了锁的安全性。

本文的改进在两个方面:

1. 要解决分布式锁的安全性问题,需要使用Redis提供的锁原语:SETNX(since 1.0.0)或者SET NX EX(since 2.6.12)。这类命令的语义是:如果Key已存在,则返回SET成功,否则返回失败。

2. 使用注解方式加锁和解锁,避免代码重复和耦合。

以下是Java实现(Spring AOP)的原型:

1. 定义注解类,接收expire参数,表示此锁的过期时间。如果在定时任务中使用,一般要大于节点间的时间差,小于定时任务的时间间隔。

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DLock {
    public String value() default "";

    public String expire() default "30";
}

2. 定义切面类,完成锁的申请和释放逻辑。

@Aspect
@Component
public class DLockAdvice {
    private static String DLOCK_PREFIX = "DLOCK_PREFIX_";

    @Autowired
    private RedisService redisService = null;

    @Around("@annotation(lock)")
    public Object lock(ProceedingJoinPoint pjp, DLock lock) throws Throwable {
        Long expire = Long.valueOf(lock.expire());;
        Signature sig = pjp.getSignature();
        String lockKey = DLOCK_PREFIX + sig.getDeclaringTypeName() + sig.getName() + expire;
        if (redisService.setValueIfAbsent(lockKey, true, expire)) {
            return pjp.proceed();
        } 
        return null;
    }
}

考虑到注解的通用性,锁名称的区分度越大越好,此处采用的“前缀+包名+类名+方法名+过期时间”,因此假如有个方法通过参数个数或者类型不同进行重载,则该锁会对这个重载的每个方法都生效,除非把参数个数或者类型信息加到锁名称里。

另外考虑到Redis服务本身或者网络的不稳定性,需要在RedisService的setValueIfAbsent()方法中对异常进行处理,假如:

1. 能够容忍多个客户端同时获得锁。那么当执行Redis命令异常时返回true。
2. 无法容忍多个客户端同时获得锁,宁愿没有客户端可以获得锁。那么当执行Redis命令异常时返回false。

--EOF--

通过Spring获取properties文件属性值

Spring提供了注解@Value,用于在程序中获取properties配置文件属性值。例如:

1. applicationContext.xml中指定配置文件。

<context:property-placeholder location="classpath:xxx.properties" 
    ignore-unresolvable="true" />

当有多个配置文件时,上述配置可以配置多条。

<context:property-placeholder location="classpath:xxx.properties" 
    ignore-unresolvable="true" />
<context:property-placeholder location="classpath:yyy.properties" 
    ignore-unresolvable="true" />

2. Spring bean中使用@Value注解获取指定参数。

// xxx.properties配置项:
// server.ip=192.168.1.1
// server.port=8080

@Value("${server.ip}")
private String ip;

@Value("${server.port}")
private int port;

使用@Value注解的前提是当前对象的生命周期由Spring管理,是Spring bean,无论通过XML配置文件还是@Component、@Service等注解声明。假如一个对象的生命周期是我们程序自己管理的,比如常规用法下的new Object(),特别是做一些框架开发,经常用到Class.forName().newInstance()来实例化对象,那么想要反射为新创建对象的成员变量赋值时,如何借助Spring来获取已经解析好的properties属性值是个值得一试的探索。上述场景可以简化为:

如何在一个拥有Spring上下文的平台上,对不受Spring管理的对象使用依赖注入,达到类似@Value注解实现的功能。

思路也很简单,既然Spring已经解析过properties文件,那么通过某种手段把这些值暴露出来就可以了,EmbeddedValueResolverAware接口很适合做这件事情。Aware接口是定义一些能在Spring bean中操作Spring上下文信息的一类接口,常见的有ApplicationContextAware,可以在Spring bean中拿到ApplicationContext;BeanFactoryAware,可以在Spring bean中拿到Spring BeanFactory。这里的EmbeddedValueResolverAware也是类似功能,它定义了一个void setEmbeddedValueResolver(StringValueResolver resolver)接口方法,在bean初始化后,Spring回调setEmbeddedValueResolver()方法,将StringValueResolver对象注入到bean中,从这个对象中就能获取properties文件中的属性名称和值。用法如下:

1. 声明一个实现EmbeddedValueResolverAware接口的实例,用@Component注解声明为Spring bean,重写setEmbeddedValueResolver()方法,将StringValueResolver实例的引用保存下来,并且对外提供getPropertiesValue()方法,用于获取properties值。

@Component
public class PropertiesUtils implements EmbeddedValueResolverAware {
    
    privat StringValueResolver stringValueResolver;

    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        stringValueResolver = resolver;
    }

    public String getPropertiesValue(String name){
        return stringValueResolver.resolveStringValue(name);
    }
}

2. 通过${key}作为name格式调用getPropertiesValue()方法,获取properties值。

String name = "${server.ip}";
String value = propertiesUtils.getPropertiesValue(name);

StringValueResolve解析出来的值都是String类型的,非String类型需要在拿到参数String值后自行转换。

本文只对特定场景下使用EmbeddedValueResolverAware接口借助Spring上下文从properties文件中获取参数值做了一个简单介绍,此方法有效但不唯一,供参考。

--EOF--

Spring @Async

Spring @Async注解可以将方法异步化处理。与『如何用Spring实现集群环境下的定时任务』提到的@Scheduled注解一样,@Async注解也需要task命名空间驱动,需要配置一个线程池。

一个被@Async注解的异步方法,它的返回值只有两种,要么void,要么返回一个Future类型,Future类型用于追踪异步方法的执行结果。Spring提供了一个Future类型的实现类AsyncResult。

比如以下场景:一个创建实例的HTTP请求,该实例由多个相同的资源组成,这些资源的创建非常耗时,但是相互间没有依赖性,资源创建完成后还要进行一些数据库操作记录实例元信息。这个场景非常适合用@Async注解+Future返回值实现。用@Async注解标注创建实例的Service方法,使得创建实例的过程放在了一个独立线程中执行,此时HTTP请求可以直接响应用户(202 Accepted),并且实例的Location(URI)也可以同时返回。为了进一步提升用户体验,减少实例创建的等待时间,可以将资源的创建分配到多个线程中完成。资源创建的结果(true or false)放进AsyncResult中返回,此处如果不用Future返回值对异步创建资源的过程进行追踪,创建实例的主线程就没法确定资源是否创建成功,则资源的创建只能在主线程中依次进行。

这个过程的大致代码如下:
1. Controller类,接收用户请求。

1
2
3
4
5
6
7
8
9
@Controller
public class InstanceController {
    @RequestMapping(value = "instances", method = RequestMethod.POST)
    public String createInstance(HttpServletResponse response, ModelMap model) {
        //doSth().
        instanceTaskService.createInstance(instance);
        return doResponse(201);
    }
}

2. Instance Service类。异步处理实例创建过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class InstanceTaskService {
    @Async
    public void createInstance(String instanceId) {
        //doSth().
        List<Future<Boolean>> futureList = new ArrayList<Future<Boolean>>();
        for (Resource res : resourceList) {
            // 将每个资源的异步执行结果放入列表。
            futureList.add(resourceTaskService.createResource(res.getId()));
        }
 
        for (Future<Boolean> future : futureList) {
            try {
                //超时判断,避免无限期等待。
                Boolean result = future.get(10, TimeUnit.MINUTES);
                if (!result) {
                    // success.
                } else {
                    throw new Exception("create instance error.");
                }
            } catch (Exception e) {
                rollback();
            }
        }
    }
}

3. Resouce Service类,异步处理资源创建过程。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class ResouceTaskService {
    @Async
    public Future<Boolean> createRescource(String resId) {
        //doSth().
        if(success()){
            return new AsyncResult<Boolean>(true);
        else{
            return new AsyncResult<Boolean>(false);
        }
    }
}

关键步骤在于createInstance()方法中对每个资源调用future.get()方法,Future类的get()方法是阻塞的:当异步方法已经返回时,调用get()方法立即返回;当异步方法还在执行中时,调用get()方法会被阻塞,直到异步方法执行结束,get(long timeout, TimeUnit unit)方法是get()方法的重载版本,可以指定阻塞的最长时间。示例程序中,创建资源可以并发进行,createInstance()方法中等待所有资源创建成功的时间就是单个资源创建成功的最长时间。

另外有一个需要注意的地方,示例程序中把createResource()方法和createInstance()方法放在了不同的类中,这么做不仅仅是因为代码风格和可读性,而是因为,一个@Async注解的方法,它只能被非本类方法调用时Spring才将其转变为异步方法执行,如果它被本类方法调用,那么无法转变为异步执行。原因在于:@Async注解的实现基于Spring AOP,『Spring AOP实践小结』一文中已有提及,Spring AOP的实现原理是Java动态代理,也就是说Spring会为带@Async注解的方法所在类生成一个代理类,当该方法被调用时,Spring将代理类放到线程池中运行。而发生在本类中的方法调用,则属于目标类的内部调用,代理类无法再参与进去进行异步处理了。

--EOF--

Spring AOP实践小结

近几天重构了一块代码,这块代码本来的逻辑是这样的:一个创建队列的操作,操作成功后,会调用监控和计费系统接口进行注册;反之,删除队列后会调用相应接口在监控和计费系统中注销。由于监控项和计费项较为特殊,导致最后写出来的createQueue()/deleteQueue()方法中监控/计费代码占30%比例。用Spring AOP进行代码重构后,createQueue()/deleteQueue()方法中只包含队列相关的业务逻辑,监控和计费代码放在独立的切面类中,实现解耦。

以下是这次的学习加实践小结。

一、 Spring AOP介绍
Spring AOP是Spring提供的面向切面编程(Aspect Oriented Programming)框架。但凡是AOP,就少不了以下几个关键概念:

1. 切面(Aspect):
切面是一个独立于常规业务逻辑的模块类,用来描述横向的关注点,它定义了这个横向关注点“何时”“何处”完成“什么样的功能”。

2. 通知(Advice):
通知定义了切面“何时”完成“什么样的功能”。目前,Spring AOP支持以下5种通知类型:
1). Before: 在被拦截方法调用之前执行自定义代码。
2). After: 在被拦截方法调用之后执行自定义代码。
3). After-returning: 在被拦截方法成功返回之后执行自定义代码。
4). After-throwing: 在被拦截方法抛出异常之后执行自定义代码。
5). Around: 包裹被拦截方法,可以在该方法之前或之后执行自定义代码。

3. 切点(Pointcut):
切点定义了切面中的自定义代码在“何处”执行。

4. 连接点(Joinpoint):
连接点是应用在正常执行过程中能够插入切面的一个点。比如调用某个方法时,修改某个字段时。

5. 织入(Weaving):
织入是指将切面类应用到被拦截的目标对象,并生成新的代理类的过程。

Spring AOP借鉴了Aspectj的切点解析与匹配的实现方式,支持@Aspectj注解驱动的切面,与Aspectj AOP框架相比,它有以下特征:

1. Spring AOP由标准Java编写,简单易学。Aspectj则是通过扩展的Java语法和语义实现,有额外的学习成本。
2. Spring AOP只支持运行期的切面织入。Aspectj则同时支持编译期、类加载期和运行期的切面织入。
3. Spring AOP只支持方法调用时的连接点。Aspectj则还支持成员变量修改和构造器调用时的连接点。

在实现原理方面,Spring AOP利用了Java的动态代理技术。Java动态代理有两种实现方式:

1. JDK动态代理:这种生成代理对象的方式需要传入目标类的接口定义,因此,基于JDK动态代理的AOP只能拦截那些实现了接口的目标类。

2. CGLib动态代理:CGLib动态代理是通过为目标类动态生成一个子类,同时这个子类也是目标类的代理类。因此,目标类中使用了final限定词的方法不能被拦截,因为它无法被子类重写。此外,CGLib生成代理子类的过程中目标类的构造方法会被调用两次(目标类实例化一次,代理子类实例化时一次),需留意。

另外,两种动态代理方式在性能上也有些不同表现。网上有一组数据,据说CGLib动态代理创建代理对象的速度比JDK动态代理慢8倍左右,但是其创建出来的代理对象运行速度要比JDK动态代理创建出来的代理对象快10倍。

两种动态代理方式各有优缺点,默认情况下,Spring AOP在为目标对象生成代理对象时会有个判断,如果目标对象是某(多)个接口的实现类,Spring会选择JDK动态代理实现方式,否则,选择CGLib动态代理实现方式。代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public AopProxy createAopProxy(AdvisedSupport config) 
                                    throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() 
        || hasNoUserSuppliedProxyInterfaces(config)) {
        Class targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException(
              "TargetSource cannot determine target class: " +
              "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface()) {
            return new JdkDynamicAopProxy(config); //目标类是接口类型,JDK动态代理实现
        }
        if (!cglibAvailable) {
            throw new AopConfigException(
              "Cannot proxy target class because CGLIB2 is not available. " +
              "Add CGLIB to the class path or specify proxy interfaces.");
        }
        return CglibProxyFactory.createCglibProxy(config); //CGLib动态代理实现
    }
    else {
        return new JdkDynamicAopProxy(config); //JDK动态代理实现
    }
}
 
/**
 * Determine whether the supplied {@link AdvisedSupport} has only the
 * {@link org.springframework.aop.SpringProxy} interface specified
 * (or no proxy interfaces specified at all).
 */
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
    Class[] interfaces = config.getProxiedInterfaces();
    return (interfaces.length == 0 || (interfaces.length == 1 
        && SpringProxy.class.equals(interfaces[0])));
}

其中hasNoUserSuppliedProxyInterfaces()方法就是用于判断目标类是否是接口的实现类。如果不想使用这种默认的动态代理策略,Spring提供了一些选项可以进行配置。

那么,Spring AOP是什么时候拦截目标类的呢?前文提过Spring AOP支持运行时的切面织入,实际上,所谓的“拦截”发生在调用ApplicationContext容器getBean()方法获取目标对象的时候,getBean()方法返回的不是目标类实例,而是目标类的代理类实例。因此程序中所有对目标类方法的调用,实际上都是对相应代理类的方法调用。

二、 Spring AOP Demo

这个Demo以Around通知为例,切点为IQueueService的createQueue()方法。

1. 修改application-context.xml文件,添加aop命名空间,添加<aop:aspectj-autoproxy />声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context.xsd
	http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
 
    <aop:aspectj-autoproxy />
</beans>

2. 切面类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Aspect
@Component
public class MonitorAdvice implements Ordered {
    @Autowired
    private IMonitorService monitorService = null;
 
    @Pointcut("execution(* com.xs.service.IQueueService.createQueue(..))")
    public void createQueue() {
    }
 
    @Around("createQueue()")
    public Object createQueue(ProceedingJoinPoint pjp) throws Throwable {
        Boolean result = (Boolean) pjp.proceed();
        if (result) {
            String[] args = (String[]) pjp.getArgs();
            monitorService.register(args);
        }
        return result;
    }
 
    @Override
    public int getOrder() {
        return 20;
    }
}

其中,@Aspect注解声明了一个切面类,@Component注解是为了将该类也纳入Spring的依赖管理,通常切面类也依赖到其他的Spring bean,例如示例中的monitorService。@Pointcut注解用于声明切点,它支持多种切点指示器,此处采用最常用的execution,表示在createQueue()方法调用前后将此切面织入。Spring AOP支持的切点指示器没有Aspectj丰富,它是Aspectj的一个子集,关于切点指示器的使用,可以查看手册:『Declaring a pointcut』。@Around注解用于声明一个Around类型的通知,此处也可以将切点表达式直接写到Around注解里,之所以将切点表达式单独提取出来用@Pointcut声明,目的是为了重用。ProceedingJoinPoint是连接点实例,从中可以获取连接点的信息,比如目标类信息,目标方法参数等等。

假如某个连接点要织入多个切面,而且比较关心各个切面的织入顺序时,可以有两种方式指定顺序:一种是如示例所示的实现Ordered接口的getOrder()方法;另外一种是在切面类上加@Order(int)的注解。数值越低,表示优先级越高,即越先织入。

三、注意事项
1. @Aspectj注解的切面类无法被继承。所以,企图将所有切点声明(@Pointcut)放入公有父类实现切点声明重用的想法可以打消了。有一个变通的方法是将所有切点声明放到一个单独类中,并且所有切点方法声明为public,然后在切面类用“类名.方法名”形式访问。如下所示:

1
2
3
4
5
6
7
8
9
10
@Aspect
public class Pointcuts {
    @Pointcut("execution(* com.xs.service.IQueueService.createQueue(..))")
    public void createQueue() {
    }
 
    @Pointcut("execution(* com.xs.service.IQueueService.deleteQueue(..))")
    public void deleteQueue() {
    }
}
1
2
3
4
5
6
7
8
9
@Around("Pointcuts.createQueue()")
public Object createQueue(ProceedingJoinPoint pjp) throws Throwable {
    Boolean result = (Boolean) pjp.proceed();
    if (result) {
        String[] args = (String[]) pjp.getArgs();
        monitorService.register(args);
    }
    return result;
}

2. 被本类中其他方法调用的方法是无法作为切点被织入通知的,例如,ClassA中有方法MethodA1()和MethodA2(),ClassB中有方法MethodB(),其中,ClassB.MethodB()调用了ClassA.MethodA1(),ClassA.MethodA1()又调用了本类中的MethodA2()。此时,切点切在MethodA2()上的话,MethodA2()不会被拦截,不会被织入通知。探其原因,因为Spring无论选择哪种动态代理方式,本质都是为目标类创建一个代理类,再由代理类调用目标类的方法。因为ClassB.MethodB()调用了ClassA代理类的MethodA1'()方法,再由MethodA1'()方法调用MethodA1()方法,当程序已经执行到ClassA.MethodA1()时,就无法再执行代理类代码了。因此,在Spring AOP框架下,可以认为连接点只存在于不同实例间的方法调用。

3. execution切点指示器,切点表达式匹配某个子类的所有方法,如果该子类继承了父类的一个方法,并且没有对其进行重写,那么当这个方法被调用时,它无法被织入通知,也就是说该切点无效。

4. 有个循环依赖问题,以Demo为例:如果切点切在IQueueService的任一个方法上,Spring都会为其创建代理类,如果此时切面类本身又依赖到IQueueService,那么就可能出现一个BeanCurrentlyInCreationException异常:

1
2
3
4
5
6
Error creating bean with name 'queueService': Bean with name 'queueService' 
has been injected into other beans [monitorAdvice] in its raw version as part 
of a circular reference, but has eventually been wrapped. This means that 
said other beans do not use the final version of the bean. This is often 
the result of over-eager type matching - consider using 'getBeanNamesOfType' 
with the 'allowEagerInit' flag turned off.

这篇文章提供了这个问题的几种解决方法。我认为,最好的方法还是从源头避免这种情况出现,比如可以为切面类单独封装一个接口,功能同IQueueService,从而使得切面类可以不依赖目标类。

最后,Spring AOP为代码的解耦提供了很好的实践。最理想的情况是业务代码感受不到切面代码的存在,如果因为设计原因,使得业务代码中嵌入了一些能感受到AOP的存在,那不如不用AOP。

--EOF--

如何用Spring实现集群环境下的定时任务

定时任务的实现方式有多种,例如JDK自带的Timer+TimerTask方式,Spring 3.0以后的调度任务(Scheduled Task),Quartz等。

Timer+TimerTask是最基本的解决方案,但是比较远古了,这里不再讨论。Spring自带的Scheduled Task是一个轻量级的定时任务调度器,支持固定时间(支持cron表达式)和固定时间间隔调度任务,支持线程池管理。以上两种方式有一个共同的缺点,那就是应用服务器集群下会出现任务多次被调度执行的情况,因为集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行。Quartz是一个功能完善的任务调度框架,特别牛叉的是它支持集群环境下的任务调度,当然代价也很大,需要将任务调度状态序列化到数据库。Quartz框架需要10多张表协同,配置繁多,令人望而却步...

经过折中考虑,还是选择了Spring的Scheduled Task来实现定时任务。如下:

1. Spring配置文件application-context.xml中添加task命名空间和描述。

1
2
3
4
5
6
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/task
	http://www.springframework.org/schema/task/spring-task.xsd">

2. 添加调度器和线程池声明。

1
2
<task:executor id="taskExecutor" pool-size="10" />
<task:annotation-driven executor="taskExecutor" />

3. 实现调度方法。基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.netease.yx.service;
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
 
@Service
public class ScheduledService {
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
       System.out.println("Scheduled Task");
    }
}

@Scheduled注解支持秒级的cron表达式,上述声明表示每天5点执行build任务。

前文已经提过,这种方式在单台应用服务器上运行没有问题,但是在集群环境下,会造成build任务在5点的时候运行多次,遗憾的是,Scheduled Task在框架层面没有相应的解决方案,只能靠程序员在应用级别进行控制。

如何控制?

1. 无非是一个任务互斥访问的问题,声明一把全局的“锁”作为互斥量,哪个应用服务器拿到这把“锁”,就有执行任务的权利,未拿到“锁”的应用服务器不进行任何任务相关的操作。
2.这把“锁”最好还能在下次任务执行时间点前失效。

在项目中我将这个互斥量放在了redis缓存里,1小时过期,这个过期时间是由任务调度的间隔时间决定的,只要小于两次任务执行时间差,大于集群间应用服务器的时间差即可。

完整定时任务类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.netease.yx.service;
 
import javax.annotation.Resource;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.netease.yx.service.ICacheService;
 
@Service
public class ScheduledService {
    @Resource
    private ICacheService cache = null;
 
    private static String CACHE_LOCK = "cache_lock";
 
    private static int EXPIRE_PERIOD = (int)DateUtils.MILLIS_PER_HOUR / 1000;
 
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
        if (cache.get(CACHE_LOCK) == null) {
            cache.set(CACHE_LOCK, true, EXPIRE_PERIOD);
            doJob();
        }
    }
}

--EOF--