在微服务环境下,因为会根据不同的业务会拆分成不同的服务,比如会员服务、订单服务、商品服务等,每个服务都有自己独立的数据库,并且是独立运行,互不影响。在多数据源的情况下,两个服务相互通讯的时候,就会出现数据不一致的问题,从而出现分布式事务产生的原因。举例:某电商平台,用户先下单后,扣库存失败,那么将会导致超卖;如果下单不成功,扣库存成功,那么会导致少卖。
先了解一些概念:ACID原理、CAP、Base理论、柔性事务与刚性事务、理解最终一致性思想。
ACID(针对数据):
数据库管理系统中事务(transaction)的四个特性(分析时根据首字母缩写依次解释):
原子性(Atomicity) 原子性是指事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
隔离性(Isolation)多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
持久性(Durability)持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
CAP(针对分布式系统):
Consistency 一致性:在分布式系统中的所有数据备份,在同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据副本。
Availability 可用性,好的响应性能。完全的可用性指的是在任何故障模型下,服务都会在有限的时间内处理完成并进行响应。
Partition tolerance 分区容忍性。尽管网络上有部分消息丢失,但系统仍然可继续工作。
CAP原理指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的。只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间。
因此大部分的 web 应用,优先考虑 AP,最后考虑C。
BASE
Basically Available(基本可用):指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。
Soft-state( 软状态/柔性事务):软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。
Eventual Consistency(最终一致性):系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。
其中,柔性事务满足BASE理论(基本可用,最终一致),刚性事务满足ACID理论。
RabbitMQ解决分布式事务原理: 采用最终一致性原理(可短暂的不一致)。需要保证以下三要素:
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认 Confirm 机制,如果发到 RabbitMQ 服务器发生异常,需要进行重试机制。)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性问题)
3、使用补单机制,即创建一个补单的消息队列,与正常的业务的消息队列绑定在同一台交换机上。补单队列检查数据库是否已经创建,如果未创建则进行数据补偿操作;如果已创建,则删除该消息。
OK,直接上代码。先下载旧,在原来的基础上修改:代码:https://pan.baidu.com/s/15FFaO24xaGAPl0abCQlySQ 提取码:4ejq
代码结构:
bootstrap.yml 配置文件如下:
spring:
rabbitmq:
#主机名
host: 127.0.0.1
#端口号
port: 5672
#账号
username: guest
#密码
password: guest
#虚拟主机,这里配置的是我们的测试主机
virtual-host: /test_host
# 开启消息确认机制 confirms
publisher-confirms: true
# 消息在未被队列接收时返回
publisher-returns: true
# 修改 RabbitMQ 的重试机制
listener:
simple:
retry:
# 开启消费者重试机制
enabled: true
#最大重试次数
max-attempts: 5
# 重试间距(单位:秒)
initial-interval: 2s
# 重试最大间隔
max-interval: 1200s
# 时间间隔的乘子,下一次间隔的时间=间隔时间 × 乘子,但最大不超过重试最大间隔
multiplier: 1
# manual=启手动应答模式,auto=自动应答模式
acknowledge-mode: manual
# 自定义配置信息
queueConfig:
# 队列名
orderQueueName: orderQueueName
# 交换机名称
orderExchangeName: orderExchangeName
# info 路由
infoRoute: infoRoute
server:
port: 8080
# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
client:
fetch-registry: false
register-with-eureka: false
配置类:
package com.study.config;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-04-05 上午 12:39
*/
@Configuration
public class QueueConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private Integer port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
//队列名
@Value("${queueConfig.orderQueueName}")
private String orderQueueName;
//交换机名称
@Value("${queueConfig.orderExchangeName}")
private String orderExchangeName;
//info 路由
@Value("${queueConfig.infoRoute}")
private String infoRoute;
/**
* 封装连接类
*
* @return
*/
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
return connectionFactory;
}
/**
* 动态的创建队列(这里仅创建配置文件里的一个)
*
* @return
* @throws Exception
*/
@Bean
public String getQueueName() throws Exception {
//获取连接
Connection connection = connectionFactory().createConnection();
//创建通道。true表示有事务功能
Channel channel = connection.createChannel(true);
//创建订单队列
channel.queueDeclare(orderQueueName, true, false, false,null);//创建队列
//创建订单交换机
channel.exchangeDeclare(orderExchangeName, BuiltinExchangeType.DIRECT, true, false, null);
//为订单队列绑定 info 路由
channel.queueBind(orderQueueName, orderExchangeName, infoRoute);
//关闭通道
channel.close();
//关闭连接
connection.close();
return "";
}
}
订单实体类:
package com.study.entity;
/**
* @author biandan
* @description 订单实体类
* @signature 让天下没有难写的代码
* @create 2021-04-11 下午 5:32
*/
public class OrderEntity {
//订单号
private String orderNo;
//商品名称
private String productName;
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
}
消息生产者类:说明,消息模板我们使用 RabbitTemplate,需要实现接口:RabbitTemplate.ConfirmCallback
package com.study.producer;
import com.alibaba.fastjson.JSONObject;
import com.study.entity.OrderEntity;
import org.springframework.amqp.core.AmqpTemplate;
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.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.UUID;
/**
* @author biandan
* @description 消息生产者(扇形交换机)
* @signature 让天下没有难写的代码
* @create 2021-04-04 下午 10:49
*/
@Component
public class DirectProducer implements RabbitTemplate.ConfirmCallback {
private SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmss");
//交换机名称
@Value("${queueConfig.orderExchangeName}")
private String orderExchangeName;
//info 路由
@Value("${queueConfig.infoRoute}")
private String infoRoute;
/**
* 注入 Rabbit 消息模板
*/
@Autowired
private RabbitTemplate template;
/**
* 每隔1分钟产生一条消息
*/
@Scheduled(fixedRate = 1000 * 60)
public void orders() {
//模拟用户下单
OrderEntity orderEntity = new OrderEntity();
String orderNo = SDF.format(new Date()) + System.currentTimeMillis();
orderEntity.setOrderNo(orderNo);//订单号
orderEntity.setProductName("霸王防脱洗发液");//商品名称
System.out.println("顾客下单信息已保存到数据库,订单号为:" + orderNo);
sendToRabbitMQ(orderNo);
}
/**
* 发送到 RabbitMQ 服务器
*
* @param orderNo
*/
private void sendToRabbitMQ(String orderNo) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("orderNo", orderNo);
String msg = jsonObject.toJSONString();
Message message = MessageBuilder.withBody(msg.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON) //json格式
.setContentEncoding("utf-8") //utf-8编码方式
.setMessageId(UUID.randomUUID() + "")//使用 UUID 作为消息ID,UUID保证全局唯一
.build();
// 构建回调返回的数据(消息id)
template.setMandatory(true);
template.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(orderNo);
//发送消息(往路由发消息,而不是往队列发消息)
template.convertAndSend(orderExchangeName, infoRoute, message, correlationData);
}
/**
* 生产消息确认机制:生产者往服务器端发送消息的时候,采用应答机制
*
* @param correlationData
* @param ack 是否以确认
* @param cause 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String orderNo = correlationData.getId();
System.out.println("确认机制返回的订单号:" + correlationData.getId());
if (ack) {
System.out.println("消息发送确认成功");
} else {
// 重试机制
sendToRabbitMQ(orderNo);
System.out.println("消息发送确认失败:" + cause);
}
}
}
启动类:
package com.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling //启用任务调度
@EnableEurekaClient
public class RabbitMQOrderApplication {
public static void main(String[] args) {
SpringApplication.run(RabbitMQOrderApplication.class, args);
}
}
OK,我们先启动服务:理论上会回调 confirm 函数,但是本人尝试了2天来解决这个问题,都没法让它回调 confirm 函数,应该是版本问题,所以决定放弃了。浪费我太多时间!如果有伙计能让程序回调此函数,不妨评论区说一下解决办法!
OK,我们知道消息生产者使用 confirm 机制能保证消息可以发送到 RabbitMQ 服务器即可!
现在我们思考一个场景:如果消息生产者已经将消息发给 RabbitMQ 服务器了,消费者也正常消费了,这时候消息生产者出现异常,导致事务回滚:比如以下代码:
解决思路:这是分布式系统经常要面对的问题之一。我们可以考虑增加一个补单队列,与订单队列绑定在相同的交换机上。交换机将订单消息同时发送给这两个队列,其中补单队列绑定补单消费者。补单消费者获取到补单队列的订单消息后,查询数据库里有没有该订单的信息,如果有,则告诉补单队列的 RabbitMQ 服务器进行删除该消息。如果没有该订单数据,则创建。
OK,我们创建一个补单队列。bootstrap.yml 配置如下:
spring:
rabbitmq:
#主机名
host: 127.0.0.1
#端口号
port: 5672
#账号
username: guest
#密码
password: guest
#虚拟主机,这里配置的是我们的测试主机
virtual-host: /test_host
# 开启消息确认机制 confirms
publisher-confirms: true
# 消息在未被队列接收时返回
publisher-returns: true
# 自定义配置信息
queueConfig:
# 队列名
orderQueueName: orderQueueName
# 交换机名称
orderExchangeName: orderExchangeName
# info 路由
infoRoute: infoRoute
# 队列名-补单队列
backOrderQueueName: backOrderQueueName
# info 路由-补单路由
backInfoRoute: backInfoRoute
server:
port: 8080
# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
client:
fetch-registry: false
register-with-eureka: false
配置文件:QueueConfig,主要是创建补单队列,绑定交换机。
package com.study.config;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-04-05 上午 12:39
*/
@Configuration
public class QueueConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private Integer port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
//队列名
@Value("${queueConfig.orderQueueName}")
private String orderQueueName;
//交换机名称
@Value("${queueConfig.orderExchangeName}")
private String orderExchangeName;
//info 路由
@Value("${queueConfig.infoRoute}")
private String infoRoute;
//补单队列名
@Value("${queueConfig.backOrderQueueName}")
private String backOrderQueueName;
//补单info 路由
@Value("${queueConfig.backInfoRoute}")
private String backInfoRoute;
/**
* 封装连接类
*
* @return
*/
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
return connectionFactory;
}
/**
* 动态的创建队列(这里仅创建配置文件里的一个)
*
* @return
* @throws Exception
*/
@Bean
public String getQueueName() throws Exception {
//获取连接
Connection connection = connectionFactory().createConnection();
//创建通道。true表示有事务功能
Channel channel = connection.createChannel(true);
//创建订单队列
channel.queueDeclare(orderQueueName, true, false, false,null);//创建队列
//创建订单交换机
channel.exchangeDeclare(orderExchangeName, BuiltinExchangeType.DIRECT, true, false, null);
//为订单队列绑定 info 路由
channel.queueBind(orderQueueName, orderExchangeName, infoRoute);
//创建补单队列
channel.queueDeclare(backOrderQueueName, true, false, false,null);
//为补单队列绑定info路由
channel.queueBind(backOrderQueueName,orderExchangeName,backInfoRoute);
//关闭通道
channel.close();
//关闭连接
connection.close();
return "";
}
}
消息生产者:
package com.study.producer;
import com.alibaba.fastjson.JSONObject;
import com.study.entity.OrderEntity;
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.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @author biandan
* @description 消息生产者(扇形交换机)
* @signature 让天下没有难写的代码
* @create 2021-04-04 下午 10:49
*/
@Component
public class DirectProducer implements RabbitTemplate.ConfirmCallback {
private SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmss");
public static List orderEntityList = new ArrayList<>();
//交换机名称
@Value("${queueConfig.orderExchangeName}")
private String orderExchangeName;
//info 路由
@Value("${queueConfig.infoRoute}")
private String infoRoute;
//补单info 路由
@Value("${queueConfig.backInfoRoute}")
private String backInfoRoute;
/**
* 注入 Rabbit 消息模板
*/
@Autowired
private RabbitTemplate template;
/**
* 每隔1分钟产生一条消息
*/
@Scheduled(fixedRate = 1000 * 60)
public void orders() {
//模拟用户下单
OrderEntity orderEntity = new OrderEntity();
String orderNo = SDF.format(new Date()) + System.currentTimeMillis();
orderEntity.setOrderNo(orderNo);//订单号
orderEntity.setProductName("霸王防脱洗发液");//商品名称
System.out.println("顾客下单信息保存到数据库,订单号为:" + orderNo);
sendToRabbitMQ(orderNo);
//模拟程序异常,导致数据回滚,没有存入数据库(或者缓存、内存)
int k = 9 /0 ;
//模拟将订单数据存入数据库(或者缓存、内存)
orderEntityList.add(orderEntity);
}
/**
* 发送到 RabbitMQ 服务器
*
* @param orderNo
*/
private void sendToRabbitMQ(String orderNo) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("orderNo", orderNo);
String msg = jsonObject.toJSONString();
Message message = MessageBuilder.withBody(msg.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON) //json格式
.setContentEncoding("utf-8") //utf-8编码方式
.setMessageId(orderNo)//使用订单号做消息ID
.build();
// 构建回调返回的数据(消息id)
template.setMandatory(true);
template.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(orderNo);
//发送消息(往路由发消息,而不是往队列发消息)
template.convertAndSend(orderExchangeName, infoRoute, message, correlationData);
template.convertAndSend(orderExchangeName, backInfoRoute, message, correlationData);
}
/**
* 生产消息确认机制:生产者往服务器端发送消息的时候,采用应答机制
*
* @param correlationData
* @param ack 是否以确认
* @param cause 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String orderNo = correlationData.getId();
System.out.println("确认机制返回的订单号:" + correlationData.getId());
if (ack) {
System.out.println("消息发送确认成功");
} else {
// 重试机制
sendToRabbitMQ(orderNo);
System.out.println("消息发送确认失败:" + cause);
}
}
}
消费者:Consumer_Order
package com.study.consumer;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
/**
* @author biandan
* @description 消费者
* @signature 让天下没有难写的代码
* @create 2021-04-04 下午 11:39
*/
@Component
public class Consumer_Order {
//订单队列的消费者
@RabbitListener(queues = "${queueConfig.orderQueueName}")//需要把 @RabbitListener 标注到方法上
public void receiveMsg(Message message, @Headers Map headers, Channel channel) throws Exception{
String messageId = message.getMessageProperties().getMessageId();
System.out.println(new Date() + " 消费者获取到的订单号 orderNo=" + messageId);
try {
String msg = new String(message.getBody(), "utf-8");
JSONObject jsonObject = JSONObject.parseObject(msg);
String orderNo = jsonObject.getString("orderNo");
System.out.println("下单成功,您的订单号是:"+orderNo);
//手动ack应答
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//手动签收
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
e.printStackTrace();
//丢弃该消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
补单消费者:Consumer_BackOrder
package com.study.consumer;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.study.entity.OrderEntity;
import com.study.producer.DirectProducer;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
/**
* @author biandan
* @description 消费者
* @signature 让天下没有难写的代码
* @create 2021-04-04 下午 11:39
*/
@Component
public class Consumer_BackOrder {
//补单队列的消费者
@RabbitListener(queues = "${queueConfig.backOrderQueueName}")//需要把 @RabbitListener 标注到方法上
public void receiveMsg(Message message, @Headers Map headers, Channel channel) throws Exception {
String orderNo = message.getMessageProperties().getMessageId();
System.out.println(new Date() + " 补单消费者获取到的消息 orderNo=" + orderNo);
try {
boolean flag = false;
//查询数据库(或者缓存、内存)
for (OrderEntity orderEntity : DirectProducer.orderEntityList) {
String existOrderNo = orderEntity.getOrderNo();
if (existOrderNo.equals(orderNo)) {//如果已经存在该订单,通知补单队列进行删除
flag = true;
System.out.println("已经存在该订单啦");
}
}
if (!flag) {//如果没有存在该订单,则将订单数据存入到数据库(或者缓存、内存)中
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderNo(orderNo);
//这个产品先固定
orderEntity.setProductName("霸王防脱洗发液");
System.out.println("订单数据不存在,已重新生成:" + JSONObject.toJSONString(orderEntity));
DirectProducer.orderEntityList.add(orderEntity);
}
//手动ack应答
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//手动签收
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
e.printStackTrace();
//丢弃该消息
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
OK,启动项目,在2个消费者方法里打上断点:
消息生产者生产消息后,查看 RabbitMQ 后台:两个队列都收到了消息。(其实可以使用扇形交换机 fanout,会自动把消息推送到所有绑定到交换机的队列上。这里演示效果,用直连交换机。)
然后查看控制台输出:订单数据已经发送给 RabbitMQ ,但是程序抛出了异常,导致数据回滚。这时候补单队列判断数据库里没有该订单,重新补单写入到数据库了。
OK,现在把生产者的报错信息去掉,再重试一次:
重启服务,控制台输出:
思考:如果补单队列仍然报错,就需要记录日志,进行人工补偿了。
完整代码:https://pan.baidu.com/s/1CJ7WPQXsaDZK5R_yag83vA 提取码:uuwt