背景
用Go重写Elixir写的对话服务,有一个需求是这样的:访客发了消息,客服在规定的时间内(比如10s)没回复,要发一条自动消息
Elixir的实现
- 访客发消息,
Process.send_after
设置一个10s的timer,并把timer存到actor的state里 - 访客发消息,
Process.cancel_timer
取消timer,在用Process.send_after
重新设置timer - 客服回复消息,
Process.cancel_timer
取消timer - 如果10s内客服没有回复,timer触发,发送一条自动消息
Golang的实现
elixir写的服务一直是热更,timer可以一直保存在内存里,又有分布式的actor可用,所以用语言自带的timer就可以实现该功能
迁移到go采用的方案是:https://github.com/vmihailenco/taskq
第一版
参照elixir的实现来处理的,由于taskq不支持取消,又在redis里用set维护一个取消任务的集合:
创建任务,生成一个唯一的任务id(采用雪花算法,借助redis的incr生成唯一的node_id),当任务需要取消的时候,将任务id加入到set中,每次任务执行会先判断set中有没有该任务id,有就取消
- 访客发消息,用taskq创建延迟10s的任务
- 访客再发消息,往set里加入任务id,取消任务,再用taskq创建延迟10s的任务
- 客服回复消息,往set里加入任务id,取消任务
- 10s后任务触发,检查set中有没有该任务id,没有就发送自动消息
这一版的缺点
来计算一下一个访客消息会最多产生多少个redis操作(taskq是用的redis的stream来存储任务)
- XAdd 添加任务
- XAck 消费任务
- XDel 删除任务
- SAdd 取消任务
- SIsMember 判断任务是否取消
- SRem 删除已取消的任务
最多会产生6个,当消息数很大的时候,会对redis产生很大的压力,之前的经验:
- redis的qps达到4.5w的时候,cpu达到80%
- redis的qps达到5w的时候,cpu达到88%
有没有一种方案,不要让延迟任务的个数和访客消息成正比?
第二版
这种需求的延迟任务有个特点:新建的延迟的任务肯定是晚于之前的延迟任务执行的。
也就是说,如果之前已经有一个延迟任务了,访客再发消息,是可以先不创建一个新的任务,也不取消之前的任务
延迟任务触发后,根据消息的时间线,判断是执行,还是创建一个新的延误任务。具体如下:
- 访客发消息,用taskq创建延迟10s的任务
- 访客再发消息,发现已有延迟任务了,就啥也不处理
- 客服回复消息,啥也不处理
- 10s后任务触发,检查最后一条消息:
- 如果是关联任务的访客消息,则触发自动消息
- 如果是客服消息,说明客服回了,则跳过
- 如果又是一条新的访客消息,则再创建一个延迟10s的任务
这样一个对话同一时间只会存在一个延迟任务,无论访客发了多少条消息