然后启动服务,某次执行的输出:
2020-03-30 23:47:27.376 INFO 53120 --- [pool-1-thread-1] club.throwable.schedule.Tasks : processTask1触发.......... 2020-03-30 23:47:37.378 INFO 53120 --- [pool-1-thread-1] club.throwable.schedule.Tasks : processTask1触发.......... .... 混合配置有些时候我们希望可以JSON配置和JDBC数据源配置进行混合配置,或者动态二选一以便灵活应对多环境的场景(例如要在开发环境使用JSON配置而测试和生产环境使用JDBC数据源配置,甚至可以将JDBC数据源配置覆盖JSON配置,这样能保证总是倾向于使用JDBC数据源配置),这样需要对前面两小节的实现加多一层抽象。这里的设计可以参考SpringMVC中的控制器参数解析器的设计,具体是HandlerMethodArgumentResolverComposite,其实道理是相同的。
其他注意事项在生产实践中,暂时不考虑生成任务执行日志和细粒度的监控,着重做了两件事:
并发控制,(多服务节点下)禁止任务并发执行。
跟踪任务的日志轨迹。
解决并发执行问题一般情况下,我们需要禁止任务并发执行,考虑引入Redisson提供的分布式锁:
// 引入依赖 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>最新版本</version> </dependency> // 配置类 @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedissonAutoConfiguration { @Autowired private RedisProperties redisProperties; @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient() { Config config = new Config(); SingleServerConfig singleServerConfig = config.useSingleServer(); singleServerConfig.setAddress(String.format("redis://%s:%d", redisProperties.getHost(), redisProperties.getPort())); if (redisProperties.getDatabase() > 0) { singleServerConfig.setDatabase(redisProperties.getDatabase()); } if (null != redisProperties.getPassword()) { singleServerConfig.setPassword(redisProperties.getPassword()); } return Redisson.create(config); } } // 分布式锁工厂 @Component public class DistributedLockFactory { private static final String DISTRIBUTED_LOCK_PATH_PREFIX = "dl:"; @Autowired private RedissonClient redissonClient; public DistributedLock provideDistributedLock(String lockKey) { String lockPath = DISTRIBUTED_LOCK_PATH_PREFIX + lockKey; return new RedissonDistributedLock(redissonClient, lockPath); } }这里考虑到项目依赖了spring-boot-starter-redis,直接复用了它的配置属性类(RedissonDistributedLock是RLock的轻量级封装,见附录)。使用方式如下:
@Autowired private DistributedLockFactory distributedLockFactory; public void task1() { DistributedLock lock = distributedLockFactory.provideDistributedLock(lockKey); // 等待时间为20秒,持有锁的最大时间为60秒 boolean tryLock = lock.tryLock(20L, 60, TimeUnit.SECONDS); if (tryLock) { try { // 业务逻辑 }finally { lock.unlock(); } } } 引入MDC跟踪任务的TraceMDC其实是Mapped Diagnostic Context的缩写,也就是映射诊断上下文,一般用于日志框架里面同一个线程执行过程的跟踪(例如一个线程跑过了多个方法,各个方法里面都打印了日志,那么通过MDC可以对整个调用链通过一个唯一标识关联起来),例如这里选用slf4j提供的org.slf4j.MDC:
@Component public class MappedDiagnosticContextAssistant { /** * 在MDC中执行 * * @param runnable runnable */ public void processInMappedDiagnosticContext(Runnable runnable) { String uuid = UUID.randomUUID().toString(); MDC.put("TRACE_ID", uuid); try { runnable.run(); } finally { MDC.remove("TRACE_ID"); } } }任务执行的时候需要包裹成一个Runnale实例:
public void task1() { mappedDiagnosticContextAssistant.processInMappedDiagnosticContext(() -> { StopWatch watch = new StopWatch(); watch.start(); log.info("开始执行......"); // 业务逻辑 watch.stop(); log.info("执行完毕,耗时:{} ms......", watch.getTotalTimeMillis()); }); }结合前面一节提到的并发控制,那么最终执行的任务方法如下:
public void task1() { mappedDiagnosticContextAssistant.processInMappedDiagnosticContext(() -> { StopWatch watch = new StopWatch(); watch.start(); log.info("开始执行......"); scheduleTaskAssistant.executeInDistributedLock("任务分布式锁KEY", () -> { // 真实的业务逻辑 }); watch.stop(); log.info("执行完毕,耗时:{} ms......", watch.getTotalTimeMillis()); }); }这里的方法看起来比较别扭,其实可以直接在任务装载的时候基于分布式锁和MDC进行封装,方式类似于ScheduledMethodRunnable,这里不做展开,因为要详细展开篇幅可能比较大(ScheduleTaskAssistant见附录)。
小结