有过后端数据库
编程经验的童鞋应该知道事务的基本理论知识同时网上有许多更为规范的文档参考,我在这里大致简单介绍一下。在数据库编程中我们通常知道ACID的基本概念,为什么会存在这个理论知识的,我个人认为人们在实践的经验中总结出来了对数据库的基本范式和编程规范。
这里简单的那一个业务场景举例,比如我们有一个这样的积分兑换场景
系统为单体
架构那么这里会设计到用户积分表、商品表、交易订单表,
当用户发起积分兑换操作时步骤,这三个操作步骤需要保证数据的一致性、原子性、持久性 ,那么就需要开启事务,我们知道在同一个数据库会话连接中就相当于一个事务那么这三个操作步骤要么全部成功、要么全部失败(数据的强一致性),在并发的场景下不同连接会话的事务是不会相互影响。
以上操作就涉及到数据库的ACID基本概念:
A:即Atomic数据操作原子性
C:即Consistency数据一致性
I:即Isolation事务的隔离性
D:即Durability数据的持久性
为什么会出现分布式事务?这个问题在当今微服务
盛行的今天我想大家应该深有体会,一个单体应用一个DB在业务操作层Service同一个事务可以完成多表操作,但是如果按照领域模型进行服务拆分后不同的领域对于各自的服务以及DB那么在单体应用中同一个场景下,服务化改造后的操作就会涉及到跨服务操作,就会涉及到不同服务有各自本地的事务操作,那么怎么来实现之前的本地事务ACID呢?显示是一个非常困难的事情,那么就出现了分布式事务的一些模式。
有个这样一个场景用户在购买商品&支付
我们的架构可能是如下:
当用户通过app或者pc打开我们的商城选好商品后下单,这里涉及到产生交易订单
、商品库存的锁定
、调用支付系统
、帐户变更
(混合支付=积分+第三方收单)等,由于我们的每一个服务都有自己DB本地事务操作只能保证本地事务的ACID,但对于整个交易场景来说会涉及到多个本地事务所有不能保证有一个统一的协同操作和回滚机制的保证。那么这个时候就出现了分布式事务的解决方案。
那么在分布式系统中CAP原则和base的基本理论大家可以自行扫盲下,一下介绍几种常见的分布式事务解决方案
强一致模型
2pc 典型的XA模型 拥有三个角色:TM(事务管理者)、RM(资源管理者)、AP(应用), 包括两个阶段:第一是资源的准备 、第二是事务提交,在第一阶段当所有的资源管理者返回预提交成功后才发起第二阶段事务提交,如果在第一阶段存在一个返回预提交失败则回滚。
问题:
柔性事务
异步消息(可靠实践模式)
业务方提供本地操作成功回查功能
在基于异步消息实现分布式事务中当操作本地业务的时候先记录一个消息到本地消息表消息状态为待发送
,然后发送预half消息到MQ,此时MQ不会投递消息到消费者,MQ立即返回队列执行结果,如果失败则不执行后面业务同时发送MQ一个rollback消息和修改本地消息状态为 完成
,如果返回成功执行本地事务提交和修改本地消息状态已发送
并发送MQ一个commit消息表示可以投递。事务回滚则发送MQ一个rollback消息、删除或者修改本地消息表,当收到队列的ack回执后删除或者修改本地消息状态为完成
,
本地消息事务表
基于消息队列(MQ)+本地事务表的形式, 在基于异步消息实现分布式事务中当操作本地业务的时候同时记录本地事务消息表在同一个事务中进行commit和rollback,然后把本地事务消息发送到MQ,当MQ成功回执后删除本地事务消息,未收到MQ回执需要重新尝试也可以开启一个定时任务去扫描发送MQ。当出现A->B->C 场景中消费者C事务异常则不断重试C,如果重试达到上限还是失败则需报警和人工介入。
Apache ServiceComb Pack is an eventually data consistency solution for micro-service applications. ServiceComb Pack currently provides TCC and Saga distributed transaction co-ordination solutions by using Alpha as a transaction coordinator and Omega as an transaction agent
也就是说Apache ServiceComb Saga 是一个微服务应用的数据最终一致性解决方案。
Saga Pack 架构是由 alpha 和 omega组成,其中:
下图展示了alpha, omega以及微服务三者的关系:
Github:https://github.com/apache/servicecomb-pack
去Github https://github.com/apache/servicecomb-pack下载源码编译最新代码使用0.6.0-SNAPSHOT
版本当然如果不想编译使用发行版本0.5.0
,注意在编译源码的使用注意选择相关的profies
引入servicecomb pack依赖
<dependency>
<groupId>org.apache.servicecomb.packgroupId>
<artifactId>omega-spring-starterartifactId>
<version>0.5.0version>
dependency>
或者
<dependency>
<groupId>org.apache.servicecomb.packgroupId>
<artifactId>omega-spring-starterartifactId>
<version>0.6.0-SNAPSHOTversion>
dependency>
其次这里我们使用到数据操作所以需要引入数据库连接池和相关驱动
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.6version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>6.0.6version>
dependency>
servicecomb实现的resttemplate
<dependency>
<groupId>org.apache.servicecomb.packgroupId>
<artifactId>omega-transport-resttemplateartifactId>
<version>0.6.0-SNAPSHOTversion>
dependency>
准备alpha-server数据库脚本
CREATE TABLE IF NOT EXISTS TxEvent (
surrogateId bigint NOT NULL AUTO_INCREMENT,
serviceName varchar(36) NOT NULL,
instanceId varchar(36) NOT NULL,
creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
globalTxId varchar(36) NOT NULL,
localTxId varchar(36) NOT NULL,
parentTxId varchar(36) DEFAULT NULL,
type varchar(50) NOT NULL,
compensationMethod varchar(512) NOT NULL,
expiryTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
payloads blob,
retries int(11) NOT NULL DEFAULT '0',
retryMethod varchar(512) DEFAULT NULL,
PRIMARY KEY (surrogateId),
INDEX saga_events_index (surrogateId, globalTxId, localTxId, type, expiryTime),
INDEX saga_global_tx_index (globalTxId)
) DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS Command (
surrogateId bigint NOT NULL AUTO_INCREMENT,
eventId bigint NOT NULL UNIQUE,
serviceName varchar(36) NOT NULL,
instanceId varchar(36) NOT NULL,
globalTxId varchar(36) NOT NULL,
localTxId varchar(36) NOT NULL,
parentTxId varchar(36) DEFAULT NULL,
compensationMethod varchar(512) NOT NULL,
payloads blob,
status varchar(12),
lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
version bigint NOT NULL,
PRIMARY KEY (surrogateId),
INDEX saga_commands_index (surrogateId, eventId, globalTxId, localTxId, status)
) DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS TxTimeout (
surrogateId bigint NOT NULL AUTO_INCREMENT,
eventId bigint NOT NULL UNIQUE,
serviceName varchar(36) NOT NULL,
instanceId varchar(36) NOT NULL,
globalTxId varchar(36) NOT NULL,
localTxId varchar(36) NOT NULL,
parentTxId varchar(36) DEFAULT NULL,
type varchar(50) NOT NULL,
expiryTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
status varchar(12),
version bigint NOT NULL,
PRIMARY KEY (surrogateId),
INDEX saga_timeouts_index (surrogateId, expiryTime, globalTxId, localTxId, status)
) DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS tcc_global_tx_event (
surrogateId bigint NOT NULL AUTO_INCREMENT,
globalTxId varchar(36) NOT NULL,
localTxId varchar(36) NOT NULL,
parentTxId varchar(36) DEFAULT NULL,
serviceName varchar(36) NOT NULL,
instanceId varchar(36) NOT NULL,
txType varchar(12),
status varchar(12),
creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (surrogateId),
UNIQUE INDEX tcc_global_tx_event_index (globalTxId, localTxId, parentTxId, txType)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS tcc_participate_event (
surrogateId bigint NOT NULL AUTO_INCREMENT,
serviceName varchar(36) NOT NULL,
instanceId varchar(36) NOT NULL,
globalTxId varchar(36) NOT NULL,
localTxId varchar(36) NOT NULL,
parentTxId varchar(36) DEFAULT NULL,
confirmMethod varchar(512) NOT NULL,
cancelMethod varchar(512) NOT NULL,
status varchar(50) NOT NULL,
creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (surrogateId),
UNIQUE INDEX tcc_participate_event_index (globalTxId, localTxId, parentTxId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS tcc_tx_event (
surrogateId bigint NOT NULL AUTO_INCREMENT,
globalTxId varchar(36) NOT NULL,
localTxId varchar(36) NOT NULL,
parentTxId varchar(36) DEFAULT NULL,
serviceName varchar(36) NOT NULL,
instanceId varchar(36) NOT NULL,
methodInfo varchar(512) NOT NULL,
txType varchar(12),
status varchar(12),
creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (surrogateId),
UNIQUE INDEX tcc_tx_event_index (globalTxId, localTxId, parentTxId, txType)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS master_lock (
serviceName varchar(36) not NULL,
expireTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
lockedTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
instanceId varchar(255) not NULL,
PRIMARY KEY (serviceName)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
微服务sql脚本
CREATE TABLE `booking` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`phone` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`price` double DEFAULT NULL,
`uuid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `carbooking` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` int(11) DEFAULT NULL,
`confirmed` tinyint(1) DEFAULT NULL,
`cancelled` tinyint(1) DEFAULT NULL,
`uuid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `hotelbooking` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` int(11) DEFAULT NULL,
`confirmed` tinyint(4) DEFAULT NULL,
`cancelled` tinyint(4) DEFAULT NULL,
`uuid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
预定相关服务,需要操作car和hotel具有分布式事务使用场景。预定car和hotel结果需要一致性(都成功or都失败)
编写提供外部访问的Controller层
@SagaStart //标志这里是全局事务的开始
@PostMapping("/booking/{name}/{rooms}/{cars}")
public String order(@PathVariable String name, @PathVariable Integer rooms,
@PathVariable Integer cars) throws Throwable {
if (cars < 0) {
throw new Exception("The cars order quantity must be greater than 0");
}
//第一步本地事务方法
saveBooking();
//第二步car服务
postCarBooking(name, cars);
if (rooms < 0) {
throw new Exception("The rooms order quantity must be greater than 0");
}
//调用hotel服务
postHotelBooking(name, rooms);
return name + " booking " + rooms + " rooms and " + cars + " cars OK";
}
第一步saveBooking
方法操作本地Service
@Transactional
@Compensable(compensationMethod = "cancel")//开启子事务,并提供cancel补偿方法
@Override
public boolean booking(Booking booking) {
Assert.notNull(booking, "booking is not null");
Assert.hasLength(booking.getPhone(), "phone is not null");
//and so on ...
bookingRepository.save(booking);
return true;
}
@Transactional
@Override
public boolean cancel(Booking booking) {
List<Booking> bookings = bookingRepository.findByUuid(booking.getUuid());
if (bookings != null && bookings.size() > 0) {
bookingRepository.delete(bookings.get(0));
}
return true;
}
注意:这里的cancel
方法前面必须和booking
方法签名一致,被标注@Compensable
方法会被omega
进行拦截并根据签名和参数产生事务上下文通过grpc发送alpha
持久化。当需要进行事务补偿时候alpha
异步调用cancel
补偿方法进行调用并注入之前的事务上下文。
第二步调用car服务
private void postCarBooking(String name, Integer cars) {
template.postForEntity(
carServiceUrl + "/order/{name}/{cars}",
null, String.class, name, cars);
}
第三步调用hotel服务
template.postForEntity(
hotelServiceUrl + "/order/{name}/{rooms}",
null, String.class, name, rooms);
配置文件application.yaml
spring:
application:
name: booking
cloud:
consul:
enabled: false
zookeeper:
enabled: false
nacos:
discovery:
enabled: false
alpha:
cluster:
address: alpha-server.servicecomb.io:8080
car:
service:
address: http://car.servicecomb.io:8082
hotel:
service:
address: http://hotel.servicecomb.io:8083
server:
port: 8081
####car service
预定car服务
编写rest api接口Controller
@PostMapping("/order/{name}/{cars}")
CarBooking order(@PathVariable String name, @PathVariable Integer cars) {
CarBooking booking = new CarBooking();
booking.setId(id.incrementAndGet());
booking.setName(name);
booking.setAmount(cars);
booking.setUuid(UUID.randomUUID().toString());
carService.bookingCar(booking);
return booking;
}
Service逻辑
@Transactional
@Override
@Compensable(compensationMethod = "cancel")//开启子事务,并提供cancel补偿方法
public void bookingCar(CarBooking booking) {
if (booking.getAmount() > 10) {
throw new IllegalArgumentException("can not order the cars large than ten");
}
booking.setId(null);
booking.confirm();
carBookingRepository.save(booking);
}
@Transactional
@Override
public void cancel(CarBooking booking) {
List<CarBooking> cars = carBookingRepository.findByUuid(booking.getUuid());
if (cars != null && cars.size()>0) {
CarBooking car = cars.get(0);
carBookingRepository.delete(car);
}
}
注意:这里的cancel
方法前面必须和booking
方法签名一致,被标注@Compensable
方法会被omega
进行拦截并根据签名和参数产生事务上下文通过grpc发送alpha
持久化。当需要进行事务补偿时候alpha
异步调用cancel
补偿方法进行调用并注入之前的事务上下文。
配置文件application.yaml
spring:
application:
name: car
cloud:
consul:
enabled: false
zookeeper:
enabled: false
nacos:
discovery:
enabled: false
alpha:
cluster:
address: alpha-server.servicecomb.io:8080
server:
port: 8082
预定hotel服务
编写rest api接口Controller
@PostMapping("/order/{name}/{rooms}")
HotelBooking order(@PathVariable String name, @PathVariable Integer rooms) {
HotelBooking booking = new HotelBooking();
booking.setId(id.incrementAndGet());
booking.setName(name);
booking.setAmount(rooms);
hotelService.order(booking);
return booking;
}
Service本地事务接口
@Transactional
@Compensable(compensationMethod = "cancel")//开启子事务,并提供cancel补偿方法
@Override
public void order(HotelBooking booking) {
if (booking.getAmount() > 2) {
throw new IllegalArgumentException("can not order the rooms large than two");
}
booking.setId(null);
booking.confirm();
booking.setUuid(UUID.randomUUID().toString());
hotelRepository.save(booking);
}
@Transactional
@Override
public void cancel(HotelBooking booking) {
List<HotelBooking> hotelBookings = hotelRepository.findByUuid(booking.getUuid());
if (hotelBookings != null && hotelBookings.size() > 0) {
hotelRepository.deleteAll(hotelBookings);
}
}
注意:这里的cancel
方法前面必须和booking
方法签名一致,被标注@Compensable
方法会被omega
进行拦截并根据签名和参数产生事务上下文通过grpc发送alpha
持久化。当需要进行事务补偿时候alpha
异步调用cancel
补偿方法进行调用并注入之前的事务上下文。
配置文件application.yaml
spring:
application:
name: hotel
cloud:
consul:
enabled: false
zookeeper:
enabled: false
nacos:
discovery:
enabled: false
alpha:
cluster:
address: alpha-server.servicecomb.io:8080
server:
port: 8083
ServiceComb pack 协调服务、事务上下文持久化等
直接在源码中找到启动类启动Alpha service或者使用jar启动,在源码中启动的时候增加一个启动参数
-Dspring.profiles.active=mysql
使用mysql数据库
场景一 car service和hotel service服务以及本地服务调用全部成功
http://127.0.0.1:8081/booking/ouwen/1/2
场景二 car service服务调用失败,booking service事务回滚
http://127.0.0.1:8081/booking/ouwen/1/20
场景三 hotel service服务调用失败 bookng service 和car service服务事务回滚
http://127.0.0.1:8081/booking/ouwen/10/2
我的博客地址
参考文章:
https://docs.servicecomb.io/saga
示例代码:
https://gitee.com/newitman/itman-blog.git