├── .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 | 
45 |
46 |
47 |
48 | #### 3、分布式服务流程
49 |
50 | 用户在订单微服务下单后,会去回调商品微服务去减库存。这个过程需要事务的一致性。
51 |
52 | 
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 | 
67 |
68 |
69 |
70 |
71 |
72 | 商品微服务执行情况(商品服务事务执行成功)
73 |
74 | 
75 |
76 |
77 |
78 | 当然你也可以通过修改参数来模拟分布式事务出现的各种情况。
79 |
80 |
81 |
82 | 
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 |
--------------------------------------------------------------------------------