一、基本介绍

①延时队列(实现定时任务)

场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。

常用解决方案: spring的 schedule定时任务轮询数据库:
缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差
解决: rabbitmqExchange的消息TTL和死信结合

②消息的TL(Time To Live)消息的TTL就是消息的存活时间。

RabbitMQ可以对队列和消息分别设置TTL
- 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
- 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的 expiration-message-字段或者x--ttl属性来设置时间,两者是一样的效果。

③ Dead Letter Exchanges (DLX)

一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。(什么是死信)
- 一个消息被Consumer拒收了,并且 reject方法的参数里是。也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/basic.nack) requeue=false上面的消息的TTL到了,消息过期了。-
- 一队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
 Dead Letter Exchangeexch其实就是一种普通的,和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到 Dead Letter Exchange中去。
·我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列

二、推荐:给队列设置延时时间

①:因为RabbitMQ采用惰性检查机制

RabbitMq采用惰性检查机制,也就是懒检查机制:比如消息队列中存放了多条消息,第一条是5分钟过期,第二条是1分钟过期,第三条是1秒钟过期,按照正常的过期逻辑,应该是1秒过期的先排出这个队列,进入死信队列中,但是实际RabbitMQ是先拿第一条消息,也就是5分钟过期的,一看5分钟还没到过期时间,然后等待5分钟会将第一条消息拿出来,放入死信队列,这里就会出现问题,第二条设置1分钟的和第三条设置1秒钟的消息必须要等待第一条5分钟过期后才能过期,等待第一条消息过期5分钟了,拿第二条、三条的时候都不需要判断就已经过期了,直接就放入死信队列中,所以第二条、三条需要等待第一条消息过了5分钟才能过期,这样的延时根本就没产生对应的效果。

②:理论结构图

③:项目结构图

④:代码实现

4.1:基础设置

4.1.1:创建信道、队列、路由(结构图)

4.1.2:MyRabbit配置

@Configuration
public class MyRabbitConfig {
/**
* 使用JSON序列化机制,进行消息转移
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
} @Bean
public Exchange stockEventExchange() {
TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
return topicExchange;
} @Bean
public Queue stockReleaseStockQueue() {
return new Queue("stock.release.stock.queue", true, false, false);
}


@Bean
public Queue stockDelayQueue() {
HashMap<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "stock-event-exchange");
args.put("x-dead-letter-routing-key", "stock.release");
args.put("x-message-ttl", 120000); //延时2min
return new Queue("stock.delay.queue", true, false, false,args);
} @Bean
public Binding stockReleaseBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
} @Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}

4.1.3:设置监听器

@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockRelaeaseListener {
@Autowired
WareSkuService wareskuService; @RabbitHandler
public void handleStockLockedRelease(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);
}
} @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);
} }
}

4.2、wareskuService类:

4.2.1:orderLockStock方法:

    /**
* 为某个订单锁定库存
*
* @param vo
* @return
* @Transactional(rollbackFor = NoStockException.class)运行出现异常时回滚
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的详情,为了追溯哪个仓库锁了多少
*/
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity); //1、按照下单的收获地址,找到就近仓库进行锁定库存
List<OrderItemVo> locks = vo.getLocks(); List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList()); for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有库存抛出异常
throw new NoStockException(skuId);
}
//如果每一个商品都锁成功,将当前商品锁定了几件发送给MQ
//如果锁定失败,前面保存的工作单信息就回滚了。
for (Long wareId : wareIds) {
//成功就返回1,否则就是0
Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
if (count == 1) {
//TODO:表明锁住了,发消息告诉MQ库存锁定成功
//在数据表wms_ware_order_task_detail中存入库存单(*仓库/*商品/*数量/被锁*件)做记号
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
orderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
//拷贝属性和数值
BeanUtils.copyProperties(entity, stockDetailTo);
//防止wms_ware_order_task_detail表内数据因为回滚丢失,所以new一个StockLockedTo类记录失败提交的数据
lockedTo.setDetail(stockDetailTo);
//将库存工作单的详情放入exchange中
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
skuStocked = true;
break;
} else {
//锁失败了,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没锁住库存数量
throw new NoStockException(skuId);
}
}
//肯定全部都是锁定
return true;
}

4.2.1、unlockStock方法:

    @Override
public void unlockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetail();
Long detailId = detail.getId();
/**
* 1、查询数据库wms_ware_order_task_detail表关于这个订单的锁定库存信息
* ①表里有关于锁库存的信息,队列设置延时时间,检查订单的状态,确认是否需要进行解锁
* 1.1:解锁前查看订单情况:则需要解锁库存
* 1.1.1查看订单状态,查看订单状态,若订单已取消则必须解锁库存
* 1.1.2查看订单状态,订单未取消则不能解锁库存
*
* 1.2:如果没有订单情况:则必须解锁库存
*
* ②没有则代表整个库存锁定失败,事务回滚了,这种情况无需解锁
*
* 只要解锁库存失败,利用手动模式
*/
WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
if (byId != null) {
//解锁
Long id = to.getId();
WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn();//根据订单号查询订单状态
//查找订单是否创建成功,此处远程调用会因为拦截器需要先登录,因此按4.3进行修改
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
//订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
//只有订单状态是取消状态/或者订单不存在才可以解锁.4为状态码代表订单是取消状态
System.out.println("Data1:" + data);
if (data == null || data.getStatus() == 4) {
//当前库存单详情状态1已锁定但是未解锁才可以解锁
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
/* if (byId.getLockStatus() == 1) {
System.out.println("Data2:" + data); }*/
}
} else {
//消息拒绝以后重新放入队列里,让其他人继续消费解锁
throw new RuntimeException("远程服务失败");
}
}
}

4.2.2:unLockStock方法:

    //库存解锁方法
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
wareSkuDao.unlockStock(skuId, wareId, num);
} //wareSkuDao.xml中更新代码: /**<update id="unlockStock">
UPDATE wms_ware_sku SET stock_locked = stock_locked- #{num} WHERE sku_id= #{skuId} AND ware_id=#{wareId}
</update>**/

4.2.3:unlockStock(OrderTo orderTo)方法:

 /*
*防止订单服务卡顿,导致订单消息一直更改不了,库存优先到期,查订单状态新建状态,什么都做不了就走了
*导致卡顿的订单,永远不能解锁
*/
@Transactional
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//进行到这一步再查一下最新的状态
WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
//获取was_ware_order_task中的id,从而以其获取was_ware_order_task_detail状态为1(未解锁)的库存
Long id = task.getId();
List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));
//Long skuId, Long wareId, Integer num, Long taskDetailId
for (WareOrderTaskDetailEntity entity:entities){
unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
}
}

4.3:拦截器修改

@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 { //此地址下不进行拦截
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
boolean match1 = new AntPathMatcher().match("/payed/notify", uri);
if (match || match1){
return true;
}

//获取登录用户的键
MemberResVo attribute = (MemberResVo) request.getSession().getAttribute(AuthServerConstant.LONG_USER);
if (attribute!=null){
loginUser.set(attribute);
return true;
}else {
request.getSession().setAttribute("msg","请先进行登录!");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}

使用RabbitMQ最终一致性库存解锁的更多相关文章

  1. 116、商城业务---分布式事务---seata的AT模式存在的问题&&最终一致性库存解锁逻辑

    seata的AT模式不适合高并发的项目,因为它需要加锁来保证回滚.因此我们的订单服务方法中就尽量不能使用@GlobalTransactional来管理分布式事务. 因此在订单服务中,我们使用下面这种方 ...

  2. .NET Core微服务之基于MassTransit实现数据最终一致性(Part 2)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.案例结构与说明 在上一篇中,我们了解了MassTransit这个开源组件的基本用法,这一篇我们结合一个小案例来了解在ASP.NET C ...

  3. 使用 Masstransit中的 Request/Response 与 Courier 功能实现最终一致性

    简介 目前的.net 生态中,最终一致性组件的选择一直是一个问题.本地事务表(cap)需要在每个服务的数据库中插入消息表,而且做不了此类事务 比如:创建订单需要 余额满足+库存满足,库存和余额处于两个 ...

  4. 分布式事务最终一致性-CAP框架轻松搞定

    前言 对于分布式事务,常用的解决方案根据一致性的程度可以进行如下划分: 强一致性(2PC.3PC):数据库层面的实现,通过锁定资源,牺牲可用性,保证数据的强一致性,效率相对比较低. 弱一致性(TCC) ...

  5. .NET Core微服务之基于MassTransit实现数据最终一致性(Part 1)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.预备知识:数据一致性 关于数据一致性的文章,园子里已经有很多了,如果你还不了解,那么可以通过以下的几篇文章去快速地了解了解,有个感性认 ...

  6. 使用kafka消息队列解决分布式事务(可靠消息最终一致性方案-本地消息服务)

    微服务框架Spring Cloud介绍 Part1: 使用事件和消息队列实现分布式事务 本文转自:http://skaka.me/blog/2016/04/21/springcloud1/ 不同于单一 ...

  7. 脑裂 CAP PAXOS 单元化 网络分区 最终一致性 BASE

    阿里技术专家甘盘:浅谈双十一背后的支付宝LDC架构和其CAP分析 https://mp.weixin.qq.com/s/Cnzz5riMc9RH19zdjToyDg 汤波(甘盘) 技术琐话 2020- ...

  8. 分布式事务(4)---最终一致性方案之TCC

    分布式事务(1)-理论基础 分布式事务(2)---强一致性分布式事务解决方案 分布式事务(3)---强一致性分布式事务Atomikos实战 强一致性分布式事务解决方案要求参与事务的各个节点的数据时刻保 ...

  9. 【Shashlik.EventBus】.NET 事件总线,分布式事务最终一致性

    [Shashlik.EventBus].NET 事件总线,分布式事务最终一致性 简介 github https://github.com/dotnet-shashlik/shashlik.eventb ...

  10. [转]CAP原理与最终一致性 强一致性 透析

    在足球比赛里,一个球员在一场比赛中进三个球,称之为帽子戏法(Hat-trick).在分布式数据系统中,也有一个帽子原理(CAP Theorem),不过此帽子非彼帽子.CAP原理中,有三个要素: 一致性 ...

随机推荐

  1. iOS网络数据指标收集

    在平时开发中有时候需要收集网络不同阶段性能数据来分析网络情况,下面总结了2种收集方式. 1.通过NSURLSession提供的代理方法收集 2.通过NSURLProtocol做统一网络请求拦截收集 通 ...

  2. YOLOV5实时检测屏幕

    YOLOV5实时检测屏幕 目录 YOLOV5实时检测屏幕 思考部分 先把原本的detect.py的代码贴在这里 分析代码并删减不用的部分 把屏幕的截图通过OpenCV进行显示 写一个屏幕截图的文件 用 ...

  3. 一次 HPC 病毒感染与解决经历

    周一的时候,有同事反馈说,HPC 的项目报告路径正在不断产生 *.exe 和 *.pif 文件,怀疑是不是被病毒感染! 收到信息,第一时间进去目录,的确发现该目录每个几秒钟就自动生成一个 *.exe ...

  4. 自然语言处理(NLP) - 前预训练时代的自监督学习

    前预训练时代的自监督学习自回归.自编码预训练的前世 神经网络(Neural Network, NN) 损失函数,度量神经网络的预测结果和真实结果相差多少 平方差损失(欧式距离角度)预测概率分部和实际标 ...

  5. 从2PC和容错共识算法讨论zookeeper中的Create请求

    最近在读<数据密集型应用系统设计>,其中谈到了zookeeper对容错共识算法的应用.这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以 ...

  6. AI 和 DevOps:实现高效软件交付的完美组合

    AI 时代,DevOps 与 AI 共价结合.AI 由业务需求驱动,提高软件质量,而 DevOps 则从整体提升系统功能.DevOps 团队可以使用 AI 来进行测试.开发.监控.增强和系统发布.AI ...

  7. 特性介绍 | MySQL测试框架 MTR 系列教程(四):语法篇

    作者:卢文双 资深数据库内核研发 序言: 以前对 MySQL 测试框架 MTR 的使用,主要集中于 SQL 正确性验证.近期由于工作需要,深入了解了 MTR 的方方面面,发现 MTR 的能力不仅限于此 ...

  8. 【阅读笔记】低照度图像增强-《Adaptive and integrated neighborhood-dependent approach for nonlinear enhancement of

    本文介绍改进INDANE算法的低照度图像增强改进算法(AINDANE算法),<Adaptive and integrated neighborhood-dependent approach fo ...

  9. 【转载】Linux虚拟化KVM-Qemu分析(八)之virtio初探

    原文信息 作者:LoyenWang 出处:https://www.cnblogs.com/LoyenWang/ 公众号:LoyenWang 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者 ...

  10. 关于quartus II的导入以前的工程,QSF文件出现的错误的解决方案。

    在有时候打开以前的工程,或者别人做好的例程会遇到一些报错信息.具体报错信息如下: 报错信息语句行: 在文件QSF文件中有几行出错,显示错误读取,即不能打开工程.打开文件发现该几行的PIN 使能信号处于 ...