RabbitMq简介、订单服务、分布式事务
1.RabbitMq简介
1.应用场景
①串行处理注册成功后发送邮件和发送短信太慢,可以通过消息队列,把注册成功信息放进去,然后发送邮件和发送短信都订阅了消息队列只要一有信息就能够进行处理。而且用户不需要等待
②解耦:主要就是订单系统生成订单后去调用库存系统,如果库存系统api更新,那么修改幅度大,可以通过消息队列来保存订单消息,库存根据消息来进行api的调用
③流量控制:其实就是秒杀活动的时候,用户请求放入队列,并且根据秒杀业务的速度来控制请求出来的速度。
2.两种类型的mq
- JMS:支持java平台,不跨语言,本质就是javaAPI,相当于就是一个规范。提供大量的message结构
- AMQP:跨语言跨平台,是一个网络线级协议。只提供byte数组传输。
3.rabbitmq的原理
- 生产者:主要就是生产消息,与消息队列建立连接通过多个信道来传输消息
- 消息:头+体还有就是路由键
- 交换机:就是通过消息的路由键来决定发送给哪一个队列。
- 虚拟主机:主要就是用于间隔两种语言平台设置的,他们各自有各自的消息代理
- 消费者:与消息代理建立连接,监听消息队列,等待消息并进行处理。
4.安装rabbitmq
4369、25672:Erlang发现和集群端口
5672、5671:AMQP高级消息队列协议端口
15672:web后台端口
61613/61614:STOMP协议端口
安装之后直接访问http端口 15672。
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
docker update rabbitmq --restart=always
5.交换机的类型
- direct:点对点,也就是只能发给一个队列
- tanout:广播所有的队列
- topic:根据队列路由键接收规则和消息路由键来发送消息,可以一发多个队列。
接着就是创建一个点对点的交换机和经典的队列。测试点对点的消息推送和消息接收。然后就是测试fanout交换机,无论是什么路由键的message最后都会广播到所有的消息队列上
绑定思路
①创建队列和交换机
②交换机绑定队,并且设定好跳转到这个队列的路由键
2.整合Springboot
1.RabbitmqAutoConfiguration
组件
- RabbitTeamplate
- AmqpAdmin:需要一个连接对象
- CachingConnectionFactory:连接创建工厂
- RabbitMessagingTemplate:操作消息的类
首先就是创建AmqpAdmin,并且这个时候需要配置一下连接工厂的配置信息,主要就是host、port和virtual-host虚拟主机
2.创建exchange、binding、queue
参数
- exchange:name、durable、autodelete
- queue:name、durable、excusive(排他,其它连接是否能够传输信息进来)、autodelete
@Test
void exchange() {
DirectExchange directExchange = new DirectExchange("hello.java.exchange",true,false);
amqpAdmin.declareExchange(directExchange);
}
@Test
void queue() {
Queue queue = new Queue("hello.java.queue",true,false,true);
amqpAdmin.declareQueue(queue);
}
@Test
void binding() {
Binding binding = new Binding("hello.java.queue", Binding.DestinationType.QUEUE,"hello.java.exchange","hello",new HashMap<>());
amqpAdmin.declareBinding(binding);
}
3.发送消息
思路
①第一个就是要知道在传送消息的时候是需要依靠模板rabbitTemplate,它里面有一个messageConverter,这个转换器主要就是能够把消息变成对应的形式。通常用的是simple,这个转换器就是序列化转换成byte数组来传输,但是我们可以增加自己配置一个Json的Config转换器。发送消息用的是json状态
②接着就是通过模板来发送
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void sendMessage(){
MemberResVo memberResVo = new MemberResVo();
memberResVo.setCity("sss");
memberResVo.setId(123L);
rabbitTemplate.convertAndSend("hello.java.exchange","hello",memberResVo);
}
4.接收消息
思路
①开启注解EnableRabbit才能够使用@RabbitListener来监听,并且指定监听队列可以是多个。
②然后就发送消息进行测试
③rabbitHandler主要只能对方法起作用,listener类和方法都可以,但是handler有利于方法的重载。
@RabbitHandler
public void acceptMes(MemberResVo resVo){
System.out.println(resVo);
}
@RabbitHandler
public void acceptMes(OrderEntity orderEntity){
System.out.println(orderEntity);
}
5.消息可靠投递
思路
①第一种方法就是事务消息串行发送,等接收到再发送下一条信息,这种消息消耗大量的性能所以不用
②第二种就是通过回调函数机制。
服务收到消息:发送方->brocker代理这里会调用confirmCallback,
消息正确抵达队列:exchange->queue的时候调用的是returnCallback,这两个需要设置在rabbitTemplate里面,所以我们可以把rabbitTem放到config中,然后自己配置
③在配置文件开启这两个调用方法
④消费端的ack机制:其实就是通过设置yml来手动确认消息已经收到,如果没有手动确认那么就一直会留到队列中。它是默认自动签收的。
rabbitmq:
host: 192.168.56.10
port: 5672
virtual-host: /
publisher-returns: true
publisher-confirm-type: correlated
listener:
simple:
acknowledge-mode: manual
@RabbitHandler
public void acceptMes(MemberResVo resVo, Message message, Channel channel){
System.out.println(resVo);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("接收到货物->"+deliveryTag);
try {
if(deliveryTag%2==0){
channel.basicAck(deliveryTag,false);
System.out.println("货物"+deliveryTag+"通过");
}else{
channel.basicNack(deliveryTag,false,false);
System.out.println("货物"+deliveryTag+"拒绝通过");
}
} catch (IOException e) {
e.printStackTrace();
}
}
@PostConstruct
public void setCallback(){
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 唯一标识
* @param b 是否抵达
* @param s 错误信息
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("data["+correlationData+"]==>b["+b+"]==>s["+s+"]");
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 回调失败的信息
* @param replyCode 回复的状态码
* @param replyText 回复的文本内容
* @param exchange 当时消息发给那个交互机
* @param routeKey 通过什么路由键进行的处理
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routeKey) {
System.out.println("message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]==>exchange["+exchange+"]==>routeKey["+routeKey+"]");
}
});
}
3.订单服务
1.配置环境
2.整合SpringSession
思路
①导入redis和session依赖
②配置host和port
③引入线程config和线程配置
④开启session注解
⑤引入session的config类
拓展与坑
这里如果没有引进session的config类问题就是取session的时候这个id是不一样的,导致没有取出来。
3.订单简介
处理信息流、资金流、物流,订单需要处理的模块非常多。包括订单,商品、支付、促销、物流、用户。
4.订单登录拦截
思路
①写一个toTrade跳到订单确认页面
②登录拦截,主要就是如果没有登录那么就跳转到登录页面并且显示没有登录,如果登录那么就直接可以跳转到订单确认页面
拓展与坑
①记得要把拦截器加入到webmvcconfigurer里面,也就是mvc的IOC容器。才会起作用。
@Controller
public class OrderWebController {
@GetMapping("toTrade")
public String toTrade(){
return "confirm";
}
}
拦截器
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResVo> loginUser=new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberResVo attribute = (MemberResVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute!=null){
loginUser.set(attribute);
return true;
}else{
request.getSession().setAttribute("msg","请先登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
5.订单确认页模型抽取
思路
①需要地址信息、商品信息(就是购物车项的信息)、优惠券、总金额、应付金额
@Data
public class OrderConfirmVo {
//1.地址列表信息
private List<MemberAddressVo> address;
//2.商品信息(购物车里面的信息)
private List<OrderItemVo> items;
//3.优惠券
private Integer integration;
//4.订单总额
private BigDecimal totalPrice;
//5.应付价格
private BigDecimal payPrice;
}
6.订单确认数据获取
思路
①第一个就是远程调用member获取地址
②然后就是远程调用获取cartItem信息,这里只能选择check是true的购物车项,cartitem这里也需要远程调用skuInfo来更新price
③最后就是设置优惠券,并且根据所有的item来计算总价和应付价格
拓展与坑
①最后就是debug调试,出现的问题就是发现没有任何购物车项返回。如果没有购物车项,也没有出错的话,那么问题就可能是没有这个loginUser,远程调用购物车项的逻辑是先看看有没有这个user,如果没有那么就是返回空的,最好就是在这个地方抛出一个异常。
②那么问题出现在什么地方呢?我们调试走进这个远程调用方法,实际上这个地方又开启了一个新的requestTem模板,创建一个新的request,但是这个request是丢失了头部了。导致一个问题就是我们的cookie,也就是sessionid无法传输过去,最后在购物车登录拦截器的时候发现没有sessionid那么也就无法获取session中的loginuser。
解决方案:
其实思路很明确,看看源码的走向,requestTem会经过一系列的拦截器,我们只需要自定义一个拦截器,通过RequestContextHodler来获取当前的request(实际上原理就是ThreadLocal),最后把头部赋值给requestTem,然后再次发送请求就能够取出登录的user,返回它的购物车项的内容。
OrderWebCon
@GetMapping("toTrade")
public String toTrade(Model model){
OrderConfirmVo orderConfirmVo=orderService.confirmOrder();
model.addAttribute("orderConfirmData",orderConfirmVo);
return "confirm";
}
OrderSer
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
//1.远程获取地址
List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
orderConfirmVo.setAddress(address);
//2.远程获取购物项信息
List<OrderItemVo> cartItem = cartFeignService.getCartItem();
orderConfirmVo.setItems(cartItem);
//3.优惠券
orderConfirmVo.setIntegration(memberResVo.getIntegration());
return orderConfirmVo;
}
7.异步编排优化订单确认数据获取
思路
主要是远程调用的时候使用异步编排
拓展与坑
异步编排出现的问题就是获取cart和address的线程不同,那么他们就无法获取通过一个threadLocal那么就不能够获取同一个request来给新的requestTem的头部赋值
解决方案
思路就是在分叉的时候可以先取出这个request,然后在处理异步编排的时候set进去
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
//0.先取出这个request,然后set进各个线程的hodler里面
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//1.远程获取地址
CompletableFuture<Void> memberAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
System.out.println("member线程" + Thread.currentThread().getId());
List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
orderConfirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartItemFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
//2.远程获取购物项信息
System.out.println("购物车项线程:" + Thread.currentThread().getId());
List<OrderItemVo> cartItem = cartFeignService.getCartItem();
orderConfirmVo.setItems(cartItem);
}, executor);
//3.优惠券
orderConfirmVo.setIntegration(memberResVo.getIntegration());
CompletableFuture.allOf(cartItemFuture,memberAddressFuture).get();
return orderConfirmVo;
}
8.渲染数据
思路
①直接找到位置,然后遍历循环就可以了
拓展与坑
①这里遇到个问题记得是用orderConfirmData而不是item。而且如果渲染失败,它会重新访问一次网页,这个时候response已经提交,登录拦截器拦截request无法取出这个session会报错。
9.显示有货和无货
思路
①查询购物车项之后才能够获取skuids,然后通过skuIds去获取所有的库存信息。
②把SkuHasStock拉到order这边接收数据,封装成map,skuid-hasStock。最后就是渲染数据
CompletableFuture<Void> cartItemFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
//2.远程获取购物项信息
System.out.println("购物车项线程:" + Thread.currentThread().getId());
List<OrderItemVo> cartItem = cartFeignService.getCartItem();
orderConfirmVo.setItems(cartItem);
}, executor).thenRunAsync(()->{
List<OrderItemVo> items = orderConfirmVo.getItems();
List<Long> skuIds = items.stream().map(item -> {
return item.getSkuId();
}).collect(Collectors.toList());
//查询是否有库存信息
R hassock = wareFeignService.hassock(skuIds);
if(hassock.getCode()==0){
List<SkuStockVo> data = hassock.getData(new TypeReference<List<SkuStockVo>>