基于Vue+SpringCloudAlibaba微服务电商项目实战-构建会员服务-008:整合XXL-Job实现分片定时任务集群模式

1 高效实现分片定时任务集群原理

今日课程任务
实现千万级定时消息推送平台

  1. 定义活动推送消息模板
  2. XXL-Job实现分布式任务调度原理
  3. 整合XXL-Job实现分布式定时任务
  4. XXL-Job分片任务调度集群原理

假设发送100万消息需要10个小时,如何实现1000万也只需要10个小时?
动态分片实现集群,只需要新增服务器集群节点即可动态扩容推送消息

2 动态分片集群执行任务实现原理

微信活动群发 查询数据库中wx_open_id不为空的数据,调用微信接口群发。

发送活动提醒,活动仅在当天有效,假设公众号有100w数据,常规做法数据库分页查询,每次查10w发送需要1小时,那么100w需要10小时。
分页查询群发效率低,如果活动有一定时效性要求,随着用户量不断增加,消息推送难以准时发给用户。
如何实现无论用户是否继续增加,都可以在有效期内全部发送完毕?
动态分片集群模式。

3 定时任务集群如何保证幂等性问题

定时群发跑批数据量大的情况下如何处理?
可以采用多线程,但是非常消耗单台服务器内存。

业务代码是否可以和定时任务代码放入同一个jar包?
定时任务模块和业务模块单独拆分。定时任务项目底层就是死循环处理,非常占用服务器资源。

定时任务集群如何防止业务重复执行?

  1. 将定时任务代码单独部署一个jar包中,不参与业务逻辑服务器集群部署;
  2. 在jar中开启一个定时任务配置开关,判断是否需要将定时任务类加载到spring容器中;
  3. 使用分布式锁。项目启动中,只要谁能够拿到分布式锁,谁就能够将定时任务的配置类加载到spring容器中,否则不加载;
  4. 数据库中插入主键id,只要谁能够往数据库中插入一条相同的主键,插入成功就可以加载定时任务配置类;
    以上方案只适合小项目,不适合互联网级别项目
  5. 采用分布式任务调度平台框架(推荐)

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一些疑问

  1. 执行器发送一半如果失败的情况下,一般看日志在哪里开始失败写个小程序补发;
  2. 如何写多个定时任务? 在方法上加上@XxlJob(“xxxHandler”)对应配置任务管理即可;
  3. 如果有100w条数据分成10个执行器,每个执行器10w,在每个执行器查询范围里面再分页查询循环处理;
  4. 假设执行器某一台宕机会丢失部分数据,执行器会记录下来重启后会自动补发数据;

描述定时消息推送流程
整合分布式任务调度平台xxl-job,定时跑批群发百万量级微信消息推送

  1. 分布式任务调度平台xxl-job环境搭建 xxljob-admin
  2. 将定时任务逻辑与业务逻辑代码完全分开,单独构建定时任务模块执行器
  3. 对执行器(定时任务项目)采用集群分片的模式部署,根据不同的index查询不同分页内容实现跑批,可以实现动态扩容与缩容
  4. 中途如果定时任务出现异常,采用日志形式记录后期人工补偿

总结:
1. 百万级定时消息推送模板的原理
对定时任务模块实现集群部署,采用分片执行
2. 分片定时执行的原理
调度中心获取到该执行器的index下标通知给执行器,每个执行器拿到的index下标不同,根据不同的下标查询不同范围数据实现定时跑批;
3. 执行器如何实现动态扩容与缩容?
用户量如果又增长10w,仅需要直接新增一个节点即可;用户量减少只需要减少服务器
4. 如果定时任务模块定时跑批宕机了如何处理?
如果执行器宕机不会影响到整体群发效果,只会丢失单台节点的数据没有群发,后期可以采用日志形式记录下来人工实现补偿。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值