Job轮询总结与思考

 

背景

在业务数据没达到一定量又不想引入分布式事务框架增加复杂性,基于Job框架实现的补偿方案不失为一种简单优雅的方案。

微服务环境下虽然使用了retry框架,对一些幂等的接口一次失败多次尝试,但有些场景比如下单后无库存,要保证库存在一定时间内一定能扣成功,也只能使用Job框架以一定的频率发起补偿。

业界常用的分布式Job框架有 Saturn与 xxl-job

 

Job并发

像Saturn跟xxl-job都有控制台页面把Job配置为单机执行,单机发生故障进行故障转移。

数据量少的场景只需要单机执行,不需要考虑并发。

但单机执行场景下仍然要注意并发问题。

一般来说Job框架会保证下一个Job等待上一个Job执行完毕才开始。

如果业务逻辑使用了线程异步执行,如下

 void executeJob() {
      new Thread(()->{
         //耗时的业务处理
      }).start();
}

由于是在线程里面异步执行,Job就当做上一个Job已经处理完毕了,毫不犹豫地就开启下一轮轮询,就有可能发生两个线程并发处理同一段数据。

为了避免耗时业务处理长时间阻塞Job,启用线程异步处理也是必要的,同时要控制两个Job的轮询间隔,避免当前Job轮询启动了,上一个Job异步线程里面还有未完成业务处理逻辑。

 

Job查询必须有结束条件

使用Job自然是要查询那些需要补偿的任务。

通常使用分页查询,把待补偿的业务数据查询出来处理,直到处理完毕结束循环。

查询分页避免了一次性查询出过量数据造成OutOfMemoryError。

//第一种场景:分页查询不知道数据总数
public void executeJob() {
    int beginPage = 0;
    int pageSize = 500;
    while (true) {
        //根据beginPage进行分页查询
        List<Object> orders = getOrders(beginPage, pageSize);
        //处理业务数据 orders
        if (orders.size() < pageSize) {
            break;
        }
        beginPage = beginPage + 1;
    }
}

//第二种场景:分页查询知道数据总数
public void executeJob() {
        int beginPage = 0;
        int pageSize = 10;
        int total = 0;
        int allTotal = 0;
        while (total<allTotal) {
            //根据beginPage进行分页查询
            PageInfo<Object> orders = getOrders(beginPage, pageSize);
            //处理业务数据 orders
            allTotal = orders.getTotal();
            total = total + orders.size();
            if (orders.size() < pageSize) {
                break;
            }
            beginPage = beginPage + 1;
        }
    }

while要有一种业务无关的结束条件

 

虽然分页查询有结束条件,但查询逻辑是会随着需求更新的,当查询条件不再满足结束条件就会陷入死循环。

由于Job是纯后台服务,测试人员一时也不易发现Job逻辑出了问题。

例如 getOrders(beginPage, pageSize) 随着分页插件的升级或Sql的更新每次总是查询出相同的500条数据。

500条相同的数据不多也不少,也不易发现查询结果出错,如下判断条件永远不满足,第一种场景:分页查询不知道数据总数就没有结束条件。

       if (orders.size() < pageSize) {
            break;
        }

来看第二种场景:分页查询知道数据总数 ,多了一个结束条件 total<allTotal ,那多了一个结束条件就靠谱吗?

虽然把每次查询出的总数累加 了total = total + orders.size() ,但分页插件或Sql通过 select count(*) from table where object = xxxx  统计出的 allTotal  也可能在变化,每次都变大,正所谓你长我也长。可能半天过去了 total<allTotal 条件仍然没有满足,while也陷入死循环。

由于根据sql查询条件去结束while有一定的不确定性,通过限制最大循环是一种简单有效的方法。

例如根据业务量,while循环不应该超过1千次,数据总量不应该超过50万。

那么保留根据分页查询结果判断结束条件的同时,应该增加最大循环次数、最大业务量控制,避免由于查询失效造成的死循环。

public void executeJob() {
        int beginPage = 0;
        while (true) {
            //分页查询
            if (orders.size() < pageSize) {
                break;
            }
            beginPage = beginPage + 1;
            if(beginPage>1000){
                break;
            }
        }
    }

    public void executeJob() {
        int beginPage = 0;
        while (total<500000) {
            //分页查询
            total = total + orders.size();
            if (orders.size() < pageSize) {
                break;
            }
            beginPage = beginPage + 1;
            if(beginPage>1000){
                break;
            }
        }
    }

Job也应该像微服务一样优雅关闭

在微服务发布过程中,通常把旧服务从Eureka等注册中心以及负载平衡里面摘掉,不再接受新的请求,旧服务把已有请求处理完毕下线。由于Job是纯后台服务,有可能被忽略掉。例如旧服务已经不接受新的Http请求,但后台的Job轮询还没处理完毕,Job里面同时开启了数据库事务,这时候强杀进程会造成数据回滚。

Job也应该同理,像微服务一样,通过Http接口向分布式Job注册中心发送优雅下线的命令,待当前微服务已经没有未完成的Job任务再停机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值