尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Go定时任务:cron与分布式调度
Go定时任务:cron与分布式调度摘要: 本篇讲解Go语言定时任务调度使用robfig/cron库配置cron表达式实现分布式锁防止多实例重复执行设计任务补偿机制处理漏执行场景分享时区配置错误导致任务提前一小时执行的踩坑经验对比单机cron、分布式cron和K8s CronJob三种方案。开篇故事去年我们有个对账系统每天凌晨2点跑一批结算任务把前一天的订单数据汇总生成报表。开发环境跑了一个月没问题上了生产环境第二天运营就找过来说报表数据不对凌晨1点的数据没跑进去。排查了一上午才发现生产服务器时区是UTC开发机器是CST。cron表达式写的是0 2 * * *在UTC时区下2点就是北京时间早上10点任务确实跑了但跑的时候当天的订单还没产生多少。运营以为任务没跑实际上跑了但时间不对。后来又遇到一个坑。我们部署了3个实例做高可用cron任务在每个实例都配置了一份。凌晨2点三个实例同时触发同一个对账任务跑了3遍数据库出现了重复记录。这两个坑让我把定时任务的时区和分布式锁认真梳理了一遍。一、robfig/cron库基础用法robfig/cron是Go生态里最成熟的定时任务库支持标准cron表达式和秒级精度的扩展表达式。packagemainimport(logtimegithub.com/robfig/cron/v3)funcmain(){// 创建cron调度器// cron.New()默认用标准表达式(5位: 分 时 日 月 周)// cron.New(cron.WithSeconds())用6位表达式(秒 分 时 日 月 周)c:cron.New(cron.WithLogger(cron.PrintfLogger(log.Default())),// 自定义日志cron.WithLocation(time.Local),// 指定时区)// 添加任务标准表达式: 每天凌晨2点执行id1,err:c.AddFunc(0 2 * * *,func(){log.Println(执行每日对账任务)runReconciliation()})iferr!nil{log.Fatalf(添加任务失败: %v,err)}log.Printf(对账任务已注册, ID%d,id1)// 每5分钟执行一次健康检查id2,err:c.AddFunc(*/5 * * * *,func(){log.Println(执行健康检查)checkHealth()})iferr!nil{log.Fatalf(添加任务失败: %v,err)}log.Printf(健康检查已注册, ID%d,id2)// 启动调度器所有任务开始按计划执行c.Start()log.Println(cron调度器已启动)// 使用Select阻塞主goroutine防止程序退出select{}}// runReconciliation 模拟对账任务funcrunReconciliation(){log.Println(开始汇总订单数据...)time.Sleep(2*time.Second)// 模拟耗时操作log.Println(对账任务完成)}// checkHealth 模拟健康检查funccheckHealth(){log.Println(检查服务状态...)}cron表达式语法需要记清楚。5位标准格式是分 时 日 月 周每位用空格分隔。*表示任意值*/5表示每5个单位0 2 * * *就是每天2点。robfig/cron还支持描述性写法比如hourly表示每小时every 5m表示每5分钟比表达式好记。二、分布式锁防止重复执行单机cron没问题多实例部署时每个实例都会触发同一个任务。需要对账任务只跑一次就得加分布式锁。用Redis实现一个简单的分布式锁谁先抢到锁谁执行其余实例跳过。packageschedulerimport(contextfmtlogtimegithub.com/redis/go-redis/v9)// DistributedCron 分布式定时任务调度器typeDistributedCronstruct{client*redis.Client// Redis客户端keyPrefixstring// 锁key前缀lockTTL time.Duration// 锁过期时间防止任务崩溃后锁不释放}// NewDistributedCron 创建分布式调度器// lockTTL必须大于任务最大执行时间否则任务还没跑完锁就过期了funcNewDistributedCron(client*redis.Client,lockTTL time.Duration)*DistributedCron{returnDistributedCron{client:client,keyPrefix:cron:lock:,lockTTL:lockTTL,}}// TryLock 尝试获取分布式锁// 返回true表示获取成功应该执行任务// 返回false表示已有其他实例在执行跳过本次func(dc*DistributedCron)TryLock(ctx context.Context,taskNamestring)(bool,error){key:dc.keyPrefixtaskName// SET key value NX EX ttl// NX: key不存在才设置保证只有一个实例能拿到锁// EX: 设置过期时间防止锁泄漏value:fmt.Sprintf(%d,time.Now().UnixNano())ok,err:dc.client.SetNX(ctx,key,value,dc.lockTTL).Result()iferr!nil{returnfalse,fmt.Errorf(获取锁失败: %w,err)}returnok,nil}// Unlock 释放锁// 使用Lua脚本保证只有锁的持有者才能释放// 防止任务执行慢锁过期后被其他实例拿到当前实例误删别人的锁func(dc*DistributedCron)Unlock(ctx context.Context,taskNamestring,valuestring)error{key:dc.keyPrefixtaskName// Lua脚本: 先比较value再删除保证原子性luaScript: if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end _,err:dc.client.Eval(ctx,luaScript,[]string{key},value).Result()returnerr}// RunWithLock 带分布式锁执行任务// 封装了获取锁、执行、释放锁的完整流程func(dc*DistributedCron)RunWithLock(ctx context.Context,taskNamestring,taskfunc()error)error{// 尝试获取锁locked,err:dc.TryLock(ctx,taskName)iferr!nil{returnerr}if!locked{// 其他实例正在执行跳过log.Printf(任务 %s 已被其他实例执行跳过,taskName)returnnil}// 获取锁的值用于释放时验证key:dc.keyPrefixtaskName value,_:dc.client.Get(ctx,key).Result()// 执行任务iferr:task();err!nil{returnerr}// 释放锁returndc.Unlock(ctx,taskName,value)}锁的TTL设置要小心。设太短任务没跑完锁就过期了另一个实例拿到锁又跑一遍等于白加锁。设太长实例崩溃后锁很久不释放这段时间任务就一直没人执行。保险做法是TTL设为任务平均执行时间的3倍同时任务执行中定期续期。三、任务补偿机制定时任务可能因为服务重启、数据库故障等原因漏执行。关键任务需要有补偿机制服务启动时检查上次执行时间如果错过了就补跑。packageschedulerimport(contextfmtlogtimegithub.com/redis/go-redis/v9)// Compensator 任务补偿器typeCompensatorstruct{client*redis.Client// Redis存储上次执行时间prefixstring// key前缀}// NewCompensator 创建补偿器funcNewCompensator(client*redis.Client)*Compensator{returnCompensator{client:client,prefix:cron:last_run:,}}// RecordExecution 记录任务执行时间func(cp*Compensator)RecordExecution(ctx context.Context,taskNamestring)error{key:cp.prefixtaskName// 记录当前时间戳设置30天过期returncp.client.Set(ctx,key,time.Now().Unix(),30*24*time.Hour).Err()}// CheckAndCompensate 检查是否需要补偿执行// interval: 任务正常执行间隔// 如果距离上次执行超过interval说明漏执行了触发补偿func(cp*Compensator)CheckAndCompensate(ctx context.Context,taskNamestring,interval time.Duration,taskfunc()error,)error{key:cp.prefixtaskName// 读取上次执行时间lastRunStr,err:cp.client.Get(ctx,key).Result()iferrredis.Nil{// 没有记录说明是第一次执行或记录被清除了// 直接执行任务并记录时间log.Printf(任务 %s 无历史记录首次执行,taskName)iferr:task();err!nil{returnerr}returncp.RecordExecution(ctx,taskName)}iferr!nil{returnfmt.Errorf(读取执行记录失败: %w,err)}// 解析上次执行时间戳lastRunUnix,err:parseUnix(lastRunStr)iferr!nil{returnerr}lastRun:time.Unix(lastRunUnix,0)now:time.Now()// 计算距上次执行的时间差elapsed:now.Sub(lastRun)ifelapsedinterval{// 超过间隔时间说明漏执行了补偿log.Printf(任务 %s 漏执行距上次 %v触发补偿,taskName,elapsed)iferr:task();err!nil{returnerr}}// 记录本次执行时间returncp.RecordExecution(ctx,taskName)}// parseUnix 解析Unix时间戳字符串funcparseUnix(sstring)(int64,error){vartsint64_,err:fmt.Sscanf(s,%d,ts)returnts,err}补偿机制有个问题要考虑。如果服务挂了3天恢复后补偿任务跑一次就行还是把3天每天补跑一次。这取决于业务场景。对账类任务一般跑一次就行取最新数据。分批同步类任务可能需要按天补跑否则中间数据丢失。四、踩坑经验:时区配置导致任务提前执行这个坑就是开篇故事里说的。线上服务器是UTC时区开发机器是CST。cron表达式0 2 * * *在UTC时区下2点执行等于北京时间10点。问题出在cron调度器默认用系统时区。开发机系统时区是Asia/Shanghai生产服务器系统时区是UTC。同一份代码两台机器跑出来的执行时间差了8小时。修复方法是显式指定时区不依赖系统时区。packagemainimport(logtimegithub.com/robfig/cron/v3)funcmain(){// 方案1: 用WithLocation指定固定时区// 不管服务器时区是什么任务都按CST执行loc,err:time.LoadLocation(Asia/Shanghai)iferr!nil{log.Fatalf(加载时区失败: %v,err)}c:cron.New(cron.WithLocation(loc),// 显式指定北京时间cron.WithLogger(cron.PrintfLogger(log.Default())),)// 这样配置后0 2 * * * 始终在北京时间凌晨2点执行c.AddFunc(0 2 * * *,func(){log.Println(北京时间凌晨2点执行对账任务)})c.Start()select{}}robfig/cron还支持在表达式里直接指定时区写法是CRON_TZAsia/Shanghai 0 2 * * *。但这个语法容易和标准cron表达式混在一起我更推荐用WithLocation统一配置。五、对比分析调度方案精确度分布式支持补偿机制运维成本单机cron秒级不支持需自建低分布式cron(Redis锁)秒级支持需自建中K8s CronJob秒级支持(按需)内置重试低单机cron适合中小项目任务数量少且没有高可用要求。分布式cron用Redis锁保证任务只执行一次适合多实例部署但补偿机制要自己写。K8s CronJob是云原生方案配置简单自带并发控制和失败重试前提是你的服务跑在K8s上。总结定时任务看起来简单时区和分布式锁是两个最容易踩的坑。robfig/cron库用起来方便但一定要显式指定时区别依赖系统时区。多实例部署必须加分布式锁TTL设为任务执行时间的3倍以上。关键任务加补偿机制服务重启后检查上次执行时间补跑漏掉的任务。下一篇聊WebSocket进阶重点讲心跳保活和断线重连。
RELATED

相关推荐

Go邮件服务:SMTP与模板渲染

Go邮件服务:SMTP与模板渲染

Go邮件服务:SMTP与模板渲染摘要: 本篇讲解Go语言发送邮件的两种方式,用net/smtp标准库实现基础邮件发送,用gomail库实现HTML模板渲染、附件和内嵌图片,分享SMTP连接被防火墙阻断导致邮件堆积的踩坑经验,对比net/smtp、gomail和第三…

📅 2026/10/18 9:42:59
Hopsum项目解析:如何利用TTL过期数据包实现分布式网络计算

Hopsum项目解析:如何利用TTL过期数据包实现分布式网络计算

上周在 Hacker News 上看到一个项目,叫 Hopsum。标题翻译过来是“让路由器用过期数据包做算术”。第一眼看到这个描述,我有点懵。路由器?过期数据包?算术?这几个词组合在一起,听起来像是某种网络协议的边缘…

📅 2026/10/13 21:10:50
Ender 5 3D打印机专业照明改造:从LED选型到主板集成全攻略

Ender 5 3D打印机专业照明改造:从LED选型到主板集成全攻略

1. 项目概述:为什么你的Ender 5需要一条灯带?如果你和我一样,把Ender 5放在书房或者一个光线不那么充足的角落,那你肯定经历过这样的场景:晚上想观察打印进度,或者检查第一层铺得是否完美,不得不…

📅 2026/10/4 17:02:19
MORE NEWS

更多资讯

📰

一帧画面八道关:rebucca视频行为分析平台源码拆解

最近在做工业场景的 AI 监控方案选型时,我注意到一个很有意思的开源项目:rebucca 视频行为分析平台。它主打工业环境下的实时行为识别,而且最吸引人的一点是“一行命令就能跑起来”,不需要折腾一堆依赖。真正开始读源码之后&#…

📰

Markdown流式渲染:构建AI生成式UI的结构信标

1. 项目概述:这不是一次简单的格式转换,而是一场交互范式的迁移“从 Markdown 到生成式 UI:AI 应用中的流式渲染实践”——这个标题里藏着三个关键动作:“从…到…”是路径,“生成式 UI”是目标形态,“流式…

📰

虚拟演唱会视频制作全解析:从PJSK渲染到中日双语字幕压制

很多读者看到“【中日双字/PJSK】初音未来生日2026 虚拟演唱会”这样的视频标题时,第一反应是“去哪个平台找资源”,第二反应是“这个字幕组剪辑得真快”。但如果从技术角度去看,这一类内容并不是简单“录屏 打轴”就能完成的:它…

📰

SpringBoot+Vue+MySQL办公用品直售系统:从下单闭环到部署答辩

简介:面向高校毕业设计、课程设计及需要完整前后端项目参考的开发者,这是一套基于JAVASpringBootVueMySQL的日常办公用品直售系统,用于满足线上采购、商品管理与订单处理等真实场景的实战需求。项目涵盖商品展示、购物车、订单处理、用户管理…

📰

键盘记录器3.0:Windows低层钩子与日志落盘全解析

简介:全能键盘记录器3.0是一款面向企业管理者、家长及安全审计人员的键盘监控工具,能够完整捕获字母、数字、特殊字符以及回车、空格等按键动作,并支持后台隐蔽运行、定期生成带时间戳的日志报告、关键词过滤与远程访问功能,适用于…

📰

DGX Spark 跑 SD3.5 + ComfyUI 全流程部署复盘

如果你手边恰好有一台 NVIDIA DGX Spark,又想跑 Stable Diffusion 3.5,我猜你大概率会选择 ComfyUI 来折腾工作流。这三个词放在一起,本身就是一句非常典型的“本地 AI 绘画全家桶”宣言:DGX Spark 负责把算力放到你桌上&#xff…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬