使用RabbitMQ最终一致性库存解锁
一、基本介绍
①延时队列(实现定时任务)
场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
常用解决方案: 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最终一致性库存解锁的更多相关文章
- 116、商城业务---分布式事务---seata的AT模式存在的问题&&最终一致性库存解锁逻辑
seata的AT模式不适合高并发的项目,因为它需要加锁来保证回滚.因此我们的订单服务方法中就尽量不能使用@GlobalTransactional来管理分布式事务. 因此在订单服务中,我们使用下面这种方 ...
- .NET Core微服务之基于MassTransit实现数据最终一致性(Part 2)
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.案例结构与说明 在上一篇中,我们了解了MassTransit这个开源组件的基本用法,这一篇我们结合一个小案例来了解在ASP.NET C ...
- 使用 Masstransit中的 Request/Response 与 Courier 功能实现最终一致性
简介 目前的.net 生态中,最终一致性组件的选择一直是一个问题.本地事务表(cap)需要在每个服务的数据库中插入消息表,而且做不了此类事务 比如:创建订单需要 余额满足+库存满足,库存和余额处于两个 ...
- 分布式事务最终一致性-CAP框架轻松搞定
前言 对于分布式事务,常用的解决方案根据一致性的程度可以进行如下划分: 强一致性(2PC.3PC):数据库层面的实现,通过锁定资源,牺牲可用性,保证数据的强一致性,效率相对比较低. 弱一致性(TCC) ...
- .NET Core微服务之基于MassTransit实现数据最终一致性(Part 1)
Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.预备知识:数据一致性 关于数据一致性的文章,园子里已经有很多了,如果你还不了解,那么可以通过以下的几篇文章去快速地了解了解,有个感性认 ...
- 使用kafka消息队列解决分布式事务(可靠消息最终一致性方案-本地消息服务)
微服务框架Spring Cloud介绍 Part1: 使用事件和消息队列实现分布式事务 本文转自:http://skaka.me/blog/2016/04/21/springcloud1/ 不同于单一 ...
- 脑裂 CAP PAXOS 单元化 网络分区 最终一致性 BASE
阿里技术专家甘盘:浅谈双十一背后的支付宝LDC架构和其CAP分析 https://mp.weixin.qq.com/s/Cnzz5riMc9RH19zdjToyDg 汤波(甘盘) 技术琐话 2020- ...
- 分布式事务(4)---最终一致性方案之TCC
分布式事务(1)-理论基础 分布式事务(2)---强一致性分布式事务解决方案 分布式事务(3)---强一致性分布式事务Atomikos实战 强一致性分布式事务解决方案要求参与事务的各个节点的数据时刻保 ...
- 【Shashlik.EventBus】.NET 事件总线,分布式事务最终一致性
[Shashlik.EventBus].NET 事件总线,分布式事务最终一致性 简介 github https://github.com/dotnet-shashlik/shashlik.eventb ...
- [转]CAP原理与最终一致性 强一致性 透析
在足球比赛里,一个球员在一场比赛中进三个球,称之为帽子戏法(Hat-trick).在分布式数据系统中,也有一个帽子原理(CAP Theorem),不过此帽子非彼帽子.CAP原理中,有三个要素: 一致性 ...
随机推荐
- .net 搜索联想词
思路: 1.ajax请求后台方法获取数据. 2.通过jquery将请求到的数据显示在页面上. 前台 <div class="sc_con" id="bbsearch ...
- R 语言中常见的 10 个错误,看到第 7 个会不会感觉很神奇?
翻译:BioIT 爱好者(部分内容有调整)原文:The top 10 R errors, the 7th one will surprise you 就像你学习走路时遇到了一些问题,你在学习 R 的过 ...
- JavaWeb编程面试题——Spring Web MVC
引言 面试题==知识点,这里所记录的面试题并不针对于面试者,而是将这些面试题作为技能知识点来看待.不以刷题进大厂为目的,而是以学习为目的.这里的知识点会持续更新,目录也会随时进行调整. 关注公众号:编 ...
- 【可视化大屏】用Python开发「淄博烧烤」微博热评舆情分析大屏
目录 一.开发背景 二.爬虫代码 2.1 爬微博列表 2.2 爬微博评论 2.3 导入MySQL数据库 三.可视化代码 3.1 大标题 3.2 词云图(含:加载停用词) 3.3 玫瑰图(含:snown ...
- [ESP] 私有版Rainmaker User Mapping
[ESP] 私有版Rainmaker User Mapping 1. 设备烧录的程序esp-rainmaker/examples/gpio这个demo 我这里是自己的工程,可以参照 idf.py se ...
- zabbix监控服务器php
1 修改php配置文件 /apollo/env/php/thirdparty.php-7.3/etc/php-fpm.conf,添加pm.status pm.status_path = /phpfpm ...
- Linux多线程(8.3 线程同步与互斥)
3. 线程的同步与互斥 为什么需要同步与互斥 一个进程运行时,数据存储在内存中.如果一个数据要进行运算,必须先将数据拷贝到寄存器中.比如要对栈上的一个int i进行"++"操作 ...
- Python编程和数据科学中的机器学习:如何处理和可视化具有噪声和干扰的数据
目录 随着数据科学和机器学习的快速发展,处理和分析具有噪声和干扰的数据成为了一个日益重要的挑战.在数据科学和机器学习中,噪声和干扰通常来自于各种因素,例如随机性和非随机性,数据缺失,数据集中的错误或错 ...
- selenium元素定位---ElementNotInteractableException(元素不可交互异常)解决方法
方法一: 增加强制等待时间 方法二: 使用js点击 element = self.browser.find_element(By.XPATH, "//td[@class='el-table_ ...
- 学习jQuery核心内容这一篇就够了
jQuery 1. 介绍 jQuery是JavaScript的工具库,对原生JavaScript中的DOM操作.事件处理.数据处理等进行封装,提供更便捷的方法. 让我们用更少的代码完成我们的js操作 ...