spring在TransactionDefinition接口中定义了七个事务传播行为
propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作
本地事务失效问题
注意同类中调用的话,被调用事务会失效,原因在于aop。
事务基于代理,同对象的方法动态代理都是同一个。
解决方案是使用代理对象调用。
引用aop-starter后,使用aspectJ,开启AspectJ动态代理,原来默认使用的是jdk动态代理。
解决方案
使用@EnableAspectJAutoProxy(exposeProxy=true)后,就取代了jdk动态代理,它没有接口也可以创建动态代理。设置true是为了对外暴露代理对象。
AopContext.currentProxy()然后强转,就是当前代理对象。
public interface AService {
public void a();
public void b();
}
/**
*
* 此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强,
* 因此b方法的事务定义“@Transactional(propagation = Propagation.REQUIRES_NEW)”将不会实施,
* 即结果是b和a方法的事务定义是一样的(我们可以看到事务切面只对a方法进行了事务增强,没有对b方法进行增强)
*
*/
@Service()
public class AServiceImpl1 implements AService{
@Transactional(propagation = Propagation.REQUIRED)
public void a() {
this.b();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b() {
}
}
Q1:b中的事务会不会生效?
A1:不会,a的事务会生效,b中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生。
Q2:如果想要b中有事务存在,要如何做?
A2:
public void a() {
((AService) AopContext.currentProxy()).b();//即调用AOP代理对象的b方法即可执行事务切面进行事务增强
}
注意事项
事务传播问题中,传播后事务设置还是原来的,如果不想用原来设置,必须new事务。
Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用
内容介绍
一致性(Consistency)
在分布式系统中的所有数据备份,在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)。
可用性(Availability)
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
分区容惜性(Partitiontolerance)
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。
分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
原则介绍
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾
CP要求一致性(有一个没同步好就不可用)
AP要求高可用
分布式一致性动画演示
http://thesecretlivesofdata.com/raft/
raft协议
是一个实现分布式一致性的协议
结点状态
follower、candidate和leader
选举leader
默认都以follower状态启动,follower监听不到leader,就称为一个candidate;
投票给自己,然后告诉其他人,同时也收到别人的投票信息。根据投票信息和投票信息里带的信息(如那个节点里的数据);
收到投票后,改选一个自己觉得最靠谱的。某一节点收到票数超过一半就变成leader
raft有两个超时时间控制领导选举
选举超时:从follower到candidate的时间,150ms-300ms(自旋时间),这个时间段内没收到leader的心跳就变为候选者。
a. 自旋时间结束后变成candidate,开始一轮新的选举(老师上课举的例子是);
b. 投出去票后重新计时自旋;
c. leader就发送追加日志给follower,follower就正常
消息发送的心跳时间:如10ms,leader收到投票后,下一次心跳时就带上消息,follower收到消息后重置选举时间
leader宕机,follower收不到心跳,开始新的选举
写数据
接下来所有的数据都要先给leader,leader派发给follower;
比如领导收到信息5后,领导先在leader的log中写入变化set 5。(上面的动态红颜色代表没提交),此时5还没提交,而是改了leader的log后;
leader下一次心跳时,顺便带着信息让follower也去改变follower的log,follower写入日志成功后,发送确认ack 5给leader,
leader收到大多数的ack后,leader就自己正式写入数据,然后告诉follower提交写入硬盘/内存吧(这个过程和响应客户端是同时的)。这个过程叫做日志复制(也有过半机制)
然后leader响应说集群写入好了
其他
5台机器因为局域网隔离又分为3、2生成两个leader(导致部分结点消息滞后)
对于1、2结点那个leader:更新log后收不到大多数的ack(得超过1个ack),所以改log不成功,一直保存不成功
对于3、4、5结点的leader:收到消息后更新log并且收到ack过半且超过1个,成功保存。
此时网络又通了,以更高轮选举的leader为主,退位一个leader。那1、2结点日志都回滚,同步新leader的log。这样就都一致性了
更多动画(可以自己选择宕机情况)
raft.github.io
注意事项
集群一般都是单数,因为有过半机制。比如原来集群6个机器,分为2半后,各3个,选leader时谁都拿不到6/2+1=4个投票,所以都没有leader,导致前端请求都无法保存数据。
一般都是保证AP,舍弃C,后续发现扣减不一致后,再恢复。
内容介绍
BASE理论是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用弱一致性,即最终一致性
基本可用(Basically Available)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
软状态(Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
最终一致性(Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。(这也是分布式事务的想法)
一致性分类
从客户端角度,多进程并发访同时,更新过的数据在不同程如何获的不同策珞,决定了不同的一致性。
对于关系型要求更新过据能后续的访同都能看到,这是强一致性;
如果能容忍后经部分过者全部访问不到,则是弱一致性;
如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。
内容介绍
数据库支持的2pc(2二阶段提交),又叫做XA Transactions
支持情况:mysql从5.5版本开始支持,SQLserver2005开始支持,Oracle7开始支持。
其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(P090此操作,并反映是否可以提交;
第二阶段:事务协调器要求每个数据库提交数据。
如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
原理介绍
如图所示,如果有订单服务和库存服务要求分布式事务,要求有一个总的事务管理器将事务分为两个阶段:
第一个阶段是预备(log);
第二个阶段是正式提交(commit)
总事务管理器接收到两个服务都预备好了log(收到ack),就告诉他们commit,如果有一个没准备好,就回滚所有事务。
小结
XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景;
XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录阶段日志,主备切换回导致主库与备库数据不一致。
许多nosql没有支持XA,这让XA的应用场景变得非常狭隘。
也有3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)。
事务分类
刚性事务:遵循ACID原则,强一致性;
柔性事务:遵循BASE理论,最终一致性。
柔性事务简介
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
一阶段prepare行为:调用自定义的prepare逻辑。
二阶段commit行为:调用自定义的commit逻憬。
二阶段rollback行为:调用自定义的rollback逻辑。
TCC模式,是指支持 自定义的 分支事务纳入到全局事务的管理中。
内容介绍
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接囗进行核对。
这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。
这种方案也是结合MQ进行实现,例如:通过MQ发送就请求,设置最大通知次数。达到通知次数后即不再通知。
案例分析
银行通知、商户通知等(各大交易业务平台间的商户涌知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调。
大业务调用订单、库存、积分。最后积分失败,则一遍遍通知他们回滚
让子业务监听消息队列
如果收不到就重新发
实现方式
业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
内容介绍
以电商项目下订单为例,下订单业务流程涉及跨系统操作:订单服务下订单 —> 库存服务锁库存 —> 用户服务扣减积分
事务保证情景分析
订单服务异常,库存锁定不运行,全部回滚,撤销操作;
库存服务事务自治,锁定失败全部回滚,订单感受到,继续回滚;
库存服务锁定成功,但是网络原因返回数据超时失败问题?
库存服务锁定成功,库存服务下面的逻辑发生故障,订单回滚了,怎么处理?
解决方案
利用消息队列实现最终一致
库存服务锁定成功后发给消息队列消息(当前库存工作单),过段时间自动解锁,解锁时先查询订单的支付状态。解锁成功修改库存工作单详情项状态为已解锁
远程服务假失败:远程服务其实成功了,由于网络故障等没有返回导致订单回滚,库存却扣减;
远程服务执行完成:下面的其他方法出现问题导致已执行的远程请求,肯定不能回滚
待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要汪意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
已付款/代发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调拨,配货,分拣,出库等操作。
待收货/已发货
仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态。
已完成
用户确认收货后,订单交易完成。后续支付则进行结算,如果订单存在间题进入售后状态。
已取消
付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
售后中
用户在付款后申请退款,或商家发货后用户申请退换货。
售后也同样存在各种状态:
当发起售后申请后生成售后订单;
售后订单状态为待审核,等待商家审核;
商家审核过后订单状态变更为待退货,等待用户将商品寄回;
商家收到货后订单
内容介绍
线上实物订单和虚拟订单的流程,线上实物订单与O2O订单等,需要根据不同的类型进行构建订单流程。
不管类型如何订单都包括正向流程(购买商品)和逆向流程(退换货),正向流程就是一个正常的网购步骤:
订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功。
展示当前用户收获地址list;
所有选中的购物项list;
支付方式;
送货清单,价格也是最新价格,不是加入购物车时的价格;
优惠信息
要注意生成订单的时候,价格得重新算;
在后面的修改中,会让提交订单时不带着购物车数据,而是在后台重新 查询购物车选项;
会带着总价,比对新总价和就总价是否一致。
订单确认数据模型
public class OrderConfirmVo { // 跳转到确认页时需要携带的数据模型。
@Getter
@Setter
/** 会员收获地址列表 **/
private List<MemberAddressVo> memberAddressVos;
@Getter @Setter
/** 所有选中的购物项 **/
private List<OrderItemVo> items;
/** 发票记录 **/
@Getter @Setter
/** 优惠券(会员积分) **/
private Integer integration;
/** 防止重复提交的令牌 **/
@Getter @Setter
private String orderToken;
@Getter @Setter
Map<Long,Boolean> stocks;
public Integer getCount() { // 总件数
Integer count = 0;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
/** 计算订单总额**/
//BigDecimal total;
public BigDecimal getTotal() {
BigDecimal totalNum = BigDecimal.ZERO;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
//计算当前商品的总价格
BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
//再计算全部商品的总价格
totalNum = totalNum.add(itemPrice);
}
}
return totalNum;
}
/** 应付价格 **/
//BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
}
异步处理
查询购物项(redis)、库存和收货地址(数据库)都要调用远程服务,串行会浪费大量时间,因此我们使用CompletableFuture进行异步编排。
防重处理
为了防止多次重复点击“订单提交按钮”。我们在返回订单确认页时,在redis中生成一个随机的令牌,过期时间为30min,提交订单时会携带这个令牌,我们将会在订单提交的处理页面核验此令牌。
利用CompletableFuture异步获取各项数据
@Override // OrderServiceImpl
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
// 获取用户,用用户信息获取购物车
MemberRespVo MemberRespVo = LoginUserInterceptor.threadLocal.get();
// 封装订单
OrderConfirmVo confirmVo = new OrderConfirmVo();
// 我们要从request里获取用户数据,但是其他线程是没有这个信息的,
// 所以可以手动设置新线程里也能共享当前的request数据
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
// 1.远程查询所有的收获地址列表
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
// 因为异步线程需要新的线程,而新的线程里没有request数据,所以我们自己设置进去
RequestContextHolder.setRequestAttributes(attributes);
List<MemberAddressVo> address;
try {
address = memberFeignService.getAddress(MemberRespVo.getId());
confirmVo.setAddress(address);
} catch (Exception e) {
log.warn("\n远程调用会员服务失败 [会员服务可能未启动]");
}
}, executor);
// 2. 远程查询购物车服务,并得到每个购物项是否有库存
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
// 异步线程共享 RequestContextHolder.getRequestAttributes()
RequestContextHolder.setRequestAttributes(attributes);
// feign在远程调用之前要构造请求 调用很多拦截器
// 远程获取用户的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
List<OrderItemVo> items = confirmVo.getItems();
// 获取所有商品的id
List<Long> skus = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wmsFeignService.getSkuHasStock(skus);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {});
if (data != null) {
// 各个商品id 与 他们库存状态的映射map // 学习下收集成map的用法
Map<Long, Boolean> stocks = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(stocks);
}
}, executor);
// 3.查询用户积分
Integer integration = MemberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4.其他数据在类内部自动计算
// TODO 5.防重令牌 设置用户的令牌
String token = UUID.randomUUID().toString().replace("-", "");
confirmVo.setOrderToken(token);
// redis中添加用户id,这个设置可以防止订单重复提交。生成完一次订单后删除redis
stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + MemberRespVo.getId(), token, 10, TimeUnit.MINUTES);
// 等待所有异步任务完成
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
有货无货状态,每个商品单独查比较麻烦,可以用skuId-list异步调用库存系统查出来;
加上运费,并且切换地址时要重新计算运费、总额;
点击提交订单时计算总额,而不是用当前页面的值,或者比对一下值,不一致让用户重新看订单
邮费数据封装
@Data
public class FareVo { // 邮费
private MemberAddressVo address;
private BigDecimal fare;
}
将页面选中地址的id传给请求获取邮费
@RequestMapping("/fare/{addrId}")
public FareVo getFare(@PathVariable("addrId") Long addrId) {
return wareInfoService.getFare(addrId);
}
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
R info = memberFeignService.info(addrId);
if (info.getCode() == 0) {
MemberAddressVo address = info.getData("memberReceiveAddress", new TypeReference<MemberAddressVo>() {
});
fareVo.setAddress(address);
String phone = address.getPhone();
//取电话号的最后两位作为邮费
String fare = phone.substring(phone.length() - 2, phone.length());
fareVo.setFare(new BigDecimal(fare));
}
return fareVo;
}
准备好订单确认数据后,返回给用户看运费等信息,同时创建防重令牌redis.set(‘order:token:(userId)’,uuid),一并返回;
用户点击提交订单按钮,带着token(hidden元素带着);
渲染订单确认页,后台处理的时候确认请求带过来token的uuid和redis库中是否一致;
此处是重点,比对后立刻删除,比对和删除要求具有原子性,通过redis-lua脚本完成;
提交订单时不要提交购买的商品,去购物车数据库重新获取即可,防止购物车变化和修改页面值;
但可以提交总额,防止商品金额变了还提交订单,用户不满意;
其他信息可以用token和session获取
订单提交携带数据
@Data
public class OrderSubmitVo {
/** 收获地址的id **/
private Long addrId;
/** 支付方式 **/
private Integer payType;
//无需提交要购买的商品,去购物车再获取一遍
//优惠、发票
/** 防重令牌 **/
private String orderToken;
/** 应付价格 **/
private BigDecimal payPrice;
/** 订单备注 **/
private String remarks;
//用户相关的信息,直接去session中取出即可
}
成功后转发至支付页面携带的数据
@Data
public class SubmitOrderResponseVo {
// 该实体为order表的映射
private OrderEntity order;
/** 错误状态码 **/
private Integer code;
}
内容介绍
提交订单成功,则携带返回数据转发至支付页面;
提交订单失败,则携带错误信息重定向至确认页
逻辑分析
在OrderWebController里接收到下单请求,然后去OrderServiceImpl里验证和下单,然后再返回到OrderWebController。相当于OrderWebController是封装了我们原来的OrderServiceImpl,用作web
调用service,具体逻辑是交给orderService.submitOrder(submitVo),service返回了失败Code信息,可以看是什么原因引起的下单失败
@PostMapping("/submitOrder") // OrderWebController
public String submitOrder(OrderSubmitVo submitVo, Model model,
RedirectAttributes redirectAttributes){
try {
// 去OrderServiceImpl服务里验证和下单
SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);
// 下单失败回到订单重新确认订单信息
if(responseVo.getCode() == 0){
// 下单成功去支付响应
model.addAttribute("submitOrderResp", responseVo);
// 支付页
return "pay";
}else{
String msg = "下单失败";
switch (responseVo.getCode()){
case 1: msg += "订单信息过期,请刷新在提交";break;
case 2: msg += "订单商品价格发送变化,请确认后再次提交";break;
case 3: msg += "商品库存不足";break;
}
redirectAttributes.addFlashAttribute("msg", msg);
// 重定向
return "redirect:http://order.gulimall.com/toTrade";
}
} catch (Exception e) {
if (e instanceof NotStockException){
String message = e.getMessage();
redirectAttributes.addFlashAttribute("msg", message);
}
return "redirect:http://order.gulimall.com/toTrade";
}
}
验证原子性令牌
为防止在【获取令牌、对比值和删除令牌】之间发生错误导入令牌校验出错,我们必须使用lua脚本保证原子性操作;
改为先锁库存再生成订单;
库存服务后面讲
// @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
// @Transactional(propagation = Propagation.REQUIRED) 设置事务的传播级别
@Transactional(rollbackFor = Exception.class)
// @GlobalTransactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
// 当前线程共享该对象
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//去创建、下订单、验令牌、验价格、锁定库存...
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
// 0:正常
responseVo.setCode(0);
//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】返回 0 - 令牌删除失败 或 1 - 删除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//通过lua脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单 挪到最后
//4、库存锁定,只要有异常,回滚订单数据
//订单号、所有订单项信息(skuId,skuNum,skuName)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
//TODO 调用远程锁定库存的方法
//出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
// int i = 10/0;
// 保存订单
saveOrder(order);
//TODO 订单创建成功,发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
//删除购物车里的数据
redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
return responseVo;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
// responseVo.setCode(3);
// return responseVo;
}
} else {
responseVo.setCode(2);
return responseVo;
}
}
}
订单创建To
最终订单要返回的数据
@Data
public class OrderCreateTo {
private OrderEntity order;
private List<OrderItemEntity> orderItems;
/** 订单计算的应付价格 **/
private BigDecimal payPrice;
/** 运费 **/
private BigDecimal fare;
}
创建订单和订单项
用IdWorker生成订单号,是时间和本身对象的组合;
构建订单。此时还没商品,用threadlocal保存一些当前线程的数据,就不用写形参了;
构建订单项。填入具体的商品,涉及锁库存的问题;
计算价格
//2. 创建订单、订单项
OrderCreateTo order =createOrderTo(memberResponseVo,submitVo);
private OrderCreateTo createOrderTo(MemberResponseVo memberResponseVo, OrderSubmitVo submitVo) {
//2.1 用IdWorker生成订单号
String orderSn = IdWorker.getTimeId();
//2.2 构建订单
OrderEntity entity = buildOrder(memberResponseVo, submitVo,orderSn);
//2.3 构建订单项
List<OrderItemEntity> orderItemEntities = buildOrderItems(orderSn);
//2.4 计算价格
compute(entity, orderItemEntities);
OrderCreateTo createTo = new OrderCreateTo();
createTo.setOrder(entity);
createTo.setOrderItems(orderItemEntities);
return createTo;
}
// 构建订单
private OrderEntity buildOrder(MemberResponseVo memberResponseVo, OrderSubmitVo submitVo, String orderSn) {
OrderEntity orderEntity =new OrderEntity();
orderEntity.setOrderSn(orderSn);
//1) 设置用户信息
orderEntity.setMemberId(memberResponseVo.getId());
orderEntity.setMemberUsername(memberResponseVo.getUsername());
//2) 获取邮费和收件人信息并设置
FareVo fareVo = wareFeignService.getFare(submitVo.getAddrId());
BigDecimal fare = fareVo.getFare();
orderEntity.setFreightAmount(fare);
MemberAddressVo address = fareVo.getAddress();
orderEntity.setReceiverName(address.getName());
orderEntity.setReceiverPhone(address.getPhone());
orderEntity.setReceiverPostCode(address.getPostCode());
orderEntity.setReceiverProvince(address.getProvince());
orderEntity.setReceiverCity(address.getCity());
orderEntity.setReceiverRegion(address.getRegion());
orderEntity.setReceiverDetailAddress(address.getDetailAddress());
//3) 设置订单相关的状态信息
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setConfirmStatus(0);
orderEntity.setAutoConfirmDay(7);
return orderEntity;
}
构建订单项
订单项指的是订单里具体的商品
**StringUtils.collectionToDelimitedString(list, “;分隔符”)**工具可以集合/数组转string;
订单项得算优惠后的价格;
用BigDecimal精确计算
// OrderServiceImpl
private List<OrderItemEntity> buildOrderItems(String orderSn) {
// 这里是最后一次来确认购物项的价格 这个远程方法还会查询一次数据库
List<OrderItemVo> cartItems = cartFeignService.getCurrentUserCartItems();
List<OrderItemEntity> itemEntities = null;
if(cartItems != null && cartItems.size() > 0){
itemEntities = cartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
}
return itemEntities;
}
/**
* 构建某一个订单项
*/ // OrderServiceImpl
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
// 1.订单信息: 订单号
// 已经在items里设置了
// 2.商品spu信息
Long skuId = cartItem.getSkuId();
// 远程获取spu的信息
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuId(spuInfo.getId());
itemEntity.setSpuBrand(spuInfo.getBrandId().toString());
itemEntity.setSpuName(spuInfo.getSpuName());
itemEntity.setCategoryId(spuInfo.getCatalogId());
// 3.商品的sku信息
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
// 把一个集合按照指定的字符串进行分割得到一个字符串
// 属性list生成一个string
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
itemEntity.setSkuAttrsVals(skuAttr);
itemEntity.setSkuQuantity(cartItem.getCount());
// 4.积分信息 买的数量越多积分越多 成长值越多
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue());
// 5.订单项的价格信息 优惠金额
itemEntity.setPromotionAmount(new BigDecimal("0.0")); // 促销打折
itemEntity.setCouponAmount(new BigDecimal("0.0")); // 优惠券
itemEntity.setIntegrationAmount(new BigDecimal("0.0")); // 积分
// 当前订单项的原价
BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
// 减去各种优惠的价格
BigDecimal subtract =
orign.subtract(itemEntity.getCouponAmount()) // 优惠券逻辑没有写,应该去coupon服务查用户的sku优惠券
.subtract(itemEntity.getPromotionAmount()) // 官方促销
.subtract(itemEntity.getIntegrationAmount()); // 京豆/积分
itemEntity.setRealAmount(subtract);
return itemEntity;
}
商品项价格计算完毕创建订单
private OrderCreateTo createOrder() {
OrderCreateTo orderCreateTo = new OrderCreateTo();
// 1. 生成一个订单号
String orderSn = IdWorker.getTimeId();
// 填充订单的各种基本信息,价格信息
OrderEntity orderEntity = buildOrderSn(orderSn);
// 2. 获取所有订单项 // 从里面已经设置好了用户该使用的价格
List<OrderItemEntity> items = buildOrderItems(orderSn);
// 3.根据订单项计算价格 传入订单 、订单项 计算价格、积分、成长值等相关信息
computerPrice(orderEntity, items);
orderCreateTo.setOrder(orderEntity);
orderCreateTo.setOrderItems(items);
return orderCreateTo;
}
计算总价
private void computerPrice(OrderEntity orderEntity, List<OrderItemEntity> items) {
// 叠加每一个订单项的金额
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal gift = new BigDecimal("0.0");
BigDecimal growth = new BigDecimal("0.0");
// 总价
BigDecimal totalPrice = new BigDecimal("0.0");
for (OrderItemEntity item : items) { // 这段逻辑不是特别合理,最重要的是累积总价,别的可以跳过
// 优惠券的金额
coupon = coupon.add(item.getCouponAmount());
// 积分优惠的金额
integration = integration.add(item.getIntegrationAmount());
// 打折的金额
promotion = promotion.add(item.getPromotionAmount());
BigDecimal realAmount = item.getRealAmount();
totalPrice = totalPrice.add(realAmount);
// 购物获取的积分、成长值
gift.add(new BigDecimal(item.getGiftIntegration().toString()));
growth.add(new BigDecimal(item.getGiftGrowth().toString()));
}
// 1.订单价格相关 总额、应付总额
orderEntity.setTotalAmount(totalPrice);
orderEntity.setPayAmount(totalPrice.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
orderEntity.setCouponAmount(coupon);
// 设置积分、成长值
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
// 设置订单的删除状态
orderEntity.setDeleteStatus(OrderStatusEnum.CREATE_NEW.getCode());
}
验价
计算完总价后返回主逻辑,将"页面提交的价格"和"后台计算的价格"进行对比,若不同则提示用户商品价格发生变化
// @GlobalTransactional
@Transactional
@Override // OrderServiceImpl
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
// 1. 验证令牌 [必须保证原子性] 返回 0 or 1
if (result == 0L) { // 令牌验证失败
} else { // 令牌验证成功
// 1 .创建订单等信息
OrderCreateTo order = createOrder();
// 2. 验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal voPayPrice = vo.getPayPrice();// 获取带过来的价格
if (Math.abs(payAmount.subtract(voPayPrice).doubleValue()) < 0.01) {
/****************/
}else {
//验价失败
responseVo.setCode(2);
return responseVo;
}
}
}
保存订单到数据库
private void saveOrder(OrderCreateTo orderCreateTo) {
OrderEntity order = orderCreateTo.getOrder();
order.setCreateTime(new Date());
order.setModifyTime(new Date());
this.save(order);
orderItemService.saveBatch(orderCreateTo.getOrderItems());
}
锁定库存发送延迟队列
锁定库存失败要取消订单
// 在订单里的逻辑:
// 前面是创建订单、订单项、验价等逻辑...
// .....
//
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList());
// 去锁库存 @RequestMapping("/lock/order")
R r = wareFeignService.orderLockStock(orderItemVos);
//5.1 锁定库存成功
if (r.getCode()==0){
responseVo.setOrder(order.getOrder());
responseVo.setCode(0);
return responseVo;
}else {
//5.2 锁定库存失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
锁定库存远程服务
找出所有库存大于商品数的仓库;
遍历所有满足条件的仓库,逐个尝试锁库存,若锁库存成功则退出遍历。
/**
* 锁定库存WareSkuController
* @param vo
*
* 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消或者被用户手动取消,都要解锁库存
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
* 3)、
*
* @return
*/
@PostMapping(value = "/lock/order")
public R orderLockStock(@RequestBody WareSkuLockVo vo) {
try {
boolean lockStock = wareSkuService.orderLockStock(vo);
return R.ok().setData(lockStock);
} catch (NoStockException e) {
return R.error(NO_STOCK_EXCEPTION.getCode(),NO_STOCK_EXCEPTION.getMessage());
}
}
/**
* 锁定库存WareSkuServiceImpl
* 为某个订单锁定库存
* @param vo
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单详情信息
* 便于追溯进行消息撤回
*/
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskEntity.setCreateTime(new Date());
wareOrderTaskService.save(wareOrderTaskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存
//2、找到每个商品在哪个仓库都有库存
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> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIdList);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
//没有任何仓库有这个商品的库存(注意可能会回滚之前的订单项,没关系)
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所以就不用解锁
for (Long wareId : wareIds) {
//锁库存,更新sql用到了cas, 锁定成功就返回1,失败就返回0
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if (count == 1) {
skuStocked = true;
WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
.skuId(skuId)
.skuName("")
.skuNum(hasStock.getNum())
.taskId(wareOrderTaskEntity.getId())
.wareId(wareId)
.lockStatus(1)
.build();
// db保存订单sku项工作单详情,告诉商品锁的哪个库存
wareOrderTaskDetailService.save(taskDetailEntity);
//TODO 发送库存锁定消息到延迟队列,告诉MQ库存锁定成功
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(wareOrderTaskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(taskDetailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
// 发送
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
break; // 一定要跳出,防止重复发送多余消息
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功的
return true;
}
-- 新建商品库存表
DROP TABLE IF EXISTS `wms_ware_sku`;
CREATE TABLE `wms_ware_sku` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id',
`ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id',
`stock` int(11) NULL DEFAULT NULL COMMENT '库存数',
`sku_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku_name',
`stock_locked` int(11) NULL DEFAULT 0 COMMENT '锁定库存',
PRIMARY KEY (`id`) USING BTREE,
INDEX `sku_id`(`sku_id`) USING BTREE,
INDEX `ware_id`(`ware_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品库存' ROW_FORMAT = Dynamic;
<update id="lockSkuStock">
UPDATE `wms_ware_sku` SET stock_locked = stock_locked + #{num}
WHERE sku_id = #{skuId}
AND ware_id = #{wareId}
AND stock-stock_locked >= #{num}
update>
小结
这里通过异常机制控制事务回滚,如果在锁定库存失败则抛出NoStockExceptions,订单服务和库存服务都会回滚。
优化逻辑为:锁库存后,把内容发到消息队列里
消息队列并不立刻消费,而是让其过期,过期后重新入队别的消息队列,别的消息队列拿到后验证订单是否被支付,没被支付的话还原到库存里。
内容介绍
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
快速开始:http://seata.io/zh-cn/docs/user/quickstart.html
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
流程分析
TM告诉TC开启一个全局事务。
storage注册分支事务,实时向TC汇报分支状态。
account失败,告诉TC失败了,TC回滚全部全局事务。
使用@GlobalTransactional 注解在业务方法上
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
创建日志表
有业务步骤,但是SEATA AT模式需要 UNDO_LOG 表,记录之前执行的操作。每个涉及的子系统对应的数据库都要新建表
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
整合应用
从 https://github.com/seata/seata/archive/v0.7.1.zip 下载服务器软件包senta-server-0.7.1,将其解压缩,作为TC;
为了节省git资源,我们下载源码的项目自己编译;
编译项目
(1) 下载后复制到guli项目下,然后在File -> Project Structure -> Modules 中点击+号Import Module,选择项目里的seata;
(2) 会有报错,protobuf这个包找不到。在idea中安装proto buffer editor插件,重启idea(还找不到就重新编译一下,在mvn中找到seata-serializer子项目,点击protobuf里的compile选项。有个grpc的test报错,先全注释掉)
(3) 有一个server项目,找到注册中心配置resource/registry.conf,修改启动的nacos信息。可以修改注册中心和配置中心(先不用管file.conf)
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 修改这个
type = "nacos"
nacos {
# 修改这个
serverAddr = "localhost:8848"
namespace = "public"
cluster = "default"
}
(4) 启动server下的主类;
(5) 在nacos中看到一个serverAddr服务
添加注解
在大事务的入口标记注解@GlobalTransactional开启全局事务,并且每个小事务标记注解@Transactional。
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
}
使用参考链接:https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata
配置数据源
注入 DataSourceProxy
因为Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚
// 方式一
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
*
* @param druidDataSource The DruidDataSource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
// 方式二
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
注意事项:
file.conf 的 service.vgroup_mapping 配置必须和spring.application.name一致
在GlobalTransactionAutoConfiguration类中,默认会使用 ${spring.application.name}-fescar-service-group作为服务名注册到 Seata Server上(每个小事务也要注册到tc上),如果和file.conf中的配置不一致,会提示 no available server to connect错误
可以通过配置yaml的 spring.cloud.alibaba.seata.tx-service-group 修改后缀,但是必须和file.conf中的配置保持一致。
修改配置文件
在order、ware中都配置好上面的配置;
然后它还要求每个微服务要有register.conf和file.conf;
将register.conf和file.conf复制到需要开启分布式事务的根目录,并修改file.conf中配置vgroup_mapping.${application.name}-fescar-service-group = "default"
service {
#vgroup->rgroup
vgroup_mapping.gulimall-ware-fescar-service-group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
小结
tcc也可以看samples。
但是上面使用的是AT模式,2pc不适用高并发,发生了几次远程调用。去保护spu,适合使用at模式。
高并发,如下单,at模式有很多锁,影响效率。所以不使用at tcc。
使用消息队列,失败了之后发消息。库存服务本身也可以使用自动解锁模式。
自动解锁:定期全部检索很麻烦,所以引入延迟队列。库存服务订阅消息队列,库存解锁发给消息队列
保存库存工作单和库存工作单详情,锁定库存后数据库记录。后面的事务失败后看前面的库存,有没解锁的就解锁。
锁库存后害怕订单失败,锁库存后发送给消息队列,只不过要暂存一会先别被消费,半小时以后再消费就可以知道大事务成功没有。
场景介绍
比如未付款订单,超过一定时间后,系统自动取消订单并释放占有商品库存。
方案对比
定时任务:spring的schedule定时任务轮询数据库
消耗系统内存、增加了数据库的压力、存在较大时间误差;
存在超时和检测时间段错开的情况(时效性问题),最高等2倍的定时任务时间
rabbitmq的消息TTL和死信Exchange结合(推荐)
订单关了之后40分钟后库存检查订单存在还是取消。
下订单延迟队列,不要设置消息过期,要设置为队列过期方式。节省一个交换机,使用bean方式创建交换机。
内容介绍
延迟队列存储的对象肯定是对应的延时消息,所谓"延时消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
内容简介
rabbitmq可以通过 设置队列的TTL + 死信路由 实现延迟队列
TTL(Time-To-Live 消息存活时间)
RabbitMQ可以针对Queue设置x-expires 或者 针对Message设置 x-message-ttl,来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
死信路由DLX
RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
流程图示
针对订单模块创建以上消息队列,创建订单时消息会被发送至队列order.delay.queue,经过TTL的时间后消息会变成死信以order.release.order的路由键经交换机转发至队列order.release.order.queue,再通过监听该队列的消息来实现过期订单的处理。
订单超时未支付触发订单过期状态修改与库存解锁
创建订单时消息会被发送至队列order.delay.queue,经过TTL的时间后消息会变成死信以order.release.order的路由键经交换机转发至队列order.release.order.queue,再通过监听该队列的消息来实现过期订单的处理
如果该订单已支付,则无需处理;
否则说明该订单已过期,修改该订单的状态并通过路由键order.release.other发送消息至队列stock.release.stock.queue进行库存解锁。
库存锁定后延迟检查是否需要解锁库存
在库存锁定后通过路由键stock.locked发送至延迟队列stock.delay.queue,延迟时间到,死信通过路由键stock.release转发至stock.release.stock.queue,通过监听该队列进行判断当前订单状态,来确定库存是否需要解锁。
内容介绍
在ware和order中配置好pom、yaml、@EnableRabbit
订单模块
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class MyRabbitMQConfig {
/* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
/**
* 死信队列
*
* @return
*/@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map 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;
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map 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);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
/**
* 商品秒杀队列
* @return
*/
@Bean
public Queue orderSecKillOrrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
}
库存模块
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class MyRabbitMQConfig {
/**
* 使用JSON序列化机制,进行消息转换
* @return
*/
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
// @RabbitListener(queues = "stock.release.stock.queue")
// public void handle(Message message) {
//
// }
/**
* 库存服务默认的交换机
* @return
*/
@Bean
public Exchange stockEventExchange() {
//String name, boolean durable, boolean autoDelete, Map arguments
TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
return topicExchange;
}
/**
* 普通队列,用于解锁库存
* @return
*/
@Bean
public Queue stockReleaseStockQueue() {
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
Queue queue = new Queue("stock.release.stock.queue", true, false, false);
return queue;
}
/**
* 延迟队列
* @return
*/
@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;
}
/**
* 交换机与普通队列绑定
* @return
*/
@Bean
public Binding stockLocked() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map arguments
Binding binding = new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
return binding;
}
/**
* 交换机与延迟队列绑定
* @return
*/
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}
库存锁定
业务逻辑
由于可能订单回滚的情况,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单,其中包括订单信息和锁定库存时的信息(仓库id,商品id,锁了几件…);
在锁定成功后,向延迟队列发消息,带上库存锁定的相关信息
代码逻辑(具体参照上面提交订单)
遍历订单项,遍历每个订单项的每个库存,直到锁到库存;
发消息后库存回滚也没关系,用id是查不到数据库的;
数据库锁定库存SQL
<update id="lockSkuStock">
UPDATE `wms_ware_sku` SET stock_locked = stock_locked + #{num}
WHERE sku_id = #{skuId}
AND ware_id = #{wareId}
AND stock-stock_locked >= #{num}
</update>
接收消息
业务逻辑
延迟队列会将过期的消息路由至"stock.release.stock.queue",通过监听该队列实现库存的解锁;
为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队
库存解锁
代码逻辑
如果工作单详情不为空,说明该库存锁定成功:
(1) 查询最新的订单状态;
(2) 如果订单不存在,说明订单提交出现异常回滚;
(3) 如果订单存在(但订单处于已取消的状态),我们都对已锁定的库存进行解锁
如果工作单详情为空,说明库存未锁定,自然无需解锁;
为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁;
解锁库存同时更改工作单状态为已解锁。
监听器异步解锁
import com.rabbitmq.client.Channel;
import com.xunqi.common.to.OrderTo;
import com.xunqi.common.to.mq.StockLockedTo;
import com.xunqi.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*
* 2、订单失败
* 库存锁定失败
*
* 只要解锁库存的消息失败,一定要告诉服务解锁失败
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.info("******收到解锁库存的信息******");
try {
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
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 {
log.info("******收到订单关闭,准备解锁库存的信息******");
try {
wareSkuService.unlockStock(orderTo);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
解锁库存核心sql
<update id="unlockStock">
UPDATE `wms_ware_sku`
SET stock_locked = stock_locked - #{num}
WHERE sku_id = #{skuId}
AND ware_id = #{wareId}
</update>
解锁库存核心代码
@Override
public void unlockStock(StockLockedTo to) {
log.info("收到解锁库存的消息");
//库存工作单的id
StockDetailTo detail = to.getDetailTo();
Long detailId = detail.getId();
/**
* 解锁
* 1、查询数据库关于这个订单锁定库存信息
* 有:证明库存锁定成功了
* 解锁:订单状况
* 1、没有这个订单,必须解锁库存
* 2、有这个订单,不一定解锁库存
* 订单状态:已取消:解锁库存
* 已支付:不能解锁库存
* 没有:就是库存锁定失败, 库存回滚了 这种情况无需回滚
*/
WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
if (taskDetailInfo != null) {
//查出wms_ware_order_task工作单的信息
Long id = to.getId();
WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
//获取订单号查询订单状态 已取消才解锁库存
String orderSn = orderTaskInfo.getOrderSn();
//远程查询订单信息
R orderData = orderFeignService.getOrderStatus(orderSn);
if (orderData.getCode() == 0) {
//订单数据返回成功
OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});
//判断订单状态是否已取消或者支付或者订单不存在
if (orderInfo == null || orderInfo.getStatus() == 4) {
//订单已被取消,才能解锁库存
if (taskDetailInfo.getLockStatus() == 1) {
//当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁
unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
}
}
} else {
//消息拒绝以后重新放在队列里面,让别人继续消费解锁
//远程调用服务失败
throw new RuntimeException("远程调用服务失败");
}
} else {
//无需解锁
}
}
/**
* 解锁库存的方法
* @param skuId
* @param wareId
* @param num
* @param taskDetailId
*/
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
//变为已解锁
taskDetailEntity.setLockStatus(2);
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
其他
boolean match = new AntPathMatcher().match("order/order/status/**", uri);
get方法,安全性还好,如果修改的url呢?前面主要是因为没带redis-key查询session,所以我们或许**可以在远程调用中想办法传入redis-key**。
提交订单
详见上方订单提交模块
监听队列
业务逻辑
创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue,因此我们对该队列进行监听,进行订单的关闭
监听器异步关单
import com.rabbitmq.client.Channel;
import com.xunqi.gulimall.order.entity.OrderEntity;
import com.xunqi.gulimall.order.service.OrderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
private 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);
}
}
}
关闭订单
业务逻辑
由于要保证幂等性,因此要查询最新的订单状态判断是否需要关单;
关闭订单后也需要解锁库存,因此发送消息进行库存、会员服务对应的解锁
核心代码
/**
* 关闭订单
* @param orderEntity
*/
@Override
public void closeOrder(OrderEntity orderEntity) {
//关闭订单之前先查询一下数据库,判断此订单状态是否已支付
OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn",orderEntity.getOrderSn()));
//如果订单还处于新创建的状态,说明超时未支付,进行关单
if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
//代付款状态进行关单
OrderEntity orderUpdate = new OrderEntity();
orderUpdate.setId(orderInfo.getId());
orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(orderUpdate);
// 关单后发送消息给MQ通知其他服务进行关单相关的操作,如解锁库存
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderInfo, orderTo);
try {
//TODO 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
//TODO 定期扫描数据库,重新发送失败的消息
}
}
}
解锁库存
监听器异步解锁库存
import com.rabbitmq.client.Channel;
import com.xunqi.common.to.OrderTo;
import com.xunqi.common.to.mq.StockLockedTo;
import com.xunqi.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
/**
* 1、库存自动解锁
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*
* 2、订单失败
* 库存锁定失败
*
* 只要解锁库存的消息失败,一定要告诉服务解锁失败
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.info("******收到解锁库存的信息******");
try {
//当前消息是否被第二次及以后(重新)派发过来了
// Boolean redelivered = message.getMessageProperties().getRedelivered();
//解锁库存
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 {
log.info("******收到订单关闭,准备解锁库存的信息******");
try {
wareSkuService.unlockStock(orderTo);
// 手动删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
// 解锁失败 将消息重新放回队列,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
解锁库存核心代码
/**
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
* 导致卡顿的订单,永远都不能解锁库存
* @param orderTo
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.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 taskDetailEntity : list) {
unLockStock(taskDetailEntity.getSkuId(),
taskDetailEntity.getWareId(),
taskDetailEntity.getSkuNum(),
taskDetailEntity.getId());
}
}
【谷粒商城】分布式事务与下单
https://blog.csdn.net/hancoder/article/details/114983771
全网最强电商教程《谷粒商城》对标阿里P6/P7,40-60万年薪
https://www.bilibili.com/video/BV1np4y1C7Yf?p=284
mall源码工程
https://github.com/CharlesKai/mall