Spring 多个微服务做定时任务,如何做负载
基本思路
多个微服务,其业务的逻辑是一样的,自然包括定时任务。负载均衡在执行的时候,到达某个节点以后,定时任务都会执行,可以控制的思路就是使用队列的方式去操作。
如下有两种思路:
将负载均衡的定时任务,从原先的直接执行业务逻辑修改为先将业务逻辑请求到队列中,然后让空闲的微服务去队列中自动领取;
将负载均衡的定时任务,加锁进行操作。
ShedLock
ShedLock does one and only one thing. It makes sure your scheduled tasks are executed at most once at the same time. If a task is being executed on one node, it acquires a lock which prevents execution of the same task from another node (or thread). Please note, that if one task is already being executed on one node, execution on other nodes does not wait, it is simply skipped.
Currently, Spring scheduled tasks coordinated through Mongo, JDBC database, Redis, Hazelcast or ZooKeeper are supported. More scheduling and coordination mechanisms and expected in the future.
ShedLock is not a distributed scheduler
Please note that ShedLock is not and will never be full-fledged scheduler, it's just a lock. If you need a distributed scheduler, please use another project. ShedLock is designed to be used in situations where you have scheduled tasks that are not ready to be executed in parallel, but can be safely executed repeatedly. For example if the task is fetching records from a database, processing them and marking them as processed at the end without using any transaction. In such case ShedLock may be right for you.
By setting lockAtMostFor we make sure that the lock is released even if the node dies and by setting lockAtLeastFor we make sure it's not executed more than once in fifteen minutes. Please note that if the task takes longer than 15 minutes, it will be executed again.
个人理解,拿JDBCTemplate进行说明,就是当第一个微服务执行定时任务的时候,会将此定时任务进行锁操作,然后其他的定时任务就不会再执行,锁操作有一定的时长,超过这个时长以后,再一次,所有的定时任务进行争抢下一个定时任务的执行权利,如此循环。其中两个配置lockAtMostFor和lockAtLeastFor,保证了在一个定时任务的区间内只有一个定时任务在执行,同时也保证了即便是其中的一个定时任务挂掉了,到一定的时间以后,锁也会释放,其他的定时任务依旧会进行执行权的争夺,执行定时任务。
使用步骤
环境支持
Java 8 、slf4j-api、Spring Framework(可选)
依赖引入
net.javacrumbs.shedlock
shedlock-spring
0.18.2
定时任务的写法
import net.javacrumbs.shedlock.core.SchedulerLock;
...
@Scheduled(...)
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
// do something
}
The @SchedulerLock annotation has several purposes. First of all, only annotated methods are locked, the library ignores all other scheduled tasks. You also have to specify the name for the lock. Only one tasks with the same name can be executed at the same time.
You can also set lockAtMostFor attribute which specifies how long the lock should be kept in case the executing node dies. This is just a fallback, under normal circumstances the lock is released as soon the tasks finishes.
Lastly, you can set lockAtLeastFor attribute which specifies minimum amount of time for which the lock should be kept. Its main purpose is to prevent execution from multiple nodes in case of really short tasks and clock difference between the nodes.
示例
Let's say you have a task which you execute every 15 minutes and which usually takes few minutes to run. Moreover, you want to execute it at most once per 15 minutes. In such case, you can configure it like this
import net.javacrumbs.shedlock.core.SchedulerLock;
...
private static final int FOURTEEN_MIN = 14 * 60 * 1000;
...
@Scheduled(cron = "0 */15 * * * *")
@SchedulerLock(name = "scheduledTaskName", lockAtMostFor = FOURTEEN_MIN, lockAtLeastFor = FOURTEEN_MIN)
public void scheduledTask() {
// do something
}
By setting lockAtMostFor we make sure that the lock is released even if the node dies and by setting lockAtLeastFor we make sure it's not executed more than once in fifteen minutes. Please note that if the task takes longer than 15 minutes, it will be executed again.
配置task
Now we need to integrate the library into Spring. It's done by wrapping standard Spring task scheduler.
import net.javacrumbs.shedlock.spring.SpringLockableTaskSchedulerFactory;
...
@Bean
public ScheduledLockConfiguration taskScheduler(LockProvider lockProvider) {
return ScheduledLockConfigurationBuilder
.withLockProvider(lockProvider)
.withPoolSize(10)
.withDefaultLockAtMostFor(Duration.ofMinutes(10))
.build();
}
Or if you already have an instance of ScheduledExecutorService
@Bean
public TaskScheduler taskScheduler(ScheduledExecutorService executorService, LockProvider lockProvider) {
return SpringLockableTaskSchedulerFactory.newLockableTaskScheduler(executorService, lockProvider, Duration.of(10, MINUTES));
}
配置锁
锁的配置有多种方式的,如下的。
Mongo
Import the project
net.javacrumbs.shedlock
shedlock-provider-mongo
0.18.2
Configure:
import net.javacrumbs.shedlock.provider.mongo.MongoLockProvider;
...
@Bean
public LockProvider lockProvider(MongoClient mongo) {
return new MongoLockProvider(mongo, "databaseName");
}
Please note that MongoDB integration requires Mongo >= 2.4 and mongo-java-driver >= 3.4.0
JdbcTemplate
Create the table
CREATE TABLE shedlock(
name VARCHAR(64),
lock_until TIMESTAMP(3) NULL,
locked_at TIMESTAMP(3) NULL,
locked_by VARCHAR(255),
PRIMARY KEY (name)
)
script for MS SQL is here
CREATE TABLE [dbo].[shedlock](
[name] [varchar](64) NOT NULL,
[lock_until] [datetime] NULL,
[locked_at] [datetime] NULL,
[locked_by] [varchar](255) NOT NULL,
CONSTRAINT [PK_shedlock] PRIMARY KEY CLUSTERED
(
[name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Add dependency
net.javacrumbs.shedlock
shedlock-provider-jdbc-template
0.18.2
Configure:
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
...
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
Tested with MySql, Postgres and HSQLDB
Plain JDBC
For those who do not want to use jdbc-template, there is plain JDBC lock provider. Just import
net.javacrumbs.shedlock
shedlock-provider-jdbc
0.18.2
and configure
import net.javacrumbs.shedlock.provider.jdbc.JdbcLockProvider;
...
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcLockProvider(dataSource);
}
the rest is the same as with JdbcTemplate lock provider.
Warning
Do not manually delete lock row or document from DB table or Mongo collection. ShedLock has an in-memory cache of existing locks so the row will NOT be automatically recreated until application restart. If you need to, you can edit the row/document, risking only that multiple locks will be held. Since 0.18.2 you can clean the cache by calling clearCache() on LockProvider.
ZooKeeper (using Curator)
Import
net.javacrumbs.shedlock
shedlock-provider-zookeeper-curator
0.18.2
and configure
import net.javacrumbs.shedlock.provider.zookeeper.curator.ZookeeperCuratorLockProvider;
...
@Bean
public LockProvider lockProvider(org.apache.curator.framework.CuratorFramework client) {
return new ZookeeperCuratorLockProvider(client);
}
By default, ephemeral nodes for locks will be created under /shedlock node.
Redis (using Spring RedisConnectionFactory)
Import
net.javacrumbs.shedlock
shedlock-provider-redis-spring
0.18.2
and configure
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
...
@Bean
public LockProvider lockProvider(JedisPool jedisPool) {
return new RedisLockProvider(connectionFactory, ENV);
}
Redis (using Jedis)
Import
net.javacrumbs.shedlock
shedlock-provider-redis-jedis
0.18.0
and configure
import net.javacrumbs.shedlock.provider.redis.jedis.JedisLockProvider;
...
@Bean
public LockProvider lockProvider(JedisPool jedisPool) {
return new JedisLockProvider(jedisPool, ENV);
}
Hazelcast
Import the project
net.javacrumbs.shedlock
shedlock-provider-hazelcast
0.18.0
Configure:
import net.javacrumbs.shedlock.provider.hazelcast.HazelcastLockProvider;
...
@Bean
public HazelcastLockProvider lockProvider(HazelcastInstance hazelcastInstance) {
return new HazelcastLockProvider(hazelcastInstance);
}
Spring XML configuration
If you are using Spring XML config, use this configuration
Annotate scheduler method(s)
@SchedulerLock(name = "taskName")
public void run() {
}
Running without Spring
It is possible to use ShedLock without Spring
LockingTaskExecutor executor = new DefaultLockingTaskExecutor(lockProvider);
...
Instant lockAtMostUntil = Instant.now().plusSeconds(600);
executor.executeWithLock(runnable, new LockConfiguration("lockName", lockAtMostUntil));