ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

分布式任务调度实战:分布式锁、任务分片与幂等设计

分布式任务调度实战:分布式锁、任务分片与幂等设计 开头搞后台开发的朋友应该都遇到过这种尴尬项目一开始就是一个单体服务定时任务用Spring自带的Scheduled随手一写跑得也挺好。可一旦上了多个实例、拆了微服务、任务量涨起来问题就接踵而至了——同一个任务在每个实例上跑一遍订单被重复关闭对账单发重短信重复推送某个实例一重启任务也跟着停任务越积越多线程池直接被打满。这时候你就知道单机定时任务这套玩法撑不住了该认真设计一个分布式任务调度系统了。这篇文章不是来讲概念PPT的而是把我实际落地分布式任务调度时的完整思路、架构设计、核心机制和踩坑过程梳理出来。内容包括什么时候该引入分布式调度、整体架构怎么搭、分布式锁和任务分片到底怎么用、如何用一套可复现的代码实现订单超时关闭与库存回滚这种经典场景以及那些你在文档里查不到、只有跑过生产环境才会懂的坑。适合正在从单体向微服务过渡的Java后端团队也适合准备自研调度平台的架构师参考。1. 项目概述与核心需求解析1.1 从单机定时任务到分布式调度的演变先说单机阶段。Spring的Scheduled、Quartz单机版用起来很爽秒级、分钟级定时都能做代码量小人畜无害。但它默认的假设是这个应用只有一个实例在跑任务在这个进程里执行就是唯一的一份。等到你做高可用部署同一套服务起了两个实例或者三个实例问题就来了。每个实例都会按照自己的cron表达式触发同一个任务结果就是重复执行。你说加个分布式锁吧锁怎么加锁过期时间设多少锁没抢到的那台机器是否要等待下一个周期这些都不是Scheduled能回答的问题。再往后任务类型变多了有实时性高的延迟5分钟检查支付状态有批量型的每天晚上跑全量数据对账有要按ID范围拆分的清点100万张订单。单机进程已经扛不住了任务执行时间超过cron周期下一轮任务又开始派发线程池堵死调度完全乱了。我判断一个团队是否要上分布式任务调度就看三条服务是否已经多实例部署且任务存在重复执行风险是否有长时间运行的批量任务单台机器完成耗时远超可接受范围是否需要对任务做动态启停、失败重试、执行监控。如果你只是两三个实例、任务量不大、失败重试靠代码写那可能还不需要一个完整的调度平台。但只要你符合上面任意两条趁早规划分布式任务调度比后期补窟窿要省心得多。1.2 分布式任务调度要解决的核心问题一句话概括分布式任务调度系统就是在一个分布式环境下把“什么时间、由哪台机器、以什么方式执行什么任务”这个决策统一管理起来。拆开来看它要解决四个核心问题调度与执行分离。调度的职责是触发执行的职责是干活。不能让每个业务服务自己又是运动员又是裁判员。防止重复执行。这是分布式环境下的第一优先级重复推送、重复扣款、重复关单任何一个都是生产事故。任务可拆可并。把一个耗时数小时的大批量任务按照数据维度拆成多片多台机器并行处理。失败可恢复可追踪。任务挂了要自动重试重试了要保证业务幂等全程要有日志和监控能把每次执行记录翻出来看。这个概念可以类比成一家快递公司。单机定时任务就是只有一个快递员按照自己手机上的闹钟去取件送件闹钟一响就干但这个快递员一请假整个片区就瘫痪了。分布式任务调度系统则是有一个调度中心统一管理所有的工单哪辆车去哪一片、几点出车、货没送完怎么补送都由调度中心安排它的好处就是任何一个快递员挂掉了调度中心可以立刻把任务重新分配给其他人。1.3 一个任务调度系统的标准能力清单我在设计或者评估一个调度系统时总爱列一张能力清单对照着看心里有底任务定义与管理能在控制台或配置文件中注册任务指定cron表达式、任务参数、超时时间、重试次数调度策略支持cron定时、延迟触发、手动触发、分片广播路由策略任务触发时选择哪台执行器去跑轮询、随机、一致性哈希、故障转移分片能力支持把任务按ID范围或取模分成多片每台执行器领走一片失败处理支持失败重试、告警通知、失败后挂起或跳过高可用调度中心可以多节点部署执行器动态上下线任务不会被单点拖死监控与日志每次执行的开始时间、结束时间、执行结果、异常堆栈都能查到动态运维任务可以随时启停可以临时修改触发时间不需要重新发布应用。这些能力不一定一次性都要做全但它决定了一个调度系统是“玩具”还是“生产可用”。很多自研系统翻车就是只做了触发和执行完全没考虑到故障转移和可观测性。2. 整体架构设计与方案选型2.1 自研还是直接使用开源框架说到选型这是每个团队遇到分布式任务调度时必须做的第一个决策。市面上现成的方案并不少XXL-JOB、ElasticJob、Quartz集群版以及一些商业产品。自研则通常基于Spring Scheduled加分布式锁、注册中心、RPC组件去拼。我个人的建议非常明确中小团队不要自研直接用开源框架。这不是说开源就一定适合你而是自研调度系统的隐性成本极高。调度触发要发消息消息需要持久化任务要在多台机器间路由路由策略需要处理扩缩容任务状态要维护失败重试要处理任务日志要落库还有管理界面、鉴权、告警。这些全部做完没有两三个月下不来而且bug会让你非常痛苦因为调度系统是基础设施一出问题就是全局性的。开源方案怎么选这里拿两种典型做对比XXL-JOB和ElasticJob在思路上有明显区别。XXL-JOB是“调度与执行分离”的代表有独立的调度中心通过HTTP回调触发执行器在控制台上点点点就能创建和管理任务学习成本很低。ElasticJob则是把分片作为核心能力它要求任务执行器在启动时把自己注册到协调服务上任务触发时各执行器自动感知分片变化特别适合大数据的批量作业场景。Quartz本身是单机任务库它也能做集群但集群模式下依赖数据库行锁并发高时性能很差而且没有管理界面失败重试和监控基本靠你自己写。我整理了一张对比表供参考方案调度模型分片能力管理界面适用场景缺点Quartz集群数据库行锁选主弱无简单的任务高可用性能瓶颈缺乏运维能力XXL-JOB调度中心独立调度执行器回执中完善微服务环境下的常规定时任务需部署调度中心分片一般够用但不够细ElasticJob协调服务感知分片执行器自我驱动强弱大数据批量并行作业需要运维ZooKeeper接入复杂度较高自研组合方案自定义自定义自定义特殊业务场景成本高如无强需求不建议2.2 通用架构的角色与交互流程不管你用哪套方案分布式任务调度系统的整体架构都跑不出下面几个角色。调度中心负责维护任务配置、解析cron、产生触发事件它本身不执行业务代码只负责“拍板”。执行器注册到调度中心接收触发指令并真正执行业务逻辑。任务存储用来存放任务定义、执行历史、日志一般用数据库。协调组件负责执行器注册、分布式锁、选主等工作Redis、ZooKeeper、Nacos都可以承担这个角色。一次完整调度的交互流程我习惯用文字描述给团队成员听你在控制台定义了一个任务配置了cron和路由策略。调度中心到点后生成一个触发事件从注册表里挑出一台执行器或者按分片广播给多台通过HTTP或RPC通知它执行。执行器收到指令后先把任务包装成独立线程放进线程池同时加分布式锁避免多个节点同时执行同一任务然后执行业务逻辑把成功、失败、耗时、异常信息回报给调度中心。调度中心将这些结果落库如果失败则按照配置的重试次数重新调度。这里有一个容易犯的错误有人会把任务执行逻辑直接写在调度中心进程里。看起来省事实际上调度中心一旦承载大量业务任务它的进程会变得不稳定而调度中心又是全链路的核心它一挂所有任务都停摆。所以“调度中心瘦身、执行器独立部署”是一个铁律。2.3 Spring Cloud架构下的落地组合如果你在用Spring Cloud微服务体系分布式任务调度有一个比较自然的落地组合。执行器就是你的各个微服务实例它们天然是分布式的。调度中心可以是一个独立服务也可以直接复用XXL-JOB这类开源调度中心。注册信息用Nacos来维护执行器启动时把自己写入Nacos调度中心从Nacos拉取可用实例列表。任务之间的调用包括调度中心向执行器下发指令还是走HTTP或OpenFeign但要在调用链路上增加超时和熔断因为任务执行时间通常比普通接口长。分布式锁这块直接用Redis加Redisson封装任务提前不知道会跑多久就靠Redisson的看门狗自动续期来保证锁不会中途失效。分布式事务则根据业务边界选择方案任务跨库操作时使用最终的补偿策略。我见过的一个比较成功的典型组合是XXL-JOB做调度Spring Boot微服务做执行器Nacos做服务发现和配置管理Redis/Redisson做分布式锁Sentinel做任务调用的熔断降级。这套组合的好处是每块都有成熟产品不需要自己从零发明整个团队上手快出了问题社区里也都有解决方案。3. 核心机制原理拆解锁、分片、幂等与事务3.1 分布式锁防止任务重复执行的最后一道防线先说分布式锁。任务重复执行的原因是多实例环境下每个实例都有一套cron触发器时间一到大家都会触发任务。分布式锁解决的就是这个同一时刻只能有一个实例真正执行某个任务。实现分布式锁最常见三种方式数据库表锁比如在任务表里对任务ID加唯一索引执行时先插入一条记录或者用SELECT FOR UPDATE锁住行。这种方式实现简单但性能弱数据库连接一多就卡不适合高并发任务。Redis分布式锁用SET key value NX PX来保证原子性同一个key同一时刻只能被一个客户端设置成功。性能好、应用广。但要注意两个点一是value必须是客户端的唯一标识释放锁时要先比较value再删除防止A持锁超时后B拿到锁A又去释放了B的锁二是锁必须有合适过期时间并且最好配合自动续期。ZooKeeper或Etcd的临时顺序节点利用节点自动消失的特性处理客户端崩溃后的锁释放可靠性高不依赖过期时间但引入额外组件性能和操作复杂度都不如Redis。我在生产环境里主要用Redisson的分布式锁因为它对开发者掩盖了太多实现细节。你只需要一个注解或者几行代码它内部会用看门狗机制默认锁30秒每过三分之一时间就自动续期只要业务没执行完锁就不会断。更重要的是Redisson的LOCK在获取失败时会订阅锁释放事件不会带来无意义的空转循环。用起来大致是这样String lockKey lock:order:timeout; String requestId UUID.randomUUID().toString(); RLock lock redissonClient.getLock(lockKey); boolean locked lock.tryLock(0, -1, TimeUnit.SECONDS); if (!locked) { // 其他节点正在处理本次调度直接退出 return; } try { doCloseTimeoutOrders(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }这里有几个坑我要提醒。首先tryLock的leaseTime传-1或者不传才能触发看门狗续期。如果你手动传入一个固定值比如10秒那看门狗就不会接管任务一旦执行超过10秒锁就没了另一个实例立刻进来重复执行。然后锁的释放时机必须放在业务事务提交之后这也是后面要专门讲的经典问题。3.2 任务分片把一个大任务拆成多台机器并行如果说分布式锁解决的是“不能重复执行”那分片解决的就是“单机执行太慢”。举一个典型的场景每天凌晨要对全量订单做一次状态补偿扫描订单总量在100万级别单台机器跑完需要两个小时。分片的思想是把这100万订单从ID维度切成10份每份10万然后10台执行器各自处理自己那份。这样理论耗时就从2小时降到了十几分钟。任务分片有几种路由方式按ID取模比如任务参数传一个分片总数N和当前节点编号M执行时查询条件加上WHERE MOD(order_id, N) M。好处是路由规则简单直接坏处是后续扩容分片数变了数据归属会全部变化对存量任务影响较大。按ID范围分段调度中心预先查出一批数据的minId和maxId均分为N段每台机器领走一段。这种方式适合ID分布比较均匀的表但每次都要做一次min/max查询。一致性哈希执行器节点通过Redis或协调服务组织的哈希环分配数据范围。扩容缩容时只有部分数据重新分布更平滑但实现复杂度高。在实际操作中我偏爱“广播任务 分片参数”的组合。调度中心在触发任务时把所有执行器的IP和分片序号传过去每台执行器拿到自己的序号和总分片数自行计算该处理哪些数据。这个模式最大的好处是执行器数量变化时不用改数据库配置调度中心自动感知。分片之后还要考虑重复和遗漏。每台机器处理完自己的分片最好把处理结果统一汇给调度中心或记录到一张分片执行表比如分片1完成、分片2失败调度中心能看到整体进度而不是各跑各的一片黑盒。3.3 失败重试与幂等性任务调度里的“柔性事务”基础任务调度最折磨人的问题不是任务失败而是“失败后又重试”重试如果幂等没做好就会把一次失败变成多次数据污染。拿订单关闭这个场景举例。任务去关一个超时未支付订单第一次执行时关单成功但网络闪断导致结果没回报给调度中心。调度中心判定执行失败按配置又在下一个周期重试该任务。重试时订单早已经是关闭状态如果代码里没有状态判断它可能把关闭时间再次刷新或者触发库存回滚的逻辑把已经回滚过的库存又回滚一次库存就成负数了。所以任务调度系统里幂等不是一种可选优化而是强约束。我通常用三层手段保证幂等状态机约定一个订单只能由支付中关闭为已关闭不允许已关闭再次进入关闭流程乐观锁更新写SQL时带上状态条件比如UPDATE t_order SET status CLOSED WHERE order_id ? AND status PAY_PENDING更新行数为0说明已经被处理过跳过即可唯一键约束对每次关单操作定义一个全局唯一的处理流水号比如order_id作为流水表唯一键重复插入会失败从而挡掉重复操作。把幂等做好之后分布式事务就有了落地的底气。任务调度里的分布式事务不是指你在一段代码里把多个数据库的操作同步提交而是通过定时任务实现最终一致性。比如订单和库存分布在两个服务订单关闭后需要回滚库存正确的做法是关单操作在本地事务里写入一条库存回滚消息表状态设为待发送然后把库存回滚指令发给消息队列。如果发送失败没关系下一轮定时任务会扫描本地消息表里所有待发送的记录重新投递。下游库存服务消费时按幂等约束保证同一订单只回滚一次。这就是任务调度系统里最典型的一种使用方式它是分布式事务的“对账者”和“补偿者”。所以我在设计每一次任务时都会问团队一个问题如果这个任务失败后重试三次你的业务数据还能保持正确吗4. 实操过程订单超时关闭与库存回滚场景落地4.1 场景背景与表结构设计选一个最经典的场景来说明整套流程订单平时下单未支付超过30分钟自动关闭并回滚预占的库存。这个场景单机阶段怎么做写一个定时任务每30分钟扫一次SELECT所有状态为待支付且创建时间超过30分钟的订单然后逐条关单、回滚库存。放到分布式环境下再用分布式锁保证只有一个实例执行。但我们要把这个流程做得更健壮一点。先设计表结构。订单表t_order必要字段order_id、user_id、sku_id、order_status、create_time、close_time。t_order里的order_status用数字状态机0待支付1已关闭2已完成。库存预占表t_stock_pre_occupy记录订单预占的库存量id、order_id、sku_id、quantity、statusstatus取值0预占中、1已回滚、2已扣减。再设计一张本地消息表t_task_message用来承载“回滚库存”这个跨服务操作id、biz_type、biz_id、payload、status、retry_count、next_retry_time、create_time。status为0待发送1发送成功2发送失败。这三张表的设计不是随意定的。订单状态必须有状态机约束这是保证幂等的基础库存预占单独成表避免在库存表上做复杂的状态更新本地消息表则是实现最终一致性的关键它把跨服务的RPC调用转换成了一轮轮可追踪的投递任务。4.2 核心代码实现分布式锁与事务顺序任务的核心逻辑分三步扫描超时订单、关闭订单、创建库存回滚消息并触发投递。我用Spring Boot MyBatis Redisson来写示例。第一步任务入口。这个入口要么由XXL-JOB这类框架通过HTTP触发要么由Spring Scheduled触发实际逻辑一样。先加分布式锁确保多实例下只有一个节点进入主流程。Component public class OrderCloseTask { Resource private RedissonClient redissonClient; Resource private OrderService orderService; private static final String LOCK_KEY lock:order:timeout; public void execute() { RLock lock redissonClient.getLock(LOCK_KEY); boolean locked lock.tryLock(0, -1, TimeUnit.SECONDS); if (!locked) { return; } try { orderService.closeTimeoutOrders(30, 200); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }第二步业务主流程。这里有一个极其容易踩坑的点分布式锁的获取和释放都在事务外层业务方法标注了Transactional那么当业务方法return的时候事务其实还没提交。如果你在finally里立即释放锁另一个实例可能立刻拿到锁进来查询读到的还是事务提交之前的状态于是又处理了一遍。所以释放锁一定要放在事务真正提交之后。我用Spring的事务同步器解决public void closeTimeoutOrders(int minutes, int batchSize) { ListOrder orders orderMapper.selectTimeoutOrders(minutes, batchSize); if (CollectionUtils.isEmpty(orders)) { return; } for (Order order : orders) { int affected orderMapper.updateOrderStatus(OrderStatus.CLOSED, order.getOrderId(), OrderStatus.PAY_PENDING); if (affected 0) { continue; } stockPreOccupyMapper.updateStatus(StockPreOccupyStatus.ROLLED_BACK, order.getOrderId(), StockPreOccupyStatus.PRE_OCCUPIED); TaskMessage message TaskMessageBuilder.buildStockRollback(order); taskMessageMapper.insert(message); } TaskMessageBatchSender.sendPendingBatch(); }这里的关键是订单状态更新的SQL必须带旧状态条件当affected为0就说明该订单已经处理过直接跳过这就是幂等。接着更新库存预占表再写入本地消息表这三个操作在同一个本地事务里要么全成功要么全回滚不会出现订单关了库存没回的消息。第三步锁的释放放到事务提交后。用TransactionSynchronizationManager注册afterCommit回调让锁的释放动作延后到事务提交完成Transactional public void closeTimeoutOrders(int minutes, int batchSize) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCommit() { // 在这里做锁的释放 OrderCloseTask.this.releaseLock(); } }); // 实际业务逻辑 }当然如果你用的框架支持直接在业务方法内拿到事务状态也可以显式控制但Spring的事务同步器无疑是侵入最小、最不容易出错的方案。4.3 关键参数怎么定线程池、批大小、锁超时时间很多团队在写任务调度的代码时对参数非常随意线程池一个固定值批大小拍脑袋锁超时随便填个10秒。等到生产一压测就崩了。先说线程池。任务执行线程池的大小取决于你要达到的多大吞吐。假设单条订单关闭加库存回滚平均耗时50毫秒一个线程一秒钟大约能处理20条一分钟就是1200条。如果你希望一个任务周期内处理完10万条超时订单单机就需要至少10万除以1200约84个线程。但这个数字通常不现实因为任务执行器还承担业务流量所以更合理的做法是拆任务或者拆批而不是一上来就开大线程池。我的建议是线程池核心线程数不要超过实例可用CPU核数的2到3倍比如8核机器给16个核心线程任务吞吐不够就分片到多台机器并行这才是分布式任务调度的意义——用机器数量换时间而不是单机硬扛。批大小则要看单条处理的平均耗时和数据库连接池配置。常见的批量是100到500条如果单条SQL是主键更新100条和500条在性能上差距不大但持锁时间差距很大。数据量级的估算公式很简单单批耗时约等于单条平均耗时乘以批大小如果超过你规划的任务间隔就要缩小批大小或者增加分片。锁超时时间的取值要分情况。如果用了Redisson看门狗leaseTime可以传-1让看门狗自动续期就不存在超时设置的问题。如果是自己写Redis没有续期机制那锁过期时间至少要大于任务正常执行时间加上一些缓冲。比如任务平均执行2分钟峰值可能到5分钟锁过期时间设10分钟宁可任务冗长一些也不能中途锁失效导致重复执行。5. 常见问题与排查技巧实录5.1 任务还是重复执行了锁到底哪里失效我见过不少团队加了分布式锁任务依然重复执行排查下来原因五花八门但归根结底就集中在这三个方向。第一种是锁的过期时间设短了。任务执行时间超过锁的leaseTime锁自动释放另一个实例马上拿到锁进入任务两者并行执行。排查方法是看Redis里锁key的TTL变化如果任务执行日志里有两条记录时间重叠基本就是这个原因。解决方案就是上Redisson看门狗让锁随任务运行自动续期。第二种是锁释放和事务提交的时序错了。业务方法执行完但事务尚未提交finally里已经把锁释放了。第二个实例拿到锁进来读到的还是旧数据又处理了一遍。这种问题的表现是重复执行的时间间隔非常短几乎是一瞬间。排查时要看数据库事务日志如果大量回滚和锁释放同时发生基本锁定了。解决方案就是用Spring的事务同步器把锁释放挂到afterCommit。第三种是Redis主从切换导致锁丢失。主节点写入锁成功但异步复制还没同步到从节点主节点挂了Sentinel把从节点提升为主节点此时锁的key在新主上不存在另一个客户端重新加锁成功。任务调度场景里这个概率很低但一旦发生就是全局重复。如果对一致性要求极其严格可以考虑多个独立的Redis实例通过MultiLock加锁但带来的代价是可用性下降、延迟变高。我个人的看法是业务侧把幂等做好远比追求分布式锁百分百可靠更务实。5.2 任务堆积和超时调度中心疯狂派活的背后任务堆积的典型现象是一个任务一分钟执行一次但单次执行要5分钟线程池很快被占满后续触发全部排队最后整个执行器线程池被拖死正常业务的线程也受影响。排查第一步看调度中心的执行日志确认调度中心是否还在频繁触发。第二步看执行器线程池的活跃线程数和队列深度如果ForkJoinPool或者ThreadPoolExecutor的队列长度持续增长就是产能跟不上。解决办法按优先级排建把任务改成串行禁止上一个周期没结束就触发下一个周期。XXL-JOB的阻塞策略选“单机串行”或“丢弃后续调度”设置任务超时时间执行超过N分钟强制终止空出线程把大任务按分片拆碎让多台机器分担实在拆不开的就把线程池独立出来避免影响业务线程。还有一个隐藏坑如果你用自研方案一定要在任务执行线程池外面加一层拒绝策略的兜底默认的AbortPolicy会让任务抛异常有可能引发另一个更隐蔽的问题——任务还没执行就被判定失败触发重试然后反复堆积。改成CallerRunsPolicy或者直接把新请求丢弃并记录日志会合理很多。5.3 时钟漂移分布式环境下最容易被忽略的问题分布式任务调度系统对物理环境的假设是“各节点时间基本同步”但实际上虚拟机、容器、宿主机之间的时钟漂移非常常见尤其是很多云主机没有配置NTP同步的默认设置。时钟漂移会造成两个问题。一个是在多实例各自用本地时间判断是否到达执行时间点时不同实例触发任务会有几十秒甚至几分钟的时间差再加上锁的权限交叠整个日志时间线会变得非常奇怪。另一个是任务调度系统里如果用时间差来判定超时比如订单创建超过30分钟时钟快的实例会提前关单时钟慢的实例会晚关单同一业务行为在不同节点上不一致。这类问题的排查比较隐蔽因为业务日志里看每台机器好像都正常只有把多台机器的时间戳拉在一起比较才看得出差异。我的建议是调度触发的判断逻辑不要分散在各个业务实例而是收敛到调度中心一个节点其他执行器只负责执行不负责判断是否到点。如果一定要在执行器上判断时间那就要确保所有节点都启用了NTP同步并且在部署检查清单里加入时间同步校验这一项。5.4 一个常见问题的速查表问题现象可能原因排查手段解决方案任务重复执行锁过期时间过短、主从切换锁丢失、锁释放早于事务提交查Redis锁TTL、事务日志、调度日志时间重叠用Redisson看门狗、事务提交后释放锁、业务幂等兜底任务执行越来越慢任务堆积占满线程池、数据库连接池耗尽看线程池队列深度、数据库活跃连接数改串行阻塞策略、设置任务超时、拆分任务分片定时触发不准各节点时钟漂移对比多机时间戳调度判断收敛到调度中心、启用NTP任务失败后重试导致数据错乱没有幂等控制看订单状态是否有重复变更记录状态机约束、乐观锁更新条件、唯一索引调度中心一重启任务就全不跑了调度中心单点部署检查调度中心自身高可用配置调度中心多节点部署任务状态持久化最后说几句经验分布式任务调度系统这个东西真正跑起来之后你会发现它本身的技术难度其实不高难的是对任务边界、幂等、监控和容错的那份敬畏。我见过不少团队把大量精力花在选框架、讨论分片算法上最后却在最简单的“任务重复执行”上栽跟头。所以我个人在项目里有一条铁律任何任务在被设计出来的时候第一件事不是写执行逻辑而是回答三个问题——这个任务会不会被重复执行、重复执行会造成什么后果、若造成后果如何兜底。占小便宜的一点是不管你最终用了XXL-JOB、ElasticJob还是自研组件任务调度系统的核心能力最终都体现在工程规范上调度与执行分离锁和事务顺序不能乱任务必须有清晰的幂等设计监控和日志要在第一天就规划好而不是等出了事故再去补。把这些想清楚之后再去看具体某个框架或者某段代码你会发现它们都不难。
返回列表