前言
在Redis秒杀功能设计与实现一篇中,我们使用redis实现了商品的秒杀抢购功能,在当前的设计中,只涉及到商品抢购,即用户锁定名额,并将成功抢购到的用户信息保存到redis中了
但是一个完整的抢购流程在业务流程中看到,包括锁定名额和下单,在超卖问题分析这篇中,我们是将抢购与订单放在一起进行的
仔细分析这样的做法,在高并发的抢购环境下,这样做是欠妥的,因为使用了分布式锁,尽管时间很短,但分布式锁的存在仍然会耗费不少性能
而且从系统的整体设计层面中,在微服务架构中,如果像双11那样瞬时订单量特别大的情况下,上面的做法带来的影响就更大了
因此在高并发环境下,另一个比较成熟且为业界广泛使用的就是利用消息中间件,将抢购成功的消息推送至消息队列,订单作为一个单独的微服务来消费这些消息,完成下单的操作
这样做的好处是显而易见的,消息队列可以堆积大量的订单消息,抢购的服务只需要将抢购成功的消息推送至消息队列即可,剩下的由订单微服务去处理
同时,实际环境中,如果发现消息队列的消息堆积过大,单个订单微服务消费能力达到瓶颈的时候,可以通过动态扩容的方式通过增加节点去消费,从而大幅度提升整体的吞吐量
在上面的业务基础上,加入了消息中间件之后,大致的处理流程可以总结如下
环境准备
新增订单表,并安装一个rabbitmq的服务
CREATE TABLE `t_order` (
`order_id` int(11) NOT NULL,
`order_no` varchar(64) DEFAULT NULL,
`order_status` int(11) DEFAULT NULL,
`userid` varchar(255) DEFAULT NULL,
`recv_name` varchar(64) DEFAULT NULL,
`recv_address` varchar(255) DEFAULT NULL,
`recv_mobile` varchar(32) DEFAULT NULL,
`postage` int(12) DEFAULT NULL,
`amount` int(10) DEFAULT NULL,
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
按照上面业务流程图的思路,我们改造原始的代码,结合消息中间件(rabbitmq)加入下单的逻辑
1、pom添加rabbitmq依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置文件yml中添加rabbitmq的连接配置
spring:
application:
name: seckill-demo
#rabbitmq相关配置
rabbitmq:
host: IP
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
#定义消费者做多同时处理10个消息
prefetch: 30
#消息手动确认
acknowledge-mode: manual
3、启动rabbitmq服务,并添加相关的订单exchange和queue
可以使用docker快速启动rabbitmq,启动完毕之后,手动创建一个本例环境需要的exchange和queue,并建立两者的绑定关系(也可以不通过可视化控制台创建,在程序中通过配置类和bean的方式创建,参考springboot整合rabbitmq)
4、添加订单消息发送逻辑
在这一步,通过前面的抢购,某个用户锁定了抢购的商品名额之后,就发送一条消息到queue.order这个队列中
@Resource
private RabbitTemplate rabbitTemplate;
public String sendOrderToQueue(String userid) {
System.out.println("准备向队列发送信息");
//订单基本信息
Map data = new HashMap();
data.put("userid", userid);
String orderNo = UUID.randomUUID().toString().replaceAll("-","").substring(0,7);
data.put("orderNo", orderNo);
//附加额外的订单信息
rabbitTemplate.convertAndSend("exchange-order" , null , data);
return orderNo;
}
然后再在抢购完毕之后调用发送消息的逻辑
@GetMapping("/processKill")
public Map processKill(Long psId , String userId) throws Exception{
Map result = new HashMap();
try {
promotionSecKillService.processSecKill(psId , userId , 1);
String orderNo = promotionSecKillService.sendOrderToQueue(userId);
Map data = new HashMap();
data.put("orderNo", orderNo);
result.put("code", "200");
result.put("message", "秒杀成功");
result.put("data", data);
} catch (SecKillException e) {
result.put("code", "500");
result.put("message", e.getMessage());
}
return result;
}
5、添加消息监听器,处理订单
在这一步,通过一个消息监听器接收队列中的消息,然后组装成订单并将订单入库,实际项目中,可以将订单单独做成一个微服务,只需要监听上面相同的队列即可
@Component
public class OrderConsumer {
private static AtomicLong atomicLong = new AtomicLong(1);
@Resource
private OrderMapper orderMapper;
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "queue-order") ,
exchange = @Exchange(value = "exchange-order" , type = "fanout")
)
)
@RabbitHandler
public void handleMessage(@Payload Map data , Channel channel ,
@Headers Map<String,Object> headers){
System.out.println("=======获取到订单数据:" + data + "===========);");
try {
//对接支付等其他业务
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Order order = new Order();
order.setOrderId(atomicLong.incrementAndGet());
order.setOrderNo(data.get("orderNo").toString());
order.setOrderStatus(0);
order.setUserid(data.get("userid").toString());
order.setRecvName("zcy");
order.setRecvMobile("1393310xxxx");
order.setRecvAddress("杭州市某小区");
order.setAmount(19);
order.setPostage(5);
order.setCreateTime(new Date());
orderMapper.insert(order);
Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(tag , false);//消息确认
System.out.println(data.get("orderNo") + "订单已创建");
} catch (IOException e) {
e.printStackTrace();
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.congge.mapper.OrderMapper">
<insert id="insert" parameterType="com.congge.entity.Order">
insert into t_order(order_id,order_no , order_status , userid , recv_name , recv_address , recv_mobile,postage , amount ,
create_time)
value (#{orderId},#{orderNo} , #{orderStatus} , #{userid} , #{recvName} , #{recvAddress} , #{recvMobile} , #{postage} ,
#{amount} , #{createTime})
</insert>
<select id="findByOrderNo" parameterType="java.lang.String" resultType="com.congge.entity.Order">
select * from t_order where order_no =#{value}
</select>
</mapper>
上面的代码逻辑全部改造工作基本上就完成了,要注意,本例是经过高度的抽象和简化之后的逻辑,切不可直接拿过去用到工作中,可以参考此思路,实际环境中,还需要考虑多种因素,在引入了消息中间件之后,一个很重要的问题就是需考虑消息发送异常之后的处理,涉及到rabbitmq的消息确认,消息回调,消息重试甚至消息补偿等场景,需综合考虑
下面将项目跑起来,按照之前的步骤我们来测试下,整体的业务逻辑是:
- 将带抢购的活动通过定时任务,加入到redis中
- 开始抢购后,确保一人只能抢一单
- 抢购成功后,向MQ发送一条消息,由监听器类向订单表插入一条订单
redis中添加了参与抢购的10条数据
直接通过接口调用来秒杀一个商品:
http://localhost:8088/processKill?psId=1&userId=0001
再次调用,说明我们的逻辑没问题
同时监听器接收到队列中发来的消息,并创建一条订单,插入到t_order表
使用jemeter压测,我们对剩下的9个名额进行抢购,
即100个用户抢购9个名额,启动jemeter之后,观察相关的数据变化
redis中,保存了抢购活动的10个用户信息
t_order表,共10条订单数据
后台的打印日志
从日志分析上,我们也可以发现,通过这种做法,真正实现了将抢购与订单创建的解耦,大家可以想象,实际在创建一条订单的时候,要处理的业务逻辑是非常复杂的,比如校验优惠券信息,对接支付,财务,风控,物流等等,如果一股脑的放在抢购逻辑中,用户就算抢到了也要等待一段时间才能看到抢购成功的结果,这样显然是不合适的
而引入了MQ之后,由于发送消息是异步的,抢购成功直接丢一条消息给MQ队列就可以立即对前台做响应了,订单后续的相关业务处理的快慢问题可以交给订单微服务去考虑和精细化即可
比如,实际中,往往可以根据消费端的消费能力,通过这个参数配置消费者每次批量消费多少条消息,从而决定单位时间内创建订单的能力,如果服务器配置足够给力,适当将这个参数调大即可
本篇通过案例,演示了在抢购业务中,通过加入rabbitmq实现订单流量的削峰,提供一个实际可操作的思路
本篇到此结束,最后感谢观看!