接着上篇文章Golang 从0到1之任务提醒(一),上篇文章把基础功能搭建的差不多了。还剩下核心的定时推送消息以及接收微信消息处理。

定时器

如何定时执行提醒推送呢?

我们可以开启一个定时器,每小时去获取一次下一个小时内需要推送的任务。

package handlersimport ("fmt""go-remind/logic""go-remind/models""go-remind/server""go-remind/tools""time"
)var isFirst bool = truefunc Scheduler() {var job logic.JobLogicfor {if !isFirst {timer := time.NewTicker(1 * time.Hour)<-timer.C}isFirst = false// 获取接下来一小时内需要发送的任务列表now := tools.GetCurrTime()h, _ := time.ParseDuration("1h")jobs, err := job.GetJobsByTime(tools.TimeString(now), tools.TimeString(now.Add(1*h)))if err != nil {fmt.Printf("出错了:%v", err)return}// 任务通道ch := make(chan models.Job, 100)jobFunc := func(ch <-chan models.Job) {for item := range ch {// 发送通知go HandleJob(item)}}// 处理任务go jobFunc(ch)// 投递任务for _, job := range jobs {ch <- job}}
}

‍‍isFirst 默认值是 true。它的作用是,如果是第一次项目启动的时候,那么作为初始化先去拉取一遍小时内全量任务。我们看 Scheduler 逻辑,前半部分的逻辑就是通过定时器固定周期去拉取要执行的任务,然后创建一个缓冲的通道,

ch := make(chan models.Job, 100)

把从数据库中拉取的任务都丢进通道中,另开一个 G 用来从通道中接收并执行提醒任务。这里每个提醒任务都单独开启一个 G 执行。

jobFunc := func(ch <-chan models.Job) {for item := range ch {// 发送通知go HandleJob(item)}}// 处理任务go jobFunc(ch)// 投递任务for _, job := range jobs {ch <- job}

主要看HandleJob(item) 操作,

func HandleJob(job models.Job) {now := tools.GetCurrTime()noticeTime, _ := time.ParseInLocation(tools.TimeFormat,job.NoticeTime.Format(tools.TimeFormat), time.Local)diff := noticeTime.Sub(now)timer := time.NewTimer(diff)<-timer.Cvar sendTool server.MessagesendTool = &server.SmsMsg{Job: job}if job.Phone == "" {sendTool = &server.EmailMsg{Job: job}}err := server.Notice(sendTool)//成功与否isOk := models.JobSuccessjobLogic := logic.JobLogic{}if err != nil {isOk = models.JobFailfmt.Printf("通知失败:%v", err)}_ = jobLogic.UpdateStatusById(job.Id, isOk)}

前半部分就是计算当前时间距离任务执行间隔时间,对应单独设置一个定时器,任务的真正执行时间取决于用户自己设定的提醒时间。

这里实现并不是很好,想象我们当前一小时内有 10w 个任务,那么必然会开启 10w 个 G 来执行,每个 G 都有自己的定时器,在更极端的情况下,所有任务都集中在五十几分钟,你已经可以想象了。

我们可以分批去取,比如按照时间顺序每次获取 100 个任务 A,这样的话后100 个任务 B,他们在任务执行时间的关系一定是:B>=A。

然后通过 go 开箱即用的并发控制技术 sync.WaitGroup 来等待控制。这算是一种优化方向。

接下来看,

var sendTool server.Message

Message 是一个接口类型,代表发送任务通知的动作。

type Message interface {SendMessage() error
}

底下的 EmailMsg 和 PhoneMsg 都隐式实现了此接口,完成具体的发送操作洗细节。(ps:短信懒的接第三方了)

type EmailMsg struct {Job models.Job
}func (email *EmailMsg) SendMessage() error {fmt.Printf("成功给%s发送邮件\n", email.Job.Email)sendMail := gomail.NewMessage()sendMail.SetHeader(`From`, ConfAll.Email.User)sendMail.SetHeader(`To`, email.Job.Email)sendMail.SetHeader(`Subject`, "来自吴亲库里的温馨提醒")sendMail.SetBody(`text/html`, email.Job.Content)err := gomail.NewDialer(ConfAll.Email.Host, ConfAll.Email.Port, ConfAll.Email.User,ConfAll.Email.Pass).DialAndSend(sendMail)if err != nil {return err}return nil
}type SmsMsg struct {Job models.Job
}func (email *SmsMsg) SendMessage() error {fmt.Printf("成功给%s发送短信\n", email.Job.Phone)return nil
}

回头看HandleJob,如果填了手机号,优先选择手机号,否则就发邮箱通知。

看这行代码,

err := server.Notice(sendTool)
func Notice(msg Message) error {return try.Do(func(attempt int) (retry bool, err error) {err = msg.SendMessage()if err != nil {return attempt < try.MaxRetries, err}return true, nil})
}

msg.SendMessage 就是实际的执行发送操作,再看看 try.Do 是干嘛的。

// 最大允许重试次数
var MaxRetries = 3
var errMaxRetriesReached = errors.New("exceeded retry limit")type Func func(attempt int) (retry bool, err error)func Do(fn Func) error {var err errorvar cont boolattempt := 1for {if attempt > 1 {time.Sleep(2 * time.Second)}cont, err = fn(attempt)if !cont || err == nil {break}attempt++if attempt > MaxRetries {return errMaxRetriesReached}}return err
}

其实就是一个重试的操作,我稍微解释一下这段代码。

首先最外面是个死循环,如果 attempt 等于1,说明是第一次执行,运行自己传递的闭包函数,也就是执行任务。否则的话,代表此次是重试,先等待一段时间。

cont 表示是否超过最大重试次数,是个 bool 值。err 代码运行有无错误。那么底下这句代码就是判断:如果超过最大重试次数或者任务处理是正常的,两者有一个满足就可以退出这段程序了。

if !cont || err == nil {break}

这个函数最终返回一个 err,如果不为空,说明最终任务经历过重试还是执行失败,那么就真的失败了,标记任务失败。

err := server.Notice(sendTool)//成功与否isOk := models.JobSuccessjobLogic := logic.JobLogic{}if err != nil {isOk = models.JobFailfmt.Printf("通知失败:%v", err)}_ = jobLogic.UpdateStatusById(job.Id, isOk)

为什么需要重试?

简单的说,与外部有依赖的操作本身就是不可控的,尤其是网络波动。出现这种情况,就需要通过重试机制去保障服务的正常。

但是重试也是有限制的,不可能短期内无休止的去重试一个服务。抛开网络波动,在一定时间内重试失败大概率不要指望短时间能恢复服务了,这样造成的数据损失,在事后恢复,经常被人高大上称之为最终一致性,很多时候,往往就是人肉罢了。

这里很骚的一个操作,重试时间直接被我定住了,真实。

time.Sleep(2 * time.Second)

解析微信消息

接下来看看处理微信消息。上篇文章已经把主要微信接收的代码写上了,现在我们主要是解析发送的信息,入库而已。没什么黑科技,靠的就是正则匹配。

//时间匹配
var TimeMatch = contentRegexp{regexp.MustCompile(`(今天|明天|后天|大后天|[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}|[\d]{8}\s[\d]{1,2}:[\d]{1,2}|[[\d]{1,2}:[\d]{1,2}|[\d]{1,2}(个月|小时|点|分钟|分|秒|周|天))`,
)}//手机号匹配
var PhoneMatch = contentRegexp{regexp.MustCompile(`(1[356789]\d)(\d{4})(\d{4})`,
)}//邮箱匹配
var EmailMatch = contentRegexp{regexp.MustCompile(`(\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*)`,
)}
func HandleMessage(content string) (string, error) {phone := tools.PhoneMatch.FindStringSubmatch(content)email := tools.EmailMatch.FindStringSubmatch(content)if phone == nil || email == nil {return "不留下联系方式我咋么联系上你", RequestFormatErr}mmp := tools.TimeMatch.FindAllStringSubmatch(content, -1)if mmp == nil {return "我得再升升级才能满足你的时间格式", RequestFormatErr}// 最大匹配到分if len(mmp) > 3 {mmp = mmp[:3]}var sendDate string//....//省略一万行解析代码//...sendTimer := tools.StringToTimer(sendDate + ":00")diff := sendTimer.Sub(tools.GetCurrTime())if diff < 0 {return "过期的时间就别让我通知了", RequestFormatErr}jobLogic := &logic.JobLogic{}job := logic.NewJob(content, sendTimer, phone[0], email[0])err := jobLogic.Insert(job)if err != nil {return "请检查输入内容", RequestFormatErr}if diff.Minutes() < 0 {return fmt.Sprintf("%s秒后短信提醒内容:%s", tools.Decimal(diff.Seconds()), content), nil}if diff.Hours() < 1 {//小于1个小时直接加入到定时器go func() {HandleJob(job)}()return fmt.Sprintf("%s分钟后短信提醒内容:%s", tools.Decimal(diff.Minutes()), content), nil}return fmt.Sprintf("%s小时后短信提醒内容:%s", tools.Decimal(diff.Hours()), content), nil
}

‍上面一部分主要是去解析数据,比如通知时间、手机号、邮箱。

后面还有一个操作,之前我们是以 1 小时为单位的定时器,如果这个新增任务的时间属于当前小时内的,那么我们在入库的同时丢给刚才的 HandleJob 。

然后在主函数中专门开个 g 去运行这个定时任务,这样整体的流程就连接起来了。

r := gin.Default()go func() {handlers.Scheduler()}()r.POST("/msg", handlers.Message)_ = r.Run(":8099")

到这里,我们就已经把最小可行性的项目运行起来了。

但是还是有很多可以优化的点,这个后续我随着优化。感兴趣的可以给我提 PR。

哦对了,单元测试都没写。咋么能不写单元测试呢,致命打击。

项目放在:https://github.com/wuqinqiang/go-remind 感兴趣可以看看。

推荐阅读:

在Go中,你犯过这些错误吗

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 

Golang 从0到1之任务提醒(二)相关推荐

  1. golang 目录分隔符号_Golang 从0到1之任务提醒(一)

    图片拍摄于2020年4月20日,舟山庙子湖. 上篇开篇介绍了一遍Golang 从0到1之任务提醒(开篇),这篇开始搭建项目,首先规划一下整体的目录. 目录就不过多解释了,这里并不复杂,主要想谈谈其他的 ...

  2. ASP.NET2.0打通文件图片处理任督二脉【月儿原创】

    ASP.NET2.0打通文件图片处理任督二脉 作者:清清月儿 主页:http://blog.csdn.net/21aspnet/           时间:2007.4.1 1.最简单的单文件上传(没 ...

  3. ASP.NET2.0打通文件图片处理任督二脉[转载]

    ASP.NET2.0打通文件图片处理任督二脉 作者:清清月儿 主页:http://blog.csdn.net/21aspnet/           时间:2007.4.1 1.最简单的单文件上传(没 ...

  4. Android 系统(41)---Android7.0 PowerManagerService亮灭屏分析(二)

    Android7.0 PowerManagerService亮灭屏分析(二) 3029 在PowerManagerService中对各种状态进行判断后,将其数值封装进DisplayPowerReque ...

  5. 【Vue2.0】—github小案例(二十三)

    [Vue2.0]-github小案例(二十三) <template><section class="jumbotron"><h3 class=&quo ...

  6. 【Vue2.0】—过渡与动画(二十一)

    [Vue2.0]-过渡与动画(二十一) 方式一:使用animate.css动画库 进入官网https://animate.style/ 一.Installing(安装) npm install ani ...

  7. C++ 与cocos2d-x-4.0完成太空飞机大战 (二)

    C++ 与cocos2d-x-4.0完成太空飞机大战 (二) 动画演示 飞机精灵编码:AircraftSprite.cpp 飞机精灵编码:AircraftSprite.h 飞机动画编码:Aircraf ...

  8. mysql数据库 SELECT COUNT(1) FROM new_comps WHERE deleted = 0 统计数据太慢了二十多秒

    @TOC使用mybatis-puls分页查询数据量大很慢,怎么处理 mysql数据库 SELECT COUNT(1) FROM new_comps WHERE deleted = 0 统计数据太慢了二 ...

  9. c++资料匠心精作C++从0到1入门编程(二)- c++核心

    目录 C++核心编程 1 内存分区模型 1.1 程序运行前 1.2 程序运行后 1.3 new操作符 2 引用 2.1 引用的基本使用 2.2 引用注意事项 2.3 引用做函数参数 2.4 引用做函数 ...

  10. webpack从0到1的配置(二)

    阅读本篇博客需要预先阅读webpack从0到1的配置(一). 接着上篇博客讲plugins. 我们希望在打包后的项目里有index.html文件,并且自动引入main.js文件 这里需要安装插件htm ...

最新文章

  1. Linux CentOS 6+复制本地前端文件压缩包解压到服务器端指定目录
  2. socket编程实践
  3. 30岁的她决定回国做AI芯片
  4. 中国算力发展指数白皮书(2021)
  5. wxWidgets:wxCalendarCtrl类用法
  6. Linux命令之 mkfs -- 在特定的分区创建 Linux 文件系统
  7. django的contenttype表
  8. java 模拟平台_用Java程序模拟登陆网站平台
  9. 去哪儿-17-detail-header
  10. java 算出下一个工作日,Java:计算一个日期加下指定工作日数(排除周六周日和一系列节日)...
  11. 转:为 setuptools 开路搭桥
  12. ai第二次热潮:思维的转变_基于属性的建议:科技创业公司如何使用AI来转变在线评论和建议
  13. 安装Scrapy失败的解决方法
  14. Android OAID 获取 基于MSA oaid_sdk_1.0.25.zip
  15. 移动端统计分析工具Firebase、AppsFlyer、Adjust、Flurry、Tap stream、Kochava 、branch不完全对比分析
  16. 四川省天府新区知识产权信息公共服务网点申报好处条件材料
  17. aspen压缩因子_利用aspen plus进行物性参数的估算
  18. 极域教室老师版,控制同学电脑
  19. 一起写RPC框架(一)RPC之我所见
  20. MATLAB算法实战应用案例精讲-【图像处理】小目标检测(补充篇)(附python代码实现)

热门文章

  1. 建筑装饰毕业论文题目
  2. 跟着团子学SAP PS:SAP PS模块常用报表介绍及增强建议
  3. 这些才是Win10真正好用之处:瞬对Win7无爱
  4. 无线局域网和蜂窝移动网络_干货!无线AP覆盖系统解决方案
  5. 学计算机专业长白头发,程序员白头发是怎样一种感受?
  6. 局域网互传文件工具_如何在 iOS、Android、macOS、Windows 之间快速文件互传?
  7. 一文了解plc编程、电脑编程、手机APP编程、组态编程、云编程(上)
  8. 在线作图|2分钟做Lefse分析
  9. vb中的print方法
  10. VB6.0调用WebService