作者:林冠宏
原文:https://juejin.im/post/5d176b5ee51d4510835e02da
前序
本文所要分享的思路就是电商应用中常用的订单队列。
一般的订单流程
电商应用中,简单直观的用户从下单到付款,最终完成整个流程的步骤可以用下图表示:
其中,订单信息持久化,就是存储数据到数据库中。而最终客户端完成支付后的更新订单状态的操作是由第三方支付平台进行回调设置好的回调链接 NotifyUrl,来进行的。
补全订单状态的更新流程,如下图表示:
思考瓶颈点
服务端的直接瓶颈点,首先要考虑 TPS。去除细分点,我们主要看订单信息持久化瓶颈点。
在高并发业务场景中,例如 秒杀、优惠价抢购等。短时间内的下单请求数会很多,如果订单信息持久化 部分,不做优化,而是直接对数据库层进行频繁的读写操作,数据库会承受不了,容易成为第一个垮掉的服务,比如下图的所示的常规写单流程:
可以看到,每持久化一个订单信息,一般要经历网络连接操作(链接数据库),以及多个 I/O 操作。
得益于连接池技术,我们可以在链接数据库的时候,不用每次都重新发起一次完整的HTTP请求,而可以直接从池中获取已打开了的连接句柄,而直接使用,这点和线程池的原理差不多。
此外,我们还可以在上面的流程中加入更多的优化,例如对于一些需要读取的信息,可以事先存置到内存缓存层,并加于更新维护,这样在使用的时候,可以快速读取。
即使我们都具备了上述的一些优化手段,但是对于写操作的I/O阻塞耗时,在高并发请求的时候,依然容易导致数据库承受不住,容易出现链接多开异常,操作超时等问题。
在该层进行优化的操作,除了上面谈到的之外,还有下面一些手段:
每种方式有各自的特点,因为本文谈的是订单队列的架构思想,所以下面我们来看下如何在订单系统中引入订单队列。
订单队列
网上有不少文章谈到订单队列的做法,大部分都漏了说明请求与响应的一致性问题。
第一种订单队列流程图:
上图是大多文章提到的队列模型,有两个没有解析的问题:
首先,要肯定的是,上面的订单流程图是没有问题的。它有下面的优缺点,所提到的两个问题也是有解决方案的。
优点:
缺点:
上面谈及的问题点,我后面都会给出解决方案。下面我们来看下另外一种订单队列流程图。
第二种订单队列流程图:
第二种订单队列的设计模型,注意它的同步等待持久化处理的结果,解决了持久化与响应的一致性问题,但是有个严重的耗时等待问题,它的优缺点如下:
优点:
缺点:
这类订单队列,我下面会放出 Golang 实现的版本代码。
总结
对比上面两种常见的订单模型,如果从用户体验的角度去优先考虑,第一种不需要用户等待持久化处理结果的是明显优于第二种的。如果技术团队完善,且技术过硬,也应该考虑第一种的实现方式。
如果仅仅想要达到宁愿用户等待到超时也不愿意存储层服务被冲垮,那么优先考虑第二种。
实现队列的选择
在这里,我们进一步细分一下,实现队列模块的功能有哪些选择。
相信很多后端开发经验比较老道的同志已经想到了,使用现有的中间件,比如知名的 Redis、RocketMQ,以及 Kafka 等,它们都是一种选择。
此外地,我们还可以直接编写代码,在当前的服务系统中实现一个消息队列来达到目的,下面我用图来分类下队列类型。
不同的队列实现方式,能直接导致不同的功能,也有不同的优缺点:
一级缓存优点:
一级缓存缺点:
中间件的优点:
中间件的缺点:
解答
回到第一种订单模型中:
问题1:
如果订单存在第三方支付情况,① 和 ② 的一致性如何保证?
首先我们看下,不一致性的时候,会产生什么结果:
上述的情况,明显地,只有 3 是需要恢复订单信息的,应对的方案有:
问题2:
如果订单存在第三方支付情况,① 完成了支付,且三方支付平台回调了 notifyUrl,而此时 ② 还在排队等待处理,这种情况又如何处理?
应对的方案参考 问题1 的 定时任务B 检测修改机制。
第二种队列的 Go 版本例子代码
定义一些常量
const (
QueueOrderKey = "order_queue"
QueueBufferSize = 1024 // 请求队列大小
QueueHandleTime = time.Second * 7 // 单个 mission 超时时间
)
定义出入队接口,方便多种实现
// 定义出入队接口,方便多种实现
type IQueue interface {
Push(key string,data []byte) error
Pop(key string) ([]byte,error)
}
定义请求与响应实体
定义队列实体
// 定义队列实体
type Queue struct {
mapLock sync.Mutex
RequestChan chan *QueueRequest // 缓存管道,装载请求
RequestMap map[string]*QueueTimeoutResp
Queue IQueue
}
实例化队列,接收接口参数
接收请求
从请求管道中取出 req 放入到队列容器中,该函数在 gorutine 中运行
取出 req 处理,该函数在 gorutine 中运行
启动
func (q *Queue) Start() {
go q.addToQueue()
go q.readFromQueue()
}
运行例子
留言与评论(共有 0 条评论) |