RocketMQ实战—3.基于RocketMQ升级订单系统架构
大纲
1.基于MQ实现订单系统核心流程的异步化改造
2.基于MQ实现订单系统和第三方系统的解耦
3.基于MQ实现将订单数据同步给大数据团队
4.秒杀系统的技术难点以及秒杀商详页的架构设计
5.基于MQ实现秒杀系统的异步化架构
6.全面引入MQ的订单系统架构的思维导图
1.基于MQ实现订单系统核心流程的异步化改造
(1)引入的RocketMQ的生产部署架构
(2)从下单核心流程开始改造订单系统
(3)通过引入MQ实现订单核心流程的异步化改造
(4)在订单系统中如何发送消息到RocketMQ
(5)其他系统改造为从RocketMQ中获取订单消息
(6)订单系统核心流程的改造总结
(1)引入的RocketMQ的生产部署架构
目前已经有了一套3台NameServer机器 + 6台Broker机器的生产集群,而且对集群的生产参数都进行了适当优化,足以抗下每秒十多万的消息请求。
RocketMQ的生产部署架构图如下:
下面开始基于MQ改造订单系统架构,在订单系统的各个环节引入MQ技术来解决订单系统目前面临的各种技术问题,全面优化订单系统的各项指标。
(2)从下单核心流程开始改造订单系统
目前订单系统面临的技术问题如下:
一.下单核心流程环节太多,性能较差
二.订单退款流程可能面临退款失败的风险
三.关闭过期订单时存在扫描大量订单数据的问题
四.跟第三方物流系统耦合,存在性能抖动的问题
五.大数据团队获取订单数据,存在不规范直接查询订单数据库的问题
六.进行秒杀时订单数据库压力过大
接下来先从第一个问题开始解决,因为下单流程性能较差是目前比较明显的问题,而且严重影响用户体验。订单退款失败是小概率出现的问题,即使出现也可以通过人工处理给解决。关闭过期订单时需要扫描大量订单数据,目前还不是很严重,因为订单数据量还没有那么大。跟第三方物流系统的耦合导致系统性能抖动,也是小概率出现的。大数据团队直接查订单数据库跑报表出来,目前压力有点大,但还不会对订单库造成过大影响。秒杀时订单数据库压力过大,也不是目前的主要问题,因为秒杀活动也不是经常有,而且即使压力过大,也可以将MySQL部署在更高配置物理机上,基本也能抗得住。
所以经过上述分析,从下单核心流程开始,引入RocketMQ进行改造,逐步解决:订单退款失败的问题、跟第三方物流系统耦合导致的性能抖动的问题、大数据团队直接查询订单库的问题、进行秒杀时订单库压力过大的问题、关闭过期订单时要扫描大量订单数据的问题。
(3)通过引入MQ实现订单核心流程的异步化改造
下面尝试在订单系统中引入MQ技术来实现订单核心流程中的部分环节的异步化改造。支付订单的核心流程如下所示:
订单系统每次支付完一个订单后,都会执行一系列动作,包括:更新订单状态、扣减库存、增加积分、发优惠券发红包、发短信推送、通知发货。
这一系列的动作会导致一次核心链路执行时间过长,可能长达好几秒种,从而导致用户等待时间较长,用户体验不好。
其实用户支付完毕后,只需要执行最核心的更新订单状态和扣减库存即可,以此来保证处理速度足够快。然后诸如增加积分、发送优惠券、发送短信、通知发货等操作,都可以通过MQ来进行异步化执行。
订单核心流程的改造图如下:
在上图中,订单系统仅仅会同步执行更新订单状态和扣减库存两个最关键的操作。因为一旦用户支付成功,只要保证订单状态变为"已支付",库存扣减掉,就可以保证核心数据不错乱。然后订单系统接着会发送一个订单支付的消息到RocketMQ中,积分系统会从RocketMQ里获取消息然后去累加积分,营销系统会从RocketMQ里获取消息然后发送优惠券,推送系统会从RocketMQ里获取消息然后推送短信,仓储系统会从RocketMQ里获取消息然后生产物流单核和发货单、通知仓库管理员打包商品、准备交接给物流公司发货。
在上面改造后的架构中,我们可以举个例子来计算一下引入MQ对订单核心流程的性能优化的效果。比如更新订单状态需要耗费30ms,调用库存服务的接口进行库存扣减需要耗费80ms,增加积分需要耗费50ms,派发优惠券需要耗费60ms,发送短信需要耗费100ms(涉及与第三方短信系统交互,性能抖动时可能1秒+),通知发货需要耗费500ms(涉及和第三方物流系统交互及与仓库管理系统交互,较耗时,性能抖动时可能1秒+)。
如果没有进行架构改造,每次支付成功后都需要由订单系统调用大量的其他系统的接口进行各种操作,可能一次订单核心链路的执行需要接近1秒钟。而且如果第三方短信系统和第三方物流系统出现性能抖动,那么执行一次核心流程可能就要几秒钟。
但经过上述改造后,一旦用户支付成功,实际上只需要总共120ms即可:更新订单状态(30ms) + 扣减库存(80ms) + 发送订单消息到RocketMQ(10ms)。
当用户支付成功后跳转回APP界面时,就可以直接展示订单支付成功的界面,不会出现加载中来提醒用户等待订单系统的处理。而积分系统、营销系统、推送系统、仓储系统都会单独从RocketMQ里获取订单支付成功的消息,来分别执行自己要处理的业务逻辑,不会再影响订单核心链路的性能。
(4)在订单系统中如何发送消息到RocketMQ
要实施这个技术方案就涉及到两个部分:一个是订单系统自身的改造,它需要去除调用积分系统、营销系统、推送系统以及仓储系统的逻辑,而改成发送一个订单支付成功的消息到RocketMQ里去。另外一个是积分系统、营销系统、推送系统以及仓储系统的改造,需要从RocketMQ里获取消息,然后根据订单支付成功的消息执行自己的业务逻辑。
一.首先展示原来的订单支付成功的接口
//收到订单支付成功的通知 public voud payOrderSuccess(Order order) { updateOrderStatus(order);//更新本地订单数据库里的订单状态 stockService.updateProductStock(order);//调用库存服务的接口,扣减库存 creditService.updateupdateCredit(order);//调用积分服务的接口,增加积分 marketingService.addVoucher(order);//调用营销服务的接口,增加优惠券 pushService.sendMessage(order);//调用推送服务的接口,发送短信 warehouseService.deliveryGoods(order);//调用仓储服务的接口,通知发货 }
需要对上述代码进行改造:去除掉一些代码逻辑,然后增加一个发送消息到RocketMQ的代码逻辑。
二.然后在项目里引入RocketMQ的依赖
如果要发送消息到RocketMQ,则首先需要在项目里引入下面的依赖:
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.3.0</version> </dependency>
三.接着需要封装如下一个RocketMQ生产者的类
类很简单,具体类的注释都写在下面了,根据类的注释就知道是怎么用的了。
public class RocketMQProducer { //这个是RocketMQ的生产者类,用这个就可以发送消息到RocketMQ private static DefatultMQProducer producer; static { //这里就是构建一个Producer实例对象 producer = new DefatultMQProducer("order_producer_group"); //这个是为producer设置NameServer的地址,让它可以拉取路由信息 //这样才知道每个Topic的数据分散在哪些Broker机器上 //然后才可以把消息发送到Broker上去 producer.setNamesrvAddr("localhost:9876"); //这里是启动一个producer producer.start(); } public static void send(String topic, String message) throws Exception { //这里进行构建一条消息对象 Message msg = new Message( topic,//这就是指定发送消息到哪个Topic上去 "",//这是消息的Tag message.getBytes(RemotingHelper.DEFAULT_CHARSET)//这是消息 ); //利用producer发送消息 SendResult sendResult = producer.send(msg); System.out.println("%s%n", sendResult); } }
通过上述代码就可以让订单系统把订单支付成功的消息发送到RocketMQ的一个Topic里去了。
(5)其他系统改造为从RocketMQ中获取订单消息
接着下一步就要推动积分系统、营销系统、推送系统、仓储系统从RocketMQ中去获取订单消息,然后根据获取到的消息执行对应的业务逻辑。下面是一段示例性的从RocketMQ中消费消息的代码:
public class RocketMQConsumer { public static void start() { new Thread() { public void run() { try { //这是RocketMQ消费者实例对象 //"credit_group"之类的就是消费者分组,一般来说比如积分系统就用"credit_consumer_group" //比如营销系统就用"marketing_consumer_group",以此类推,不同的系统给自己取不同的消费组名字 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("credit_group"); //这是给消费者设置NameServer的地址 //这样就可以拉取到路由信息,知道Topic的数据在哪些Broker上,然后从对应的Broker上拉取数据 consumer.setNamesrvAddr("localhost:9876"); //选择订阅"TopicOrderPaySuccess"的消息 //这样会从这个Topic的Broker机器上拉取订单消息过来 consumer.subscribe("TopicOrderPaySuccess", "*"); //注册消息监听器来处理拉取到的订单消息 //如果consumer拉取到了订单消息,就会回调这个方法给这里处理 consumer.registerMessageListener(new MessageListenerConcurrently() { public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { //在这里对获取到的msgs订单消息进行处理,比如增加积分、发送优惠券、通知发货等 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //启动消费者实例 consumer.start(); Ststem.out.println("Consumer Started.%n"); //别让线程退出,就让创建好的consumer不停消费数据 while(true) { Thread.sleep(1000); } } catch(Exception e) { e.printStackTrace(); } } }.start(); } }
通过上述代码:积分系统、营销系统、推送系统、仓储系统,就可以从RocketMQ里消费"TopicOrderPaySuccess"中的订单消息,然后根据订单消息执行增加积分、发送优惠券、发送短信、通知发货之类的业务逻辑了。
(6)订单系统核心流程的改造总结
当各个系统都落地该方案并且部署上线后,订单系统就会如下图所示。每次支付成功后仅仅更新自己的订单状态,同步扣减库存,接着就会发送消息到RocketMQ里去。然后推送系统、营销系统、积分系统、仓储系统就会从RocketMQ里获取订单支付成功的消息,执行对应的业务逻辑。
通过上述改造,可以将订单核心流程的性能从1秒~几秒的情况优化到100ms+,大幅度提升性能。
2.基于MQ实现订单系统和第三方系统的解耦
(1)接着要解决和第三方系统耦合的问题
(2)现在订单系统已经和第三方系统解耦
(3)什么是同步发送消息到RocketMQ
(4)什么是异步发送消息到RocketMQ
(5)什么是单向发送消息到RocketMQ
(6)这三种发送消息的方式到底用哪一种
(7)什么是Push消费模式
(8)什么是Pull消费模式
(1)接着要解决和第三方系统耦合的问题
完成订单系统核心流程的异步化改造后,核心流程的性能便提升了10倍以上:从原来需要1秒甚至几秒才能执行完成的多个步骤,变成现在只需要100ms就能执行完成的订单状态更新、扣减库存以及发送一条消息到RocketMQ的三个步骤。
接下来解决订单系统的"跟第三方物流系统耦合在一起,存在性能抖动"的问题。订单系统间接耦合的第三方系统有两个:一个是第三方短信系统,用来推送短信给用户。另一个是第三方物流系统,用来生成物流单通知物流公司来收货和配送。
如果按最早的订单系统核心流程:订单系统会同步调用推送系统,然后推送系统调用第三方短信系统去发送短信给用户,接着订单系统会同步调用仓储系统,然后仓储系统调用第三方物流系统去生成物流单以及通知发货。这样订单系统是间接和第三方短信系统和第三方物流系统耦合在一起的,一旦第三方系统出现性能抖动,那么就会影响到订单系统的性能。
(2)现在订单系统已经和第三方系统解耦
现在订单系统已经不需要直接调用推送系统和仓储系统的接口,只需要发送一条消息到RocketMQ。所以订单系统跟第三方系统耦合导致的性能抖动问题,其实已经解决了。
因为通过引入MQ,订单系统已成功和推送系统以及仓储系统解耦,现在订单系统跟仓储系统和推送系统已经没关系了。最多就是仓储系统自己跟第三方物流系统耦合,推送系统自己跟第三方短信系统耦合。此时即使第三方系统出现严重的性能抖动,甚至是接口故障无法访问,也不会影响到订单系统。
(3)什么是同步发送消息到RocketMQ
首先看同步发送消息到MQ的代码:
public class RocketMQProducer { //这个是RocketMQ的生产者类,用这个就可以发送消息到RocketMQ private static DefaultMQProducer producer; static { //这里就是构建一个Producer实例对象 producer = new DefaultMQProducer("order_producer_group"); //这个是为Producer设置NameServer的地址,让它可以拉取路由信息 //这样才知道每个Topic的数据分散在哪些Broker机器上,然后才可以把消息发送到Broker上去 producer.setNamesrvAddr("localhost:9876"); //这里是启动一个Producer producer.start(); } public static void send(String topic, String message) throws Exception { //这里进行构建一条消息对象 Message msg = new Message( topic,//这就是指定发送消息到哪个Topic上去 "",//这是消息的Tag message.getBytes(RemotingHelper.DEFAULT_CHARSET)//这是消息 ); //利用producer发送消息 SendResult sendResult = producer.send(msg); System.out.println("%s%n", sendResult); } }
同步的意思就是:通过代码producer.send(msg)发送消息到MQ去,然后程序会卡在这里不能往下执行,需要一直等待MQ返回结果,拿到SendResult后,程序才会继续往下执行。这也就是RocketMQ的同步发送模式。
(4)什么是异步发送消息到RocketMQ
接下来看异步发送消息到MQ的代码,首先要在构造Producer时加入设置异步发送失败时的重试次数:
//这里就是构建一个Producer实例对象 producer = new DefaultMQProducer("order_producer_group"); //这个是为Producer设置NameServer的地址,让它可以拉取路由信息 //这样才知道每个Topic的数据分散在哪些Broker机器上,然后才可以把消息发送到Broker上去 producer.setNamesrvAddr("localhost:9876"); //这里是启动一个Producer producer.start(); //设置异步发送失败时重试次数为0 producer.setRetryTimesWhenSendAsyncFailed(0);
接着把发送消息的代码改成如下所示:
producer.send(message, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { } @Override public void onException(Throwable e) { } });
异步的意思就是:通过代码producer.send(msg)发送消息时,不会卡在这里等待MQ返回结果,而会继续执行后面的代码。当MQ返回结果时,会回调SendCallback()方法。这也就是RocketMQ的异步发送模式。
所以上述代码的意思就是:把消息发送出去后,代码直接往下执行,不会卡在那里等待MQ返回结果。然后当MQ返回结果时,Producer会回调SendCallback里的方法。如果发送成功就回调onSuccess()方法,如果发送失败就回调onExceptino()方法。
(5)什么是单向发送消息到RocketMQ
还有一种发送消息的方法,叫做发送单向消息,就是用下面的代码来发送消息:
producer.sendOneway(msg);
这个sendOneway的意思就是:发送一个消息给MQ,然后代码就直接往下执行,根本不会关注MQ有没有返回结果回来,也不需要MQ返回的结果,无论发送的消息是成功还是失败,都不处理。这就是RocketMQ的单向发送模式。
(6)这三种发送消息的方式到底用哪一种
上面介绍了三种消息发送的模式,那么到底应该要用哪一种,这需要结合消息不丢失、消息顺序性等案例场景来分析。根据场景来决定到底是适合同步发送、异步发送、还是单向发送。
(7)什么是Push消费模式
如下是RocketMQ的Push消费模式的代码片段:
//这是RocketMQ消费者实例对象 //"credit_group"之类的就是消费者分组,一般来说比如积分系统就用"credit_consumer_group" //比如营销系统就用"marketing_consumer_group",以此类推,不同的系统给自己取不同的消费组名字 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("credit_group"); //这是给消费者设置NameServer的地址 //这样就可以拉取到路由信息,知道Topic的数据在哪些Broker上,然后从对应的Broker上拉取数据 consumer.setNamesrvAddr("localhost:9876"); //选择订阅"TopicOrderPaySuccess"的消息 //这样会从这个Topic的Broker机器上拉取订单消息过来 consumer.subscribe("TopicOrderPaySuccess", "*"); //注册消息监听器来处理拉取到的订单消息 //如果consumer拉取到了订单消息,就会回调这个方法给这里处理 consumer.registerMessageListener(new MessageListenerConcurrently() { public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { //在这里对获取到的msgs订单消息进行处理,比如增加积分、发送优惠券、通知发货等 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //启动消费者实例 consumer.start(); Ststem.out.println("Consumer Started.%n");
Consumer的类名是DefaultMQPushConsumer,从类名就可以看出使用了Push消费模式。Push消费模式其实就是:Broker会主动把消息发送给消费者,消费者是被动接收Broker推送给过来的消息,然后进行处理。
(8)什么是Pull消费模式
如下是RocketMQ的Pull消费模式的代码片段:
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("test_consumer_group"); consumer.start(); Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1"); for (MessageQueue mq : mqs) { System.out.printf("Consume from the queue: %s%n", mq); SINGLE_MQ: while (true) { try { PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32); System.out.printf("%s%n", pullResult); putMessageQueueOffset(mq, pullResult.getNextBeginOffset()); switch (pullResult.getPullStatus()) { case FOUND: break; case NO_MATCHED_MSG: break; case NO_NEW_MSG: break SINGLE_MQ; case OFFSET_ILLEGAL: break; default: break; } } catch (Exception e) { e.printStackTrace(); } } }
Consumer的类名是DefaultMQPullConsumer,从类名就可以看出使用了Pull消费模式。Pull消费模式其实就是:Broker不会主动推送消息给Consumer,而是消费者主动发送请求到Broker去拉取消息,然后才进行处理。
(9)总结
到目前为止,可以发现通过引入MQ到订单核心流程中,已经解决了两个问题:
一.核心流程环节过多导致性能较差的问题
二.耦合第三方系统导致性能易抖动的问题
另外还介绍了使用RocketMQ的几种方式:同步发送消息、异步发送消息、单向发送消息、Push消费消息以及Pull消费消息。
3.基于MQ实现将订单数据同步给大数据团队
(1)大数据团队的几百行SQL影响订单数据库
(2)如何避免大数据团队直接查询订单数据库
(3)大数据团队不应使用TopicOrderPaySuccess里的订单支付成功消息
(4)如何将完整的订单数据发送到RocketMQ里
(1)大数据团队的几百行SQL影响订单数据库
大数据团队的系统每天都会直接在订单数据库里执行上百次几百行的大SQL,每次执行几百行的大SQL都需要耗时几秒到十几秒不等,从而导致MySQL数据库服务器的CPU、内存、磁盘IO负载快速升高。
一旦MySQL数据库服务器的资源负载快速升高,又会导致订单系统在MySQL上执行的普通SQL语句的性能出现下降,最终导致订单系统的性能出现抖动,这就是大数据团队目前对订单系统的影响。
(2)如何避免大数据团队直接查询订单数据库
要解决这个问题,就必须要避免大数据团队直接查询订单数据库,那么如何避免大数据团队直接查询订单数据库呢?
可以由订单系统将订单数据推送到MQ,然后大数据团队从MQ里获取订单数据,接着将订单数据落地到大数据团队自己的存储中。比如大数据团队将订单数据落地到自己的MySQL数据库中,然后从自己的MySQL数据库里导出报表。
(3)大数据团队不应使用TopicOrderPaySuccess里的订单支付成功消息
订单系统在支付成功时,会将订单支付成功的消息发送到RocketMQ里,然后其他系统会订阅这个订单支付成功的消息进行对应的业务处理。不过这个订单支付成功的消息,还不足以让大数据团队使用。
因为大数据团队需要的是跟订单数据库一模一样的一份完整的数据,而不仅仅是订单支付成功的消息,所以大数据团队不能直接使用前面创建的TopicOrderPaySuccess这个Topic里的消息。因此需要想办法将完整的订单数据都发送到RocketMQ里,然后让大数据团队去获取。
(4)如何将完整的订单数据发送到RocketMQ里
方案一:
一个比较简单的办法,就是在订单系统中对订单执行的增删改操作都发送到RocketMQ里去。然后大数据团队的数据同步系统从RocketMQ里获取订单的增删改操作,然后在自己数据库里执行这些操作。通过还原执行一样的Insert、Update和Delete语句,就可以在自己的数据库里还原出一样的订单数据。
但这种方案的一个问题就是订单系统为了将数据同步给大数据团队,必须在自己的代码里增加大量的代码去发送增删改操作到RocketMQ,这会导致订单系统的代码出现严重污染,因为这些发送增删改操作到RocketMQ里的代码是跟订单业务没关系的。
方案二:
利用MySQL Binlog同步系统,这个系统会监听MySQL数据库的Binlog,所谓Binlog其实就是MySQL的增删改操作日志。然后MySQL Binlog同步系统会将监听到的MySQL Binlog(也就是增删改操作日志)发送给大数据团队的系统,让大数据团队来处理这些增删改操作日志。
这种MySQL Binlog系统现在是有不少成熟的开源技术方案的,比如阿里开源的Canal,以及Linkedin开源的Databus,都可以监听MySQL Binlog。
所以方案二具体就是:通过Canal监听MySQL Binlog,然后直接发送到RocketMQ里。接着大数据团队的数据同步系统从RocketMQ中获取到MySQL Binlog,也就获取到了订单数据库的增删改操作。最后把增删改操作还原到自己的数据库中。
而且这样的方案还有一个好处,就是由订单技术团队将完整的订单数据库的MySQL Binlog推送到RocketMQ里。无论是大数据团队,还是未来公司的其他技术团队,比如说开放平台团队,人工智能团队等,只要想要订单数据,都可以直接从这个RocketMQ里去获取完整的订单数据。
(5)总结
在如何将数据同步给大数据团队这个问题上:先是考虑在订单系统内嵌入一些额外代码,将订单的增删改操作发送到RocketMQ,但发现这样会污染订单系统代码。后来提出完美的方案,即用Canal、Databus这样的MySQL Binlog同步系统,监听订单数据库的Binlog然后发送到RocketMQ里。之后大数据团队的数据同步系统就能从RocketMQ里获取订单数据的增删改Binlog日志,还原到自己的数据存储中去。其中的数据存储可以是自己的数据库,或者是Hadoop之类的大数据生态技术。
当大数据团队将完整的订单数据还原到自己的数据存储中后,就可以根据自己的技术能力去出数据报表了,不会再影响订单系统的数据库了。
4.秒杀系统的技术难点以及秒杀商详页的架构设计
(1)接着要解决秒杀活动压力过大的问题
(2)秒杀活动压力过大难道直接加机器吗
(3)不归订单系统管的高并发商品详情页请求
(4)页面数据静态化优化商品详情页系统的秒杀架构
(5)多级缓存化优化商品详情页系统的秒杀架构
(1)接着要解决秒杀活动压力过大的问题
目前已经解决了核心流程环节太多性能差、耦合第三方系统易抖动、大数据团队直接查订单数据库这3个问题了,还剩下订单退款失败、扫描大量订单、秒杀活动压力过大这3个问题。
假设运营花了很多钱做活动拉新用户,APP的日活用户一直在增长。现在每天在高峰时间段开启的秒杀活动,已经比以前有更多的用户在参与了。当特价商品的秒杀时间一到,就有大量的并发请求过来,给订单系统带来非常大的压力。
如果仅仅是订单系统自己本身压力过大,还不是太大的问题。因为订单系统目前部署了20台4核8G的机器,整个集群抗每秒上万请求压力是可以的。即使用户量越来越大,也可以给订单系统加更多机器。
但是这里有一个问题,20台订单系统的机器都是访问同一台机器上部署的MySQL数据库。那一台数据库服务器在秒杀活动开启时,瞬时并发量已经达到上万。已经明显发现数据库的负载越来越高,CPU、IO、内存、磁盘的负载几乎快要达到极限了。
因此各个技术团队都需要为秒杀活动进行系统优化,务必让各个系统特别是订单系统用合理的架构、有限的资源去抗下未来越来越多用户参与的秒杀活动。
(2)秒杀活动压力过大难道直接加机器吗
第一个问题,秒杀活动目前压力过大,应该如何解决?是不是简单的堆机器或者加机器就可以解决的?比如给订单系统部署更多的机器,是不是可以抗下更高的并发?这个是没问题的,订单系统可以通过部署更多的机器进行线性扩展。
第二个问题,那么数据库呢?是不是也要部署更多的服务器,进行分库分表。然后让更多的数据库服务器来抗超高的数据库高并发访问?所谓分库分表,就是把目前的一台数据库服务器变成多台数据库服务器,然后把一张订单表变成多张订单表。比如目前订单表里有1200万条数据存放在一台数据库服务器里,现在将1台数据库服务器变成3台数据库服务器,那么就可以在每台数据库服务器里放400万条订单数据,这就是所谓的分库分表。
这种做法的好处是什么呢?比如未来订单系统的整体访问压力达到了每秒3万请求了,此时订单系统通过扩容可以部署很多机器。然后其中1万请求写入到一台数据库服务器,1万请求写入到另一台数据库服务器,剩下1万请求写入最后一台数据库服务器。这样就可以通过增加更多的数据库服务器来抗下更高的并发请求了。
但是事实上这个方案不太靠谱,除非是技术能力比较弱的公司,没有厉害的架构师去利用已有的技术合理设计优秀的架构,才会用这种堆机器的方法简单的来抗下超高的并发。因为如果用堆机器的方法来解决这个问题,必然随着用户量越来越大,并发请求越来越多,会导致更多的机器。如果现在每秒的并发请求量是1万,就需要20台4核8G的订单服务器 + 1台高配置的数据库服务器,才能扛下来。那么未来用户量增长10倍,每秒有10万并发请求,难道让订单系统部署200台机器,然后将数据库服务器增加到10台?这样会导致服务器成本急剧飙升,所以解决问题往往不能用这种简单粗暴堆机器的方案。
为了应对秒杀活动这种特殊场景,不能采取无限制扩容服务器的方案,而是要仔细分析秒杀活动的核心请求链路,利用各种技术去合理设计更加优秀的架构,在有限的机器资源条件下,优雅抗下更高的并发。
(3)不归订单系统管的高并发商品详情页请求
其实秒杀活动中面临并发压力的主要就两块:一个是高并发的读,一个是高并发的写。
首先可以思考一下,平时大量的用户是怎么参与到秒杀活动里来的。往往是这样,很多用户都知道APP每天晚上如8:30会有秒杀商品开始售卖。因此每次到了晚上8:30之前,就有很多用户会登录APP,然后在APP前等秒杀特价商品。所以这时,必然会出现一种场景,就是大量用户会拿着APP不停地刷新一个秒杀商品的页面。
那么这些秒杀商品页面是从哪儿加载出来的呢?通常是从商品技术团队负责的商品详情页系统中加载出来的,商品详情页系统会负责提供用户看到的各种秒杀商品页面。
所以这个商品详情页系统就是在秒杀活动开始之前最先被大量用户高并发访问的一个系统。如果没有秒杀活动时,其实大量的用户会分散在不同时间段里来逛APP,而且逛的是不同的商品页面。但在秒杀活动开始时,面临的第一个问题就是:可能有几十万、甚至百万用户,会同时频繁访问秒杀商品的详情页。
(4)页面数据静态化优化商品详情页系统的秒杀架构
为了解决秒杀商品详情页被同一个时间点的大量用户频繁访问,造成商品系统压力过大的问题,一般会采取页面数据静态化 + 多级缓存的方案。
首先,需要将秒杀商品详情页的数据实现静态化。
如果秒杀商品详情页的数据是动态化的,那么用户每次访问秒杀商品详情页,就必须发送一次请求到商品系统来获取详情页数据,比如商品的标题、价格、优惠、库存、图片、详情说明、售后政策等。当大量用户频繁并发访问秒杀商品的详情页时,就会有大量请求到商品详情页系统获取详情页数据,从而导致商品数据库承受高并发的访问。
如果秒杀商品详情页的数据是静态化的,也就是提前从商品数据库里把商品详情页的数据都提取出来组装成一份静态数据放在别的地方,那么就可以避免每次访问秒杀商品详情页都要访问后端数据库。
(5)多级缓存化优化商品详情页系统的秒杀架构
然后,使用CDN + Nginx + Redis的多级缓存架构。
秒杀商品详情页的数据,首先会放一份在离用户地理位置比较近的CDN上。
CDN大致可以这么理解:比如公司的机房在上海,系统也部署在上海,那么对于陕西的用户,难道每次都要发送请求到上海机房里来获取数据吗?不是的,完全可以将一些静态化好的数据放在陕西的一个CDN上。同样对于广州的用户,可以把这些静态化好的数据放在广州的CDN上。这个CDN现在都是各种云厂商提供的服务。
然后不同地方的用户在加载这个秒杀商品的详情页数据时,都从最近的CDN上加载,不需要每次都将请求发送到上海的机房。这个CDN缓存就是多级缓存架构里的第一级缓存。
如果因为缓存过期等问题,CDN上没有用户要加载的商品详情页数据。此时用户的请求就会发送到公司机房里的机器,来加载这个商品的数据。这时就需要在Nginx这样的服务器里做一层缓存。
在Nginx中是可以基于Lua脚本实现本地缓存的,可以提前把秒杀商品详情页的数据放到Nginx中进行缓存。如果请求发送过来,就可以从Nginx中直接加载缓存数据,不需要把请求转发到商品系统上。
如果在Nginx服务器上也没加载到秒杀商品的数据呢?比如同样因为Nginx上的缓存数据过期等问题,导致没找到用户需要的数据。此时就可以由Nginx中的Lua脚本发送请求到Redis集群中去加载提前放进去的秒杀商品数据。
如果在Redis中还是没有找到呢秒杀商品的数据呢?那么就由Nginx中的Lua脚本直接把请求转发到商品详情页系统里进行加载,也就是从数据库中获取商品详情页数据,如下图所示。但一般来说数据都可以从CDN、Nginx、Redis中加载到,可能只有极少数请求会直接访问到商品详情页系统,然后从数据库里加载商品详情页的数据。
通过这样的一套方案,就可以把秒杀商品详情页的数据进行静态化,然后把静态化以后的一串商品数据(比如可能就是一个大的JSON串)放到CDN、Ngxin、Redis组成的多级缓存里,这样大量的用户频繁并发访问秒杀商品的详情页时,就不会对商品系统产生太大压力了。
因为分布在全国各地的用户的大量请求都会分散发送给各个地方的CDN,所以CDN就分摊掉了大量的请求。即使请求到达了商品后端,也会先由单机抗10万+并发的Nginx和Redis来返回商品详情页数据。
(7)总结
这里分析了秒杀场景下堆机器方案的弊端,同时从秒杀活动发生的场景入手,分析了秒杀活动发生的某个时间点前后,大量用户会集中访问秒杀商品页面。因此为了优化这个问题,介绍了商品技术团队需要做的页面数据静态化 + 多级缓存的架构。
但实际上的秒杀系统非常复杂,里面涉及很多细节。这里主要是借秒杀场景去介绍RocketMQ限流削峰的功效。所以并没有介绍太多秒杀系统的细节,主要是从整体角度简单介绍秒杀系统的架构设计和思路。
5.基于MQ实现秒杀订单系统的异步化架构
(1)秒杀场景下的抢购流程分析
(2)用答题的方法避免作弊抢购以及延缓下单
(3)为秒杀独立出一套订单系统
(4)基于Redis实现下单时精准扣减库存
(5)抢购完毕后提前过滤无效请求
(6)瞬时高并发下单请求进入RocketMQ进行削峰
(7)秒杀架构的核心要点总结
(1)秒杀场景下的抢购流程分析
订单技术团队为了应对秒杀的问题,需要进行哪些架构的优化。
首先从秒杀活动的场景入手来分析:假设每天晚上8:30都有一个秒杀活动,都会主推一个特别好的商品进行3折限量秒杀抢购。比如一个价值6888的手机就3折出售,而且限量每天100个。
那么在8:30的时间点前,大量的用户(可能多达几万)会集中登录到APP上。然后同时访问这个秒杀活动的商品页面,这个频繁访问商品页面的问题已经被商品技术团队解决掉了。
接着一到8:30,秒杀商品详情页面就会让一个立即抢购的按钮变得可以点击,在之前这个按钮是灰色的,不能点击。然后瞬间就会有几万用户同时点击这个按钮,尝试对订单系统发起请求去抢购商品。
在这个过程中,大量用户的抢购请求要做的事情,就是下订单、支付、扣减库存以及后续一系列事情。如果按照之前的策略,让所有请求都访问到订单系统以及订单数据库,那么不可避免导致订单系统和数据库压力过大。如果为了每天一个秒杀活动就加10倍、20倍的机器,那么公司的成本就太高了。
(2)用答题的方法避免作弊抢购以及延缓下单
首先考虑一个问题,有没有可能有人写一个抢购的脚本或者作弊软件,疯狂的发送请求去抢商品。答案是肯定的,肯定是有人会写作弊的脚本或者软件。
所以,可以在用户参与抢购前,通过答题的方式,来让用户获得发起抢购的资格。
这个办法是非常有效的,因为可以避免一些作弊软件去发送抢购请求。另外不同用户的答题速度是不一样的,可以通过答题让不同用户的请求时间错开,不会都在一个时间点发起请求。
(3)为秒杀独立出一套订单系统
接着用户下单抢购的请求发送出去后,会到达订单系统。对于订单系统而言,需要考虑是否直接使用目前已有的订单系统去抗所有的秒杀商品抢购请求。答案是否定的,这么做会有问题。
假设有10万用户在这个时间段很活跃都在购买商品。但可能只有其中1万用户在参与秒杀活动,在同一时间发送了大量的抢购请求到订单系统。而剩余的9万其他用户这时并不参与秒杀活动,他们在进行其他商品的常规性浏览和下单。
因此,如果让订单系统同时处理秒杀下单请求和普通下单请求,那么可能会出现秒杀下单请求耗尽订单系统的资源、或者系统不稳定的情况,然后导致其他普通下单请求出现问题,没办法完成下单。
所以一般会对订单系统部署两个集群,一个集群是秒杀订单系统集群,一个集群是普通订单系统集群。当两套系统独立部署之后,就可以为秒杀场景下的订单系统做很多特殊的优化。
(4)基于Redis实现下单时精准扣减库存
然后订单系统中接着要做的一个事情,就是扣减库存。因为秒杀商品的数量是有限制的,所以当大量请求到达订单系统后,第一步就是先去扣减库存。
扣减库存时,如果直接由订单系统调用库存系统的接口,通过访问库存数据库进行扣减,那么必然会导致库存数据库的压力快速增大。
因此在秒杀场景下,通常会将秒杀商品的库存提前写入Redis。然后当抢购请求到来后,直接对Redis中的库存进行扣减。Redis可以轻松用单机抗下每秒几万请求,因此可以抗下这里高并发的库存扣减。
(5)抢购完毕后提前过滤无效请求
当Redis中的库存被扣减完后,说明后续其他请求都没必要发送到秒杀系统中了,因为商品已经被抢购完毕了。此时可以让Nginx收到抢购请求时,直接把请求过滤掉。
比如一旦商品抢购完毕,可以往ZooKeeper中写入一个秒杀完毕的标志位,然后ZooKeeper会反向通知Nginx中我们自己写的Lua脚本。后续有抢购请求过来时,就可以通过Lua脚本直接过滤掉不要向后转发。这样就可以大幅减少对秒杀系统的请求压力。
(6)瞬时高并发下单请求进入RocketMQ进行削峰
接着,如果1万件商品同时被1万个用户秒杀成功了,那么可能瞬间会有1万个请求进入到订单系统,此时的订单数据库就会有上万的订单创建请求。所以可以引入RocketMQ进行削峰处理。
也就是当订单系统通过Redis尝试库存扣减 -> 发现库存还大于0 -> 表明秒杀成功需要创建订单的时候,直接发送一条秒杀成功的消息到RocketMQ,之后再让订单系统从RocketMQ中消费秒杀成功的消息进行常规的订单流程处理,这样瞬间上万的订单创建压力就被RocketMQ抗下来了。
当订单系统根据自己的工作负载慢慢地从RocketMQ中拉取秒杀成功的消息,然后进行后续操作时,就不会对订单数据库造成过大的压力。
(7)秒杀架构的核心要点总结
一.在客户端设置秒杀答题,阻止作弊器刷单
二.独立出来一套秒杀系统,专门负责处理秒杀请求
三.基于Redis进行库存扣减,库存扣完则秒杀结束
四.秒杀结束后,可以在Nginx层过滤掉无效的请求
五.秒杀时产生的大量瞬时创建订单请求直接进入RocketMQ进行削峰,后续订单系统再慢慢拉取消息完成订单创建操作
对于瞬时超高并发的商品抢购场景:
首先要避免直接基于数据库进行高并发的库存扣减,否则会对库存数据库造成过大压力。因为数据库单机可能每秒只能抗几千请求,但是改成基于Redis进行高并发扣减库存,每秒可以轻松抗几万请求。
一旦库存扣减为0之后,秒杀结束。只有前面少量的请求可以进入后台系统,后续客户端过来的99%请求,都可以在Nginx层面被拦截掉,没必要转发到订单系统,避免造成额外压力。
接着瞬时生成的大量秒杀成功后的订单创建请求,不会直接交给订单系统去处理,否则可能会对订单数据库造成过大压力。可以发送消息到RocketMQ进行削峰,先让RocketMQ抗下高并发压力,再让订单系统消费RocketMQ的消息来进行订单创建。
所以通过上述分析,像秒杀这种瞬时超高并发的场景:架构优化的核心就是独立出来一套系统专门处理,避免高并发请求落在MySQL上。因为MySQL不擅长抗高并发,需要通过Redis、Nginx、RocketMQ这些可以单机抗几万甚至十万并发的系统来进行优化。
6.全面引入MQ的订单系统架构的思维导图
这里介绍了如何使用MQ技术来解决:链路过长导致的性能较差的问题、耦合第三方系统导致的性能不稳定的问题、耦合其他团队导致数据库被不规范访问的问题,以及瞬时高并发下的过高请求压力问题。
详细介绍后端技术栈的基础内容,包括但不限于:MySQL原理和优化、Redis原理和应用、JVM和G1原理和优化、RocketMQ原理应用及源码、Kafka原理应用及源码、ElasticSearch原理应用及源码、JUC源码、Netty源码、zk源码、Dubbo源码、Spring源码、Spring Boot源码、SCA源码、分布式锁源码、分布式事务、分库分表和TiDB、大型商品系统、大型订单系统等