一、案例介绍
1.1、业务分析
模拟电商网站购物场景中的【下单】和【支付】业务
1.1.1、下单
流程
-
用户请求订单系统下单
-
订单系统通过RPC调用订单服务下单
-
订单服务调用优惠券服务,扣减优惠券
-
订单服务调用调用库存服务,校验并扣减库存
-
订单服务调用用户服务,扣减用户余额
-
订单服务完成确认订单
1.1.2、支付
流程
- 用户请求支付系统
- 支付系统调用第三方支付平台API进行发起支付流程
- 用户通过第三方支付平台支付成功后,第三方支付平台回调通知支付系统
- 支付系统调用订单服务修改订单状态
- 支付系统调用积分服务添加积分
- 支付系统调用日志服务记录日志
1.2、问题分析
1.2.1、问题1
用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。
如何保证数据的完整性?
使用MQ保证在下单失败后系统数据的完整性
1.2.2、问题2
用户通过第三方支付平台(支付宝、微信)支付成功后,第三方支付平台要通过回调API异步通知商家支付系统用户支付结果,支付系统根据支付结果修改订单状态、记录支付日志和给用户增加积分。
商家支付系统如何保证在收到第三方支付平台的异步通知时,如何快速给第三方支付凭条做出回应?
通过MQ进行数据分发,提高系统处理性能
二.、技术分析
2.1、技术选型
- SpringBoot
- Dubbo
- Zookeeper
- RocketMQ
- Mysql
2.2、SpringBoot整合RocketMQ
下载rocketmq-spring项目
将rocketmq-spring安装到本地仓库
mvn install -Dmaven.skip.test=true
2.2.1、消息生产者
2.2.1.1、添加依赖
<?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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>rocketmq-producer</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-boot-version>2.3.2.RELEASE</spring-boot-version>
</properties>
<dependencies>
<!-- 配置web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- rocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-version}</version>
</plugin>
</plugins>
</build>
</project>
2.2.1.2、配置文件
server:
port: 8094
spring:
application:
name: rocketmq-producer
rocketmq:
name-server: 127.0.0.1:9876 # rocketMQ的名称服务器,格式为:' host:port;host:port '。
# 生产端配置
producer:
group: ${
spring.application.name} # 生产着组名
#access-key: access-key # rocketMQ服务端配置acl授权信息,没有则不需要
#secret-key: secret-key # rocketMQ服务端配置acl授权信息,没有则不需要
# 消费端配置
# consumer:
# access-key: access-key #如果开启了acl,一定要配置。否则集群模式下会正常,广播模式消费端会失效!
# secret-key: secret-key #如果开启了acl,一定要配置。否则集群模式下会正常,广播模式消费端会失效!
2.2.1.3、消息发送
/**
* <p>
* RocektMQ 事务消息监听器
* </p>
*
**/
@Slf4j
@RocketMQTransactionListener
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
/**
* 检测半消息,在该方法中,执行本地事务
*
* @param msg 发送消息
* @param arg 外部参数
* @return commit:提交事务,它允许消费者消费此消息。bollback:回滚事务,它代表该消息将被删除,不允许被消费。 unknown:中间状态,它代表需要检查消息队列来确定状态(checkLocalTransaction方法)。
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info(">>>> MQ事务执行器,执行本地事务 message={},args={} <<<<", msg, arg);
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderPaidEvent payload = JSON.parseObject(jsonString, OrderPaidEvent.class);
//模拟业务操作,当paidMoney >5 则提交,否则等事务会查
if (payload.getPaidMoney().compareTo(new BigDecimal("5")) > 0) {
//提交事务
log.info("MQ提交事务啦!payload ={} ", payload);
return RocketMQLocalTransactionState.COMMIT;
}
//不知道状态,转 checkLocalTransaction 回查执行
log.info("MQ无法确定,等回查!payload ={} ", payload);
return RocketMQLocalTransactionState.UNKNOWN;
} catch (Exception e) {
log.error("事务消息出错啦~ e:{}", e.getMessage(), e);
//回滚
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 该方法时MQ进行消息事务状态回查、
* <p>
*
* @param msg
* @return bollback, commit or unknown
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
log.info(">>>> MQ事务执行器,事务状态回查 message={} <<<<", msg);
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderPaidEvent payload = JSON.parseObject(jsonString, OrderPaidEvent.class);
log.info("事务回查:checkLocalTransaction提交事务啦!payload ={} ", payload);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("回调的事务出错啦~ e:{}", e.getMessage(), e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
/**
* <p>
* RocektMQ生产者常用发送消息方法
* 最佳实践:https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md
* </p>
*
**/
public interface IRocketMQService {
/**
* 发送同步消息(这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。)
* <p>
* (send消息方法只要不抛异常,就代表发送成功。但不保证100%可靠投递(所有消息都一样,后面不在叙述)。
* 要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。
* 解析看:https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md
* )
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendMessage(String destination, Object msg);
/**
* 发送同步消息(这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。)
*
* @param topicName 主题名 topicName
* @param tags 标签 tags
* @param msg 发送对象
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendMessage(String topicName, String tags, Object msg);
/**
* 发送同步消息(这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。)
*
* @param topicName 主题名 topicName
* @param tags 标签 tags
* @param key 唯一标识码要设置到keys字段,方便将来定位消息丢失问题
* @param msg 发送对象
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendMessage(String topicName, String tags, String key, Object msg);
/**
* 发送同步消息-SQL92模式
* 需要配置RocketMQ服务器 vim conf/broker.conf ##支持sql语句过滤 enablePropertyFilter=true
* 在console控制台查看集群状态 enablePropertyFilter=true 才正常
*
* @param topicName 主题名 topicName
* @param map 自定义属性
* @param msg 发送对象
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendMessageBySql(String topicName, Map<String, Object> map, Object msg);
/**
* 发送同步消息-SQL92模式
* 需要配置RocketMQ服务器 vim conf/broker.conf ##支持sql语句过滤 enablePropertyFilter=true
* 在console控制台查看集群状态 enablePropertyFilter=true 才正常
*
* @param topicName 主题名 topicName
* @param map 自定义属性
* @param key 唯一标识码要设置到keys字段,方便将来定位消息丢失问题
* @param msg 发送对象
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendMessageBySql(String topicName, Map<String, Object> map, String key, Object msg);
/**
* 发生异步消息(异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。)
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
* @param sendCallback 异步回调函数
*/
void sendAsyncMessage(String destination, Object msg, SendCallback sendCallback);
/**
* 发送单向消息(这种方式主要用在不特别关心发送结果的场景,例如日志发送。)
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
*/
void sendOneway(String destination, Object msg);
/**
* 发送批量消息(发送超过1MB,做了自动分割,超时时间设置30s(默认3s)),注:默认最大是4MB,为了避免ListSplitter.calcMessageSize计算不精确及大批量数据发送超时才设置1MB
*
* @param destination 主题名:标签 topicName:tags
* @param list 批量消息
*/
void sendBatchMessage(String destination, List<?> list);
/**
* 发送批量消息(发送超过1MB,做了自动分割。),注:默认最大是4MB,为了避免ListSplitter.calcMessageSize计算不精确及大批量数据发送超时才设置1MB
*
* @param topicName 主题名 topicName
* @param tags 标签 tags
* @param timeout 超时时间,空则默认设为30s
* @param list 批量消息
*/
void sendBatchMessage(String topicName, String tags, Long timeout, List<?> list);
/**
* 发送延时消息(超时时间,设置30s(默认3s))
* 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
* @param delayTimeLevel 延时等级(从1开始)
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendDelayLevel(String destination, Object msg, int delayTimeLevel);
/**
* 发送延时消息
* 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
* @param timeout 超时时间(单位毫秒)
* @param delayTimeLevel 延时等级(从1开始)
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendDelayLevel(String destination, Object msg, int timeout, int delayTimeLevel);
/**
* 发送顺序消息(分区有序,多个queue参与,即相对每个queue,消息都是有序的。)
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
* @param hashKey 根据其哈希值取模后确定发送到哪一个queue队列
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendInOrder(String destination, Object msg, String hashKey);
/**
* 发送事务消息
* 事务消息使用上的限制
* 1:事务消息不支持延时消息和批量消息。
* 2:为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionalMessageCheckListener 类来修改这个行为。
* 3:事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionTimeout 参数。
* 4:事务性消息可能不止一次被检查或消费。
* 5:提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
* 6:事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
*
* @param destination 主题名:标签 topicName:tags
* @param msg 发送对象
* @param arg arg
* @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
*/
SendResult sendMessageInTransaction(String destination, Object msg, Object arg);
}
/**
* <p>
* RocektMQ生产者常用发送消息方法
* 最佳实践:https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md
* </p>
*
*/
@Slf4j
@Service
public class RocketMQServiceImpl implements IRocketMQService {
@Autowired
private RocketMQTemplate rocketMqTemplate;
/**
* 原生的producer
*/
@Autowired
private DefaultMQProducer producer;
@Override
public SendResult sendMessage(String destination, Object msg) {
String[] split = destination.split(":");
if (split.length == 2) {
return this.sendMessage(split[0], split[1], msg);
}
return this.sendMessage(destination, null, msg);
}
@Override
public SendResult sendMessage(String topicName, String tags, Object msg) {
return this.sendMessage(topicName, tags, null, msg);
}
@Override
public SendResult sendMessage(String topicName, String tags, String key, Object msg) {
MessageBuilder<?> messageBuilder = MessageBuilder.withPayload(msg);
//设置key,唯一标识码要设置到keys字段,方便将来定位消息丢失问题
if (StringUtils.isNotBlank(key)) {
messageBuilder.setHeader(MessageConst.PROPERTY_KEYS, key);
}
Message<?> message = messageBuilder.build();
SendResult sendResult = this.rocketMqTemplate.syncSend(StringUtils.isBlank(tags) ? topicName : (topicName + ":" + tags), message);
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
log.info("MQ发送同步消息成功,topicName={},tags={},msg={},sendResult={}", topicName, tags, msg, sendResult);
} else {
log.warn("MQ发送同步消息不一定成功,topicName={},tags={},msg={},sendResult={}", topicName, tags, msg, sendResult);
}
return sendResult;
}
@Override
public SendResult sendMessageBySql(String topicName, Map<String, Object> map, Object msg) {
return this.sendMessageBySql(topicName, map, null, msg);
}
@Override
public SendResult sendMessageBySql(String topicName, Map<String, Object> map, String key, Object msg) {
MessageBuilder<?> messageBuilder = MessageBuilder.withPayload(msg);
//设置key,唯一标识码要设置到keys字段,方便将来定位消息丢失问题
if (StringUtils.isNotBlank(key)) {
messageBuilder.setHeader(MessageConst.PROPERTY_KEYS, key);
}
//设置自定义属性
if (map != null && !map.isEmpty()) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
messageBuilder.setHeader(entry.getKey(), entry.getValue());
}
}
Message<?> message = messageBuilder.build();
SendResult sendResult = this.rocketMqTemplate.syncSend(topicName, message);
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
log.info("发送同步消息-SQL92模式成功,topicName={},map={},msg={},sendResult={}", topicName, map, msg, sendResult);
} else {
log.warn("发送同步消息-SQL92模式不一定成功,topicName={},map={},msg={},sendResult={}", topicName, map, msg, sendResult);
}
return sendResult;
}
@Override
public void sendAsyncMessage(String destination, Object msg, SendCallback sendCallback) {
this.rocketMqTemplate.asyncSend(destination, msg, sendCallback);
log.info("MQ发送异步消息,destination={} msg={}", destination, msg);
}
@Override
public void sendOneway(String destination, Object msg) {
this.rocketMqTemplate.sendOneWay(destination, msg);
log.info("MQ发送单向消息,destination={} msg={}", destination, msg);
}
@Override
public void sendBatchMessage(String destination, List<?> list) {
String topicName = destination;
String tags = "";
String[] split = destination.split(":");
if (split.length == 2) {
topicName = split[0];
tags = split[1];
}
this.sendBatchMessage(topicName, tags, 30000L, list);
}
@Override
public void sendBatchMessage(String topicName, String tags, Long timeout, List<?> list) {
//转为message
List<org.apache.rocketmq.common.message.Message> messages = list.stream().map(x ->
new org.apache.rocketmq.common.message.Message(topicName, tags,
//String类型不需要转JSON,其它类型都要转为JSON模式
x instanceof String ? ((String) x).getBytes(StandardCharsets.UTF_8) : JSON.toJSONBytes(x))
).collect(Collectors.toList());
//自动分割发送,把大的消息分裂成若干个小的消息(每次发送最大只能4MB)
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
try {
List<org.apache.rocketmq.common.message.Message> listItem = splitter.next();
SendResult sendResult = producer.send(listItem, timeout == null ? 30000L : timeout);
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
log.info("MQ发送批量消息成功,topicName={} tags={}, size={},sendResult={}", topicName, tags, listItem.size(), sendResult);
} else {
log.warn("MQ发送批量消息不一定成功,topicName={} tags={}, size={},sendResult={}", topicName, tags, listItem.size(), sendResult);
}
} catch (Exception e) {
//处理error
log.error("MQ发送批量消息失败,topicName={} tags={},,errorMessage={}", topicName, tags, e.getMessage(), e);
throw new RuntimeException("MQ发送批量消息失败,原因:" + e.getMessage());
}
}
}
@Override
public SendResult sendDelayLevel(String destination, Object msg, int delayTimeLevel) {
return this.sendDelayLevel(destination, msg, 30000, delayTimeLevel);
}
@Override
public SendResult sendDelayLevel(String destination, Object msg, int timeout, int delayTimeLevel) {
Message<?> message = MessageBuilder
.withPayload(msg)
.build();
SendResult sendResult = this.rocketMqTemplate.syncSend(destination, message, timeout, delayTimeLevel);
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
log.info("MQ发送延时消息成功,destination={} msg={} sendResult={}", destination, message, sendResult);
} else {
log.warn("MQ发送延时消息不一定成功,destination={} msg={} sendResult={}", destination, message, sendResult);
}
return sendResult;
}
@Override
public SendResult sendInOrder(String destination, Object msg, String hashKey) {
Message<?> message = MessageBuilder
.withPayload(msg)
.build();
//hashKey: 根据其哈希值取模后确定发送到哪一个队列
SendResult sendResult = this.rocketMqTemplate.syncSendOrderly(destination, message, hashKey);
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
log.info("MQ发送顺序消息成功,destination={} msg={} sendResult={}", destination, message, sendResult);
} else {
log.warn("MQ发送顺序消息不一定成功,destination={} msg={} sendResult={}", destination, message, sendResult);
}
return sendResult;
}
@Override
public SendResult sendMessageInTransaction(String destination, Object msg, Object arg) {
Message<?> message = MessageBuilder
//转为JSON格式
.withPayload(msg instanceof String ? msg : JSON.toJSONString(msg))
.build();
TransactionSendResult sendResult = rocketMqTemplate.sendMessageInTransaction(destination, message, arg);
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
log.info("MQ发送事务消息成功,destination={} msg={} sendResult={}", destination, message, sendResult);
} else {
log.warn("MQ发送事务消息不一定成功,destination={} msg={} sendResult={}", destination, message, sendResult);
}
return sendResult;
}
}
/**
* <p>
* 消息列表分割,复杂度只有当你发送大批量时才会增长,你可能不确定它是否超过了大小限制(4MB)。这时候你最好把你的消息列表分割一下:
* </p>
*
**/
public class ListSplitter implements Iterator<List<Message>> {
/**
* 最大4MB,这里每次只发送1MB。(为了避免ListSplitter.calcMessageSize计算不精确及大批量数据发送超时才设置1MB)
*/
private final int SIZE_LIMIT = 1024 * 1024 * 1;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int startIndex = getStartIndex();
int nextIndex = startIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = calcMessageSize(message);
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(startIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
private int getStartIndex() {
Message currMessage = messages.get(currIndex);
int tmpSize = calcMessageSize(currMessage);
while (tmpSize >