目录
实现场景:用户下单service - 扣减用户积分 service
结合Springboot 处理分布式事务-为减少篇幅,只列出主要几块代码,详见文末源码链接:
以下是Maven-module.orderProducer 生产者模块内容
maven-module.orderProducer.application.yml
maven-module.orderProducer-com.study.mqconfig.OrderMQMsgConfig.java
maven-module.orderProducer-com.study.service.OrderService.java
maven-module.orderProducer-com.study.listen.ReCreateorderConsumer.java
以下是Maven-module.scoreConsumer 积分消费者模块内容
maven-module.scoreConsumer.application.yml
maven-module.scoreConsumer-com.study.listen.ScoreReceiverConsumer.java
分布式事务应用场景:
- 【分库分表时】- 服务内存在跨数据库场景;
- 【应用SOA化】- 不同服务容器之间服务调用场景;
- 用户注册service -> 调用【积分服务-赠送积分service】;
- 用户下单service -> 调用【扣减用户积分service】or【扣减用户消费券service】or 【派单service】;
- 用户支付service -> 调用【更新订单service】+【账户记账service】+【增加用户积分service】;
- 。。。
如何使用RabbitMQ 解决分布式事务-最终一致性:
- 确认Producer 一定将消息投递到MQ服务中;
- 消息持久化;
- 确认Consumer 一定将消息消费了,ACK手动应答模式(消息补偿-考虑消息处理幂等);
实现场景:用户下单service - 扣减用户积分 service
结合Springboot 处理分布式事务-为减少篇幅,只列出主要几块代码,详见文末源码链接:
maven-parent . 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>
<groupId>com.mq.study</groupId>
<artifactId>MqParent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>orderProducer</module>
<module>scoreConsumer</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.report.outputEncoding>UTF-8</project.report.outputEncoding>
<java.version>1.8</java.version>
<spring.boot.mybatis>1.3.0</spring.boot.mybatis>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${spring.boot.mybatis}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.1.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.3.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
以下是Maven-module.orderProducer 生产者模块内容
maven-module.orderProducer.application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=round&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
publisher-confirms: true #消息发送到交换机确认机制,是否确认回调
publisher-returns: true #消息发送到交换机确认机制,是否返回回调
#consumer
listener:
simple:
prefetch: 1
retry:
#开启消费者重试
enabled: true
#重试间隔时间ms
initial-interval: 3000
#最大重试次数
max-attempts: 3
#采用手动回答
acknowledge-mode: manual
mybatis:
type-aliases-package: com.study.model
mapper-locations: classpath:mapper/*Mapper.xml
#log_level config
LOG_LEVEL: INFO
maven-module.orderProducer-com.study.mqconfig.OrderMQMsgConfig.java
package com.study.mqconfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 订单消息MQconfig
* @Author mengfanzhu
* @Date 2019/6/4 22:28
* @Version 1.0
*/
@Configuration
@Slf4j
public class OrderMQMsgConfig {
/**
* 积分处理队列
*/
public static final String QUEUE_NAME_TRANSACTION = "api.score.queue";
/**
* 补单队列
*/
public static final String CREATE_QUEUE_NAME_TRANSACTION = "api.order.reCreate.queue";
/**
* 积分处理路由KEY
*/
public static final String ROUTE_NAME_TRANSACTION = "api.score.route";
/**
* 积分处理交换机
*/
public static final String EXCHANGE_NAME_TRANSACTION = "api.score.exchange";
/**
* 积分队列
*
* @return
*/
@Bean
public Queue scoreQueue() {
return new Queue(QUEUE_NAME_TRANSACTION, true);
}
/**
* 补单队列
*
* @return
*/
@Bean
public Queue createOrderReceiver() {
return new Queue(CREATE_QUEUE_NAME_TRANSACTION, true);
}
@Bean
public DirectExchange transExchange() {
return new DirectExchange(EXCHANGE_NAME_TRANSACTION);
}
/**
* 交换机绑定到积分队列
* @return
*/
@Bean
public Binding bindingExchangeOrderReceiverQueue() {
return BindingBuilder.bind(scoreQueue()).to(transExchange()).with(ROUTE_NAME_TRANSACTION);
}
/**
* 交换机绑定到补单队列
* @return
*/
@Bean
public Binding bindingExchangeCreateOrderQueue() {
return BindingBuilder.bind(createOrderReceiver()).to(transExchange()).with(ROUTE_NAME_TRANSACTION);
}
}
maven-module.orderProducer-com.study.service.OrderService.java
package com.study.service;
import com.alibaba.fastjson.JSONObject;
import com.study.mapper.OrderMapper;
import com.study.model.OrderInfoModel;
import com.study.mqconfig.OrderMQMsgConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* @Description: 订单服务
* @Author mengfanzhu
* @Date 2019/6/4 14:19
* @Version 1.0
*/
@Service
@Slf4j
public class OrderService implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单
*/
public void saveOrder() {
OrderInfoModel orderInfoModel = new OrderInfoModel();
orderInfoModel.initOrder();
orderInfoModel.setAmount(new BigDecimal(100));
orderInfoModel.setCustomerName("张三");
Long result = orderMapper.insert(orderInfoModel);
//send mq 消费积分
if (result == 1L) {
sendMsg(orderInfoModel);
}
}
/**
* 发送消息
*
* @param orderInfoModel
*/
public void sendMsg(OrderInfoModel orderInfoModel) {
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(this);
//消息封装
Message message = MessageBuilder.withBody(JSONObject.toJSONString(orderInfoModel).getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(orderInfoModel.getOrderNo()).build();
CorrelationData correlationData = new CorrelationData(orderInfoModel.getOrderNo());
rabbitTemplate.convertAndSend(OrderMQMsgConfig.EXCHANGE_NAME_TRANSACTION,
OrderMQMsgConfig.ROUTE_NAME_TRANSACTION, message, correlationData);
log.info("Transactional Message success.");
}
/**
* 用来确认消息是否有送达消息队列
*
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info(JSONObject.toJSONString(correlationData));
if (ack) {
System.out.println("success");
} else {
// retry
System.out.println("failed");
}
}
/**
* 消息找不到对应的Exchange会先触发此方法
*
* @param message
* @param replyCode
* @param replyText
* @param tempExchange
* @param tmpRoutingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String tempExchange
, String tmpRoutingKey) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Send message failed:" + replyCode + " " + replyText);
rabbitTemplate.send(message);
}
}
maven-module.orderProducer-com.study.listen.ReCreateorderConsumer.java
package com.study.listen;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.study.mapper.OrderMapper;
import com.study.model.OrderInfoModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @Description: 补单消费者
* @Author mengfanzhu
* @Date 2019/6/4 15:06
* @Version 1.0
*/
@Component
@Slf4j
public class ReCreateOrderConsumer {
@Autowired
private OrderMapper orderMapper;
/**
* 补单队列
*/
public static final String CREATE_QUEUE_NAME_TRANSACTION = "api.order.reCreate.queue";
/**
* @param message
* @param headers
* @param channel
*/
@RabbitListener(queues = CREATE_QUEUE_NAME_TRANSACTION)
public void orderReceiver(Message message, @Headers Map<String, Object> headers, Channel channel) {
try {
log.info("supplement-message;{}", JSONObject.toJSONString(message));
String msgBody = new String(message.getBody(), "UTF-8");
OrderInfoModel infoModel = JSONObject.parseObject(msgBody,OrderInfoModel.class);
log.info("supplement-message body is {} ", infoModel);
String orderNo = infoModel.getOrderNo();
OrderInfoModel orderInfoModel = orderMapper.getByOrderNo(orderNo);
if (null == orderInfoModel) {
Long result = orderMapper.insert(infoModel);
if(result != 1L){
throw new Exception("ReCreateOrderInfo Failed!");
}
}
log.info("手动签收消息,通知MQ 删除此消息");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("supplement error {}", e.toString(), e);
}
}
}
以下是Maven-module.scoreConsumer 积分消费者模块内容
maven-module.scoreConsumer.application.yml
server: port: 8081 spring: datasource: url: jdbc:mysql:/localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=round&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver rabbitmq: host: 127.0.0.1 port: 5672 username: root password: root #consumer listener: simple: #全局配置:公平派遣消息-消费者无应答并发消费数,默认为循环派遣 prefetch: 2 retry: #开启消费者重试-考虑幂等处理 enabled: true #重试间隔时间ms initial-interval: 3000 #最大重试次数 max-attempts: 2 #采用手动回答 acknowledge-mode: manual mybatis: type-aliases-package: com.study.model mapper-locations: classpath:mapper/*Mapper.xml #log_level config LOG_LEVEL: INFO
maven-module.scoreConsumer-com.study.listen.ScoreReceiverConsumer.java
package com.study.listen;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.study.mapper.ScoreMapper;
import com.study.model.ScoreModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* @Description: 积分 消费者
* @Author mengfanzhu
* @Date 2019/6/4 15:06
* @Version 1.0
*/
@Component
@Slf4j
public class ScoreReceiverConsumer {
@Autowired
private ScoreMapper scoreMapper;
/**
* 积分队列
*/
public static final String QUEUE_NAME_TRANSACTION = "api.score.queue";
@Bean
public Queue scoreQueue() {
return new Queue(QUEUE_NAME_TRANSACTION, true);
}
@RabbitListener(queues = QUEUE_NAME_TRANSACTION)
public void orderReceiver(Message message, @Headers Map<String, Object> headers, Channel channel) {
try {
log.info("score-message:{}", JSONObject.toJSONString(message));
String msgBody = new String(message.getBody(), "UTF-8");
JSONObject jsonObject = JSONObject.parseObject(msgBody);
String orderNo = jsonObject.getString("orderNo");
log.info("score-message body is :{}" + jsonObject.toJSONString());
//幂等处理
ScoreModel scoreModel = scoreMapper.getByOrderNo(orderNo);
if (scoreModel != null) {
//丢弃消息
log.info("repeat consumer ,discard the message.");
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
//处理积分逻辑,插入积分数据,成功则签收消息
scoreModel = new ScoreModel();
scoreModel.setOrderNo(orderNo);
scoreModel.setScore(100);
Boolean isInsertSuccess = scoreMapper.insert(scoreModel) == 1L;
if (isInsertSuccess) {
//手动签收消息,通知MQ 删除此消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("scoreConsumer handler success.");
}
} catch (IOException e) {
log.error("scoreConsumer handler Faild,{}", e.toString(), e);
}
}
}
测试结果
1.执行module.orderProducer - test.java com.study.mq.hello.HelloProducerTest.java - method saveServiceOrder()
发送消息到MQ,可见日志打印success, MQ 服务器QUEUE tab中也显示了对应两个队列的待消费消息数
2.启动积分消费者模块
3.启动补单消费者模块
本文源码:https://github.com/fzmeng/MqParent