目录
定时任务常见的使用场景
指定时间点执行
- 铁路定时放票,美团定时发放优惠券、红包
- 商品定时上下架(开售/结束),活动定时开始/结束
- 定时推送营销信息
- 修复历史数据
间隔指定时间自动执行
- 同步数据:从第三方拉取信息,同步到我们自己的库中,比如定时查询合作银行的转账进度、合作快递的物流进度。
- 更新状态:超过三十分钟未支付自动取消订单,到期自动解冻账号。
- 消息通知:扫描即将过期的优惠券,即将开始的车票、机票、电影票,通知用户;
- 备份数据、统计账单流水、更新缓存
- 检测节点心跳,确定节点状态、是否可用
定时任务常见组件
1、jdk自带的 java.util.Timer
使用简单、功能单一,对复杂任务、多任务并发执行支持差,适用于简单的单个任务,eg. 间隔指定时间清除本地缓存。
2、jdk自带的 java.util.concurrent.ScheduledExecutorService
支持线程池,对多任务支持好,适合执行多个单机任务。
3、springboot自带的Schedule
使用简单,与springboot无缝接入,适合用于springboot项目的简单单机任务。
4、Quartz
专业的定时任务框架,功能强大,使用略微麻烦,适合用于复杂的单机任务。
以上三个对分布式的支持都差,集群部署服务时容易出现多个节点重复执行定时任务的问题,通常只用于实现单机定时任务。可以自行实现分布式(eg.使用zk做分布式协调)、将任务状态持久化(db),但比较麻烦。
5、elastic-job(推荐)
当当网开源的弹性分布式任务调度框架,已成为apache ShardingSphere的子项目;功能丰富强大,提供web管理界面,新版界面使用 Vue + ElementUI 编写,漂亮美观,文档齐全,接入、使用都十分简便;但需要引入zk实现分布式协调,部署略微麻烦。
6、xxl-job
大众点评开源的弹性分布式任务调度框架,功能丰富强大,轻量,提供web管理界面,springboot + freemarker 界面一般,文档丰富,使用db持久化任务状态,无需额外引入组件。
elastic job、xxl-job 二者相比,不看github star的话,elastic job 的优势在于使用体验,管理界面比 xxl-job 漂亮、好用,接入也比 xxl-job 简便;xxl-job 的优势在于无需额外引入组件。如果项目本身要引入zk,更推荐使用 elastic-job lite实现分布式定时任务。
jdk自带的Timer
1、编写定时任务:继承抽象类TimerTask,实现run()方法
public class MyTask extends TimerTask {
@Override
public void run() {
try {
//...
} catch (Exception e) { //必须catch异常
}
}
}
2、调度、执行
//创建time实例:会创建 TimerThread、TaskQueue 实例,并调用 thread.start(); 启动线程
Timer timer = new Timer();
MyTask myTask1 = new MyTask();
MyTask myTask2 = new MyTask();
//第2个参数指定多少ms后开始初次执行,第3个参数执行间隔周期
timer.schedule(myTask1, 10000L, 60000L);
timer.scheduleAtFixedRate(myTask2, 5000L, 1000L);
//当前线程继续往下执行
Timer内部有2个成员变量:Thread、Queue,一个Time实例对应一个线程、队列,此Time实例所有的任务都会添加到队列中,由一个线程负责执行。如果某个任务执行时抛出异常,会造成线程执行中断,不会再执行后续批次的任务,所以实现 TimerTask#run 方法时,一定要catch异常。
schedule()、scheduleAtFixedRate()的区别
- schedule:间隔周期从上次执行完毕时开始算,如果期间有任务执行耗时较长,耽搁了其它任务的执行,其它任务的执行时间会推迟、顺延。
- scheduleAtFixedRate:间隔周期从上次执行开始时算,执行时间点是可预知的、固定的,如果期间有任务执行耗时较长,耽搁了其它任务的执行,其它任务会补上之前未执行的场次(连续执行多次)。
通常使用 schedule,不使用 scheduleAtFixedRate。
//取消指定的定时任务
myTask.cancel();
//取消此timer中所有的定时任务
timer.cancel();
jdk自带的ScheduledExecutorService
Timer位于 java.util 包下,使用单线程执行所有任务,多任务并发执行支持差,可靠性低,使用Timer时会提示“使用ScheduledExecutorService 替代Timer”。
ScheduledExecutorService 位于 java.util.concurrent 包下,支持线程池、多种时间单位,对多任务并发执行支持好。
//虽然ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,但继承的线程池调优参数不会生效,指定核心线程数即可
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(7);
//还可以指定 ThreadFactory、任务拒绝策略(默认AbortPolicy 直接抛异常)
ScheduledExecutorService scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(7,
new BasicThreadFactory.Builder().namingPattern("task-thread-pool-%d").daemon(false).build(),
new ThreadPoolExecutor.AbortPolicy());
//从ThreadPoolExecutor继承的方法
scheduledThreadPoolExecutor.shutdown();
scheduledThreadPoolExecutor.shutdownNow();
//schedule系列方法返回值都是ScheduledFuture,可用于任务的取消、状态判断、返回值获取
//schedule只执行1次,指定延迟执行时间。支持Runnable(无返回值)、Callable(有返回值)
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor.schedule(new MyRunnable(), 10, TimeUnit.SECONDS);
ScheduledFuture<String> scheduledFuture = scheduledThreadPoolExecutor.schedule(new MyCallable(), 10, TimeUnit.SECONDS);
//实质是调用 schedule(command, 0, NANOSECONDS) 立刻执行1次,无返回值,只支持 Runnable
scheduledThreadPoolExecutor.execute(new MyRunnable());
//固定频率执行:距上次执行开始时 固定时间后执行下一次。如果执行时间较长,超过了间隔时间,则会在执行完毕后立刻执行下一次,不会出现多次并发执行
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor.scheduleAtFixedRate(new MyRunnable(), 10, 10, TimeUnit.SECONDS);
//固定延迟执行:距上次执行完毕时 固定时间后执行下一次
ScheduledFuture<?> scheduledFuture = scheduledThreadPoolExecutor.scheduleWithFixedDelay(new MyRunnable(), 10, 10, TimeUnit.SECONDS);
//需要多次执行的任务,通常使用 scheduleWithFixedDelay
//任务是执行完毕,正常执行完毕、异常终止、已取消 都算执行完毕
boolean isDone = scheduledFuture.isDone();
//取消任务,参数指定如果任务正在执行是否尝试中断,返回取消结果
//如果任务已经执行完毕、已经取消、或由于其它原因无法取消,返回false
boolean cancel = scheduledFuture.cancel(false);
boolean cancelled = scheduledFuture.isCancelled();
springboot自带的定时任务
参考:https://blog.csdn.net/chy_18883701161/article/details/120387438
Quartz
quartz的体系结构
3个核心概念
- 任务 Job
- 触发器 Trigger
- 调度器 Scheduler
quartz提供了2种作业存储类型
- RAMJobStore :默认使用的作业存储类型,将任务调度状态保存在内存中,性能好但不具备持久性,发生故障时会丢失所有的任务状态信息。
- JDBC作业存储:将任务调度保存到数据库中,需要自行建一些表,表名、字段名都是固定的,quartz会自动持久化任务数据到数据库中,发生故障时也能恢复调度现场,性能略差。
springboot整合quartz
1、依赖
创建项目时勾选 I/O -> Quartz Scheduler,也可以手动添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
在yml中输入quartz即可查看quartz的配置项,一般使用默认配置即可。
2、编写定时任务,一个类对应一个定时任务
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
/**
* 继承QuartzJobBean,重写executeInternal(),无需要放到spring容器中
*/
public class XxxJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//...
}
}
3、配置定时任务,一个定时任务对应一个配置类
@Configuration
public class XxxJobConfig {
/**
* 创建JobDetail实例,放到spring容器中,一个job对应一个JobDetail实例
*/
@Bean
public JobDetail xxxJobDetail() {
//绑定Job
return JobBuilder.newJob(XxxJob.class)
//设置此job实例的唯一标识(name、所属分组)
.withIdentity("xxxJob", "defaultJobGroup")
//如果此JobDetail实例没有关联Trigger,也不删除此JobDetail实例
.storeDurably()
.build();
}
/**
* 创建trigger实例,放到spring容器中,一个job对应一个trigger实例
*/
@Bean
public Trigger xxxTrigger() {
return TriggerBuilder.newTrigger()
//设置此trigger实例的唯一标识(name、所属分组)
.withIdentity("xxxTrigger", "defaultTriggerGroup")
//绑定要调度的job
.forJob("xxxJob", "defaultJobGroup")
//调度计划
.withSchedule(CronScheduleBuilder.cronSchedule("*/10 * * * * ?"))
.startNow()
.build();
}
}
Elastic Job(推荐)
官网:https://shardingsphere.apache.org/elasticjob/
github:https://github.com/apache/shardingsphere-elasticjob
ElasticJob包含2个子项目
- ElasticJob-Lite:轻量级无中心化解决方案,只需要引入ZK,;
- ElasticJob-Cloud:使用 ZK + Mesos + Docker 的解决方案,额外提供资源治理、应用分发以及进程隔离等服务,跟 Lite 的区别只是部署方式不同,它们使用相同的 API,只要开发一次。
通常使用 Lite 即可,下文只介绍 Lite 的使用方式
springboot整合elasticjob lite
1、添加依赖
<dependency>
<groupId>org.apache.shardingsphere.elasticjob</groupId>
<artifactId>elasticjob-lite-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
2、编写定时任务
elastic job提供了2种类型的定时任务:SimpleJob、DataflowJob,根据需要实现即可,都需要放到spring容器中
@Component
public class MySimpleJob implements SimpleJob {
@Override
public void execute(ShardingContext context) {
//...
}
}
@Component
public class MyDataflowJob implements DataflowJob<Order> {
@Override
public List<Order> fetchData(ShardingContext context) {
//...
}
@Override
public void processData(ShardingContext shardingContext, List<Order> orders) {
// ...
}
}
3、yml
建议 elastic-job 的配置放在单独的 elastic-job.yml 文件中,然后在 application.yml 中引入
elasticjob:
#注册中心配置
regCenter:
#zk节点
serverLists: localhost:2181
#命名空间,即存储elastic job数据要zk node
namespace: elasticjob-task
#定时任务配置
jobs:
#自定义的任务名称
mySimpleJob:
#对应的类
elasticJobClass: com.chy.mall.job.MySimpleJob
cron: 0/10 * * * * ?
#自定义参数
jobParameter: xxx
#分片总数
shardingTotalCount: 3
#自定义的分片参数
shardingItemParameters: 0=Beijing,1=Shanghai,2=Guangzhou
myDataFlowJob:
elasticJobClass: com.chy.mall.job.MyDataflowJob
cron: 0/10 * * * * ?
shardingTotalCount: 3
如果定时任务已经注册到zk,后续在yml中修改cron表达式、自定义参数、分片总数这些任务配置项是不会生效的,需要到web控制台修改,来更新zk保存的任务信息。
邮件预警(可选)
定时任务执行出错时,自动发送邮件通知指定人员
<dependency>
<groupId>org.apache.shardingsphere.elasticjob</groupId>
<artifactId>elasticjob-error-handler-email</artifactId>
<version>3.0.0</version>
</dependency>
elasticjob:
regCenter:
...
jobs:
...
jobErrorHandlerType: EMAIL
props:
email:
host: host
port: 465
username: username
password: password
useSsl: true
subject: ElasticJob error message
from: from@xxx.xx
to: to1@xxx.xx,to2@xxx.xx
cc: cc@xxx.xx
bcc: bcc@xxx.xx
debug: false
web控制台的部署
1、下载 lite ui 的二进制tar包(不是源码包),版本尽量与 maven 引入的 lite 版本保持一致
2、解压,根据需要修改 conf/application.properties
#使用的端口
server.port=8088
#账号、密码,root账号可修改配置,guest游客账号只能看、不能修改
auth.root_username=root
auth.root_password=root
auth.guest_username=guest
auth.guest_password=guest
#事件追踪数据源配置,这个不是必需的,数据库连接后续可以在web界面配置
#配置了elastic job会自动创建几张表,用于记录任务执行的历史记录、结果、总结摘要,便于分析,建议配上
#默认值引入了h2、postgresql的数据库驱动,如果要配置为mysql等其它数据库,可以把对应的驱动jar包放到 ext-lib目录下
spring.datasource.default.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.default.url=jdbc:mysql://localhost:3306/mall?serverTimezone=UTC
spring.datasource.default.username=root
spring.datasource.default.password=root
spring.jpa.show-sql=false
3、执行bin下的start脚本启动应用,浏览器访问 127.0.0.1:8088,输入账号、密码
4、添加注册中心,信息与yml配置的注册中心保持一致
常见操作说明
1、状态
- 分片待调整:新的定时任务注册上去,尚未触发过即为此状态,到cron表达式指定周期时自动触发过1次,状态就正常了。
- 失效:暂时下线该定时任务,后续还可以上线。
- 终止:永久下线该定时任务,不能再上线。
2、分片总数、参数
这些配置参数都可以从上下文中获取到,用于向定时任务传递额外的配置,均可以在web控制台进行配置
#自定义参数
jobParameter: xxx
#分片总数,此定时任务每次触发时使用几个分片来执行
shardingTotalCount: 3
#自定义的分片参数,index=value,数量与分片总数保持一致
shardingItemParameters: 0=Beijing,1=Shanghai,2=Guangzhou
@Component
public class MySimpleJob implements SimpleJob {
@Override
public void execute(ShardingContext context) {
//自定义参数
String jobParameter = context.getJobParameter();
//分片总数
int shardingTotalCount = context.getShardingTotalCount();
//当前分片的index
int shardingItem = context.getShardingItem();
//当前分片项对应的值
String shardingParameter = context.getShardingParameter();
//...
}
}
分片总数:指定该定时任务触发1次时,需要分配给几个副本执行。假设分片总数设置为3,定时任务所在服务
- 部署了5个实例,分片分散情况往往是 1+1+1 ,触发1次时这3个实例会分别执行1次;
- 部署了2个实例,分片分散情况往往时 1+2,触发1次时1个实例执行1次,另一个实例执行2个;
- 部署了1个实例,这个定时任务的所有分片都集中在这个实例上,触发1次时这个实例会执行3次;
分片总数通常设置为1,避免触发时重复执行。
如果定时任务要处理的数据量级很大,可以将数据分配给多个分片共同处理,shardingItemParameters 指定分段(id分段、状态划分等),定时任务中判断当前分片对应的 shardingItemParameters,做相应处理
shardingTotalCount: 3
shardingItemParameters: 0=待付款,1=待发货,2=待退款
@Component
public class MySimpleJob implements SimpleJob {
@Override
public void execute(ShardingContext context) {
switch (context.getShardingParameter()) {
case "待付款":
//...
break;
case "待发货":
//...
break;
case "待退款":
//...
break;
default:
}
}
}
xxl-job
官网:https://www.xuxueli.com/xxl-job
github:https://github.com/xuxueli/xxl-job
web控制台的部署
1、下载最新稳定版的压缩包,解压,在IDEA中导入
父项目 xxl-job包含3个子项目
- xxl-job-core:核心模块,提供公共依赖
- xxl-job-admin:调度中心,提供web界面的控制台
- xxl-job-executor-samples:执行器(定时任务)示例,不需要的可以删除这个模块。包含2个子项目,一个是springboot版本的示例,一个是frameless无框架版本的示例。
2、执行doc/db下的初始化sql脚本
3、修改 xxl-job-admin 的配置文件
- application.properties:根据需要调整应用使用的端口、访问路径、数据库配置、报警邮箱配置。邮箱配置可以不管,但不能注释掉。
- logback.xml 日志配置
xxl-job-admin 集群部署时,各 xxl-job-admin 节点务必连接同一个mysql数据库、节点机器的时钟务必保持一致;如果 mysql 做主从,xxl-job-admin 各节点务必强制走主库。
4、打包部署,访问 http://localhost:8080/xxl-job-admin 登录系统,默认账号密码 admin/123456
5、初始化系统数据
- 在用户管理中修改密码、增删用户
- 在任务管理中删除示例任务
- 在执行器管理中增删、修改执行器
编写定时任务
从springboot版本的示例中copy所需内容
1、添加依赖,最好与控制台的版本保持一致
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
2、复制 application.properties 的相关配置项
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30
3、复制配置类
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
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;
/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@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.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
4、仿照 service.jobhandler.SampleXxlJob 写定时任务
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
@Component //放到容器中
public class XxxJob {
/**
* 简单任务示例
*/
@XxlJob("sampleJobHandler")
public void sampleJobHandler() throws Exception {
//执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
XxlJobHelper.log("sampleJobHandler start");
//可通过 XxlJobHelper.handleFail()/handleSuccess() 指定任务执行结果,未设置时默认默认为 handleSuccess() 成功
try {
int i = 1 / 0;
} catch (Exception e) {
XxlJobHelper.handleFail();
}
}
/**
* 分片广播任务示例
*/
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {
// 分片参数
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
//...
}
}
5、启动应用,调度中心 -> 任务管理 -> 增删、修改任务实例
运行模式使用 BEAN,JobHandler 与 @XxlJob 指定的 jobName 保持一致。
策略说明
执行器集群部署时,调度中心支持的路由策略如下
- ROUND(轮询)
- FIRST(第一个):固定选择第一个节点
- LAST(最后一个):固定选择最后一个节点
- RANDOM(随机):随机选择在线的节点
- CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某个节点,且所有任务均匀散列在不同节点上。
- LEAST_FREQUENTLY_USED(最不经常使用):优先选择使用频率最低的节点。
- LEAST_RECENTLY_USED(最近最久未使用):优先选择最久未使用的节点。
- FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的节点选定为目标执行器并发起调度。
- BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的节点选定为目标执行器并发起调度。
- SHARDING_BROADCAST(分片广播):广播触发对应集群中所有节点执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务。
调度过于密集,执行器来不及处理任务实例时,调度中心提供的阻塞处理策略如下
- 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行。
- 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败。
- 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务。
分批处理数据
前提:id有序递增
1、直接 limit 分页,where… order by id asc limit 1000
数据量大时可能存在深分页性能问题。
2、select min(id), max(id) where… 先查出待处理记录的id区间,再根据id分批处理 where… and id > #{minId} and id < #{minId} + 1000。
由于物理删除记录、使用雪花算法等非自增id,id可能是不连续的、存在很大的跳跃,难以确定每批次处理的记录数,可能出现大量无效批次。
3、起始id + limit,where id > #{startId} order by id asc limit 1000,推荐。示例
final int limitSize = 500;
long startId = 0L;
while (true) {
//查询需要处理的记录
List<UserPo> userPos = userDao.listByQo(GoldCardQuery.builder()
.startId(startId)
.orderBy("id asc")
.limitSize(limitSize)
.build());
//批量处理
for (UserPo userPo : userPos) {
try {
//...
} catch (Exception e) {
log.error("xxx执行异常,userPo={}", JSON.toJSONString(userPo), e);
}
}
//判断循环条件
if (userPos.size() < limitSize) {
break;
}
startId = userPos.get(userPos.size() - 1).getId();
}