├── .gitignore ├── HELP.md ├── README.md ├── config-service ├── config_server.sql ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── oujiong │ │ └── config │ │ └── service │ │ └── ConfigserviceApplication.java │ └── resources │ └── application.yml ├── eureka ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── oujiong │ │ └── eureka │ │ └── EurekaserverApplication.java │ └── resources │ └── application.yml ├── pom.xml ├── service-order ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── oujiong │ │ └── service │ │ └── order │ │ ├── OrderApplication.java │ │ ├── client │ │ └── ProduceClient.java │ │ ├── config │ │ └── Jms.java │ │ ├── controller │ │ └── OrderController.java │ │ ├── model │ │ └── ProduceOrder.java │ │ ├── mqservice │ │ └── TransactionProducer.java │ │ ├── service │ │ ├── ProduceOrderService.java │ │ └── impl │ │ │ └── ProduceOrderServiceImpl.java │ │ └── untils │ │ └── JsonUtils.java │ └── resources │ └── bootstrap.yml └── service-produce ├── pom.xml └── src └── main ├── java └── com │ └── oujiong │ └── service │ └── produce │ ├── ProduceApplication.java │ ├── config │ └── Jms.java │ ├── controller │ └── ProduceController.java │ ├── model │ └── Produce.java │ ├── mqseivice │ └── OrderConsumer.java │ └── service │ ├── ProduceService.java │ └── impl │ └── ProduceServiceImpl.java └── resources └── bootstrap.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /**/.DS_Store 2 | /logs/ 3 | /dubbo/ 4 | /**/*.iml 5 | /**/test/**/*.java 6 | 7 | target/ 8 | data/ 9 | !.mvn/wrapper/maven-wrapper.jar 10 | 11 | ### STS ### 12 | .apt_generated 13 | .classpath 14 | .factorypath 15 | .project 16 | .settings 17 | .springBeans 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | 25 | ### NetBeans ### 26 | nbproject/private/ 27 | build/ 28 | nbbuild/ 29 | dist/ 30 | nbdist/ 31 | 32 | 33 | .nb-gradle/ 34 | .settings 35 | .classpath 36 | .project 37 | .springBeans 38 | mvnw 39 | mvnw.cmd 40 | .mvn 41 | /bin/ 42 | 43 | 44 | .flattened-pom.xml -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #
RocketMQ实现分布式事务
2 | 3 | 有关RocketMQ实现分布式事务前面写了一篇博客 4 | 5 | 1、[RocketMQ实现分布式事务原理](https://www.cnblogs.com/qdhxhz/p/11191399.html) 6 | 7 | 下面就这个项目做个整体简单介绍。 8 | 9 | ## 一、项目概述 10 | 11 | #### 1、技术架构 12 | 13 | 项目总体技术选型 14 | 15 | ``` 16 | SpringCloud(Finchley.RELEASE) + SpringBoot2.0.4 + Maven3.5.4 + RocketMQ4.3 +MySQL + lombok(插件) 17 | ``` 18 | 19 | 有关SpringCloud主要用到以下四个组建 20 | 21 | ``` 22 | Eureka Server +config-server(配置中心)+ Eureka Client + Feign(服务间调用) 23 | ``` 24 | 25 | 配置中心是用MySQL存储数据。 26 | 27 | #### 2、项目整体结构 28 | 29 | ```makefile 30 | config-service # 配置中心 31 | eureka # 注册中心 32 | service-order #订单微服务 33 | service-produce #商品微服务 34 | ``` 35 | 36 | 各服务的启动顺序就安装上面的顺序启动。 37 | 38 | `大致流程` 39 | 40 | 启动后,配置中心、订单微服务、商品微服务都会将信息注册到注册中心。 41 | 42 | 如果访问:`localhost:7001`(注册中心地址),以上服务都出现说明启动成功。 43 | 44 | ![](https://img2018.cnblogs.com/blog/1090617/201907/1090617-20190717002438786-605382528.png) 45 | 46 | 47 | 48 | #### 3、分布式服务流程 49 | 50 | 用户在订单微服务下单后,会去回调商品微服务去减库存。这个过程需要事务的一致性。 51 | 52 | ![](https://img2018.cnblogs.com/blog/1090617/201907/1090617-20190717002448277-331707552.png) 53 | 54 | 55 | 56 | #### 4、测试流程 57 | 58 | 页面输入: 59 | 60 | ``` 61 | http://localhost:9001/api/v1/order/save?userId=1&productId=1&total=4 62 | ``` 63 | 64 | 订单微服务执行情况(订单服务事务执行成功) 65 | 66 | ![](https://img2018.cnblogs.com/blog/1090617/201907/1090617-20190717002934582-471936462.png) 67 | 68 | 69 | 70 | 71 | 72 | 商品微服务执行情况(商品服务事务执行成功) 73 | 74 | ![](https://img2018.cnblogs.com/blog/1090617/201907/1090617-20190717002506834-1271705975.png) 75 | 76 | 77 | 78 | 当然你也可以通过修改参数来模拟分布式事务出现的各种情况。 79 | 80 |
81 | 82 | ![acda64387e0896604b5932dc433c8b77](https://user-images.githubusercontent.com/37285812/142141841-4f32957b-a85a-4041-9e6f-950dab724821.gif) 83 | -------------------------------------------------------------------------------- /config-service/config_server.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | DROP TABLE IF EXISTS `config_server`; 4 | 5 | CREATE TABLE `config_server` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `akey` varchar(30) DEFAULT NULL, 8 | `avalue` varchar(128) DEFAULT NULL, 9 | `application` varchar(30) DEFAULT NULL, 10 | `aprofile` varchar(30) DEFAULT NULL, 11 | `label` varchar(30) DEFAULT NULL, 12 | PRIMARY KEY (`id`) 13 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 14 | 15 | 16 | 17 | INSERT INTO `config_server` (`id`, `akey`, `avalue`, `application`, `aprofile`, `label`) 18 | VALUES 19 | (2,'name_server','ip(rocketmq服务器地址)','product-service','dev','dev'), 20 | (3,'name_server','ip(rocketmq服务器地址)','order-service','dev','dev'), 21 | (4,'order_topic','order_topic','order-service','dev','dev'), 22 | (5,'order_topic','order_topic','product-service','dev','dev'); 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /config-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-rocketmq-transaction 7 | com.jincou 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | config-service 13 | 14 | 15 | 16 | 17 | org.springframework.cloud 18 | spring-cloud-starter-netflix-eureka-client 19 | 20 | 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-config-server 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-jdbc 31 | 32 | 33 | mysql 34 | mysql-connector-java 35 | 5.1.21 36 | 37 | 38 | -------------------------------------------------------------------------------- /config-service/src/main/java/com/oujiong/config/service/ConfigserviceApplication.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.config.service; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.config.server.EnableConfigServer; 6 | 7 | /** 8 | * @ClassName: ConfigserviceApplication 9 | * @Description: 配置中心 添加注解@EnableConfigServer 10 | * @author xub 11 | * @date 2019/7/12 下午3:39 12 | */ 13 | @SpringBootApplication 14 | @EnableConfigServer 15 | public class ConfigserviceApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(ConfigserviceApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #服务名称 2 | server: 3 | port: 5001 4 | 5 | #连接配置信息 6 | spring: 7 | application: 8 | name: config-server-jdbc 9 | profiles: 10 | active: jdbc 11 | cloud: 12 | config: 13 | server: 14 | default-label: dev 15 | jdbc: 16 | sql: SELECT akey , avalue FROM config_server where APPLICATION=? and APROFILE=? and LABEL=? 17 | ##################################################################################################### 18 | # mysql 属性配置 19 | datasource: 20 | driver-class-name: com.mysql.jdbc.Driver 21 | url: jdbc:mysql://127.0.0.1/config 22 | username: root 23 | password: root 24 | ##################################################################################################### 25 | 26 | 27 | #指定注册中心地址 28 | eureka: 29 | client: 30 | serviceUrl: 31 | defaultZone: http://localhost:7001/eureka/ 32 | 33 | -------------------------------------------------------------------------------- /eureka/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-rocketmq-transaction 7 | com.jincou 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | eureka 13 | 14 | 15 | 16 | 17 | org.springframework.cloud 18 | spring-cloud-starter-netflix-eureka-server 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /eureka/src/main/java/com/oujiong/eureka/EurekaserverApplication.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.eureka; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | 8 | /** 9 | * @Description: 注册中心 10 | * 11 | * @author xub 12 | * @date 2019/7/12 下午12:21 13 | */ 14 | @SpringBootApplication 15 | @EnableEurekaServer 16 | public class EurekaserverApplication { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(EurekaserverApplication.class, args); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /eureka/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 7001 3 | 4 | eureka: 5 | instance: 6 | hostname: localhost 7 | client: 8 | #声明自己是个服务端 9 | registerWithEureka: false #false表示不向注册中心注册自己 10 | fetchRegistry: false #false表示自己就是注册中心,职责是维护实例,不参加检索 11 | serviceUrl: #设置eureka server的交互地址,即对外暴露的地址 12 | defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ 13 | 14 | 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | pom 6 | 7 | eureka 8 | service-produce 9 | service-order 10 | config-service 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 2.0.4.RELEASE 16 | 17 | 18 | com.jincou 19 | spring-cloud-rocketmq-transaction 20 | 0.0.1-SNAPSHOT 21 | spring-cloud-rocketmq-transaction 22 | Demo project for Spring Boot 23 | 24 | 25 | UTF-8 26 | UTF-8 27 | 1.8 28 | Finchley.RELEASE 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-web 40 | 41 | 42 | 43 | 44 | org.projectlombok 45 | lombok 46 | provided 47 | 48 | 49 | 50 | 51 | org.apache.rocketmq 52 | rocketmq-client 53 | 4.3.0 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.springframework.cloud 61 | spring-cloud-dependencies 62 | ${spring-cloud.version} 63 | pom 64 | import 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /service-order/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-rocketmq-transaction 7 | com.jincou 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | service-order 13 | 14 | 15 | 16 | org.springframework.cloud 17 | spring-cloud-starter-netflix-eureka-client 18 | 19 | 20 | 21 | 22 | org.springframework.cloud 23 | spring-cloud-starter-openfeign 24 | 25 | 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter-netflix-hystrix 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.cloud 36 | spring-cloud-config-client 37 | 38 | 39 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/OrderApplication.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | 8 | /** 9 | * @Description: 支持熔断降级 和 Feign 10 | * 11 | * @author xub 12 | * @date 2019/7/12 下午12:54 13 | */ 14 | @SpringBootApplication 15 | @EnableFeignClients 16 | //添加熔断降级注解 17 | @EnableCircuitBreaker 18 | public class OrderApplication { 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(OrderApplication.class, args); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/client/ProduceClient.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.client; 2 | 3 | 4 | import org.springframework.cloud.openfeign.FeignClient; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestParam; 7 | 8 | /** 9 | * @Description: 商品服务客户端 10 | * name = "product-service"是你调用服务端名称 11 | * 12 | * @author xub 13 | * @date 2019/7/12 下午1:01 14 | */ 15 | @FeignClient(name = "product-service") 16 | public interface ProduceClient { 17 | 18 | /** 19 | * @Title: 20 | * @Description: 这样组合就相当于http://product-service/api/v1/product/find 21 | * @author xub 22 | * @throws 23 | */ 24 | @GetMapping("/api/v1/produce/find") 25 | String findById(@RequestParam(value = "produceId") int produceId); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/config/Jms.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.config; 2 | 3 | 4 | import lombok.Data; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | 9 | /** 10 | * @Description: 连接RocketMQ服务器实体 11 | * 12 | * @author xub 13 | * @date 2019/7/15 下午11:30 14 | */ 15 | @Data 16 | @Configuration 17 | public class Jms { 18 | 19 | /** 20 | * 配置中心读取 服务器地址 21 | */ 22 | @Value("${name_server}") 23 | private String nameServer; 24 | 25 | /** 26 | * 配置中心读取 主题 27 | */ 28 | @Value("${order_topic}") 29 | private String orderTopic; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.controller; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.oujiong.service.order.config.Jms; 5 | import com.oujiong.service.order.mqservice.TransactionProducer; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.rocketmq.client.exception.MQClientException; 8 | import org.apache.rocketmq.client.producer.SendResult; 9 | import org.apache.rocketmq.client.producer.SendStatus; 10 | import org.apache.rocketmq.common.message.Message; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.UUID; 16 | 17 | 18 | /** 19 | * @author xub 20 | * @Description: 订单服务相关接口 21 | * @date 2019/7/12 下午6:01 22 | */ 23 | @Slf4j 24 | @RestController 25 | @RequestMapping("api/v1/order") 26 | public class OrderController { 27 | 28 | @Autowired 29 | private Jms jms; 30 | 31 | @Autowired 32 | private TransactionProducer transactionProducer; 33 | 34 | 35 | /** 36 | * 商品下单接口 37 | * @param userId 用户ID 38 | * @param productId 商品ID 39 | * @param total 购买数量 40 | */ 41 | @RequestMapping("save") 42 | public Object save(int userId, int productId, int total) throws MQClientException { 43 | //通过uuid 当key 44 | String uuid = UUID.randomUUID().toString().replace("_", ""); 45 | 46 | //封装消息 47 | JSONObject msgJson = new JSONObject(); 48 | msgJson.put("productId", productId); 49 | msgJson.put("total", total); 50 | String jsonString = msgJson.toJSONString(); 51 | 52 | //封装消息实体 53 | Message message = new Message(jms.getOrderTopic(), null, uuid,jsonString.getBytes()); 54 | //发送消息 用 sendMessageInTransaction 第一个参数可以理解成消费方需要的参数 第二个参数可以理解成消费方不需要 本地事务需要的参数 55 | SendResult sendResult = transactionProducer.getProducer().sendMessageInTransaction(message, userId); 56 | System.out.printf("发送结果=%s, sendResult=%s \n", sendResult.getSendStatus(), sendResult.toString()); 57 | 58 | if (SendStatus.SEND_OK == sendResult.getSendStatus()) { 59 | return "成功"; 60 | } 61 | return "失败"; 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/model/ProduceOrder.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | import java.util.Date; 10 | 11 | /** 12 | * 商品订单实体类 13 | */ 14 | @Data 15 | @ToString 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class ProduceOrder implements Serializable { 19 | 20 | private static final long serialVersionUID = 1L; 21 | 22 | /** 23 | * 订单ID 24 | */ 25 | private Integer orderId; 26 | 27 | /** 28 | * 商品名称 29 | */ 30 | private String produceName; 31 | 32 | /** 33 | * 订单号 34 | */ 35 | private String tradeNo; 36 | 37 | /** 38 | * 价格,分 39 | */ 40 | private Integer price; 41 | 42 | /** 43 | * 订单创建时间 44 | */ 45 | private Date createTime; 46 | 47 | /** 48 | * 用户id 49 | */ 50 | private Integer userId; 51 | 52 | /** 53 | * 用户名 54 | */ 55 | private String userName; 56 | } 57 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/mqservice/TransactionProducer.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.mqservice; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.oujiong.service.order.config.Jms; 5 | import com.oujiong.service.order.service.ProduceOrderService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.rocketmq.client.exception.MQClientException; 8 | import org.apache.rocketmq.client.producer.LocalTransactionState; 9 | import org.apache.rocketmq.client.producer.TransactionListener; 10 | import org.apache.rocketmq.client.producer.TransactionMQProducer; 11 | import org.apache.rocketmq.common.message.Message; 12 | import org.apache.rocketmq.common.message.MessageExt; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.util.concurrent.*; 17 | 18 | 19 | /** 20 | * @author xub 21 | * @Description: 分布式事务RocketMQ 生产者 22 | * @date 2019/7/15 下午11:40 23 | */ 24 | @Slf4j 25 | @Component 26 | public class TransactionProducer { 27 | 28 | /** 29 | * 需要自定义事务监听器 用于 事务的二次确认 和 事务回查 30 | */ 31 | private TransactionListener transactionListener ; 32 | 33 | /** 34 | * 这里的生产者和之前的不一样 35 | */ 36 | private TransactionMQProducer producer = null; 37 | 38 | /** 39 | * 官方建议自定义线程 给线程取自定义名称 发现问题更好排查 40 | */ 41 | private ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, 42 | new ArrayBlockingQueue(2000), new ThreadFactory() { 43 | @Override 44 | public Thread newThread(Runnable r) { 45 | Thread thread = new Thread(r); 46 | thread.setName("client-transaction-msg-check-thread"); 47 | return thread; 48 | } 49 | 50 | }); 51 | 52 | public TransactionProducer(@Autowired Jms jms, @Autowired ProduceOrderService produceOrderService) { 53 | transactionListener = new TransactionListenerImpl(produceOrderService); 54 | // 初始化 事务生产者 55 | producer = new TransactionMQProducer(jms.getOrderTopic()); 56 | // 添加服务器地址 57 | producer.setNamesrvAddr(jms.getNameServer()); 58 | // 添加事务监听器 59 | producer.setTransactionListener(transactionListener); 60 | // 添加自定义线程池 61 | producer.setExecutorService(executorService); 62 | 63 | start(); 64 | } 65 | 66 | public TransactionMQProducer getProducer() { 67 | return this.producer; 68 | } 69 | 70 | /** 71 | * 对象在使用之前必须要调用一次,只能初始化一次 72 | */ 73 | public void start() { 74 | try { 75 | this.producer.start(); 76 | } catch (MQClientException e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | 81 | /** 82 | * 一般在应用上下文,使用上下文监听器,进行关闭 83 | */ 84 | public void shutdown() { 85 | this.producer.shutdown(); 86 | } 87 | } 88 | 89 | /** 90 | * @author xub 91 | * @Description: 自定义事务监听器 92 | * @date 2019/7/15 下午12:20 93 | */ 94 | @Slf4j 95 | class TransactionListenerImpl implements TransactionListener { 96 | 97 | @Autowired 98 | private ProduceOrderService produceOrderService ; 99 | 100 | public TransactionListenerImpl( ProduceOrderService produceOrderService) { 101 | this.produceOrderService = produceOrderService; 102 | } 103 | 104 | @Override 105 | public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { 106 | log.info("=========本地事务开始执行============="); 107 | String message = new String(msg.getBody()); 108 | JSONObject jsonObject = JSONObject.parseObject(message); 109 | Integer productId = jsonObject.getInteger("productId"); 110 | Integer total = jsonObject.getInteger("total"); 111 | int userId = Integer.parseInt(arg.toString()); 112 | //模拟执行本地事务begin======= 113 | /** 114 | * 本地事务执行会有三种可能 115 | * 1、commit 成功 116 | * 2、Rollback 失败 117 | * 3、网络等原因服务宕机收不到返回结果 118 | */ 119 | log.info("本地事务执行参数,用户id={},商品ID={},销售库存={}",userId,productId,total); 120 | int result = produceOrderService.save(userId, productId, total); 121 | //模拟执行本地事务end======== 122 | //TODO 实际开发下面不需要我们手动返回,而是根据本地事务执行结果自动返回 123 | //1、二次确认消息,然后消费者可以消费 124 | if (result == 0) { 125 | return LocalTransactionState.COMMIT_MESSAGE; 126 | } 127 | //2、回滚消息,Broker端会删除半消息 128 | if (result == 1) { 129 | return LocalTransactionState.ROLLBACK_MESSAGE; 130 | } 131 | //3、Broker端会进行回查消息 132 | if (result == 2) { 133 | return LocalTransactionState.UNKNOW; 134 | } 135 | return LocalTransactionState.COMMIT_MESSAGE; 136 | } 137 | 138 | /** 139 | * 只有上面接口返回 LocalTransactionState.UNKNOW 才会调用查接口被调用 140 | * 141 | * @param msg 消息 142 | * @return 143 | */ 144 | @Override 145 | public LocalTransactionState checkLocalTransaction(MessageExt msg) { 146 | log.info("==========回查接口========="); 147 | String key = msg.getKeys(); 148 | //TODO 1、必须根据key先去检查本地事务消息是否完成。 149 | /** 150 | * 因为有种情况就是:上面本地事务执行成功了,但是return LocalTransactionState.COMMIT_MESSAG的时候 151 | * 服务挂了,那么最终 Brock还未收到消息的二次确定,还是个半消息 ,所以当重新启动的时候还是回调这个回调接口。 152 | * 如果不先查询上面本地事务的执行情况 直接在执行本地事务,那么就相当于成功执行了两次本地事务了。 153 | */ 154 | // TODO 2、这里返回要么commit 要么rollback。没有必要在返回 UNKNOW 155 | return LocalTransactionState.COMMIT_MESSAGE; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/service/ProduceOrderService.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.service; 2 | 3 | 4 | /** 5 | * @Description: 订单业务类 6 | * 7 | * @author xub 8 | * @date 2019/7/12 下午12:57 9 | */ 10 | public interface ProduceOrderService { 11 | 12 | /** 13 | * @Description: 下单接口 14 | * @author xub 15 | */ 16 | int save(int userId, int produceId, int total); 17 | } 18 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/service/impl/ProduceOrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.service.impl; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.oujiong.service.order.client.ProduceClient; 5 | import com.oujiong.service.order.service.ProduceOrderService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * @author xub 12 | * @Description: 商品订单实现类 13 | * @date 2019/7/15 下午2:05 14 | */ 15 | @Slf4j 16 | @Service 17 | public class ProduceOrderServiceImpl implements ProduceOrderService { 18 | 19 | @Autowired 20 | private ProduceClient produceClient; 21 | 22 | @Override 23 | public int save(int userId, int produceId, int total) { 24 | 25 | //下单之前肯定要 检查该商品是否存在 库存是否够 26 | String response = produceClient.findById(produceId); 27 | //Json字符串转换成JsonNode对象 28 | JSONObject jsonObject = JSONObject.parseObject(response); 29 | Integer store = jsonObject.getInteger("store"); 30 | String produceName = jsonObject.getString("produceName"); 31 | if (store == null) { 32 | log.info("找不到商品消息,商品ID = {}", produceId); 33 | return 1; 34 | } 35 | log.info("商品存在,商品ID = {},商品当前库存 = {}", produceId, store, produceName); 36 | // 如果实际库存小于库存 37 | if (store - total < 0) { 38 | log.info("库存不足,扣减失败。商品ID = {},商品当前库存 = {},所需库存 = {},分布式事务key = {}", produceId, store, total); 39 | return 1; 40 | } 41 | 42 | log.info("===订单模块=== 本地事务执行成功,订单生成成功"); 43 | return 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /service-order/src/main/java/com/oujiong/service/order/untils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.order.untils; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * @Description: json工具类 10 | * 11 | * @author xub 12 | * @date 2019/7/12 下午12:58 13 | */ 14 | public class JsonUtils { 15 | 16 | 17 | private static final ObjectMapper objectMappper = new ObjectMapper(); 18 | 19 | /** 20 | * json字符串转JsonNode对象的方法 21 | */ 22 | public static JsonNode str2JsonNode(String str){ 23 | try { 24 | return objectMappper.readTree(str); 25 | } catch (IOException e) { 26 | return null; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /service-order/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9001 3 | 4 | #指定注册中心地址 5 | eureka: 6 | client: 7 | serviceUrl: 8 | defaultZone: http://localhost:7001/eureka/ 9 | 10 | #服务的名称 11 | spring: 12 | application: 13 | name: order-service 14 | #指定从哪个配置中心读取 15 | cloud: 16 | config: 17 | discovery: 18 | service-id: config-server-jdbc 19 | enabled: true 20 | profile: dev 21 | label: dev 22 | 23 | 24 | 25 | #开启feign支持hystrix (注意,一定要开启,旧版本默认支持,新版本默认关闭) 26 | # #修改调用超时时间默认是1秒就算超时 27 | feign: 28 | hystrix: 29 | enabled: true 30 | client: 31 | config: 32 | default: 33 | connectTimeout: 5000 34 | readTimeout: 5000 -------------------------------------------------------------------------------- /service-produce/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-rocketmq-transaction 7 | com.jincou 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | servce-produce 13 | 14 | 15 | 16 | 17 | 18 | org.springframework.cloud 19 | spring-cloud-starter-netflix-eureka-client 20 | 21 | 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-config-client 26 | 27 | 28 | 29 | 30 | com.alibaba 31 | fastjson 32 | 1.2.49 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/ProduceApplication.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @ClassName: ProduceApplication 8 | * @Description: 商品服务启动类 9 | * @author xub 10 | * @date 2019/7/12 下午12:29 11 | */ 12 | @SpringBootApplication 13 | public class ProduceApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(ProduceApplication.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/config/Jms.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce.config; 2 | 3 | 4 | import lombok.Data; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | 9 | /** 10 | * @Description: 连接RocketMQ服务器实体 11 | * 12 | * @author xub 13 | * @date 2019/7/15 下午11:30 14 | */ 15 | @Data 16 | @Configuration 17 | public class Jms { 18 | 19 | /** 20 | * 配置中心读取 服务器地址 21 | */ 22 | @Value("${name_server}") 23 | private String nameServer; 24 | 25 | /** 26 | * 配置中心读取 主题 27 | */ 28 | @Value("${order_topic}") 29 | private String orderTopic; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/controller/ProduceController.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce.controller; 2 | 3 | 4 | import com.alibaba.fastjson.JSON; 5 | import com.oujiong.service.produce.service.ProduceService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | 10 | /** 11 | * @author xub 12 | * @Description: 商品服务对外提供接口 13 | * @date 2019/7/12 下午12:43 14 | */ 15 | @RestController 16 | @RequestMapping("/api/v1/produce") 17 | public class ProduceController { 18 | 19 | @Autowired 20 | private ProduceService produceService; 21 | 22 | /** 23 | * 根据主键ID获取商品 24 | */ 25 | @GetMapping("/find") 26 | public String findById(@RequestParam(value = "produceId") int produceId) { 27 | return JSON.toJSONString(produceService.findById(produceId)); 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/model/Produce.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * @ClassName: Produce 11 | * @Description: 商品实体信息 12 | * @author xub 13 | * @date 2019/7/12 下午12:33 14 | */ 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class Produce implements Serializable { 19 | 20 | private static final long serialVersionUID = 1L; 21 | 22 | /** 23 | * 商品ID 24 | */ 25 | private Integer produceId; 26 | 27 | /** 28 | * 商品名称 29 | */ 30 | private String produceName; 31 | 32 | /** 33 | * 商品价格 34 | */ 35 | private Integer price; 36 | 37 | /** 38 | * 商品库存 39 | */ 40 | private Integer store; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/mqseivice/OrderConsumer.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce.mqseivice; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.oujiong.service.produce.config.Jms; 5 | import com.oujiong.service.produce.service.ProduceService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; 8 | import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; 9 | import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; 10 | import org.apache.rocketmq.client.exception.MQClientException; 11 | import org.apache.rocketmq.common.message.MessageExt; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | 16 | /** 17 | * @author xub 18 | * @Description: 消费端跟之前普通消费没区别 19 | * 因为分布式事务主要是通过 生产端控制 消息的发送 20 | * @date 2019/7/15 下午12:43 21 | */ 22 | @Slf4j 23 | @Component 24 | public class OrderConsumer { 25 | 26 | private DefaultMQPushConsumer consumer; 27 | 28 | private String consumerGroup = "produce_consumer_group"; 29 | 30 | public OrderConsumer(@Autowired Jms jms, @Autowired ProduceService produceService) throws MQClientException { 31 | //设置消费组 32 | consumer = new DefaultMQPushConsumer(consumerGroup); 33 | // 添加服务器地址 34 | consumer.setNamesrvAddr(jms.getNameServer()); 35 | // 添加订阅号 36 | consumer.subscribe(jms.getOrderTopic(), "*"); 37 | // 监听消息 38 | consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { 39 | MessageExt msg = msgs.get(0); 40 | String message = new String(msgs.get(0).getBody()); 41 | JSONObject jsonObject = JSONObject.parseObject(message); 42 | Integer productId = jsonObject.getInteger("productId"); 43 | Integer total = jsonObject.getInteger("total"); 44 | String key = msg.getKeys(); 45 | log.info("消费端消费消息,商品ID={},销售数量={}",productId,total); 46 | try { 47 | produceService.updateStore(productId, total, key); 48 | return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; 49 | } catch (Exception e) { 50 | log.info("消费失败,进行重试,重试到一定次数 那么将该条记录记录到数据库中,进行如果处理"); 51 | e.printStackTrace(); 52 | return ConsumeConcurrentlyStatus.RECONSUME_LATER; 53 | } 54 | }); 55 | 56 | consumer.start(); 57 | System.out.println("consumer start ..."); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/service/ProduceService.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce.service; 2 | 3 | import com.oujiong.service.produce.model.Produce; 4 | 5 | 6 | /** 7 | * @ClassName: ProduceService 8 | * @Description: 获取商品信息相关接口 9 | * @author xub 10 | * @date 2019/7/12 下午12:37 11 | */ 12 | public interface ProduceService { 13 | 14 | /** 15 | * 根据商品ID查找商品 16 | */ 17 | Produce findById(int produceId); 18 | 19 | /** 20 | * 更新库存 21 | * @param produceId 商品ID 22 | * @param store 销售库存数量 23 | */ 24 | /** 25 | * 更新库存 26 | * @param key 唯一值 分布式事务用 27 | * @param produceId 商品ID 28 | * @param store 销售库存数量 29 | */ 30 | void updateStore(int produceId,int store,String key) throws Exception; 31 | } 32 | -------------------------------------------------------------------------------- /service-produce/src/main/java/com/oujiong/service/produce/service/impl/ProduceServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.oujiong.service.produce.service.impl; 2 | 3 | 4 | import com.oujiong.service.produce.model.Produce; 5 | import com.oujiong.service.produce.service.ProduceService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * @Description: 商品模块实现类 13 | * 14 | * @author xub 15 | * @date 2019/7/16 下午10:05 16 | */ 17 | @Slf4j 18 | @Service 19 | public class ProduceServiceImpl implements ProduceService { 20 | 21 | private static final Map daoMap = new HashMap<>(); 22 | 23 | /** 24 | * 模拟数据库商品数据 25 | */ 26 | static { 27 | Produce p1 = new Produce(1, "苹果X", 9999, 10); 28 | Produce p2 = new Produce(2, "冰箱", 5342, 19); 29 | Produce p3 = new Produce(3, "洗衣机", 523, 90); 30 | 31 | daoMap.put(p1.getProduceId(), p1); 32 | daoMap.put(p2.getProduceId(), p2); 33 | daoMap.put(p3.getProduceId(), p3); 34 | } 35 | 36 | 37 | @Override 38 | public Produce findById(int id) { 39 | return daoMap.get(id); 40 | } 41 | 42 | @Override 43 | public void updateStore(int produceId, int store, String key) throws Exception { 44 | Produce produce = daoMap.get(produceId); 45 | 46 | // 如果实际库存小于库存 那么需要把这条数据记录到一张专门用于记录分布式事务的表,通过这个key当业务逻辑保证事务最终一致性 47 | if (produce.getStore() - store < 0) { 48 | /** 49 | * TODO 首先实际开发 不可能到这里才判断库存是否不足,而是下订单那边就确定好库存是否充足 50 | * 因为RocketMQ是最终一致性事务,不可能这边异常那边确已经告知用户下单正常,最后为了保证事务一致性在去修改这个订单为失败,用户会懵逼的 51 | */ 52 | //模拟保存MQ异常表 用于人工处理 保证事务一致性 53 | log.info("库存不足,扣减失败。商品ID = {},商品当前库存 = {},所需库存 = {},分布式事务key = {}", produceId, produce.getStore(), store, key); 54 | 55 | throw new Exception("库存不足,更新库存失败"); 56 | } 57 | log.info("更新库存成功。商品ID = {},商品当前库存 = {},销售库存 = {},分布式事务key = {}", produceId, produce.getStore(), store, key); 58 | 59 | log.info("===商品模块=== 本地事务执行成功,商品库存扣除成功"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /service-produce/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8001 3 | 4 | #指定注册中心地址 5 | eureka: 6 | client: 7 | serviceUrl: 8 | defaultZone: http://localhost:7001/eureka/ 9 | 10 | #服务的名称 11 | spring: 12 | application: 13 | name: product-service 14 | #指定从哪个配置中心读取 15 | cloud: 16 | config: 17 | discovery: 18 | service-id: config-server-jdbc 19 | enabled: true 20 | profile: dev 21 | label: dev 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------