008:整合XXL-Job实现分片定时任务集群模式
1 高效实现分片定时任务集群原理
今日课程任务
实现千万级定时消息推送平台
- 定义活动推送消息模板
- XXL-Job实现分布式任务调度原理
- 整合XXL-Job实现分布式定时任务
- XXL-Job分片任务调度集群原理
假设发送100万消息需要10个小时,如何实现1000万也只需要10个小时?
动态分片实现集群,只需要新增服务器集群节点即可动态扩容推送消息
2 动态分片集群执行任务实现原理
微信活动群发 查询数据库中wx_open_id不为空的数据,调用微信接口群发。
发送活动提醒,活动仅在当天有效,假设公众号有100w数据,常规做法数据库分页查询,每次查10w发送需要1小时,那么100w需要10小时。
分页查询群发效率低,如果活动有一定时效性要求,随着用户量不断增加,消息推送难以准时发给用户。
如何实现无论用户是否继续增加,都可以在有效期内全部发送完毕?
动态分片集群模式。
3 定时任务集群如何保证幂等性问题
定时群发跑批数据量大的情况下如何处理?
可以采用多线程,但是非常消耗单台服务器内存。
业务代码是否可以和定时任务代码放入同一个jar包?
定时任务模块和业务模块单独拆分。定时任务项目底层就是死循环处理,非常占用服务器资源。
定时任务集群如何防止业务重复执行?
- 将定时任务代码单独部署一个jar包中,不参与业务逻辑服务器集群部署;
- 在jar中开启一个定时任务配置开关,判断是否需要将定时任务类加载到spring容器中;
- 使用分布式锁。项目启动中,只要谁能够拿到分布式锁,谁就能够将定时任务的配置类加载到spring容器中,否则不加载;
- 数据库中插入主键id,只要谁能够往数据库中插入一条相同的主键,插入成功就可以加载定时任务配置类;
以上方案只适合小项目,不适合互联网级别项目 - 采用分布式任务调度平台框架(推荐)
4 分布式任务调度平台分片集群模式
构建分布式任务调度平台Admin
1.官方下载XXL-Job Admin的源代码
2.数据库创建xxl-job需要依赖的库表doc/db/tables_xxl_job.sql
3.在xxl-job jdbc链接配置加上&serverTimezone=UTC 否则报错
idea导入项目直接启动即可
访问http://127.0.0.1:8080/xxl-job-admin/
默认账号密码: admin 123456
5 本地构建执行器项目注册到注册中心上
创建项目
mt-shop-service-job
----mt-shop-service-member-job
引入maven依赖
<dependencies>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
引入配置文件
application.properties
# web port
server.port=8083
# log config
logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### xxl-job executor address
xxl.job.executor.appname=mayikt-member-executor-job
xxl.job.executor.ip=
xxl.job.executor.port=9999
### xxl-job, access token
xxl.job.accessToken=
### xxl-job log path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job log retention days
xxl.job.executor.logretentiondays=30
bootstrap.yml
spring:
cloud:
nacos:
discovery:
##服务的注册
server-addr: 127.0.0.1:8848
### nacos 配置中心
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
datasource:
url: jdbc:mysql://localhost:3306/meite_member?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 127.0.0.1
port: 6369
password: 123456
application:
name: mayikt-member-job
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
配置类
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
任务类
@Component
@Slf4j
public class WeChatActivityJob {
/**
* @param param
* @return
* @XxlJob 该任务的名称id
*/
@XxlJob("weChatActivityJobHandler")
public ReturnT<String> weChatActivityJobHandler(String param) {
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
int index = shardingVO.getIndex();
log.info(">>>定时任务开始触发<<<param:{},index:{}", param, index);
return ReturnT.SUCCESS;
}
}
任务调度中心配置执行器,测试结果:
执行器任务集群轮询策略执行
6 分片集群分页算法原理分析
选择分片广播模式,多台节点同时执行,每台节点拿到的index值不一样
计算分片公式
收到客户端{pageNo:1,pagesize:10}
select * from table limit (pageNo-1)*pageSize, pageSize;
假设数据库一共有6条数据,分成3个节点
分片 0 pageSize=0+1 起始值1-1 * 2 =0
分片 1 pageSize=1+1 起始值2-1 * 2 =2
分片 2 pageSize=2+1 起始值3-1 * 2 =4
7 动态实现执行器快速扩容与缩容
@Data
public class UserDO {
/**
* userid
*/
private Long userId;
/**
* 手机号码
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 密码
*/
private String passWord;
/**
* 用户名称
*/
private String userName;
/**
* 性别 0 男 1女
*/
private char sex;
/**
* 年龄
*/
private Long age;
/**
* 注册时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date updateTime;
/**
* 账号是否可以用 1 正常 0冻结
*/
private char isAvailable;
/**
* 用户头像
*/
private String picImg;
/**
* 用户关联 QQ 开放ID
*/
private String qqOpenId;
/**
* 用户关联 微信 开放ID
*/
private String wxOpenId;
}
public interface UserMapper {
@Update("\n" +
"update meite_user set WX_OPENID=#{wxOpenId} where user_id=#{userId};")
int updateUseOpenId(@Param("userId") Long userId, @Param("wxOpenId") String wxOpenId);
@Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
",user_name as username ,user_name as username,sex as sex \n" +
",age as age ,create_time as createtime,IS_AVAILABLE as ISAVAILABLE\n" +
",\n" +
"pic_img as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
"\n" +
"from meite_user where trim(WX_OPENID)!='' limit #{index},#{pageSize}; ")
List<UserDO> selectByOpenIdNotIsNull(@Param("index") Integer index, @Param("pageSize") Integer pageSize);
}
@Component
@Slf4j
public class WeChatActivityJob {
@Autowired
private UserMapper userMapper;
@Value("${mayikt.member.job.WeChatActivitiePageSize}")
private Integer pageSize;
/**
* @param param
* @return
* @XxlJob 该任务的名称id
*/
@XxlJob("weChatActivityJobHandler")
public ReturnT<String> weChatActivityJobHandler(String param) {
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
int index = shardingVO.getIndex();
// int startIndex = ((index + 1) - 1) * pageSize;
int startIndex = index * pageSize;
// log.info(">>>定时任务开始触发<<<param:{},index:{}", param, index);
List<UserDO> userDos = userMapper.selectByOpenIdNotIsNull(startIndex, pageSize);
log.info("userDos:" + JSONObject.toJSONString(userDos));
return ReturnT.SUCCESS;
}
}
测试效果:
8 分布式任务调度平台xxl-job一些疑问
- 执行器发送一半如果失败的情况下,一般看日志在哪里开始失败写个小程序补发;
- 如何写多个定时任务? 在方法上加上@XxlJob(“xxxHandler”)对应配置任务管理即可;
- 如果有100w条数据分成10个执行器,每个执行器10w,在每个执行器查询范围里面再分页查询循环处理;
- 假设执行器某一台宕机会丢失部分数据,执行器会记录下来重启后会自动补发数据;
描述定时消息推送流程
整合分布式任务调度平台xxl-job,定时跑批群发百万量级微信消息推送
- 分布式任务调度平台xxl-job环境搭建 xxljob-admin
- 将定时任务逻辑与业务逻辑代码完全分开,单独构建定时任务模块执行器
- 对执行器(定时任务项目)采用集群分片的模式部署,根据不同的index查询不同分页内容实现跑批,可以实现动态扩容与缩容
- 中途如果定时任务出现异常,采用日志形式记录后期人工补偿
总结:
1. 百万级定时消息推送模板的原理
对定时任务模块实现集群部署,采用分片执行
2. 分片定时执行的原理
调度中心获取到该执行器的index下标通知给执行器,每个执行器拿到的index下标不同,根据不同的下标查询不同范围数据实现定时跑批;
3. 执行器如何实现动态扩容与缩容?
用户量如果又增长10w,仅需要直接新增一个节点即可;用户量减少只需要减少服务器
4. 如果定时任务模块定时跑批宕机了如何处理?
如果执行器宕机不会影响到整体群发效果,只会丢失单台节点的数据没有群发,后期可以采用日志形式记录下来人工实现补偿。