分布式事务
1、前言
现在有这样一个场景,我要使用京东买东西,微信支付成功后那边会成功生成订单,并且商品库存的数量会减少。一般订单会由订单系统维护,库存会由库存系统维护,也就是说我购买成功后,会涉及到两个不同系统的运作,实际可能会涉及到更多的系统,它们之间协调来完成用户支付成功后的一系列动作。如下图:
过程如下:
- 用户支付完成,微信支付系统会主动通知京东支付结果,京东也可以主动请求微信支付查询订单的支付结果。最终得到支付结果后将订单支付结果保存到订单数据库中。
- 订单支付完成系统自动减少对应商品的库存数量。
上面就涉及到了分布式事务,订单服务和库存服务是隶属于京东系统的两个不同子服务,这两个服务需要协调来完成用户支付成功后添加订单以及减少库存的任务。
2、分布式事务
2.1、需求描述
根据上面的需求,分析如下:
用户支付完成会将支付状态及订单状态保存在订单数据库中,由订单服务去维护订单数据库。而库存信息在库存数据库,由库存服务去维护库存数据库的信息。如下图:
如何实现订单服务、库存服务这两个分布式服务共同完成一件事即订单支付成功自动减少商品的库存数量,关键在于保证两个分布式服务的事务的一致性。
如果是在订单服务中远程调用库存服务的接口,如下伪代码:
订单成功后的通知方法{
更新支付表的状态为成功;
远程调用库存服务减少商品库存数量;
}
说明:
- 更新支付表状态是本地数据库操作。
- 远程调用是网络远程调用请求。
- 为保存事务上边两步操作由spring控制事务,遇到异常回滚本地数据库操作。
可能出现的问题:
- 更新支付表失败抛出异常,不再执行远程调用。这个没问题。
- 更新支付表成功,网络远程调用超时会拉长本地数据库事务时间,影响数据库性能。
- 更新支付表成功,远程调用减少库存成功,最后更新支付表时commi提交失败,这就会出现不一致的情况。
其实上面的问题就涉及到了分布式事务的控制。
2.2、关于分布式事务
先了解什么是分布式系统。
分布式系统:
分布式系统指部署在不同结点上的系统通过网络交互来完成协同工作的系统。
比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
再了解什么是事务。
事务:
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
- 原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。
- 一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。
- 隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。
- 持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
本地事务:
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。
分布式事务:
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。
这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,如下图:
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况。
第一种形式用的比较多。
分布式事务的使用场景:
- 电商系统中的下单扣库存
电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成。 - 金融系统中的银行卡充值
在金融系统中通过银行卡向平台充值需要通过银行系统和金融系统协同完成。 - 教育系统中下单选课业务
在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统和选课系统协同完成。 - SNS系统的消息发送
在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统和手机通信系统协同完成。
2.3、CAP理论
如何进行分布式事务控制?先要了解CAP理论,CAP理论是分布式事务处理的理论基础。
CAP理论是:分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance)中满足其中两种,无法同时满足三种。
以下图示来理解CAP:
以上四个节点都保存了用户数据,A节点是主节点,保存用户数据,其他B、C、D节点保存的是用户的数据副本。
- 一致性(Consistency):服务A、B、C、D四个结点都存储了用户数据, 四个结点的数据需要保持同一时刻的数据一致性。
- 可用性(Availability):服务A、B、C、D四个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C、D是为了保证系统的可用性。
- 分区容忍性(Partition Tolerance):分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。
网络分区
分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区。
2.3.1、能否同时兼顾CAP
在保证分区容忍性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个结点,如果要保证数据的一致性就要实现每个结点的数据一致,结点越多可用性越好,但是数据一致性越差。所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。
2.3.2、CAP的组合方式
(1)CA
放弃分区容忍性,加强一致性和可用性,关系数据库按照CA进行设计。
(2)AP
放弃一致性,加强可用性和分区容忍性,追求最终一致性,很多NoSQL数据库按照AP进行设计。这里说的放弃一致性是指放弃强一致性,强一致性就是写入成功立刻要查询出最新数据。追求最终一致性是指允许暂时的数据不一致,只要最终在用户接受的时间内数据 一致即可。
(3)CP
放弃可用性,加强一致性和分区容忍性,一些强一致性要求的系统按CP进行设计,比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。由于网络问题的存在,CP系统可能会出现待等待超时,如果没有处理超时问题则整个系统会出现阻塞。
小结:在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最新数据),保证数据最终一致性。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的时间内退款事务走完即可。
2.4、分布式事务的解决方案
对于分布式事务的一致性,解决方案一般有3种:两阶段提交协议(2PC)、事务补偿(TCC)、消息队列实现最终一致。
2.4.1、 两阶段提交协议(2PC)
为解决分布式系统的数据一致性问题出现了两阶段提交协议(2 Phase Commitment Protocol),两阶段提交由协调者和参与者组成,共经过两个阶段和三个操作,部分关系数据库如Oracle、MySQL支持两阶段提交协议。
2PC协议流程图:
- 第一阶段:准备阶段(prepare)
协调者通知参与者准备提交订单,参与者开始投票。协调者完成准备工作向协调者回应Yes。 - 第二阶段:提交(commit)/回滚(rollback)阶段
协调者根据参与者的投票结果发起最终的提交指令。如果有参与者没有准备好则发起回滚指令。
通过下单减少库存的例子说明:
说明:
- 应用程序连接两个数据源。
- 应用程序通过事务协调器向两个数据库发起prepare,两个数据库收到消息分别执行本地事务(记录日志),但不提交,如果执行成功则回复yes,否则回复no。
- 事务协调器收到回复,只要有一方回复no则分别向参与者发起回滚事务,参与者开始回滚事务。
- 事务协调器收到回复,全部回复yes,此时向参与者发起提交事务。如果参与者有一方提交事务失败则由事务协调器发起回滚事务。
2PC的优缺点:
- 优点:实现强一致性,部分关系数据库支持(Oracle、MySQL等)。
- 缺点:整个事务的执行需要由协调者在多个节点之间去协调,增加了事务的执行时间,性能低下。
- 解决方案:springboot+Atomikos/Bitronix。
相关的有3PC,3PC主要是解决协调者与参与者通信阻塞问题而产生的,它比2PC传递的消息还要多,性能不高。
2.4.2、 事务补偿(TCC)
TCC事务补偿是基于2PC实现的业务层事务控制方案,它是Try、Confirm和Cancel三个单词的首字母组成,说明如下:
- Try检查及预留业务资源
完成提交事务前的检查,并预留好资源。 - Confirm确定执行业务操作
对try阶段预留的资源正式执行。 - Cancel取消执行业务操作
对try阶段预留的资源释放。
同样以下单减少库存的例子说明:
- Try阶段
下单业务由订单服务和库存服务协同完成,在try阶段订单服务和库存服务完成检查和预留资源。订单服务检查当前是否满足提交订单的条件(比如:当前存在未完成订单的不允许提交新订单)。库存服务检查当前是否有充足的库存,并锁定资源。 - Confirm阶段
订单服务和库存服务成功完成Try后开始正式执行资源操作。订单服务向订单写一条订单信息。库存服务减去库存。 - Cancel阶段
如果订单服务和库存服务有一方出现失败则全部取消操作。订单服务需要删除新增的订单信息。库存服务将减去的库存再还原。
TCC的优缺点:
- 优点:最终保证数据的一致性,在业务层实现事务控制,灵活性好。
- 缺点:开发成本高,每个事务操作每个参与者都需要实现try/confirm/cancel三个接口。
说明:TCC的try/confirm/cancel接口都要实现幂等性,在try、confirm、cancel失败后要不断重试。
幂等性
幂等性是指同一个操作无论请求多少次,其结果都相同。实现幂等性的方式如下:
- 操作之前在业务方法进行判断,如果执行过了就不再执行。
- 缓存所有请求和处理的结果,已经处理的请求则直接返回结果。
- 在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。
2.4.3、 消息队列实现最终一致性
本方案是将分布式事务拆分成多个本地事务来完成,并且由消息队列异步协调完成。如下图:
过程如下:
- 订单服务和库存服务完成检查和预留资源。
- 订单服务在本地事务中完成添加订单表记录和添加“减少库存任务消息”。
- 由定时任务根据消息表的记录发送给MQ通知库存服务执行减库存操作。
- 库存服务执行减少库存,并且记录执行消息状态(为避免重复执行消息,在执行减库存之前查询是否执行过此消息)。
- 库存服务向MQ发送完成减少库存的消息。
- 订单服务接收到完成库存减少的消息后删除原来添加的“减少库存任务消息”。
实现最终事务一致要求:预留资源成功理论上要求正式执行成功,如果执行失败会进行重试,要求业务执行方法实现幂等。
使用MQ实现的优缺点:
- 优点:由MQ按异步的方式协调完成事务,性能较高。不用实现try/confirm/cancel接口,开发成本比TCC低。
- 缺点:此方式基于关系数据库本地事务来实现,会出现频繁读写数据库记录,浪费数据库资源,另外对于高并发操作不是最佳方案。
3、测试
选择第三种解决方式,使用MQ来实现事务的最终一致性。流程图如下:
现在用代码进行测试。
3.1、准备工作
(1)数据表
在mysql中创建2个新的数据库:jd_order和jd_warehouse。
jd_order是订单服务连接的,库有两张表:jd_task、jd_task_his。
jd_task是任务表,MQ需要定时扫描的。如下:
jd_task_his是历史任务表,当任务完成后任务表会删除任务,历史任务表中会添加历史任务,相当于任务完成后任务信息从任务表中移到了历史任务表中。和任务表字段完全一样,如下:
jd_warehouse是仓库服务连接的,库中有两张表:jd_goods_number、jd_task_his。
jd_goods_number里面记录着商品的数量,当接收到MQ的消息后,此表对应商品数量减少,即update更新操作。如下:
jd_task_his和jd_order中的历史表完全一样,只有在库存数量减少成功后才会向这个历史表中写入记录。
(2)pojo
以上4张表,1张重复,应该对应3个实体类,这几个实体类单独放在model工程了。如下:
@Data
@ToString
@Entity
@Table(name = "jd_task")
@GenericGenerator(name = "jpa-uuid",strategy = "uuid")
public class JdTask implements Serializable{
private static final long serialVersionUID = -5428501845772184256L;
@Id
@GeneratedValue(generator = "jpa-uuid")
@Column(length = 32)
private String id;
@Column(name = "create_time")
private Date createTime;
@Column(name = "updateTime")
private Date updateTime;
@Column(name = "delete_time")
private Date deleteTime;
@Column(name = "task_type")
private String taskType;
@Column(name = "mq_exchange")
private String mqExchange;
@Column(name = "mq_routingkey")
private String mqRoutingkey;
@Column(name = "request_body")
private String requestBody;
private Integer version;
private String status;
private String errormsg;
}
@Data
@ToString
@Entity
@Table(name = "jd_task_his")
@GenericGenerator(name = "jpa-uuid",strategy = "uuid")
public class JdTaskHis implements Serializable{
private static final long serialVersionUID = -5428501845772184256L;
@Id
@GeneratedValue(generator = "jpa-uuid")
@Column(length = 32)
private String id;
@Column(name = "create_time")
private Date createTime;
@Column(name = "updateTime")
private Date updateTime;
@Column(name = "delete_time")
private Date deleteTime;
@Column(name = "task_type")
private String taskType;
@Column(name = "mq_exchange")
private String mqExchange;
@Column(name = "mq_routingkey")
private String mqRoutingkey;
@Column(name = "request_body")
private String requestBody;
private Integer version;
private String status;
private String errormsg;
}
@Data
@ToString
@Entity
@Table(name = "jd_goods_number")
public class JdGoodsNumber implements Serializable{
private static final long serialVersionUID = -3932526252941241357L;
@Id
private Integer id;
@Column(name = "goods_name")
private String goodsName;
@Column(name = "sales_date")
private Date salesDate;
@Column(name = "inventory_quantity")
private Integer inventoryQuantity;
}
3.2、订单服务
(1)创建新的Maven工程
(2)pom依赖
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath />
</parent>
<groupId>com.ycz</groupId>
<artifactId>order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 依赖的model模块 -->
<dependency>
<groupId>com.ycz</groupId>
<artifactId>ycz-model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 德鲁伊数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- spring和mybatis整合包依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- 分页插件依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<!-- orm依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
<!-- jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- json转换包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.3</version>
</dependency>
<!-- commons工具包 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<!-- mq依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)application.yml
server:
port: 25000
spring:
application:
name: ycz-order
## 数据源配置
datasource:
druid:
url: jdbc:mysql://rm-m5e130nm7h37n6v982o.mysql.rds.aliyuncs.com:3306/jd_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: xxxxx
password: xxxxx
driverClassName: com.mysql.cj.jdbc.Driver
initialSize: 5 #初始建立连接数量
minIdle: 5 #最小连接数量
maxActive: 20 #最大连接数量
maxWait: 10000 #获取连接最大等待时间,毫秒
testOnBorrow: true #申请连接时检测连接是否有效
testOnReturn: false #归还连接时检测连接是否有效
timeBetweenEvictionRunsMillis: 600000 #配置间隔检测连接是否有效的时间(单位是毫秒)
minEvictableIdleTimeMillis: 300000 #连接在连接池的最小生存时间(毫秒)
## MQ配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
publisher-confirms: true
virtual-host: /
(4)启动类
创建启动类:
@SpringBootApplication
//扫描实体
@EntityScan(value = {"com.ycz.domain.task"} )
//扫描本项目下的所有包
@ComponentScan(basePackages = {"com.ycz.order"})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
最好先测试一下看不能跑得起来,我这里测试过,没问题。
(5)dao层
在dao包下创建两个dao,如下:
package com.ycz.order.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import com.ycz.domain.task.JdTaskHis;
public interface JdTaskHisRepository extends JpaRepository<JdTaskHis, String>{
}
package com.ycz.order.dao;
import java.util.Date;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.ycz.domain.task.JdTask;
public interface JdTaskRepository extends JpaRepository<JdTask, String>{
//取出指定时间之前的记录
Page<JdTask> findByUpdateTimeBefore(Pageable pageable,Date updateTime);
//更新update_time字段
@Modifying
@Query("update JdTask jt set jt.updateTime = :updateTime where jt.id = :id")
int updateTaskTime(@Param("id") String id,@Param("updateTime") Date updateTime);
//更新版本号version
@Modifying
@Query("update JdTask jt set jt.version = :version+1 where jt.id = :id and jt.version = :version")
int updateTaskVersion(@Param("id") String id,@Param("version") int version);
}
(6)Service层
在service包下创建新的TaskService,如下:
@Service
public class TaskService {
@Autowired
JdTaskRepository jdTaskRepository;
@Autowired
JdTaskHisRepository jdTaskHisRepository;
@Autowired
RabbitTemplate rabbitTemplate;
// 取出指定时间之前的任务,分页
public List<JdTask> findTaskList(Date updateTime, int size) {
// 获取分页对象
Pageable pageable = new PageRequest(0, size);
Page<JdTask> all = jdTaskRepository.findByUpdateTimeBefore(pageable, updateTime);
// 获取分页列表
List<JdTask> tasks = all.getContent();
return tasks;
}
//获取任务
@Transactional
public int getTask(String id,int version) {
int res = jdTaskRepository.updateTaskVersion(id, version);
//只会返回1和0,1代表取任务成功然后更新版本号
//0代表取任务失败,不会更新版本号
return res;
}
//给MQ发送减少商品库存数量消息
public void publish(JdTask jdTask,String exchange,String routingKey) {
//先查询任务
Optional<JdTask> optional = jdTaskRepository.findById(jdTask.getId());
//有任务的话再发消息
if(optional.isPresent()) {
rabbitTemplate.convertAndSend(exchange,routingKey,jdTask);
JdTask one = optional.get();
//更新任务时间为当前
one.setUpdateTime(new Date());
//保存
jdTaskRepository.save(one);
}
}
//接收到MQ的库存数量减少成功消息后删除任务
@Transactional
public void finishTask(String taskId) {
//先查询任务是否存在
Optional<JdTask> optional = jdTaskRepository.findById(taskId);
//存在的话
if(optional.isPresent()) {
JdTask task = optional.get();
//设置删除日期
task.setDeleteTime(new Date());
JdTaskHis jdTaskHis = new JdTaskHis();
//属性拷贝
BeanUtils.copyProperties(task, jdTaskHis);
//保存任务历史
jdTaskHisRepository.save(jdTaskHis);
//删除任务表中的任务记录
jdTaskRepository.delete(task);
}
}
}
(7)定时任务配置类
在config包下创建定时任务的配置类,如下:
/*
* 定时任务配置类
*/
@Component
// 开启定时任务
@EnableScheduling
public class ReduceNumberTask {
@Autowired
TaskService taskService;
// 定时任务方法
// 每分钟执行一次
@Scheduled(cron = "* 0/1 * * * *")
public void sendReduceNumberTask() {
// 获取一分钟之前的时间
Calendar calendar = new GregorianCalendar();
calendar.setTime(new Date());
calendar.set(GregorianCalendar.MINUTE, -1);
Date date = calendar.getTime();
// 查询指定时间之前的100条任务
List<JdTask> tasks = taskService.findTaskList(date, 100);
// 有任务,发消息给MQ
if (tasks != null && tasks.size() > 0) {
for (JdTask task : tasks) {
// 为保证消息一分钟只发一次,采用乐观锁机制
int result = taskService.getTask(task.getId(), task.getVersion());
// 获取任务成功,发送消息
if (result > 0) {
taskService.publish(task, task.getMqExchange(), task.getMqRoutingkey());
System.out.println(task.getId() + "号库减消息已发送!");
}
}
} else {// 无任务
System.out.println("当前无任务!");
}
}
}
如果要实现并行任务的话,要采用多线程,还需创建一个异步任务配置类,如下:
/*
* 异步任务配置类
*/
@Configuration
public class AsyncTaskConfig implements SchedulingConfigurer, AsyncConfigurer {
// 线程池容量
private static final int CRON_POOL_SIZE = 10;
// 注册任务线程池
@Bean
public ThreadPoolTaskScheduler getThreadPool() {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
// 初始化
threadPoolTaskScheduler.initialize();
// 设置线程池容量
threadPoolTaskScheduler.setPoolSize(CRON_POOL_SIZE);
return threadPoolTaskScheduler;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(getThreadPool());
}
@Override
public Executor getAsyncExecutor() {
Executor executor = getThreadPool();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
(8)MQ配置类
MQ用的是RabbitMQ,所以在config包下创建RabbitMQ的配置类,如下:
/*
* RabbitMQ配置类
* 主要进行队列、交换机注册以及队列绑定交换机
*/
@Configuration
public class RabbitMQConfig {
// 库存减少的队列名称
public static final String JD_REDUCE_NUMBER = "jd_reduce_number";
// 库存减少完成的队列名称
public static final String JD_REDUCE_NUMBER_FINISH = "jd_reduce_number_finish";
// 交换机名称
private static final String JD_REDUCE_NUMBER_EXCHANGE = "jd_reduce_number_exchange";
// routingKey
private static final String JD_REDUCE_NUMBER_ROUTINGKEY = "jd_reduce_number_routingkey";
// 注册库存减少队列
@Bean(JD_REDUCE_NUMBER)
public Queue JD_REDUCE_NUMBER() {
Queue queue = new Queue(JD_REDUCE_NUMBER);
return queue;
}
//注册交换机
@Bean(JD_REDUCE_NUMBER_EXCHANGE)
public Exchange JD_REDUCE_NUMBER_EXCHANGE() {
return ExchangeBuilder.directExchange(JD_REDUCE_NUMBER_EXCHANGE).
durable(true).build();
}
//队列绑定到交换机
@Bean
public Binding BINDING_REDUCE_NUMBER(
@Qualifier(JD_REDUCE_NUMBER)Queue queue,
@Qualifier(JD_REDUCE_NUMBER_EXCHANGE)Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).
with(JD_REDUCE_NUMBER_ROUTINGKEY).noargs();
}
}
(9)MQ的监听类
订单服务需要监听MQ,接收库存减少完成的消息,所以需要配置一个监听类,在mq包下创建监听类,如下:
/*
* MQ的监听类
*/
@Component
public class MqListener {
@Autowired
TaskService taskService;
@Autowired
RabbitTemplate rabbitTemplate;
//监听库存减少完成队列
@RabbitListener(queues = {RabbitMQConfig.JD_REDUCE_NUMBER_FINISH})
public void receiveFinishReduceNumber(JdTask jdTask) {
//获取消息id
String id = jdTask.getId();
//删除任务,添加历史任务
taskService.finishTask(id);
}
}
order订单服务的代码就全部完成了,启动RabbitMQ,然后启动工程,看能不能跑得起来,应该会报AMQP的错误,因为监听的队列还没创建。
3.3、仓库服务
(1)创建新的Maven工程
(2)pom依赖
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath />
</parent>
<groupId>com.ycz</groupId>
<artifactId>warehouse</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 依赖的model模块 -->
<dependency>
<groupId>com.ycz</groupId>
<artifactId>ycz-model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql驱动包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- 德鲁伊数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- spring和mybatis整合包依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- 分页插件依赖 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<!-- orm依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
<!-- jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- json转换包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.3</version>
</dependency>
<!-- commons工具包 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- mq依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3)application.yml配置
server:
port: 26000
spring:
application:
name: ycz-warehouse
## 数据源配置
datasource:
druid:
url: jdbc:mysql://rm-m5e130nm7h37n6v982o.mysql.rds.aliyuncs.com:3306/jd_warehouse?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: xxxxxx
password: xxxxxx
driverClassName: com.mysql.cj.jdbc.Driver
initialSize: 5 #初始建立连接数量
minIdle: 5 #最小连接数量
maxActive: 20 #最大连接数量
maxWait: 10000 #获取连接最大等待时间,毫秒
testOnBorrow: true #申请连接时检测连接是否有效
testOnReturn: false #归还连接时检测连接是否有效
timeBetweenEvictionRunsMillis: 600000 #配置间隔检测连接是否有效的时间(单位是毫秒)
minEvictableIdleTimeMillis: 300000 #连接在连接池的最小生存时间(毫秒)
## MQ配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
publisher-confirms: true
virtual-host: /
(4)启动类
创建启动类,如下:
@SpringBootApplication
// 扫描实体
@EntityScan(basePackages = {"com.ycz.domain.warehouse", "com.ycz.domain.task" })
// 扫描本项目的所有包
@ComponentScan(basePackages = {"com.ycz.warehouse" })
public class WarehouseApplication {
public static void main(String[] args) {
SpringApplication.run(WarehouseApplication.class, args);
}
}
启动类创建好之后,先启动项目,看能不能跑的起来,这里已经测试过,没问题。
(5)dao层
在dao包下创建3个接口:
public interface JdGoodsNumberRepository extends JpaRepository<JdGoodsNumber, Integer>{
}
@Mapper
public interface JdGoodsMapper {
@Select("select * from jd_goods_number where goods_name = #{name} and inventory_quantity >=0 ")
JdGoodsNumber findByName(String name);
@Update("update jd_goods_number set inventory_quantity = inventory_quantity - 1")
int reduceNumber();
}
public interface JdTaskHisRepository extends JpaRepository<JdTaskHis, String> {
}
(6)Service层
在service包下创建WarehouseService,如下:
@Service
public class WarehouseService {
@Autowired
JdGoodsMapper jdGoodsMapper;
@Autowired
JdGoodsNumberRepository jdGoodsNumberRepository;
@Autowired
JdTaskHisRepository jdTaskHisRepository;
// 完成商品库存数量减少
@Transactional
public int reduceGoodsNumber(String userId,int goodsId, String goodsName, JdTask jdTask) throws Exception {
if(StringUtils.isEmpty(userId)) {
throw new Exception("用户不存在,无法下单!");
}
if (StringUtils.isEmpty(Integer.toString(goodsId))) {
throw new Exception("商品不存在!");
}
if (jdTask == null && StringUtils.isEmpty(jdTask.getId())) {
throw new Exception("任务为空!");
}
// 查询
JdGoodsNumber one = jdGoodsMapper.findByName(goodsName);
if (one == null) {
throw new Exception("商品不存在或库存数量为0!");
}
// 存在,更新库存数量
int res = jdGoodsMapper.reduceNumber();
// 更新成功,写历史表
if (res > 0) {
JdTaskHis his = new JdTaskHis();
// 拷贝属性
BeanUtils.copyProperties(jdTask, his);
// 保存
jdTaskHisRepository.save(his);
}
return 1;
}
}
(7)MQ配置类
在config包下创建MQ的配置类,如下:
/*
* RabbitMQ配置类
* 主要进行队列、交换机注册以及队列绑定交换机
*/
@Configuration
public class RabbitMQConfig {
// 库存减少的队列名称
public static final String JD_REDUCE_NUMBER = "jd_reduce_number";
// 库存减少完成的队列名称
public static final String JD_REDUCE_NUMBER_FINISH = "jd_reduce_number_finish";
// 交换机名称
public static final String JD_REDUCE_NUMBER_EXCHANGE = "jd_reduce_number_exchange";
// routingKey
public static final String JD_REDUCE_NUMBER_FINISH_ROUTINGKEY = "jd_reduce_number_finish_routingkey";
// 注册库存减少完成队列
@Bean(JD_REDUCE_NUMBER_FINISH)
public Queue JD_REDUCE_NUMBER() {
Queue queue = new Queue(JD_REDUCE_NUMBER_FINISH);
return queue;
}
//注册交换机
@Bean(JD_REDUCE_NUMBER_EXCHANGE)
public Exchange JD_REDUCE_NUMBER_EXCHANGE() {
return ExchangeBuilder.directExchange(JD_REDUCE_NUMBER_EXCHANGE).
durable(true).build();
}
//队列绑定到交换机
@Bean
public Binding BINDING_REDUCE_NUMBER(
@Qualifier(JD_REDUCE_NUMBER_FINISH)Queue queue,
@Qualifier(JD_REDUCE_NUMBER_EXCHANGE)Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).
with(JD_REDUCE_NUMBER_FINISH_ROUTINGKEY).noargs();
}
}
(8)MQ监听类
在mq包下创建监听类,如下:
/*
* MQ监听类
*/
@Component
public class MqListener {
@Autowired
WarehouseService warehouseService;
@Autowired
RabbitTemplate rabbitTemplate;
/*
* 监听库存减少队列
*/
@RabbitListener(queues = {RabbitMQConfig.JD_REDUCE_NUMBER})
public void receiveReduceNumber(JdTask jdTask) {
//获取任务ID
String id = jdTask.getId();
String requestBody = jdTask.getRequestBody();
//JSON转Map
Map map = JSON.parseObject(requestBody);
//从map中获取数据
String userId = (String) map.get("userId");
Integer goodsId = Integer.parseInt((String)map.get("goodsId")) ;
String goodsName = (String) map.get("goodsName");
//减少库存
try {
int res = warehouseService.reduceGoodsNumber(userId,goodsId, goodsName, jdTask);
if(res == 1) {
//发送消息
rabbitTemplate.convertAndSend(RabbitMQConfig.JD_REDUCE_NUMBER_EXCHANGE,
RabbitMQConfig.JD_REDUCE_NUMBER_FINISH_ROUTINGKEY,jdTask);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
仓库服务的代码也全部完成了。
3.4、测试
手动向表中添加任务:
INSERT INTO jd_order
.jd_task
(id
, create_time
, update_time
, delete_time
, task_type
, mq_exchange
, mq_routingkey
, request_body
, version
, status
, errormsg
) VALUES (‘1’, ‘2021-02-01 22:00:00’, ‘2021-01-31 22:00:00’, NULL, NULL, ‘jd_reduce_number_exchange’, ‘jd_reduce_number_routingkey’, ‘{“userId”:“999”,“goodsId”:“25”,“goodsName”:“艾斯奥特曼手办”}’, 1, ‘’, NULL);
执行这条SQL语句,添加成功后jd_task表:
然后向jd_goods_number中加一条记录:
现在库存数量是1000。
启动order工程、warehouse工程、RabbitMQ。
控制台:
MQ:
队列和交换机已经注册创建了。
交换机和通道也正常。
最后查看数据库:
任务表已经没有任务了。
历史任务表中成功添加了1条历史记录。
仓库数量表中对应商品的库存数量减了1。
同样历史表中添了1条历史记录。
那么这个使用MQ来实现分布式事务的最终一致性的功能就完成了。
4、总结
分布式事务在开发中非常普遍,要会用MQ,并且使用MQ来处理分布式事务的一致性问题。而且CAP理论要牢牢掌握,这对于我们处理分布式事务一致性的问题很有帮助,开发中会经常用到。