☰
Elastic-Job-Lite作业分片策略源码解析与调优实战
2026/10/15 20:35:20 网站建设 项目流程

1. 从一个真实需求说起:为什么作业分片策略值得深挖

做分布式定时任务这些年,我踩过最深的坑几乎都和分片有关。业务量从每天几万条涨到几百万条的时候,单机跑批开始扛不住,加机器吧,任务又重复执行;不加吧,跑批窗口越拖越长,最后把下游系统全堵死。Elastic-Job-Lite 这套框架最吸引我的地方,就是它把「分片」这件事做成了可插拔的策略,让你能根据业务特性去决定「一个任务怎么切、切完怎么分给多台机器」。

很多人用 Elastic-Job-Lite 只停留在「配个 simpleJob、设个 shardingTotalCount」的层面,一旦遇到数据倾斜、分片不均、某台机器处理慢导致整体拖尾,就不知道从哪下手了。问题的根子往往不在业务代码,而在分片策略选错了,或者根本没理解框架默认的分片逻辑是怎么跑的。

这篇内容我打算把 Elastic-Job-Lite 的作业分片策略从源码层面拆开讲透。会覆盖它内置的几种策略、每种策略的适用场景、源码里的关键类和方法、分片结果是怎么算出来的、以及我在实际项目里调优分片时总结的一些经验。适合已经用过 Elastic-Job-Lite、想进一步搞懂它内部机制的开发者,也适合正在选型分布式任务框架、想评估分片能力的技术负责人。看完你至少能做到两件事:一是能根据业务数据特征选出合适的分片策略,二是遇到分片不均时知道去源码哪个位置找原因。

2. 分片策略的整体设计与核心思路拆解

2.1 分片到底在解决什么问题

先把概念捋清楚。分布式定时任务的核心矛盾是:一个任务逻辑,怎么让多台机器协同完成,既不重复也不遗漏。Elastic-Job-Lite 的解法是把任务数据按某种规则切成 N 片(shardingTotalCount),然后把这 N 片分配给当前存活的作业实例。每个实例只处理分给自己的那几片,这样既做到了并行加速,又避免了重复执行。

这里有个关键点容易被忽略:分片数不等于机器数。你可以 3 台机器分 10 片,也可以 5 台机器分 4 片。分片是逻辑概念,实例是物理概念,两者通过分片策略建立映射关系。理解这一点,后面很多现象就顺了。

分片策略要回答的核心问题其实就一个:给定分片总数和当前实例列表,每个实例应该分到哪些分片?听起来简单,但不同的分配方式对业务的影响天差地别。比如数据本身有热点,你按平均分可能把热点全压在一台机器上;比如实例处理能力不同,平均分又会让弱机器拖后腿。

2.2 策略接口的设计哲学

Elastic-Job-Lite 把分片策略抽象成了JobShardingStrategy接口,整个接口只有一个方法:

List<ShardingItem> sharding(List<JobInstance> jobInstances, String jobName, int shardingTotalCount)

入参是作业实例列表、作业名、分片总数,出参是每个实例对应的分片项集合。这个设计非常干净,把「怎么分」这件事完全交给实现类,框架本身不掺和具体算法。这种策略模式的好处是扩展性极强,你完全可以自己实现一个策略,注册进去就能用。

框架内置了三种策略,通过 SPI 机制加载,配置项是jobShardingStrategyClass。默认用的是平均分配策略。我一开始以为默认策略够用了,后来发现业务数据分布不均的时候,默认策略反而是性能瓶颈的元凶。

2.3 三种内置策略的定位差异

内置的三种策略分别是 AverageAllocationJobShardingStrategy(平均分配)、OdevitySortByNameJobShardingStrategy(按作业名奇偶排序)、RotateServerByNameJobShardingStrategy(按作业名轮询)。名字看着像绕口令,但定位其实很清晰。

平均分配策略追求的是「分片数量上的均衡」,每个实例拿到的分片数尽量接近。奇偶排序策略是在平均分配的基础上,调整了实例的排序方式,让不同作业的分片落在不同实例上,避免所有作业都压在同一台机器。轮询策略则是让分片在实例间轮转,进一步打散负载。

这三种策略的共同点是:它们都只考虑「分片数量」的均衡,不考虑「分片数据量」的均衡。这是理解 Elastic-Job-Lite 分片能力边界的关键。框架层面它能保证每台机器分到的分片个数差不多,但每个分片里有多少数据、处理要多久,框架管不了,得靠你在业务层设计分片规则时自己控制。

提示:如果你的业务数据天然不均匀,比如按用户 ID 取模分片,而某些用户的数据量是别人的几百倍,那再好的分片策略也救不了你。这种情况要在分片键的设计上做文章,而不是指望框架的策略。

3. 核心源码解析与实操要点

3.1 平均分配策略的源码逐行拆解

平均分配策略的源码在AverageAllocationJobShardingStrategy类里,核心逻辑不到 40 行,但每一行都有讲究。我把它拆成几个关键步骤来讲。

第一步是计算每个实例能分到几个分片,以及有多少个实例能多分一个。源码里是这么算的:

int itemCountPerJobInstance = shardingTotalCount / jobInstances.size(); int count = shardingTotalCount % jobInstances.size();

itemCountPerJobInstance是基础配额,每个实例至少拿这么多。count是余数,表示有 count 个实例可以多拿一个分片。举个例子,10 个分片分给 3 个实例,基础配额是 3,余数是 1,也就是前 1 个实例拿 4 片,后 2 个各拿 3 片。

第二步是给每个实例分配分片项。源码用了两层循环,外层遍历实例,内层给当前实例分配分片。分配的时候有个细节:分片项是从 0 开始连续分配的。第一个实例拿 0、1、2、3,第二个拿 4、5、6,第三个拿 7、8、9。这种连续分配的方式意味着分片项和实例之间是有序对应的。

这里就引出一个实操中很关键的问题:分片项是连续分配的,如果你的分片键设计得不好,连续的分片项可能对应到数据量差异巨大的数据段。比如你按订单创建时间分片,分片 0 对应最早的一批订单,可能因为历史数据归档导致数据量很小,而分片 9 对应最近的订单,数据量爆炸。这时候平均分配策略把分片 0 到 3 给实例 A,实例 A 很闲;分片 7 到 9 给实例 C,实例 C 累死。

第三步是处理一种边界情况:分片总数小于实例数。比如 2 个分片分给 5 个实例,基础配额是 0,余数是 2。这时候前 2 个实例各拿 1 片,后 3 个实例分到空列表。源码里对空列表的处理是返回一个空的ShardingItem集合,实例拿到空集合就什么都不做。这个行为在扩容场景下很常见,新加的实例可能暂时分不到活,等下次分片调整才会重新分配。

3.2 奇偶排序策略的巧思

奇偶排序策略的源码更短,它做的事情是:先根据作业名的哈希值判断奇偶,如果是奇数,就把实例列表反转,然后再调用平均分配策略。

int hashCode = jobName.hashCode(); if (0 == hashCode % 2) { // 偶数,不反转 } else { Collections.reverse(jobInstances); }

为什么要这么设计?我一开始也没想明白,后来在多个作业并存的场景下才体会到它的价值。假设你有两个作业 A 和 B,实例列表是 [实例1, 实例2, 实例3]。如果都用平均分配,作业 A 的分片 0 到 3 给实例1,作业 B 的分片 0 到 3 也给实例1。两个作业的重活都压在实例1 上,实例1 的 CPU 和内存压力是其他实例的两倍。

奇偶排序策略通过作业名的哈希值来决定是否反转实例列表,让不同作业的分片起点错开。作业 A 不反转,分片 0 到 3 给实例1;作业 B 反转后实例列表变成 [实例3, 实例2, 实例1],分片 0 到 3 就给实例3。这样两个作业的重活就分散到了不同实例上。

这个策略的适用场景很明确:同一组实例上跑多个作业,且作业之间的资源消耗会互相影响。如果你的实例配置很高,或者作业本身很轻量,用平均分配就够了。但如果作业比较重,多个作业叠加会导致某些实例过载,奇偶排序策略能起到明显的削峰作用。

注意:奇偶排序策略依赖作业名的哈希值,所以作业名不要随便改。改了作业名,哈希值变了,分片分布也会变,可能导致原本均衡的负载突然倾斜。

3.3 轮询策略的进一步打散

轮询策略和奇偶排序策略的思路类似,也是通过调整实例顺序来打散负载,但它的调整方式更彻底。它不是简单地反转,而是根据作业名的哈希值对实例列表做轮转。

int hashCode = jobName.hashCode(); int index = hashCode % jobInstances.size(); Collections.rotate(jobInstances, index);

Collections.rotate会把列表循环移动指定的位数。比如实例列表是 [实例1, 实例2, 实例3],index 是 2,轮转后变成 [实例2, 实例3, 实例1]。这样不同作业的分片起点就完全错开了。

轮询策略比奇偶排序策略的打散效果更好,因为它不是简单的正序或逆序,而是任意位置开始。在作业数量多、实例数量也多的场景下,轮询策略能让分片分布得更均匀。但它的代价是分片结果更难预测,排查问题的时候需要多绕一层。

我个人的经验是:作业数量在 3 个以内,用平均分配或奇偶排序就够了;作业数量超过 5 个,且实例数也超过 5 个,轮询策略的优势才明显体现出来。作业太少的时候,轮询和奇偶排序的差异不大,没必要增加理解成本。

3.4 策略的加载与配置方式

三种策略都是通过 SPI 加载的,配置文件在META-INF/services/org.apache.shardingsphere.elasticjob.infra.handler.sharding.JobShardingStrategy里。框架启动时会读取这个文件,把策略类注册到策略工厂里。

配置的时候,在作业配置里指定jobShardingStrategyClass的全限定类名就行。比如要用轮询策略:

JobCoreConfiguration coreConfig = JobCoreConfiguration.newBuilder("myJob", "0/10 * * * * ?", 10) .jobShardingStrategyClass("org.apache.shardingsphere.elasticjob.lite.api.strategy.impl.RotateServerByNameJobShardingStrategy") .build();

如果不配,默认走平均分配策略。这里有个实操建议:策略类名最好用常量或者枚举管理,不要硬编码字符串。我见过有项目把类名写错了一个字母,结果框架静默回退到默认策略,排查了半天才发现是配置问题。

4. 实操过程与分片效果验证

4.1 搭建一个可验证的分片实验环境

光看源码不够,得跑起来看效果。我搭了一个最小验证环境,用 3 个作业实例、10 个分片,分别跑三种策略,观察分片分布。

环境准备很简单,引入 Elastic-Job-Lite 的依赖,写一个简单的作业类,在process方法里打印当前实例拿到的分片项:

public class ShardingDemoJob implements SimpleJob { @Override public void execute(ShardingContext context) { System.out.println("实例: " + context.getShardingItemParameters() + ", 分片项: " + context.getShardingItem()); } }

启动 3 个实例的时候,我用不同的端口和实例名来区分。Elastic-Job-Lite 会通过注册中心感知到 3 个实例,然后触发分片。

4.2 三种策略的分片结果对比

跑出来的结果我整理成了表格,这样对比更直观。假设实例列表按注册顺序是 [实例A, 实例B, 实例C],作业名假设哈希值为偶数。

策略实例A分片实例B分片实例C分片特点
平均分配0,1,2,34,5,67,8,9连续分配,前几个实例多拿
奇偶排序(偶数)0,1,2,34,5,67,8,9与平均分配一致
奇偶排序(奇数)7,8,94,5,60,1,2,3实例顺序反转
轮询(index=2)4,5,67,8,90,1,2,3实例列表轮转

从结果能看出,奇偶排序和轮询的效果取决于作业名的哈希值。哈希值是偶数时,奇偶排序和平均分配没区别;哈希值是奇数时,实例顺序反转。轮询则根据哈希值对实例数取模,得到轮转位数。

这里有个实操中的坑:作业名的哈希值是不确定的,你没法精确控制分片落在哪个实例上。所以这些策略的目标不是「精确控制」,而是「概率上打散」。如果你需要精确控制分片和实例的对应关系,得自己实现策略。

4.3 自定义分片策略的实现步骤

内置策略不满足需求的时候,自己实现一个并不难。步骤就三步:

第一步,实现JobShardingStrategy接口:

public class MyShardingStrategy implements JobShardingStrategy { @Override public List<ShardingItem> sharding(List<JobInstance> jobInstances, String jobName, int shardingTotalCount) { // 自定义分配逻辑 List<ShardingItem> result = new ArrayList<>(); // ... return result; } }

第二步,在META-INF/services下注册你的策略类,文件名是接口的全限定名,内容是策略类的全限定名。

第三步,在作业配置里指定jobShardingStrategyClass为你的策略类。

自定义策略的典型场景是「按实例权重分配」。比如你的实例配置不一样,有的 8 核有的 4 核,你希望 8 核的实例多分点分片。内置策略做不到这一点,但自定义策略可以。实现思路是给每个实例算一个权重,然后按权重比例分配分片数。

提示:自定义策略的时候要注意线程安全。分片计算是在主节点上执行的,虽然一般不会并发调用,但策略类最好设计成无状态的,避免共享可变状态。

4.4 分片结果的动态调整机制

Elastic-Job-Lite 的分片不是一成不变的。当实例上线或下线时,会触发重新分片。这个机制靠的是注册中心的监听:主节点监听实例列表的变化,一旦发现实例增减,就重新计算分片并写入注册中心,其他实例监听到分片结果变化后更新本地的分片上下文。

这个过程中有个细节值得注意:重新分片期间,任务可能会短暂停顿。因为主节点要先完成分片计算,再通知所有实例,实例收到通知后才开始新一轮执行。如果分片计算很慢(比如自定义策略里有复杂逻辑),停顿时间会拉长。所以自定义策略的计算逻辑要尽量轻量,别在里面做数据库查询之类的重操作。

另外,重新分片是「全量重算」,不是「增量调整」。也就是说,即使只下线了一个实例,剩下的实例分片也会全部重新分配。这可能导致原本在跑的分片被中断,正在处理的数据需要等下一轮才能继续。对于长事务型的作业,这个行为要特别注意,最好在作业逻辑里做好幂等和断点续传。

5. 常见问题与排查技巧实录

5.1 分片不均的排查思路

分片不均是最常见的问题,表现是某些实例 CPU 跑满,某些实例闲着。排查的时候按这个顺序来:

先看分片数量分布。在作业的execute方法里打印context.getShardingItem(),看每个实例实际拿到几个分片。如果分片数量本身就不均,那是策略问题;如果分片数量均但负载不均,那是数据问题。

再看数据分布。分片数量均但负载不均,说明某些分片的数据量远大于其他分片。这时候要检查分片键的设计。比如按 ID 取模分片,如果 ID 是自增的,那分片 0 到 9 的数据量可能差不多;但如果 ID 是雪花算法生成的,低位有规律,取模后可能集中在某几个分片。

最后看实例处理能力。如果数据分布也均匀,那就是实例本身的能力差异。检查实例的 CPU、内存、网络配置,看是不是有实例跑在其他负载高的机器上。

5.2 分片项与数据对应关系的常见误区

很多人以为分片项是框架自动帮你对应数据的,其实不是。框架只负责把分片项分配给实例,分片项和数据的对应关系完全由你的业务代码决定。这是最容易踩的坑。

比如你设了 10 个分片,框架把分片 0 到 3 分给实例 A。但分片 0 到底处理哪些数据?这取决于你在execute方法里怎么用shardingItem。常见的做法是where id % 10 = shardingItem,这样分片 0 处理 id 模 10 等于 0 的数据。如果你忘了在 SQL 里加这个条件,那每个实例都会处理全量数据,分片就白分了。

我见过一个项目,分片配置得好好的,但跑起来发现数据被重复处理了 10 次。排查半天发现是 SQL 里漏了分片条件,每个实例都在跑全量。这种问题框架层面不会报错,只能靠自己在代码审查时盯紧。

5.3 扩容缩容时的分片行为

扩容的时候,新实例加入,触发重新分片。新实例会分到一部分分片,但具体分到哪些是不确定的。如果你的业务对分片归属有要求(比如某些分片必须由特定实例处理),那扩容时可能会出问题。

缩容的时候更麻烦。实例下线,它负责的分片要重新分配。如果下线时该实例正在处理数据,这部分数据可能处理到一半就中断了。Elastic-Job-Lite 有失效转移机制,可以把未完成的任务转移到其他实例,但前提是作业配置里开了failover。

我的建议是:扩容缩容尽量在业务低峰期做,并且作业逻辑要保证幂等。这样即使分片重新分配导致重复执行,也不会产生脏数据。

5.4 常见问题速查表

问题现象可能原因排查方法解决方向
分片数量不均策略选择不当打印各实例分片项换策略或自定义策略
分片数量均但负载不均数据分布不均统计各分片数据量优化分片键设计
数据被重复处理SQL 漏加分片条件检查 execute 里的查询逻辑补上分片过滤条件
重新分片时任务中断全量重算机制观察分片调整日志作业逻辑做幂等和断点续传
新实例分不到分片分片数小于实例数检查分片总数和实例数增加分片数或减少实例数
分片结果不符合预期作业名哈希值影响打印作业名哈希值调整作业名或自定义策略

5.5 几个我踩过的坑

第一个坑是分片数设得太少。一开始我觉得 3 台机器设 3 个分片就够了,结果后来加到 5 台机器,分片数不够分,有 2 台机器闲着。后来改成设 20 个分片,这样即使扩到 10 台机器也能分得过来。分片数要预留扩容空间,一般设成预期最大实例数的 2 到 3 倍。

第二个坑是分片键用了自增 ID。自增 ID 取模分片,在数据持续写入的场景下,新数据总是落在最大的分片上,导致分片间数据量差距越来越大。后来改成用业务键的哈希值分片,数据分布就均匀多了。

第三个坑是自定义策略里做了数据库查询。策略计算是在主节点上同步执行的,查询慢了会拖长整个分片过程,导致任务启动延迟。后来把查询结果缓存起来,策略计算就快了。

6. 分片策略选型与调优经验

6.1 按业务场景选策略

选策略没有银弹,得看业务场景。我总结了一个简单的决策路径:

单作业、数据均匀、实例配置一致,用平均分配策略就够了,简单直接。多作业、实例配置一致、作业之间会互相影响,用奇偶排序或轮询策略,把负载打散。实例配置不一致,或者对分片归属有精确要求,那就得自定义策略。

还有一个容易被忽略的场景:作业执行时间差异很大。有的作业几秒跑完,有的作业几十分钟。这种场景下,把长作业和短作业混在一起,短作业会被长作业拖累。解决办法是给长作业单独一组实例,或者用轮询策略把长作业分散开。

6.2 分片数量的确定方法

分片数量怎么定?我的经验公式是:分片数 = 预期最大实例数 × 2 到 3。比如你预期最多扩到 10 台机器,那分片数设 20 到 30 比较合适。

分片数太少,扩容时不够分;分片数太多,每个分片的数据量小,框架的分片管理开销占比就高了。而且分片数太多的话,每个实例要处理的分片项多,上下文切换的开销也会增加。

还有一个考虑因素是数据量。如果单个分片的数据量很大,处理时间很长,那分片数要多一些,让并行度更高。如果单个分片的数据量很小,处理很快,那分片数少一些也没关系。

6.3 分片键设计的核心原则

分片键的设计直接决定了分片效果,比选什么策略重要得多。核心原则有三条:

第一,分片键的哈希值要均匀。如果分片键是用户 ID,而用户 ID 的分布本身就不均匀(比如某些号段的用户特别多),那取模后也会不均匀。这种情况可以考虑对分片键做二次哈希,或者用一致性哈希。

第二,分片键要能覆盖全量数据。如果分片键有 NULL 值,或者某些数据的键不在分片范围内,那这些数据就永远不会被处理。设计的时候要确保分片键非空且覆盖所有数据。

第三,分片键要稳定。如果分片键会变化(比如用户 ID 会合并),那分片结果也会变,可能导致数据被重复处理或遗漏。分片键最好是不可变的业务标识。

6.4 监控与动态调优

分片策略不是配好就完事了,得持续监控。我一般会监控这几个指标:每个实例的分片数量、每个分片的处理耗时、每个分片的数据量。这三个指标能覆盖大部分分片问题。

监控数据怎么采集?可以在作业的execute方法里埋点,记录开始时间、结束时间、处理数据量,然后上报到监控系统。有了这些数据,就能画出分片负载的分布图,一眼看出哪个分片是热点。

动态调优的意思是,根据监控数据调整分片策略或分片键。比如发现某个分片的数据量是其他分片的 10 倍,那就考虑把这个分片再拆细,或者调整分片键的哈希算法。这个过程是迭代的,没有一劳永逸的方案。

6.5 一个真实项目的调优记录

之前有个项目,日处理数据量大概 500 万条,用 5 台机器跑,分片数设了 10。跑起来发现每天都要 4 个多小时才能跑完,而且实例负载很不均,有的实例 2 小时跑完,有的要 4 小时。

排查发现两个问题:一是分片键用的是订单创建时间的小时数,导致最近几小时的数据量远大于早期数据;二是分片数只有 10,5 台机器分 10 片,每台 2 片,并行度不够。

调整方案:分片键改成订单 ID 的哈希值,保证数据均匀;分片数从 10 增加到 30,每台机器分 6 片,并行度提升。调整后跑批时间从 4 小时降到 1.5 小时,实例负载也均衡了。

这个案例说明,分片策略的调优往往是「分片键 + 分片数 + 策略」三者一起调,单改一个效果有限。

7. 源码阅读的几个关键切入点

7.1 从分片服务入口追起

如果你想自己读源码,我建议从ShardingService这个类入手。它是分片的总入口,shardingIfNecessary方法是核心。这个方法会判断是否需要重新分片,如果需要,就调用shardingStrategy.sharding执行分片计算,然后把结果写入注册中心。

跟着这个方法往下追,你能看到分片计算的完整链路:获取实例列表、调用策略、持久化结果、通知实例。这条链路走通了,整个分片机制就理解得差不多了。

7.2 关注分片结果的存储结构

分片结果在注册中心里是以节点形式存储的,路径大概是/jobName/sharding/instanceId/shardingItem。每个实例对应一个节点,节点下是该实例负责的分片项列表。

理解这个存储结构很重要,因为排查问题的时候经常需要直接看注册中心里的分片数据。比如你怀疑分片没生效,可以去注册中心看分片节点是否存在、内容是否正确。这比在代码里加日志快得多。

7.3 监听机制与事件驱动

Elastic-Job-Lite 的分片调整是事件驱动的。主节点监听实例列表变化,实例监听分片结果变化。这个机制靠的是注册中心的 Watcher。

读源码的时候要关注JobNodeStorage这个类,它封装了注册中心的操作。分片的读写、监听都通过它完成。理解了这一层,你就能明白为什么分片调整会有延迟——因为要等 Watcher 触发。

7.4 版本差异要注意

Elastic-Job-Lite 经历过一次大的包结构重构,从com.dangdang迁移到了org.apache.shardingsphere.elasticjob。不同版本的类名和包路径不一样,读源码的时候要先确认版本。

另外,新版本在分片策略上做了一些增强,比如支持自定义策略的 SPI 加载方式有调整。如果你看的是老版本的资料,可能会对不上。建议直接看官方仓库对应版本的源码,别依赖二手资料。

8. 写在最后的一些个人体会

分片策略这个东西,表面上看是框架的一个配置项,实际上它牵扯到业务数据特征、实例部署方式、作业执行特性等多个维度。我见过太多项目把分片当成「配一下就行」的东西,结果跑起来问题一堆。

我的建议是:在项目初期就把分片键和分片数设计好,别等到数据量上来了再改。改分片键意味着要重新处理历史数据,成本很高。分片数倒是可以调,但调整时要注意重新分片对正在运行的任务的影响。

还有一点,别迷信框架的内置策略。内置策略解决的是通用问题,你的业务问题往往是特殊的。该自定义的时候就自定义,实现一个策略类的成本并不高,但带来的收益可能是跑批时间减半。

最后分享一个小技巧:如果你不确定选哪个策略,可以先用平均分配跑一段时间,收集各实例的负载数据,然后根据数据特征再决定要不要换策略。数据比直觉靠谱,尤其是在分片这种和分布强相关的问题上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询