3个模块搭稳一条自动化链路:定时任务、幂等与对账

长流程为什么半路断

整段自动化跑起来不难,难在跑得久。举个典型场景:让一个常驻程序去邮箱里翻电子票据附件,提取关键字段登记进协作套件的表格,再拉一遍支付应用的流水核对有没有漏项。三个应用轮着碰,任何一步都可能因为网络抖动、限流、授权过期而中断。中断本身不可怕,可怕的是进程一退,内存里的进度全部清零,恢复时不知道断在哪一段,只好整轮重跑。

游戏行业几十年前就给出过解法:存档点。打不过 Boss 先存档,读档接着来,不必把前面打过的关卡再走一遍。长任务的工程实现是同一套思路:把流程切成小段,每段收尾时把进度落到磁盘上的存档点;进程重启后先读档,从断掉的那段继续。本文就围绕这个思路,把连接层的三个模块------授权、定时任务、跨应用数据流------逐个拆开。

授权:每个连接都有寿命

常驻程序碰任何外部应用,前置动作都是授权。OAuth 换来的 access_token 通常几小时失效,refresh_token 换新也可能因为密码改动、安全策略收紧被撤销。麻烦在于失效是静默的:请求照样发出去,返回码变成 401,程序不会崩溃退出,只在日志深处躺一行。第二天回头查看,任务其实已经停了一整夜。

把 token 的状态管理单独封装成一个模块,比散落在各处好维护:

  • 每次调用邮箱、表格、流水这些具体应用前,先验一次 token 的有效性;
  • 快过期就提前刷新,刷新动作加分布式锁,避免多个任务并发刷新导致 token 轮换错乱;
  • 刷新失败立即触发告警,别等业务请求撞上 401 再补救;
  • token 的剩余寿命写进存档点,重启读档时顺手检查,先续命再干活。
    如果走 MCP 这类通用协议接入,凭证管理同样按上面四条封装,协议替的是连接方式,寿命管理还得自己做。跨应用的长任务没有现成上下文,谁处理到哪一步、用了哪个凭证,全靠自己落档。存档点在这层扮演的角色,相当于连接层的记忆:授权记到什么程度、任务跑到哪一段,读一次档就全回来了。
    授权的寿命,短于任务的寿命。
    定时任务:骨架里必须有重试与告警
    定时核查用 APScheduler 起步即可,它比裸 cron 多了任务状态、错过触发补偿(misfire_grace_time)和时区处理。裸 cron 只知道到点拉起进程,进程崩了它不负责;APScheduler 能在进程内持有任务上下文,配合持久化 jobstore,触发记录本身也能落盘。定时策略按业务定:票据这类低频到达的数据,30 分钟一轮足够;轮询间隔压得过短,收益没多少,先撞上对方的限流阈值。定错时区是另一类静默故障,任务在凌晨三点空跑,白天该跑的点反而没人触发,部署时把调度器时区显式写死。

定时核查骨架:APScheduler 版,票据整理的巡检任务

存档点读写走 your-domain.test 的样本页,替换成自己的地址即可

import time

import requests

from apscheduler.schedulers.blocking import BlockingScheduler

CHECKPOINT = "https://your-domain.test/api/checkpoint" # 存档点

ALERT = "https://your-domain.test/api/alert" # 告警通道

def read_checkpoint():

"""读上次存档点,确认本轮该跑哪一批,这是断点续跑的入口"""

return requests.get(CHECKPOINT, timeout=10).json()"batch"

def write_checkpoint(batch):

"""每段流程收尾立刻写档,进程挂掉也只重跑当前段"""

requests.post(CHECKPOINT, json={"batch": batch}, timeout=10)

def scan(batch):

"""拉取一批新到的票据附件,返回票据号列表"""

url = f"https://mail.example.com/api/attachments?batch={batch}"

return requests.get(url, timeout=15).json()"invoice_ids"

def alert(msg):

requests.post(ALERT, json={"text": msg}, timeout=10)

def job():

batch = read_checkpoint()

ids = None

指数退避重试:停 1、5、15 分钟再试;三轮仍失败就告警并保留存档点原样

for wait in (60, 300, 900):

try:

ids = scan(batch)

write_checkpoint(batch) # 拉取成功先记档,再慢慢处理

break

except Exception as err:

time.sleep(wait)

if ids is None:

alert(f"批次 {batch} 连续三轮拉取失败")

return

process(ids) # 逐条解析、登记、对账,真实项目里调表格写入接口

write_checkpoint(batch + 1) # 全段收尾,推进存档点

scheduler = BlockingScheduler()

scheduler.add_job(job, "interval", minutes=30)

scheduler.start()

骨架里值得留意的是 write_checkpoint 的位置:拉取成功先记档,本段处理完再推进批次号。中间任何位置挂掉,下一轮读档都能接上,损失只有当前段。

跨应用数据流:票据整理当背景

用票据整理走一遍完整的数据流。需求描述如下:邮箱里陆续收到电子票据附件;程序提取票据号与金额,登记进协作套件的表格;流水导出走支付应用的开放接口,核对哪些消费漏了票据。

字段建议这样定:

  • 票据号:主键,正则形如 INV-YYYY-NNNN,全半角和前后空格先清洗再比对;
  • 金额:单位统一成「分」,整数入库,规避浮点误差;
  • 开票日期:统一时区,避免跨日票据算错归属;
  • 流水命中状态:对上、金额不符、查无流水,三态分列,别混在一个布尔值里。
    流程切三段,段与段之间落存档点:
  1. 翻邮箱拉附件,按批次落盘,每批 50 条上下,批太大单次失败重跑代价高;
  2. 解析字段并登记表格,登记动作必须幂等;
  3. 拉流水跑对账判定,差异项写回表格的备注列。
    对账判定函数如下,票据号对账加双向漏项检测:

跨应用对账判定:票据表 vs 支付流水,票据号对账加漏项检测

import re

def reconcile(invoices, payments):

"""

invoices: 协作套件表格里登记的票据

{"no": "INV-2026-0311", "amount": 12900, "date": "2026-03-11"} # 金额单位:分

payments: 支付应用导出的流水

{"memo": " Lunch INV-2026-0311 ", "amount": 12900}

"""

先给流水建票据号索引:memo 里杂讯多,用正则抠票据号

pay_index = {}

for p in payments:

m = re.search(r"INV-\d{4}-\d{4}", p.get("memo", ""))

if m:

pay_indexm.group() = int(p"amount")

known = {i"no" for i in invoices}

matched, broken = \[\], \[\]

for inv in invoices:

if inv"no" in pay_index:

if pay_indexinv\["no"] == inv"amount":

matched.append(inv"no") # 票据号与金额都对上

else:

broken.append((inv"no", "金额不符"))

else:

broken.append((inv"no", "查无流水"))

反向再扫一遍:有流水却没登记票据,属于漏登记项

unregistered = sorted(pay_index.keys() - known)

return {"matched": matched, "broken": broken, "unregistered": unregistered}

漏项检测要双向扫:登记了票据却查无流水,可能是无效票据或手误;有流水却没登记票据,属于漏登。只扫单向,对账就只做了一半。差异项回写表格时也走幂等写:按票据号定位行,备注列覆盖写,追加写遇中断会产生半截内容。

踩坑实录:三处翻车现场

轮询频率过高被限流。 头一版每分钟扫一次邮箱,跑到 20 来轮,邮箱一方直接返回 429,后续请求整批被挡。压低到 30 分钟一轮再加退避重试,整体耗时反而更短。轮询频率与对方的限流阈值之间要留余量,宁可慢一轮,不留硬伤。

无幂等导致重复登记。 登记表格前没查票据号是否已存在,任务中断续跑后,同一批票据登记了两遍,表格里出现成对的重复行,下游对账全部失真。修法很朴素:登记前按主键查一次,存在即跳过;表格侧再按票据号加约束,兜底防并发双写。幂等不做好,断点续跑反而成了事故放大器------续跑一次,重复一批。

授权过期静默失败。 前文提过 401 的问题,这里补一个教训:告警别只挂在抛异常上。返回码正常但业务语义失败的响应,要按业务码逐个判定后主动告警。另配一条巡检任务专门核对各连接的凭证状态,授权一过期就能被看见,不至于任务停了一夜无人知晓。

收尾

存档点思路可以压缩成一句话:授权记寿命,定时带重试,对账留差异,每段进度都落档。上下文自己建,记忆自己落,进程随时可以死,任务不能跟着死。这三块各管一摊,拼起来,成了一条能在半路摔倒又爬起来的自动化链路------判断标准也简单:随手 kill 掉进程,重启后看它接不接得上。接得上,这条链路过关;接不上,说明还有哪一档进度漏落盘了。

任务能断点续跑,长流程才敢放手。

相关推荐
搞科研的小刘选手1 小时前
【IEEE出版,快至会后3-4个月检索 | 北方民族大学主办 | 中国·银川(线上+线下)】2026年机器人、智能计算与自动化学术会议(RAIC 2026)
机器人·自动化·学术会议·智能计算·会议推荐·北方民族大学·银川
Ruiery1 小时前
Linux 6.6内核内存管理深度解析(十):KSM 同页合并 — 内容去重,把 N 份相同内存合并成 1 份
linux·运维·服务器
Huangjin007_1 小时前
【Linux 系统篇(三十三)】通信(三):System V 共享内存详解
linux·运维·服务器
叼烟扛炮2 小时前
Linux 学习笔记(三十八):从实模式到分页——处理器内存管理的发展史
linux·运维·服务器
润滑卫士2 小时前
油液水分在线传感器技术对比:电容法、电阻法与微波法的测量原理与适用性
运维
Decay__2 小时前
附-01 · MySQL 压测与容量规划 —— sysbench 基准测试、容量评估与参数基线
运维·数据库·sql·mysql·架构
mennekes2 小时前
暗装插座箱 VS 明装插座箱,工地和车间怎么取舍
运维·网络·科技·安全·制造
叼烟扛炮2 小时前
Linux 学习笔记(三十六):并发进阶——读写锁 vs 自旋锁,到底怎么选?
linux·运维·服务器·笔记·学习
正经教主2 小时前
【FDE系列】阶段2:Day 43(UP详细版):Docker Compose — 多容器一键编排
运维·docker·容器