在系统开发的初期,通常都是一个单体的架构,后面随着业务的发展,单体架构已经扛不住业务的压力。慢慢的会向微服务的方向去发展,在多节点的情况下,就会出现多个节点的定时任务可能会重复执行的情况。
1、Shedlock基本介绍
相对于xxl-job,Shedlock的集成更加的轻量,同时也不需要对服务进行过多的改造,我们当时在技术选型的时候就是使用Shedlock。
Shedlock从严格意义上来说,并不是一个分布式任务调度框架,设计的初衷也不是作为一个调度框架,而是一种分布式锁。所谓的分布式锁,解决的核心问题就是各个节点中无法通信的痛点。各个节点并不知道这个定时任务有没有被其他节点的定时器执行,所以理论上只需要有一个各个节点都能够访问到的资源,用这个资源去标记这个定时任务有没有执行就可以了。
Shedlock也有很多种方案:
- JdbcTemplate
- Mongo
- DynamoDB
- DynamoDB 2
- ZooKeeper (using Curator)
- Redis (using Spring RedisConnectionFactory)
- Redis (using Jedis)
- Hazelcast
- Couchbase
- ElasticSearch
- CosmosDB
- Cassandra
- Multi-tenancy
我们选用的是Redis方式。
2、使用RedisConnectionFactory方式
- 在pom文件中,引入以下依赖
<dependency><!--shedLock依赖--> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-spring</artifactId> <version>4.2.0</version> </dependency> <dependency><!--shedLock依赖--> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-provider-redis-spring</artifactId> <version>4.2.0</version> </dependency>
- 编写配置类
import net.javacrumbs.shedlock.core.LockProvider; import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider; import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.EnableScheduling; @Configuration @EnableScheduling @EnableSchedulerLock(defaultLockAtMostFor = "PT30S") public class ShedlockConfig { @Bean public LockProvider lockProvider(RedisTemplate redisTemplate) { return new RedisLockProvider(redisTemplate.getConnectionFactory()); } }
这种是使用的redisTemplate直接获取到ConnectionFactory的写法,如果是老工程的话,可能还要换一种写法,等下会再介绍。
- 在定时器方法上加上@SchedulerLock
package com.xiaohuihui.task; import net.javacrumbs.shedlock.core.LockAssert; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; @Component public class OrderExpireTestTask { @Autowired private StringRedisTemplate redisTemplate; /** * 通过设置lockAtMostFor,我们可以确保即使节点死亡,锁也会被释放;通过设置lockAtLeastFor, * 我们可以确保它在9分钟内不会执行超过一次。请注意,对于执行任务的节点死亡的情况, * lockAtMostFor只是一个安全网,所以将它设置为一个时间,这个时间远远大于最大估计执行时间。 * 如果任务花费的时间比lockAtMostFor更长,那么它可能会再次执行,结果将是不可预测的(更多的进程将持有锁)。 */ @Scheduled(cron = "0 0/10 0 * * ? ") @SchedulerLock(name = "scheduledTaskName", lockAtMostFor = "9m", lockAtLeastFor = "9m") public void sendExpireOrderMsg() { // To assert that the lock is held (prevents misconfiguration errors) LockAssert.assertLocked(); // do something String nowTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); redisTemplate.opsForValue().set("testTime", nowTime); System.out.println("==========> 开始发送订单过期消息 " + nowTime); } }
等定时器执行的时候,就会在redis中看到对应的key,job-lock:default:scheduledTaskName,还可以看到过期时间。
3、使用Jedis方式
- 在pom文件中,引入以下依赖
<dependency><!--shedLock依赖--> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-spring</artifactId> <version>2.2.0</version> </dependency> <dependency><!--shedLock依赖--> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-provider-redis-jedis</artifactId> <version>2.2.0</version> </dependency>
- 编写配置类
import net.javacrumbs.shedlock.core.LockProvider; import net.javacrumbs.shedlock.provider.redis.jedis.JedisLockProvider; import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @Configuration @EnableScheduling @EnableSchedulerLock(defaultLockAtMostFor = "PT30S") public class ShedlockConfig { private static final Logger logger = LoggerFactory.getLogger(ShedlockConfig.class); @Value("${redis.server.host}") private String host; @Value("${redis.server.port}") private Integer port; @Value("${redis.server.timeout}") private Integer timeout; @Value("${redis.server.password}") private String password; @Value("${redis.server.database}") private Integer database; @Bean public LockProvider lockProvider(JedisPoolConfig jedisPoolConfig) { JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password, database); return new JedisLockProvider(jedisPool); } }
使用的方式跟上面的一种方式是一样的,只不过注解当中的配置会稍稍的有却别,点进源码中,写的也比较清楚。
4、Shedlock原理
使用AOP对定时器方法进行了代理,对于加了注解方法,会先执行下面的代码。
此时如果向redis中设置值的操作成功(博主的截图中的redis key不一样,忽略,是两个不同的定时器),就会执行定时器中的逻辑。
没有设置成功,就不能够执行,直接打印日志。
在加锁成功的情况下,只要redis中的key没有过期,在这个时间内,这个方法都是不会被执行的。