分布式定时任务/分布式锁

JDK中Timer类

java.util.Timer定时器实际上是一个单线程,定时调度所拥有的TimerTask任务。

TimerTask类是一个定时任务类,实现了Runnable接口,而且是一个抽象类,需要定时执行的任务都需要重写它的run方法。

TImer类的缺陷

1)单线程,如果存在多个任务,某个任务执行时间过长,就会导致任务时间延迟。

2)异常终止,如果TimerTask抛出了未捕获的异常,则也会导致Timer线程终止,已经被安排但尚未执行的TimerTask也不会再执行了。

3)执行周期任务时依赖系统时间:如果当前系统时间发生了变化,会出现一些执行上的变化。

ScheduledExecutorServiceh和Spring Schedule

jdk1.5推出了基于线程池设计的ScheduledExecutorService,其设计思想是:每个被调度的任务都会由线程池中的一个线程去执行,因此任务是并发的,相互之间不会受到干扰。

其实除了自己可以实现定时任务,还可以通过使用Spring实现定时任务。

1.在Spring配置文件头中添加命名空间及描述(第9行、14和15行)

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4        xmlns:context="http://www.springframework.org/schema/context"
 5        xmlns:aop="http://www.springframework.org/schema/aop"
 6        xmlns:tx="http://www.springframework.org/schema/tx"
 7        xmlns:jpa="http://www.springframework.org/schema/data/jpa" 
 8        xmlns:jaxws="http://cxf.apache.org/jaxws"       
 9        xmlns:task="http://www.springframework.org/schema/task"       
10        xsi:schemaLocation="http://www.springframework.org/schema/beans      
11        http://www.springframework.org/schema/beans/spring-beans.xsd
12        http://www.springframework.org/schema/context 
13        http://www.springframework.org/schema/context/spring-context.xsd
14        http://www.springframework.org/schema/task
15        http://www.springframework.org/schema/task/spring-task-3.0.xsd       
16        http://www.springframework.org/schema/aop 
17        http://www.springframework.org/schema/aop/spring-aop.xsd
18        http://www.springframework.org/schema/tx
19        http://www.springframework.org/schema/tx/spring-tx.xsd
20        http://www.springframework.org/schema/data/jpa 
21        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
22        http://cxf.apache.org/jaxws 
23        http://cxf.apache.org/schemas/jaxws.xsd
24        ">
View Code

2.添加支持注解的配置 <task:annotation-driven>

3.定义任务,代码如下

    @Override
    @Scheduled(cron="0/20 * *  * * ? ")   //每20秒执行一次  
    public void testAop() {
        System.out.println("excute Service***********");
    }

Cron表达式

Cron表达式的格式为"秒 分 时 日 月 周 年"  推荐大家使用一些工具生成Cron表达式http://www.pppet.net/

分布式定时任务

分布式场景下定时任务的一个问题就是:怎么让某一个定时任务在一个触发时刻上仅有一台服务器在运行。

1.只在一台服务器执行

可以指定所有的调度任务只在固定的单台服务器上执行,虽然该方法解决了重复执行的问题,但是存在明显的两个缺陷:

单点风险和资源分配不均衡

2.通过配置参数分散运行

可以创建一个配置项,其中包含执行的定时任务类名,这样可以在部署服务时手动分散定时任务到不同的服务器上。

该方法虽然解决了资源分配不均衡的问题,不过依然存在单点风险,同时增加了运维管理难度。

3.通过全局"锁"互斥运行

 可以通过分布式锁来实现,当节点获取到锁就执行任务,在没有获取到锁时就不执行(抢占执行),这样就可以解决多节点重复执行任务的问题。可以使用Zookeeper、Redis或者数据库的方式来实现分布式锁。

接下来采用Redis来实现一个简单的分布式锁,示例代码如下。

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    private static final String KEY="lock_hello";
    
    public void doTask() {
        boolean lock=false;
        try {
            //获取锁
            lock=stringRedisTemplate.opsForValue().setIfAbsent(KEY, "1",10,TimeUnit.SECONDS);
            if(!lock) {
                //获取不到锁,直接退出
                return;
            }
            //设置超时,防止程序意外终止而导致key锁无法释放
            stringRedisTemplate.expire(KEY, 5,TimeUnit.MINUTES);
            //to do something
            System.out.println("do task...");
        } finally {
            //最终释放锁
            stringRedisTemplate.delete(KEY);
        }
    }

在程序中调用setIfAbsent方法来获取锁,如果返回true,则说明该key值不存在,表示获取到了锁;

如果返回false,则说明该key值存在,已经有程序在使用这个key值,从而实现了分布式加锁的功能。serIfAbsent封装了Redis原生的SETNX原子操作。

疑问1:获取锁的时候为了防止机器宕机导致锁一直释放不了,所以增加了过期时间TTL,但是一个线程1请求TTL导致锁被释放此时线程1任务还没结束,则另一个线程2进来的时候 同样会获得到锁,

这样线程2可能会释放线程1的锁。

解决办法:1.value放进去的是一个requestId释放锁的时候比较requestId是否一致。

             2.设置定时任务检查 比如锁是10s时间,定时任务5s检查一次 如果锁还有1s到期,则重新更新锁的过期时间

疑问3:因为redis是主从架构,在主节点setnx的值之后,还没有同步到从节点,但此时主节点挂掉了,把从节点选为新的master。

        新的线程发现没有这个key 可以再次加锁成功。 redis对于这没有很好的解决办法:可以采用ZooKeeper  参考zookeeper一致性原理:https://www.cnblogs.com/ssskkk/p/14940829.html#_label4

 

posted @ 2019-01-29 15:13  palapala  阅读(1666)  评论(0编辑  收藏  举报