事务基础概念
事物的回顾
事务的定义
是数据库的操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向系统提交,要么都执行,要么都不执行,事务是一组不可在分割的操作集合
事务的ACID原则
事务具有四个基本特性:原子性,一致性,隔离性,持久性,也就是我们常说的ACID原则
原子性(Atomicity):
一个事务已经是一个不可在分割的工作单位。事务中的全部操作,要么都做,要么不做
一致性(Consistency):
事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定
隔离性(lsolation)
事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性同时,并行事务的修改必须与其他并行事务的修改相互独立
持久性
一个事务一旦提交,它对数据库中的数据的改变会永久存储起来,其他操作不会对他产生影响
隔离级别
为什么要事务隔离?
如果没有定义事务隔离级别:
脏读:
在一个事务中读取到了另外一个事务修改的未提交的数据,而导致多次读取同一个数据返回的结果不一致
不可重复读:
在一个事务中读取到另一个事务修改的已提交的数据,而导致多次读取同一个数据返回的结果不一致
幻读
一个事务读取了几行记录后,另一个事务插入些记录,幻读就发生了,再后来的查询中,第一个事务就会发现原理啊没有的记录
spring事务的隔离级别
事务隔离就是帮助我们解决藏独,不可重复读,幻读
隔离级别由低到高读未提交,读已提交,可重复读,序列化操作
对大多数数据库来说就是:READ_CPMMITTED 读已提交
MYSQL默认采用:REPEATABLE_READ(可重复读)
Oracle采用READ_COMMITTED(读已提交)
Spring的隔离级别默认数据库 的隔离级别:ISOLATION_DEFAULT
事务分类讨论
本地事务
本地事务是关系型数据库中,由一组SQL组成的一个执行单元,该单元要么整体成功,要么整体失败。它的缺点就是:仅支持单库事务,并不支持跨库事务。
分布式事务
可随着业务量的不断增长,单体架构扛不住巨大的流量,此时就需要对数据库做分库分表处理,将应用SOA服务化拆分。也就产生了订单中心、用户中心、库存中心等,由此带来的问题就是业务间相互隔离,每个业务都是维护着自己的数据库,数据的交换只能进行RPC调用
当用户再次下单时,需要同时对订单库order,账户库account,交易库Trading尽心操作,可此时我们只能保证自己本地的数据一致性
无法保证调用其他服务操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入
分布式基础理论
CAP原理
一句话概括CAP:在分布式系统中,即使网络故障,服务出现瘫痪,整个系统都的数据保持一致性
美团下单到派单,并且扣除优惠卷100元
张三在美团上点了外卖,然后下订单,然后通知外卖小哥接单。思考三个问题:
1:如何体现数据的一致性
整个分布式系统中,一致性体现这笔订单,必须通知外卖小哥送单,必须扣除100的优惠券
如何体现A可用性
在真个分布式系统中,可用性体现在张三下订单的时候,如果送单服务和卡券服务瘫痪了,这时候不能影响张三下单
如何体现分区容错性
整个分布式系统中,分区容错主要体现中张三下单的时候,突然订单服务和卡券服务之间的网路断开,但是不能影响张三下订单
当前服务之间出现网络故障的情况下:
1:如何保证订单服务跟卡券服务高可用
2:下一笔订单同时扣除100元优惠券如何实现
分布式系统解决方案
AP:牺牲一致性,保持高可用(保证订单服务可以正常访问,保证卡券服务可以正常访问,是牺牲了数据一致性),张三下单成功,但是不扣除100元优惠券,这种情况下:张三下订单成功后再去查看100元优惠券,居然还存在
如何解决呢?一般做法是当玩网络恢复正常的情况下,订单服务重试请求卡券服务,再扣除100元优惠券,使用消息队列来做
CP:牺牲可用性,保证数据一致性,既保证数据的强一致性,当张三来下单的时候,提示:系统维护中等服务间的网络恢复正常后,张三再来下单
CA:可以实现吗?当然是不可以的,因为网络故障是一定会存在的,因为我们美版大去控制网络。也就是P必须要容忍。不要P分区容错性。不要P分区容错性,即不允许网络你出现故障,这事不可能实现的,所以在分布式系统中,是不存在CA的。即使单体系统也做不到CA因为单体系统也会出现单一故障问题,你可能说我可以用集群,但是一旦做了集群就由网络问题
BASE是指基本可用,软状态,最终一致性,核心思想即使无法做到强一致性CAP的一致性就是强一致性,但应用可以采用合适的方式达到最终一致性
基本可用(BASICALLY AVAILABLE)
分布式系统出现故障,允许损失部分可用性,既保证核心可用
这里的关键词是‘’部分‘和“核心”,具体选择那些作为可以损失的业务,那些事必须保证的业务,是一项有挑战凡人工作,例如对于一个用户管理系统来说,登录是核心功能,而注册可以算是非核心功能,因为未注册的用户本来就还没有使用系统的业务,注册不了最多就是流失一部分用户,而且这部分用户数量较少,如果用户已经注册但是无法登录,那就意味着用户无法使用系统,例如充了钱的游戏不能玩了,云存储不能用了,这些会对用户造成较大的损失,而且登录用户数量远远大于新注册用户,影响范围更大
软状态 SOFT STATE
允许系统存在中间状态,而该中间状态不会影响系统的整体可用性,这里的中间状态就是CAP理论中的数据不一致
最终一致性 EVENTUAL CONSISTENCY
系统中的所有数据副本经过一定的时间后,最终能达到的一致的状态
这里的关键子是一定时间和数据的特性是强关联的,不同的数据能够容忍的不一致时间是不同的,举一个微博系统的例子,用户账号能在一分钟就能达到一致状态,因为用户在A节点注册或者登录后,1分钟内不太可能立刻切换到另一个节点,但是10分钟后可能就重新登录到另一个节点上了,而且用户发布的微博,可以容忍30分钟内达到一致状态,因为对用户来说看不到某个明星发布的微博,用户是无感知的会认为明星没有发布微博,最终的含义就是不管多长时间还是要达到一致性的状态
BASE理论的本质是对CAP的延伸和补充,更具体地说是对CAP中AP方案的一个补充,前面在刨析CAP理论时,提到了其实和BASE相关的两点:其实base理论就是告诉我们:虽然在分布式开发中,存在网络问题,我们虽然达不到一致性,但是可以通过一些技术和手段让我们数据达到最终一致性
CAP理论是忽略延时的,而实际应用中延时是无法避免的
分布式事务的解决方案
实现分布式事务的解决方案比较多,常见的比如基于XA协议的2pc、3pc,基于业务层的TCC,还有应用消息队列+消息表实现的最终一致性方案,还有今天要说的Seata中间件,下边看看各个方案的优缺点
1刚性事务
2PC基于XA协议
基本要求“:
XA协议是需要数据库支持的,我们不会采用传统的数据源支持,而是我们需要使用支持XA的数据源,mysql5.6以后对这个XA事务的支持比较友好
使用事务管理器的时候,需要配置支持XA事务的食物管理器【JTA】
流程图解
两阶段提交2PC,对业务的侵入小,它最大的优势就是对使用方透明,用户可以像使用本地事务一样使用XA协议的分布式事务,能够严格保障事务的ACID特性
可2PC的缺点也是显而易见的,它是一个强一致性的同步阻塞协议,事务执行过程中需要将所需将所需要资源的全部锁定,也就是俗称的刚性事务。所以它比较适用于执行时间确定的短事务,整体性能比较差
一旦事务协调者宕机或者网络抖动,会让参与者一直处于锁定资源的状态或者只要有一部分参与者提交成功,导致数据的不一致,因此,在高并发性能至上的场景中,基于XA协议的分布式事务并不是最佳选择
【1.2】案例编写
pom.xml文件
transaction-xa继承itheitma-transaction-parent模块,居然POM文件如下:
<?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>itheitma-transaction-parent</artifactId>
<groupId>com.itheima.transaction</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>transaction-xa</artifactId>
<name>transaction-xa</name>
<dependencies>
<!--springboot的web支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok支持-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--swagger2支持-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!-- knife4j版接口文档 访问/doc.html -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!--MySQL支持-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid的配置-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!--springboot关于mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--代码生成器模板引擎 相关依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!--springboot的freemarker支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- sharding-jdbc -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
</dependency>
<!-- 使用XA事务时,需要引入此模块 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!-- 拷贝对象 -->
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>${orika-core.version}</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<!--springboot的测试支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build>
</build>
</project>
application.yml文件
这里我们配置:3个数据源、彼此之间互不干扰,具体内容如下
#服务配置
server:
#端口
port: 8080
#服务编码
tomcat:
uri-encoding: UTF-8
#spring相关配置
spring:
#应用配置
application:
#应用名称
name: transaction-xa
#开启jta事务的支持
jta:
atomikos:
properties:
log-base-dir: D:/home/transaction-xa-atomikos
log-base-name: ${spring.application.name}
#数据源配置
shardingsphere:
datasource:
names: order-db,account-db,storage-db
order-db:
type: com.alibaba.druid.pool.xa.DruidXADataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/t_order-db?useAffectedRows=true&serverTimezone=UTC&characterEncoding=utf-8
username: root
password: root
account-db:
type: com.alibaba.druid.pool.xa.DruidXADataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/t_account-db?useAffectedRows=true&serverTimezone=UTC&characterEncoding=utf-8
username: root
password: root
storage-db:
type: com.alibaba.druid.pool.xa.DruidXADataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/t_storage-db?useAffectedRows=true&serverTimezone=UTC&characterEncoding=utf-8
username: root
password: root
sharding:
tables:
tab_account:
actualDataNodes: account-db.tab_account
tab_order:
actualDataNodes: order-db.tab_order
tab_storage:
actualDataNodes: storage-db.tab_storage
props:
sql.show: true
#mubatis配置
mybatis-plus:
# MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.itheima.springboot.pojo
# 该配置请和 typeAliasesPackage 一起使用,如果配置了该属性,则仅仅会扫描路径下以该类作为父类的域对象 。
type-aliases-super-type: com.itheima.transaction.basic.BasicPojo
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
# 驼峰下划线转换
map-underscore-to-camel-case: true
use-generated-keys: true
default-statement-timeout: 60
default-fetch-size: 100
global-config:
db-config:
#主键类型(雪花ID)
id-type: assign_id
#机器 ID 部分(影响雪花ID)
worker-id: 1
#数据标识 ID 部分(影响雪花ID)
datacenter-id: 1
logging:
config: classpath:logback.xml
OrderServiceImpl
在下列代码中:我们需要吧下订单业务,收付款业务、扣库存业务、都放在一个事务单元中完成,他所依赖的是基于JTA【atomikos】的事务,并且依赖于sharding-jdbc的XA事务类型的支持
package com.xxxx.transaction.service.impl;
import com.xxxx.transaction.pojo.Order;
import com.xxxx.transaction.mapper.OrderMapper;
import com.xxxx.transaction.service.IAccountService;
import com.xxxx.transaction.service.IOrderService;
import com.xxxx.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.transaction.service.IStorageService;
import com.xxxx.transaction.utils.SnowflakeIdWorker;
import org.apache.shardingsphere.transaction.annotation.ShardingTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* @Description:订单表 服务实现类
*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Autowired
IAccountService accountService;
@Autowired
IStorageService storageService;
@Autowired
SnowflakeIdWorker snowflakeIdWorker;
@Override
@Transactional
@ShardingTransactionType(TransactionType.XA)
public Boolean payOrder(Long goodsId,
Long goodsNum,
Long payeeId,
Long payerId,
BigDecimal money) {
Boolean flagAccount = true;
Order order = Order.builder()
.productOrderNo(snowflakeIdWorker.nextId())
.goodsId(goodsId)
.goodsNum(goodsNum)
.payerId(payerId)
.payerName("李四")
.payeeId(payeeId)
.payeeName("王婆")
.realAmount(money)
.build();
flagAccount = save(order);
if (!flagAccount){
throw new RuntimeException("下订单出错");
}
//李四买瓜
flagAccount = accountService.updateAccountBalance(payerId, new BigDecimal("200").negate());
if (!flagAccount){
throw new RuntimeException("支付出错");
}
//王婆收钱
flagAccount = accountService.updateAccountBalance(payeeId, new BigDecimal("200"));
if (!flagAccount){
throw new RuntimeException("收钱出错");
}
flagAccount = storageService.updateStorageResidue(goodsId,goodsNum);
if (!flagAccount){
throw new RuntimeException("扣库存出错");
}
return flagAccount;
}
}
案例测试:
使用TransactionXAStart启动项目
数据源初始化完成
JTA事务管理器初始化完成
执行保存订单时候,把当前事务加入到分布式事务中
李四买瓜,修改李四余额,吧当前事务加入到分布式事务中
王婆收钱,修改玩牌余额,继续沿用李四的数据源,此时此数据源是沿用的,对于事务的支持也沿用
修改库存:把当前事务加入到分布式事务
最终完成,完成第二段提交,分布式事务提交
XA事务小结
性能问题:所有参与者再事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈
可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
数据一致性问题:在阶段2中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致
有点:尽量保证了数据的强一致,适合对数据一致要求高的关键领域
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能的场景
3PC
三阶段提交【3PC】是二阶段提交的一种改进版
为解决两阶段提交协议的阻塞问题,上边提到两阶段提交当协调者崩溃时,参与者不能最后的选择,就会一直保持阻塞锁定资源
2PC中没有协调者有超时机制,3PC在协调者和参与者中都引入了超时机制,协调者出现故障后,参与者就不会一直阻塞。而且在第一阶段和第二阶段中又插入了一个准备阶段,保证了在最后阶段提交之前各参与节点的状态是一致的
虽然3PC用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能变得反而更差,也不太推荐
柔性事务
最终一致性【基于RabbitMQ】
需要保证以下三要素
1确认生产者订单系统,一定要将数据投递到MQ服务器中,采用MQ消息发送确认机制
2,MQ消费者消息能够正确消费消息,采用手动ACK模式==注意重试幂等性问题,重试机制【自己定义重试次数】
3.如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单,如果第一个事务中出错,补单消费者会在重新执行一次第一个事务,例如第一个事务是添加订单表,如果失败再补单的时候重新生成订单记录,由于订单号是唯一的,所以不会重复的
流程图解
消息事务其实就是基于消息中间件的两阶段提交,将本地事务和发消息放在同一个事务里,保证本地操作,和发送消息同时成功。下单扣库存原理图
生产者下订单
Rabbitmq配置:信息如下
package com.xxxx.transaction.config;
import com.xxxx.transaction.contants.RabbitContants;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName RabbitMqConfig.java
* @Description rabbitMq配置
*/
@Configuration
public class RabbitMqConfig {
// 1.定义扣库存队列
@Bean
public Queue storageQueue() {
return new Queue(RabbitContants.QUEUE_STORAGE);
}
// 2.定义订单补偿队列
@Bean
public Queue orderCompensationQueue() {
return new Queue(RabbitContants.QUEUE_ORDER_COMPENSATION);
}
// 2.定义事务交换机
@Bean
public Exchange transactionTopicExchange() {
return new TopicExchange(RabbitContants.EXCHANGE_TRANSACTION_TOPIC);
}
// 3.扣库存队列与交换机绑定
@Bean
Binding bindingStorageQueue() {
return BindingBuilder
.bind(storageQueue())
.to(transactionTopicExchange())
.with(RabbitContants.ROUTINGKEY_PAY_ORDER).noargs();
}
// 3.订单补偿队列与交换机绑定
@Bean
Binding bindingOrderCompensationQueue() {
return BindingBuilder
.bind(orderCompensationQueue())
.to(transactionTopicExchange())
.with(RabbitContants.ROUTINGKEY_PAY_ORDER).noargs();
}
}
生产发送接口:OrderSender实现RabbitTemplate.ConfirmCallback,从写confirm,用于确定当前消息发送到交换机或队列中
package com.xxxx.transaction.rabbitmq;
import com.alibaba.fastjson.JSONObject;
import com.xxxx.transaction.contants.RabbitContants;
import com.xxxxx.transaction.pojo.RabbitMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/***
* @description 订单扣库存消息发送
* @return
*/
@Slf4j
@Component
public class OrderSender implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
/***
* @description 订单下单发送消息的方法
* @param rabbitMessage 发送的消息
* @param rabbitMessage 订单信息
* @return
*/
public void sendMessage(RabbitMessage rabbitMessage){
log.info("发送消息给库存服务");
// 构建回调返回的数据
CorrelationData correlationData = new CorrelationData(JSONObject.toJSONString(rabbitMessage));
//设置投递规则
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
// 发送消息
rabbitTemplate.convertAndSend(
RabbitContants.EXCHANGE_TRANSACTION_TOPIC,
RabbitContants.ROUTINGKEY_PAY_ORDER,
rabbitMessage,correlationData);
}
/**
* 订单发送交换机确认回调的处理方法
* 如果ack为true,则表示当前的消息已经发送到的交换机
* 俄国ack为false,则进行再次投递,直到投递成功
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String errorInfo) {
String rabbitMessage = correlationData.getId();
if (ack) {
log.info("订单消息发送交换机确认成功{}",rabbitMessage);
} else {
log.warn("订单消息发送交换机确认失败:{},错误:{}",rabbitMessage,errorInfo);
log.info("订单消息重新投递{}",rabbitMessage);
sendMessage(JSONObject.parseObject(rabbitMessage,RabbitMessage.class));
}
}
}
package com.xxxx.transaction.rabbitmq;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xxx.transaction.contants.RabbitContants;
import com.xxxxx.transaction.contants.RedisConstant;
import com.xxxx.transaction.pojo.Order;
import com.xxxx.transaction.pojo.OrderVo;
import com.xxxx.transaction.pojo.RabbitMessage;
import com.xxxx.transaction.service.IOrderService;
import com.xxxx.transaction.utils.BeanConv;
import com.xxxx.transaction.utils.EmptyUtil;
import com.rabbitmq.client.Channel;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RabbitmqListener.java
* @Description 订单补单监听
*/
@Component
@RabbitListener(queues = RabbitContants.QUEUE_ORDER_COMPENSATION)
public class OrderListener {
@Autowired
IOrderService orderService;
@Autowired
RedissonClient redissonClient;
@RabbitHandler
public void process(RabbitMessage rabbitMessage, Channel channel, Message message) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
OrderVo orderVo = JSONObject.parseObject(rabbitMessage.getConten(), OrderVo.class);
RLock lock = redissonClient.getLock(RedisConstant.LOCK_PREFIX+orderVo.getProductOrderNo().toString());
try {
//幂等性处理
if (lock.tryLock(RedisConstant.REDIS_WAIT_TIME,RedisConstant.REDIS_LEASETIME, TimeUnit.MILLISECONDS)) {
//查询订单是否存在
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(StringUtils.camelToUnderline(Order.Fields.productOrderNo),orderVo.getProductOrderNo());
Order orderResult = orderService.getOne(queryWrapper);
boolean flag = true;
//不存在则补单
if (EmptyUtil.isNullOrEmpty(orderResult)){
orderVo.setId(null);
Order order = BeanConv.toBean(orderVo, Order.class);
flag = orderService.save(order);
}
if (flag){
//签收消息
channel.basicAck(deliveryTag, false);
}else {
throw new RuntimeException("未正常处理补单业务,进入重试:"+orderVo.toString());
}
}
}catch (Exception ex){
// 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。
// 设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行
channel.basicReject(deliveryTag, true);
}finally {
lock.unlock();
}
}
}
OrderListener:此监听主要处理,当发送RabbitMQ成功,但是由于生产端数据库网络波动,导致丢单,此处会从新发送。【注意:幂等性处理】
package com.xxxx.transaction.rabbitmq;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xxxx.transaction.contants.RabbitContants;
import com.xxxx.transaction.contants.RedisConstant;
import com.xxxx.transaction.pojo.Order;
import com.xxxx.transaction.pojo.OrderVo;
import com.xxxx.transaction.pojo.RabbitMessage;
import com.xxxx.transaction.service.IOrderService;
import com.xxxx.transaction.utils.BeanConv;
import com.xxxx.transaction.utils.EmptyUtil;
import com.rabbitmq.client.Channel;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RabbitmqListener.java
* @Description 订单补单监听
*/
@Component
@RabbitListener(queues = RabbitContants.QUEUE_ORDER_COMPENSATION)
public class OrderListener {
@Autowired
IOrderService orderService;
@Autowired
RedissonClient redissonClient;
@RabbitHandler
public void process(RabbitMessage rabbitMessage, Channel channel, Message message) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
OrderVo orderVo = JSONObject.parseObject(rabbitMessage.getConten(), OrderVo.class);
RLock lock = redissonClient.getLock(RedisConstant.LOCK_PREFIX+orderVo.getProductOrderNo().toString());
try {
//幂等性处理
if (lock.tryLock(RedisConstant.REDIS_WAIT_TIME,RedisConstant.REDIS_LEASETIME, TimeUnit.MILLISECONDS)) {
//查询订单是否存在
QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(StringUtils.camelToUnderline(Order.Fields.productOrderNo),orderVo.getProductOrderNo());
Order orderResult = orderService.getOne(queryWrapper);
boolean flag = true;
//不存在则补单
if (EmptyUtil.isNullOrEmpty(orderResult)){
orderVo.setId(null);
Order order = BeanConv.toBean(orderVo, Order.class);
flag = orderService.save(order);
}
if (flag){
//签收消息
channel.basicAck(deliveryTag, false);
}else {
throw new RuntimeException("未正常处理补单业务,进入重试:"+orderVo.toString());
}
}
}catch (Exception ex){
// 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。
// 设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行
channel.basicReject(deliveryTag, true);
}finally {
lock.unlock();
}
}
}
【1.2.2】消费者【扣库存】
transaction-mq-customer的application.yml中设置,手动签收,且设置重试
server:
port: 8081
spring:
application:
name: transaction-mq-product
redis:
redisson:
config: classpath:singleServerConfig.yaml
datasource:
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/t_storage-db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
username: root
password: root
rabbitmq:
host: 192.168.112.129
port: 5672
virtual-host: /itheima
username: admin
password: pass
listener:
simple:
#手动签收
acknowledge-mode: manual
#并发消费者初始化值
concurrency: 10
#并发消费者的最大值
max-concurrency: 20
#每个消费者每次监听时可拉取处理的消息数量
prefetch: 5
retry:
#是否开启消费者重试(为false时关闭消费者重试,这时消费端代码异常会一直重复收到消息)
enabled: true
#最大重试次数
max-attempts: 2
#重试间隔时间(单位毫秒)
initial-interval: 20000
#重试最大时间间隔(单位毫秒):当前时间间隔<max-interval(重试最大时间间隔)
max-interval: 1200000
#应用于前一重试间隔的乘法器:当前时间间隔=上次重试间隔*multiplier。
multiplier: 1
#mybatis配置
mybatis-plus:
# MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.itheima.springboot.pojo
# 该配置请和 typeAliasesPackage 一起使用,如果配置了该属性,则仅仅会扫描路径下以该类作为父类的域对象 。
type-aliases-super-type: com.itheima.transaction.basic.BasicPojo
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
# 驼峰下划线转换
map-underscore-to-camel-case: true
use-generated-keys: true
default-statement-timeout: 60
default-fetch-size: 100
global-config:
db-config:
#主键类型(雪花ID)
id-type: assign_id
#机器 ID 部分(影响雪花ID)
worker-id: 1
#数据标识 ID 部分(影响雪花ID)
datacenter-id: 1
logging:
config: classpath:logback.xml
OrderListener:设计消费的幂等性,如果业务执行失败,是将数据重新丢回队列里,那么下次还会消费这消息
package com.xxxx.transaction.rabbitmq;
import com.alibaba.fastjson.JSONObject;
import com.xxxx.transaction.contants.RabbitContants;
import com.xxxx.transaction.contants.RedisConstant;
import com.xxxx.transaction.pojo.OrderVo;
import com.xxxx.transaction.pojo.RabbitMessage;
import com.xxxx.transaction.service.IStorageService;
import com.rabbitmq.client.Channel;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RabbitmqListener.java
* @Description 扣款监听
*/
@Component
@RabbitListener(queues = RabbitContants.QUEUE_STORAGE)
public class OrderListener {
@Autowired
IStorageService storageService;
@Autowired
RedissonClient redissonClient;
@RabbitHandler
public void process(RabbitMessage rabbitMessage, Channel channel, Message message) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
OrderVo orderVo = JSONObject.parseObject(rabbitMessage.getConten(), OrderVo.class);
RLock lock = redissonClient.getLock(RedisConstant.LOCK_PREFIX+"storage:"+orderVo.getProductOrderNo().toString());
try {
//幂等性处理
if (lock.tryLock(RedisConstant.REDIS_WAIT_TIME,RedisConstant.REDIS_LEASETIME, TimeUnit.MILLISECONDS)) {
Boolean flag = storageService.updateStorageResidue(orderVo.getGoodsId(), orderVo.getGoodsNum());
if (flag){
//签收消息
channel.basicAck(deliveryTag, false);
}else {
throw new RuntimeException("未正常处理扣库存业务,进入重试:"+orderVo.toString());
}
}
}catch (Exception ex){
// 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。
// 设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行
channel.basicReject(deliveryTag, true);
}finally {
lock.unlock();
}
}
}
【2】TCC机制【基于seata】
TCC分布式事务模型需要业务系统提供三段业务逻辑
1.初步操作TRY:完成所有业务检查,预留必须的业务资源
2.确认操作Confirm:真正执行的业务逻辑,不做任何业务检查