订单服务的分布式事务--延迟队列
1.需求:高并发下,远程分布式事务seata不合适。如订单服务
1.1 解决方法-延迟队列
高并发下,分布式事务Seata方案不适合。
方案:可靠消息 + 最终一致性方案(异步确保型)
可以使用延迟队列,实现远程服务的事务,完成数据最终一致性。
1.2 延迟队列的介绍
1.2.1 延迟队列的场景-定时任务
1). 定时任务的时效性问题
2). 定时任务的时效性问题的解决–RabbitMQ延时队列(实现定时任务)
1.2.2 延迟队列的场景-RabbitMQ延时队列(实现定时任务)
场景:
比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
常用解决方案:
spring的 schedule 定时任务轮询数据库
缺点:
消耗系统内存、增加了数据库的压力、存在较大的时间误差
解决: rabbitmq的消息TTL和死信Exchange结合
1).消息的TTL(Time To Live)-消息的存活时间
• 消息的TTL就是消息的存活时间。
• RabbitMQ可以对队列和消息分别设置TTL。
…• 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的
设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
…• 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队
列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的
TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x- message-ttl属性来设置时间,两者是一样的效果。
2).Dead Letter Exchanges(DLX)-死信路由
• 一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,
一个路由可以对应很多队列。(什么是死信,是没有的消息)
…• 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。
也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)requeue=false
…• 上面的消息的TTL到了,消息过期了。
…• 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
• Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。
只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
• 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机。
结合二者,其实就可以实现一个延时队列
• 手动ack&异常消息统一放在一个队列处理,建议的两种方式
…• catch异常后,手动发送到指定队列,然后使用channel给rabbitmq确认消息已消费
…• 给Queue绑定死信队列,使用nack(requque为false)确认消息消费失败
3).延时队列实现-1(推荐)
推荐 延时队列1:给队列设置过期时间。因为RabbitMQ采用惰性检查机制,
4).延时队列实现-2
1.2.3 延迟队列的模拟
使用延迟队列的实现方式:给队列加过期时间
1).延迟队列-简单模式
2个交换机、2个队列、2个绑定
2).延迟队列-升级模式(推荐)
一个交换机、2个队列、2个绑定。(共享一个交换机)
3).测试代码-order项目
a.RabbitMQ的配置类
@Configuration
public class MyRabbitMQConfig {
/**
* 容器中的2个Queue、1个Exchange、2个Binding。 会自动创建(在RabbitMQ)不存在的情况下
* RabbitMQ只要有, @Bean声明属性不会发生变化也不会覆盖
* 采用:1个交换机、2个队列、2个绑定的模式
* 原理:订单创建成功,先把消息放入延迟队列(死信队列)。如果过期时间到,把消息放入订单队列,等待消费者释放消息。
*/
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity order, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单 "+ order.getOrderSn());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//不批量保存
}
//死信队列(延迟队列)-存放原始的消息
@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
//普通队列(存放死信的队列)
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
//TopicExchange。交换机
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map<String, Object> arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
//绑定:绑定延迟队列
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map<String, Object> arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
//绑定:绑定普通队列
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
}
b.测试发送消息的接口
@Controller
public class HelloController {
@Autowired
RabbitTemplate rabbitTemplate;
//测试RabbitMQ的发送消息
@GetMapping("/test/createOrder")
@ResponseBody
public String createOrderTest( ){
//订单下单成功
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给MQ发送消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "ok";
}
}
注意添加amq依赖,spring-boot-starter-amqp
1.3 订单延迟队列-订单关单
加amq依赖:
spring-boot-starter-amqp
添加RabbitMQ配置:
配置消息队列MQ
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/
启动类:开启rabbitMQ @EnableRabbit
1.3.1 仓库自动解锁
逻辑:订单创建,订单超时,库存自动解锁。上图路线1
1.3.2 订单关闭,库存解锁
逻辑:订单由于网络等原因,库存解锁消息 早于 订单释放解锁的信息。造成订单库存无法解锁。上图路线2
订单关闭(释放),库存解锁,发送消息
1)提交订单。submitOrder
R r = wmsFeignService.orderLockStock(lockVo);
System.out.println("r.getCode(): "+r.getCode() );
if (r.getCode() == 0) {
//锁定库存成功
response.setOrder(order.getOrder());
//todo 远程扣减积分,出现异常
// int m = 10 / 0;//订单回滚,库存不滚。使用Seata分布式事务,让远程ware也回滚
//Todo 订单创建成功,发送消息给MQ
System.out.println("订单创建成功,发送消息给MQ");
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
return response;
2).订单项目的RabbitMQ配置类
@Configuration
public class MyRabbitMQConfig {
/**
* 容器中的2个Queue、1个Exchange、2个Binding。 会自动创建(在RabbitMQ)不存在的情况下
* RabbitMQ只要有, @Bean声明属性不会发生变化也不会覆盖
* 采用:1个交换机、2个队列、2个绑定的模式
* 原理:订单创建成功,先把消息放入延迟队列(死信队列)。如果过期时间到,把消息放入订单队列,等待消费者释放消息。
*/
//死信队列(延迟队列)-存放原始的消息
@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
// arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
//普通队列(存放死信的队列)
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
//TopicExchange。交换机
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map<String, Object> arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
//绑定:绑定延迟队列
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map<String, Object> arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
//绑定:绑定普通队列
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
}
3).订单order项目的监听器
/**
* @Description: 定时关闭订单
**/
@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
4).库存ware项目的RabbitMQ配置类
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
// 使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
//库存服务默认的交换机
@Bean
public Exchange stockEventExchange() {
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
return topicExchange;
}
//普通队列(存放死信-超时的消息)
@Bean
public Queue stockReleaseStockQueue() {
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
Queue queue = new Queue("stock.release.stock.queue", true, false, false);
return queue;
}
//延迟队列
@Bean
public Queue stockDelay() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
return queue;
}
//交换机与延迟队列绑定
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
//交换机与普通队列绑定
@Bean
public Binding stockLocked() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
Binding binding = new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
return binding;
}
}
4)库存ware项目的库存监听器
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
* 2、订单失败
* 库存锁定失败
* 只要解锁库存的消息失败,一定要告诉服务解锁失败
*/
//1.库存自动解锁
@RabbitHandler
public void handleLockedStockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息。。");
try {
wareSkuService.unLockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
//2.订单创建,订单释放,库存解锁
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("订单关闭准备解锁库存。。");
try {
wareSkuService.unLockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
5).订单服务的OrderServiceImpl的closeOrder
public void closeOrder(OrderEntity entity) {
//查询当前订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
//如果订单状态是刚创建,待付款0。关单
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
System.out.println("关闭订单,并更新订单状态为:"+OrderStatusEnum.CANCLED.getMsg());
//关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
//TODO 再发给MQ(订单释放服务、库存解锁)
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
/**
* 库存自动解锁:
* 1、下订单成功,库存解锁成功。接下来业务调用失败,导致订单回滚。
* 之前锁定的库存需要自动解锁
* 2、订单失败:
* 锁库存失败
* 只要解锁库存失败,一定要告诉服务解锁失败
*/
6).解锁库存的方法-WareSkuServiceImpl
/**
* 查询数据库,关于订单的解锁消息
* 订单的解锁消息:
* 1.有: 证明库存锁定成功。是否解锁库存,看订单情况
* 1).订单不存在,必须解锁库存 ok
* 2).有此订单。是否解锁库存,看订单的状态
* 订单的状态:
* 已取消:说明用户取消了订单。必须解锁库存 ok
* 没取消:不能解锁库存
* 2.没有:库存锁定失败,库存回滚了。这种情况无需解锁
*/
//解锁库存
@Override
public void unLockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetail();//订单库存工作单详情
Long detailId = detail.getId();
//解锁
WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
if (byId != null) {
//解锁
//根据订单号获得订单信息orderSn,远程order
Long taskId = to.getId();//订单工作单id
WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(taskId);
String orderSn = taskEntity.getOrderSn();
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
//订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
if (data == null || data.getStatus() == OrderStatusEnum.CANCLED.getCode()) {
//订单不存在,必须解锁库存
//订单被客户取消,必须解锁库存
if (byId.getLockStatus() == 1) {
//当前库存工作单详情,状态1(已锁定),才可以解锁
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
}
} else {
//拒绝消息后重新放入队列。让别人继续消费解锁
throw new RuntimeException("远程服务失败");
}
} else {
//不用解锁
}
}
7).关闭订单的方法-WareSkuServiceImpl
//解锁订单
//防止订单服务卡顿,导致订单状态信息一直改不了,而库存信息优先到期。
//结果:导致卡顿的订单,永远无法进行库存解锁
@Override
@Transactional(rollbackFor = Exception.class)
public void unLockStock(OrderTo to) {
String orderSn = to.getOrderSn();
//查下最新的库存解锁状态,防止重复解锁库存(订单工作单)
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//按照工作单的id找到所有 没有解锁的库存,进行解锁
Long id = orderTaskEntity.getId();
List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
.eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity task : list) {
//库存自动解锁
unLockStock(task.getSkuId(), task.getWareId(), task.getSkuNum(), task.getId());
}
}