RocketMQ實現可靠消息最終一致性的原理圖:
廢話不多說,直接上代碼,這個案例用了RocketMQ、Spring cloud Alibaba組件中的nacos來實現服務的註冊與發現、mybatis-plus等等,案例中使用到了兩個數據庫,流程就是用戶在訂單微服務中下單,然後在庫存微服務中扣減庫存;
一、項目結構:
rocketmq-transaction工程分爲三個子模塊,base-framework-mysql-support模塊(作爲基礎模塊,被其它服務模塊引用)存放數據庫相關jar包和配置類,order-service模塊是訂單微服務,storage-service是庫存微服務模塊;
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lucifer</groupId>
<artifactId>rocketmq-transaction</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>order-service</module>
<module>storage-service</module>
<module>base-framework-mysql-support</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
二、模塊介紹
(1)base-framework-mysql-support:
此模塊只有一個關於mybatis-plus的配置:代碼如下:
package com.lucifer.config;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lucifer
* @date 2020/4/14 21:54
* @description mybatis-plus 配置
*/
@Configuration
@MapperScan(value = {"com.lucifer.mapper"})
public class MybatisPlusConfig {
/**
* SQL執行效率插件
*/
@Bean
// @Profile({"dev", "test"})// 設置 dev test 環境開啓
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rocketmq-transaction</artifactId>
<groupId>com.lucifer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>base-framework-mysql-support</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
</project>
(1)order-service:訂單模塊
pojo:存放實體類的包
/**
* 訂單表
*/
@Data
@NoArgsConstructor
@TableName("order_tbl")
public class Order {
@TableId(type = IdType.AUTO)
private Integer id;
private String userId;
private String commodityCode;
private Integer count;
private BigDecimal money;
@TableField(exist = false)
private String txNum;
}
/**
* @author lucifer
* @date 2020/4/15 13:04
* @description 事務日誌表
*/
@Data
//@Builder
@NoArgsConstructor
//@Accessors(chain = true)
@TableName("tx_log")
public class TxLog {
@TableId
private String txNum;
private Date createTime;
}
mapper:
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.Order;
public interface OrderMapper extends BaseMapper<Order> {
}
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.TxLog;
public interface TxLogMapper extends BaseMapper<TxLog> {
}
service:接口
package com.lucifer.service;
import com.lucifer.pojo.Order;
public interface OrderService {
/**
* 發送訂單消息
*
* @param order
*/
void sendOrder(Order order);
/**
* 新增訂單
*
* @param order
*/
void insertOrder(Order order);
}
實現類:
/**
* @author lucifer
* @date 2020/4/14 19:31
* @description
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private RocketMQTemplate rocketMQTemplate;
@Resource
private OrderMapper orderMapper;
@Resource
private TxLogMapper txLogMapper;
@Override
public void sendOrder(Order order) {
String str = JSON.toJSONString(order);
Message<String> message = MessageBuilder.withPayload(str).build();
/**
* 發送一條事務消息
* String txProducerGroup: 生產組
* String destination:topic
* Message<?> message: 消息內容
* Object arg: 參數
*/
rocketMQTemplate.sendMessageInTransaction("producer_group_tx1", "topic_tx", message, null);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void insertOrder(Order order) {
//用事務id冪等處理
if (txLogMapper.selectById(order.getTxNum()) != null) {
return;
}
orderMapper.insert(order);
//插入事務日誌
TxLog txLog = new TxLog();
txLog.setTxNum(order.getTxNum());
System.out.println("order.getTxNum():" + order.getTxNum());
txLog.setCreateTime(new Date());
txLogMapper.insert(txLog);
//模擬異常,檢查事務是否回滾
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("commodity_code", "product-1");
if (orderMapper.selectList(queryWrapper).size()== 6) {
throw new RuntimeException("人爲模擬異常");
}
}
}
rocketmq的事務監聽器:(重要)
/**
* @author lucifer
* @date 2020/4/15 0:59
* @description TODO
*/
@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "producer_group_tx1")
public class ProducerTransactionListener implements RocketMQLocalTransactionListener {
@Resource
private OrderService orderService;
@Resource
private TxLogMapper txLogMapper;
/**
* 事務消息發送mq成功後的回調方法
*
* @param msg
* @param arg
* @return 返回事務狀態
* RocketMQLocalTransactionState.COMMIT:提交事務,提交後broker才允許消費者使用
* RocketMQLocalTransactionState.ROLLBACK:回滾事務,回滾後消息將被刪除,並且不允許別消費
* RocketMQLocalTransactionState.Unknown:中間狀態,表示MQ需要覈對,以確定狀態
*/
@Transactional(rollbackFor = Exception.class)
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
String str = new String((byte[]) msg.getPayload());
Order order = JSON.parseObject(str, Order.class);
orderService.insertOrder(order);
//當返回RocketMQLocalTransactionState.COMMIT,自動向mq發送commit,mq將消息的狀態改爲可消費
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 事務狀態回查,查詢是否下單成功
*
* @param msg
* @return 返回事務狀態
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String str = new String((byte[]) msg.getPayload());
Order order = JSON.parseObject(str, Order.class);
//事務id
String txNo = order.getTxNum();
TxLog txLog = txLogMapper.selectById(txNo);
if (txLog != null) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
application.yml: 配置文件
server:
port: 8081
spring:
application:
name: order-service
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.160.131:3306/order?autoReconnect=true&useUnicode=true&createDatabaseIfNotExist=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 192.168.160.131:8848
# main:
# allow-bean-definition-overriding: true
logging:
level:
com.lucifer.mapper: debug
rocketmq:
producer:
group: producter_tx
name-server: 192.168.160.131:9876
springboot啓動類:
/**
* @author lucifer
* @date 2020/4/14 19:28
* @description TODO
*/
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
controller層:
/**
* @author lucifer
* @date 2020/4/14 19:32
* @description TODO
*/
@RestController
@RequestMapping(value = "order")
public class OrderController {
@Resource
OrderService orderService;
/**
* 下單:插入訂單表、扣減庫存,模擬回滾
*
* @return
*/
@GetMapping("/placeOrder/commit")
public Boolean placeOrderCommit() {
//將uuid作爲事務id,發送到mq
String uuid = UUID.randomUUID().toString();
Order order = new Order();
order.setCommodityCode("product-1");
order.setUserId("1");
order.setCount(1);
order.setTxNum(uuid);
order.setMoney(new BigDecimal(12.5));
System.out.println("準備下單了=======》" + order);
orderService.sendOrder(order);
return true;
}
}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>rocketmq-transaction</artifactId>
<groupId>com.lucifer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>com.lucifer</groupId>
<artifactId>base-framework-mysql-support</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- nacos -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)storage-service:庫存微服務模塊
mapper:
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.Storage;
public interface StorageMapper extends BaseMapper<Storage> {
}
package com.lucifer.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.TxLog;
public interface TxLogMapper extends BaseMapper<TxLog> {
}
pojo:
/**
* 庫存表
*/
@Data
@Accessors(chain = true)
@TableName("storage_tbl")
public class Storage {
private Long id;
private String commodityCode;
private Long count;
}
ps:order、txlog兩個實體類從order-service中複製過來即可;
service:
public interface StorageService {
/**
* 扣減庫存
*
* @param commodityCode
* @param count
* @param txNum 事務id
*/
void deduct(String commodityCode, int count,String txNum);
}
實現類:
/**
* @author lucifer
* @date 2020/4/14 20:07
* @description TODO
*/
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
@Resource
private TxLogMapper txLogMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public void deduct(String commodityCode, int count, String txNum) {
log.info("扣減庫存,商品編碼:{},數量:{}", commodityCode, count);
TxLog txLog = txLogMapper.selectById(txNum);
if (txLog != null) {
return;
}
//扣減庫存
QueryWrapper<Storage> wrapper = new QueryWrapper<>();
wrapper.setEntity(new Storage().setCommodityCode(commodityCode));
Storage storage = storageMapper.selectOne(wrapper);
if (storage == null) {
throw new RuntimeException("商品" + commodityCode + ",不存在");
}
storage.setCount(storage.getCount() - count);
storageMapper.updateById(storage);
//添加事務記錄,用於冪等
TxLog tLog = new TxLog();
tLog.setTxNum(txNum);
tLog.setCreateTime(new Date());
txLogMapper.insert(tLog);
//模擬異常,檢查事務是否回滾
if(storageMapper.selectById(1).getCount()==4996){
throw new RuntimeException("人爲模擬異常");
}
}
}
rocketmq監聽類:
/**
* @author lucifer
* @date 2020/4/15 0:59
* @description TODO
*/
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "consumer_group_tx2", topic = "topic_tx")
class ConsumerTransactionListener implements RocketMQListener<String> {
@Resource
private StorageService storageService;
@Override
public void onMessage(String message) {
log.info("開始消費消息:{}", message);
//解析消息
Order order = JSON.parseObject(message, Order.class);
//扣減庫存
storageService.deduct(order.getCommodityCode(), order.getCount(), order.getTxNum());
}
}
springboot啓動類:
/**
* @author lucifer
* @date 2020/4/14 20:23
* @description 庫存服務
*/
@EnableDiscoveryClient
@SpringBootApplication
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
application.yml:
server:
port: 8082
spring:
application:
name: storage-service
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.160.131:3306/storage?autoReconnect=true&useUnicode=true&createDatabaseIfNotExist=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 192.168.160.131:8848
# main:
# allow-bean-definition-overriding: true
logging:
level:
com.lucifer.mapper: debug
rocketmq:
producer:
group: consumer_tx
name-server: 192.168.160.131:9876
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>rocketmq-transaction</artifactId>
<groupId>com.lucifer</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>storage-service</artifactId>
<dependencies>
<dependency>
<groupId>com.lucifer</groupId>
<artifactId>base-framework-mysql-support</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<!-- nacos -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
啓動order-service服務,啓動storage-service服務;
測試場景:
(1)order-service 本地事務失敗,order-service不會發送下訂單消息
(2)storage-service 接收到下單的消息,扣減庫存失敗,會不斷重試扣減庫存(當然這個嘗試次數有限制的),控制檯會不斷打印重試信息:如果一直這樣重複消費都持續失敗到一定次數(默認16次),就會投遞到DLQ死信隊列,此時需要人工干預了。
場景(2)數據庫截圖: