说明:本文章是基于springcloud+springboot+rabbitmq实现的分布式事务,注册中心为eureka,服务调用为openfeign。使用简单消息队列完成分布式事务【即补偿机制】,下篇文章使用死信队列完成分布式。
业务需求:用户支付功能,如果用户支付成功,需要改变订单表中订单状态为支付状态【跨服务调用修改订单状态接口】,同时库存表中减库存数量【跨服务调用修改库存数量接口】。
下面提到的回款操作是商家不愿意看到,很多商家不愿意这样,这里回款操作只是文章需要。
实现逻辑分析:
1.如果用户支付失败,直接抛异常不走下面操作,只需要本地事务回滚即可。
2.如果用户支付成功,调用修改订单状态接口失败,需要在支付接口中抛异常,通过异常处理给已经支付从用户进行回款操作
3.如果用户支付成功,调用修改订单状态接口成功,调用修改库存接口失败【调用修改库存需要使用消息中间件处理】,也就是消息中间件发送消息失败或者消息消费失败两种情况:a.如果是发送消息失败,处理方案为采取消息发送回调机制,在回调机制中进行重试机制或者说消息发送失败根本就不想进行重试机制,那么就选择补偿机制,即在回调机制中进行回款操作和更改订单为未支付状态。b.如果是消费消息失败,那么采用手动签收消息机制,通过异常处理消息,catch到异常进行补偿机制,即回款操作和修改订单状态为未支付状态【其实回款操作也是通过消息队列实现,这里就不说了】。
不说了,上代码,代码分为 用户支付服务 和 商品订单库存服务
一:用户支付服务工程目录说明:
pom.xml文件
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
application.yml文件【重点为rabbitmq配置,更改你的mq配置】
server: port: 7200 spring: application: name: cloud-affair-start-7200-service datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/myMq?useSSL=false username: postgres password: root rabbitmq: #host: 10.88.104.177 host: 11.11.177.170 #host: 11.11.177.170 port: 45672 username: liqingwei01 # sod-rabbitmq password: liqingwei01 virtual-host: mymqdemo ###开启消息确认机制 confirms publisher-confirms: true publisher-returns: true eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:80/eureka mybatis-plus: mapper-locations: classpath:mapping/*.xml type-aliases-package: com.seats.com.affstart.model #开启驼峰 configuration: map-underscore-to-camel-case: true #分页插件 pagehelper: auto-dialect: postgresql reasonable: true support-methods-arguments: true
GoodsStockModel实体
import lombok.Data; /** * 商品库存 * @author 李庆伟 * @date 2021/6/11 10:32 */ @Data public class GoodsStockModel { private String id;//商品id private String goodsName;//商品名称 private String goodsNum;//商品数量 private String goodsType;//商品类型 }
OrderModel实体
import lombok.Data; import java.util.Date; /** * 订单 * @author 李庆伟 * @date 2021/6/11 10:31 */ @Data public class OrderModel { private String id;//订单id private String orderNo;//订单编号 private String goodsId;//商品id private String goodsNum;//商品数量 private String orderType;//订单状态 0未支付 1支付 private Date createDate;//订单创建时间 private String createUser;//下单人 }
RabbitMqConfig文件
import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; /** * 交换机和队列声明 * @author 李庆伟 * @date 2021/6/11 16:58 */ @Component public class RabbitMqConfig { // 减库存队列 public static final String GOODS_STOCK_CUT_QUEUE = "goods_stock_cut_queue"; // 库存队列交换机 private static final String GOODS_STOCK_EXCHANGE = "goods_stock_exchange"; // 1.减库存队列 @Bean public Queue directGoodsCutQueue() { return new Queue(GOODS_STOCK_CUT_QUEUE); } // 2.库存队列交换机 @Bean DirectExchange directGoodsExchange() { return new DirectExchange(GOODS_STOCK_EXCHANGE); } // 3.减库存队列与交换机绑定 @Bean Binding bindingExchangeGoodsCutQueue() { return BindingBuilder.bind(directGoodsCutQueue()).to(directGoodsExchange()).with("goodsStockRoutingKey"); } }
RabbitGoodsStockUtil文件
import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * 发送消息实现减库存操作 * @author 李庆伟 * @date 2021/6/7 20:41 */ @Component public class RabbitGoodsStockUitl implements RabbitTemplate.ConfirmCallback{ @Autowired private RabbitTemplate rabbitTemplate; public void send(String orderId) { // 封装消息 Message message = MessageBuilder.withBody(orderId.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON) .setContentEncoding("utf-8").setMessageId(orderId).build(); // 构建回调返回的数据 CorrelationData correlationData = new CorrelationData(orderId); // 发送消息 rabbitTemplate.setMandatory(true); rabbitTemplate.setConfirmCallback(this); rabbitTemplate.convertAndSend("goods_stock_exchange", "goodsStockRoutingKey", message, correlationData); System.out.println("消息发送完成了。。。。。。。。。"); System.out.println("消息发送完成了。。。。。。。。。"); System.out.println("消息发送完成了。。。。。。。。。"); } @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { String orderId = correlationData.getId(); //id 都是相同的哦 全局ID System.out.println("消息id:" + correlationData.getId()); if (ack) { //消息发送成功 System.out.println("消息发送确认成功"); } else { //1.如果进行重试机制 System.out.println("消息发送确认失败:" + cause); //重试机制 send(orderId); //2.消息发送失败根本就不想进行重试机制,就注释掉 send(orderId); // 2.1回款操作 // 2.2更改订单为未支付状态(补偿机制)即调用订单支付状态接口修改订单为未支付状态 } } }
AffairProService文件
import com.affstart.model.OrderModel; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; /** * 远程调用服务 * @author 李庆伟 * @date 2021/6/6 17:26 */ @FeignClient("cloud-affair-end-9200-service") public interface AffairProService { /** * 订单查询 * [orderId] * @return {@link OrderModel} * @throws * @author 李庆伟 * @date 2021/6/20 19:46 */ @PostMapping("/orders/show/{orderId}") public OrderModel show(@PathVariable("orderId") String orderId); /** * 修改订单状态 * [orderModel] * @return {@link String} * @throws * @author 李庆伟 * @date 2021/6/20 19:46 */ @PostMapping("/orders/update") public String update(@RequestBody OrderModel orderModel); }
PaymentsController文件
import com.affstart.service.PaymentsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * 支付服务 * @author 李庆伟 * @date 2021/6/11 10:47 */ @RequestMapping("payments") @RestController public class PaymentsController { @Autowired private PaymentsService paymentsService; /** * 模拟支付接口 * 支付完成后会调用: * 1.修改订单为支付状态接口 * 2.减库存接口 * [orderId] * @return {@link String} * @throws * @author 李庆伟 * @date 2021/6/11 14:20 */ @PostMapping("makePayments/{orderId}") public String makePayments(@PathVariable("orderId") String orderId){ paymentsService.makePayments(orderId); return "ok"; } }
PaymentsService文件
/** * @author 李庆伟 * @date 2021/6/7 11:24 */ public interface PaymentsService { void makePayments(String orderId); }
PaymentsServiceImpl文件
import com.affstart.config.RabbitGoodsStockUitl; import com.affstart.feign_service.AffairProService; import com.affstart.model.OrderModel; import com.affstart.service.PaymentsService; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; /** * @author 李庆伟 * @date 2021/6/7 11:24 */ @Service public class PaymentsServiceImpl implements PaymentsService { @Resource private AffairProService affairProService; @Autowired private RabbitGoodsStockUitl rabbitGoodsStockUitl; /** * 模拟支付接口 * 支付完成后会调用: * 1.修改订单为支付状态接口 * 2.减库存接口 */ @Transactional public void makePayments(String orderId) { //调用查询订单 OrderModel orderModel = affairProService.show(orderId); if(orderModel == null){ throw new RuntimeException("订单错误。。。。"); } //模拟支付 System.out.println("支付完成啦啦啦。。。。。。。。。"); //调用订单支付状态接口 orderModel.setOrderType("1"); String orderType = affairProService.update(orderModel); if(orderType == null || StringUtils.isEmpty(orderType)){ throw new RuntimeException(orderId+"-订单状态修改失败" + "【异常处理时需要对已经支付的用户进行回款操作,此次省略】"); } //消息队列调用减库存接口 rabbitGoodsStockUitl.send(orderId); } }
二:订单库存服务工程目录说明:
pom.xml文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
application.yml文件【重点为rabbitmq配置,更改你的mq配置】
server: port: 9200 spring: application: name: cloud-affair-end-9200-service datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/myMq?useSSL=false username: postgres password: root rabbitmq: host: 11.11.177.170 port: 45672 username: liqingwei01 password: liqingwei01 virtual-host: mymqdemo listener: simple: retry: ####开启消费者(程序出现异常的情况下会)进行重试 enabled: true ####最大重试次数 max-attempts: 5 ####重试间隔次数 initial-interval: 3000 ####开启手动ack acknowledge-mode: manual eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:80/eureka mybatis-plus: mapper-locations: classpath:mapping/*.xml type-aliases-package: com.seats.com.affstart.model #开启驼峰 configuration: map-underscore-to-camel-case: true #分页插件 pagehelper: auto-dialect: postgresql reasonable: true support-methods-arguments: true
GoodsStockModel
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; /** * 商品库存 * @author 李庆伟 * @date 2021/6/11 10:32 */ @Data @TableName("t_goods_stock") public class GoodsStockModel { @TableId private String id;//商品id private String goodsName;//商品名称 private Integer goodsNum;//商品数量 private String goodsType;//商品类型 }
实体OrderModel实体
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.util.Date; /** * 订单 * @author 李庆伟 * @date 2021/6/11 10:31 */ @Data @TableName("t_order") public class OrderModel { @TableId private String id;//订单id private String orderNo;//订单编号 private String goodsId;//商品id private Integer goodsNum;//商品数量 private String orderType;//订单状态 0未支付 1支付 private Date createDate;//订单创建时间 private String createUser;//下单人 }
RabbitMqConfig文件
import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; /** * 交换机和队列声明 * @author 李庆伟 * @date 2021/6/11 16:58 */ @Component public class RabbitMqConfig { // 减库存队列 public static final String GOODS_STOCK_CUT_QUEUE = "goods_stock_cut_queue"; // 库存队列交换机 private static final String GOODS_STOCK_EXCHANGE = "goods_stock_exchange"; // 1.减库存队列 @Bean public Queue directGoodsCutQueue() { return new Queue(GOODS_STOCK_CUT_QUEUE); } // 2.库存队列交换机 @Bean DirectExchange directGoodsExchange() { return new DirectExchange(GOODS_STOCK_EXCHANGE); } // 3.减库存队列与交换机绑定 @Bean Binding bindingExchangeGoodsCutQueue() { return BindingBuilder.bind(directGoodsCutQueue()).to(directGoodsExchange()).with("goodsStockRoutingKey"); } }
GoodsStockMqListener文件
import com.affend.model.GoodsStockModel; import com.affend.model.OrderModel; import com.affend.service.GoodsStockService; import com.affend.service.OrderService; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.stereotype.Component; import com.rabbitmq.client.Channel; import java.util.Map; /** * 商品库存服务 * @author 李庆伟 * @date 2021/6/11 10:47 */ @Component public class GoodsStockMqListener { @Autowired private GoodsStockService goodsStockService; @Autowired private OrderService orderService; @RabbitListener(queues = "goods_stock_cut_queue") public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception { String orderId = new String(message.getBody(), "UTF-8"); OrderModel orderModel = orderService.show(orderId); GoodsStockModel goodsStockModel = goodsStockService.show(orderModel.getGoodsId()); goodsStockModel.setGoodsNum(goodsStockModel.getGoodsNum() - orderModel.getGoodsNum()); try { //int a = 1/0;//模拟减库存失败 int cutNum = goodsStockService.update(goodsStockModel);//减库存操作 if (cutNum > 0) { // 手动签收消息,通知mq服务器端删除该消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } } catch (Exception e) { e.printStackTrace(); orderModel.setOrderType("0"); int orderNum = orderService.update(orderModel);//修改订单为未支付状态 //这里省略用户回款操作 if(orderNum == 1){ //丢弃该消息 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); }else {//会再次执行process方法 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } } } }
GoodsStockController文件和OrderController文件
import com.affend.model.GoodsStockModel; import com.affend.service.GoodsStockService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * 商品库存服务 * @author 李庆伟 * @date 2021/6/11 10:47 */ @RequestMapping("goodsStock") @RestController public class GoodsStockController { @Autowired private GoodsStockService goodsStockService; /** * 修改库存 * 订单支付成后,扣除库存 * [goodsStockModel] * @return {@link String} * @throws * @author 李庆伟 * @date 2021/6/11 11:06 */ @PostMapping("update") public String update(@RequestBody GoodsStockModel goodsStockModel){ int num = goodsStockService.update(goodsStockModel); if(num != 1){ return "error"; } return "ok"; } /** * 库存详情 * [goodsId] * @return {@link GoodsStockModel} * @throws * @author 李庆伟 * @date 2021/6/20 20:10 */ @PostMapping("show/{goodsId}") public GoodsStockModel show(@PathVariable("goodsId") String goodsId){ return goodsStockService.show(goodsId); } }
import com.affend.model.OrderModel; import com.affend.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * 订单服务 * @author 李庆伟 * @date 2021/6/11 10:47 */ @RequestMapping("orders") @RestController public class OrderController { @Autowired private OrderService orderService; /** * 修改订单 * 支付成后后,修改订单状态 * [orderModel] * @return {@link String} * @throws * @author 李庆伟 * @date 2021/6/11 11:05 */ @PostMapping("update") public String update(@RequestBody OrderModel orderModel){ int num = orderService.update(orderModel); if(num != 1){ return "error"; } return "ok"; } /** * 根据订单id查询订单详情 * [orderId] * @return {@link String} * @throws * @author 李庆伟 * @date 2021/6/11 11:25 */ @PostMapping("show/{orderId}") public OrderModel show(@PathVariable("orderId") String orderId){ return orderService.show(orderId); } }
GoodsStockService文件和OrderService文件
import com.affend.model.GoodsStockModel; /** * @author 李庆伟 * @date 2021/6/11 10:50 */ public interface GoodsStockService { int update(GoodsStockModel goodsStockModel); GoodsStockModel show(String goodsId); }
import com.affend.model.OrderModel; /** * @author 李庆伟 * @date 2021/6/11 10:50 */ public interface OrderService { int update(OrderModel orderModel); OrderModel show(String orderId); }
GoodsStockServiceImpl文件和OrderServiceImpl文件
import com.affend.mapper.GoodsStockMapper; import com.affend.model.GoodsStockModel; import com.affend.service.GoodsStockService; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * @author 李庆伟 * @date 2021/6/11 10:50 */ @Service public class GoodsStockServiceImpl implements GoodsStockService { @Resource private GoodsStockMapper goodsStockMapper; /** * 修改库存 * [goodsStockModel] * @return {@link int} * @throws * @author 李庆伟 * @date 2021/6/20 20:13 */ public int update(GoodsStockModel goodsStockModel) { return goodsStockMapper.updateById(goodsStockModel); } /** * 库存详情 * [goodsId] * @return {@link GoodsStockModel} * @throws * @author 李庆伟 * @date 2021/6/20 20:13 */ public GoodsStockModel show(String goodsId) { return goodsStockMapper.selectById(goodsId); } }
import com.affend.mapper.OrderMapper; import com.affend.model.OrderModel; import com.affend.service.OrderService; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * @author 李庆伟 * @date 2021/6/11 10:50 */ @Service public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; /** * 修改订单 * [orderModel] * @return {@link int} * @throws * @author 李庆伟 * @date 2021/6/20 20:13 */ public int update(OrderModel orderModel) { return orderMapper.updateById(orderModel); } /** * 订单详情 * [orderId] * @return {@link OrderModel} * @throws * @author 李庆伟 * @date 2021/6/20 20:13 */ public OrderModel show(String orderId) { return orderMapper.selectById(orderId); } }
GoodsStockMapper文件和OrderMapper文件
import com.affend.model.GoodsStockModel; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; /** * @author 李庆伟 * @date 2021/6/11 10:53 */ @Mapper public interface GoodsStockMapper extends BaseMapper<GoodsStockModel> { }
import com.affend.model.OrderModel; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; /** * @author 李庆伟 * @date 2021/6/11 10:53 */ @Mapper public interface OrderMapper extends BaseMapper<OrderModel> { }
库表信息:
到此文章结束。。。。。。
希望对你有所帮助。。。。。。。。。。。。。
下篇文章更新死信队列实现分布式事务。。。。。。。。。。。。