定时发送消息看着简单,实际做起来坑不少——发送时间不准、批量发被限流、任务中断不知道发到哪、用户取消后还在发。一个"每天早上9点发早报"的需求,背后是一套调度加进度管理加限流加补偿的机制。
一、定时实现选型——延迟队列还是定时任务
两种实现各有适用场景。延迟队列(Redis ZSet或消息队列延迟插件)精确到秒,适合单次延迟发送。定时任务(cron或调度框架)按周期触发,适合每天的固定发送。
实际项目里两者常常结合——定时任务负责周期性触发,触发后生成延迟消息投进延迟队列。这样周期性和精确延迟都能满足。任务创建和延迟投递的接口在 Eyun 开发文档 中有对应说明。
二、消息序列与限流控制
一个定时任务往往不是发一条,而是发一个序列。比如"每周一给所有客户发通知",几百条消息要发出去。这时候它就是消息序列——一批消息按顺序发送,每条之间要有间隔。
间隔不能省。连续高频发送会触发限流,一批全失败。经验值是每条间隔3秒以上,批量发送分批处理。序列必须记录进度——发到第几条了要落库,任务中断后从断点续发。不记进度的话,中断重启后不知道发到哪,重发会造成重复投放。
三、取消机制与失败补偿
定时任务要支持取消。用户改了需求,之前排的任务要撤掉。取消要真取消——延迟队列里的消息要能删除;删不掉的场景加状态标记,消费时检查状态跳过。只标记不处理等于没取消。
失败补偿分两级。单条发送失败做有限次重试,重试仍失败的记录到失败列表并告警。整个任务级别的失败要保留原任务,不能因为一次失败就把任务丢掉。定时发送和失败通知的接口在 Eyun 开发文档 中有对应能力。
定时发送方案对照
实现方式 | 适用场景 | 精度 | 主要坑 |
|---|---|---|---|
延迟队列 | 单次延迟发送 | 秒级 | 取消难 |
定时任务 | 周期性发送 | 分钟级 | 任务重叠 |
两者结合 | 周期+精确延迟 | 秒级 | 进度管理 |
定时发送与进度管理实现
class ScheduledSender: def __init__(self): self.delay_queue = RedisDelayQueue() # 延迟队列 self.db = ProgressDB() # 进度存储 def schedule_batch(self, task_id, receivers, content, send_at): # 批量定时:定时任务生成延迟消息 for idx, wxid in enumerate(receivers): self.delay_queue.add({ "task_id": task_id, "seq": idx, # 序列号 "wxid": wxid, "content": content }, delay=send_at - time.time()) def consume(self, msg): # 消费延迟消息:检查取消状态 task = self.db.get_task(msg["task_id"]) if task.status == "cancelled": return # 已取消直接跳过 try: resp = send_wechat(msg["wxid"], msg["content"]) if resp.ok: self.db.mark_sent( # 记录进度 msg["task_id"], msg["seq"] ) else: self.retry(msg) except Exception: self.retry(msg) def retry(self, msg): # 有限重试:最多3次 if msg.get("retry", 0) < 3: msg["retry"] = msg.get("retry", 0) + 1 self.delay_queue.add(msg, delay=30) # 30秒后重试 else: self.db.add_failure(msg) # 记录失败告警 self.alert(msg) def cancel(self, task_id): # 取消:标记状态+清理队列 self.db.update_task(task_id, "cancelled") self.delay_queue.remove_by_task(task_id)落地建议
定时实现按场景选——单次延迟用延迟队列,周期性用定时任务,两者结合最常用。消息序列必须记进度——发到第几条落库,中断能续发,不记就会重发。取消要真取消——能删的删,不能删的加状态标记并在消费时跳过。限流不能省——每条间隔3秒,批量分批发。微信侧的消息发送和定时触发由Eyun这类个人微信API平台 提供,调度、进度和补偿逻辑在自建服务实现,接口字段以平台开发文档为准。