分布式事务
单体项目,为了防止多个线程并发访问修改同一数据,那么这个数据就会出现并发异常,导致超卖,异常数的情况(对于购票系统/购物库存的业务有极大的影响),所以给单体项目加上同步锁/锁,为了线程安全,而这里加的锁只对当前 JVM 有效(一个服务一个 JVM),只会在堆里存放获得锁对象的信息,而若多个单体项目,也就是多个微服务,同时并发访问同一个数据,那么这时单体锁就行了,因为每个微服务都有一个 JVM,每个都有一个 Object 锁对象,所以这样就不能锁住并发情况,这时需要引进一个进程锁/分布式锁,来给所有的微服务设置同一把锁,这就是分布式锁的由来。
基于 Redis,Zookeeper,Mysql 可以实现分布式锁
- Mysql 设置表字段库存的属性为无符号,减不到负数
解决库存问题-各种锁
查询 拿共享锁,更新拿互斥锁
1. 悲观锁
默认失败,直接加锁
2. 乐观锁
先判断后改值
事务的 ACID 原则
引入
1.当用户下单时,订单成功的情况----- 创建订单成功,扣款成功,扣库存成功
2. 当用户下单时,订单失败的情况----- 创建订单失败,扣款成功,扣库存失败
第二种情况就属于异常情况,无法保证不同服务的事务同成功/同回滚,只能保证单个服务的同成功同回滚,那怎么能保证其统一呢??
分析: @Transactional 只能控制单个项目的下的事务,无法控制其他项目(微服务)的事务
分布式事务
在分布式系统下,当一个业务逻辑 A横跨多个服务 /数据源,那么这之间的每一个服务都是分支事务,所以业务 A 不仅得保证自己的事务一致性,还得保证其他事务的也要一致性。
CAP 定理
CAP 定理: 当分布式系统无法同时满足
一致性Consistency
,可用性Availability
,分区容错性Partition tolerance
一致性 C
用户访问订单服务/库存时,应保证数据库的一致性。比如小明调用吃业务,吃了一个苹果,还剩 9 个,
小方查看小明的书包只有 2 个,这就是数据库不一致性,属于异常,访问不同节点数据不一致。
可用性 A
用户访问多个服务器模块,都可以进行通信,得到响应,而不是超时或拒绝
分区容错性 Partition tolerance
Partition分区:因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接, 形成独立分区
Tolerance 容错: 在集群出现分区时,整个系统也要持续对外提供服务
总结
- 分布式系统节点通过网络连接,一定会出现分区问题(p)
- 当分区出现时,系统的一致性 (c) 和可用性 (A) 就无法同时
BASE 理论
BASE 理论是对 CAP 的一种解决思路
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态): 数据不一致,可访问
- 强状态:数据必须一致,不可访问
- Eventually Consistent(最终一致性):软状态下,使用浑身解数使数据一致性
两个模式
- AP 模式: 各个子事务分别执行和提交事务(sql 语句的执行和提交), 数据不一致,后面通过补救实现数据一致性(一般选择 AP 模式)
- CP 模式: 各个子事务执行后相互等待,同时提交,同时回滚,达成强一致,如果都成功就提交事务,否则就同时回滚
阿里巴巴Seata 的角色
用于解决分布式事务的一个服务
http://seata.io/
- TC (Transaction Coordinator) 事务协调者:(判断每一个子事务的 sql 是否执行成功的状态,裁判的角色) 维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) 事务管理器:(划分每一个事务的范围,提交给事务协调者,哪些事务要一起提交/回滚) 定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) 资源管理器: (用来管理分支事务)管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
一般当微服务 A 和微服务 B 划分到一个通过 TM 划分到一个组,当 TM 将分组的情况传给 TC,这时事务协调者会去查看微服务 A 和B 这两个被 RM 管理的资源是否提交成功,提交成功不会被记录,有一个失败则记录到 TC 告诉 TM 你们分组的事务需要回滚
Seata 的四个分布式事务解决方案
-
XA模式:(CP 模式)强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
-
TCC模式:(AP 模式)最终一致的分阶段事务模式,有业务侵入
-
AT模式:(AP 模式)最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
-
SAGA模式:(AP 模式)长事务模式,有业务侵入
XA 模式
TC 管理很多分支事务
AT 模式
实现 AT 模式
AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
只不过 AT 模式需要一个表来记录全局锁、另一张表来记录数据快照 undo_log。其中 lock_table 导入到 TC 服务关联的数据库,undo_log 表导入到微服务关联的数据库
- 导入数据库表,记录全局锁
- 修改application.yml文件,将事务模式修改为AT模式
seata:
data-source-proxy-mode: AT # 默认就是AT
AT 和 XT 的区别
部署 Seata
- 下载seata-server包:
https://seata.apache.org/
- 修改软件的配置文件
registry.conf
,引入 nacos 平台配置文件
# 这里选择的是 nacos 平台,删减其他不需要的配置平台信息
registry {
# tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服务注册到 nacos的服务名称,可以自定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
- Nacos 平台添加配置文件
seataServer.properties
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid # 德鲁伊
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5 #连接池的最小
store.db.maxConn=30 #连接池的最大
store.db.globalTable=global_table #数据库原始的表 global_table
store.db.branchTable=branch_table #数据库提交后的表 branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table #数据库事务锁表lock_table
store.db.maxWait=5000 # 最大等待时间
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
- 创建库创表 (全局事务、分支事务、全局锁信息),这两个表是用于软状态,通过表记录和逻辑处理数据不一致
Global_table 记录原始的数据 Branch_table 表记录更新的数据,如果成功则删除这两个表记录的数据,如果中途失败,则从 Global_table回滚数据到最初,再将两个表数据删除
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
- 启动 TC 服务
双击seata-server.bat
微服务集成 seata
1. 引入seata依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
2. 修改每一个微服务的application.yml配置文件
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
# 参考tc服务自己的registry.conf中的配置
type: nacos
nacos:
application: seata-server # tc服务在nacos中的服务名称
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
namespace: ""
# cluster = "SH"
username: nacos
password: nacos
tx-service-group: seata-demo # 事务分组,这样就是一个队伍的,同成功同回滚
service:
vgroup-mapping: # 事务组与TC服务cluster的映射关系
seata-demo: SH
Nacos查找顺序
先找 namespace 隔离空间 -> group 分组 -> application服务名 -> cluster集群 -> 服务实例