Loading... # 分布式事务 **事务(Transaction)**,一般是指要做的或所做的事情,由**事务开始(begin transaction)**和**事务结束(end transaction)**之间执行的全体操作组成。 **简单的讲就是,要么全部被执行,要么就全部失败。** 那**分布式事务**,自然就是运行在分布式系统中的事务,是由**多个不同的机器上的事务组合而成**的。同上,只有分布式系统中所有事务执行了才能是成功,否则失败。 事务的基本特征ACID: - 原子性(Atomicity) - 一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 - 一致性 - 指事务执行前和执行后,数据是完整的。 - 隔离性 - 一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 - 持久性 - 也称为永久性,一个事务一旦提交,它对数据库中数据的改变就应该是永久性的保存下来了。 ------ **分布式事务的目标:解决多个独立事务一致性的问题。** 一个功能,横跨多个微服务,由于每个微服务不在一个库,没法用数据库事务来保证事务。 ## 分布式事务解决方案 ### 二阶段提交协议 基于XA协议的,采取强一致性,遵从ACID. 2PC:(2阶段提交协议),是基于XA/JTA规范。 **XA** XA是由X/Open组织提出的分布式事务的架构(或者叫协议)。XA架构主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。也就是说,在基于XA的一个事务中,我们可以针对多个资源进行事务管理,例如一个系统访问多个数据库,或既访问数据库、又访问像消息中间件这样的资源。这样我们就能够实现在多个数据库和消息中间件直接实现全部提交、或全部取消的事务。XA规范不是java的规范,而是一种通用的规范。 **JTA** JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。它主要定义了: 一个事务管理器的接口javax.transaction.TransactionManager,定义了有关事务的开始、提交、撤回等操作。 一个满足XA规范的资源定义接口javax.transaction.xa.XAResource,一种资源如果要支持JTA事务,就需要让它的资源实现该XAResource接口,并实现该接口定义的两阶段提交相关的接口。 ![二阶段提交协议.png][1] 流程: 1.请求阶段(commit-request phase,或称表决阶段,voting phase) 在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。 在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。 2.提交阶段(commit phase) 在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。 当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。 参与者在接收到协调者发来的消息后将执行响应的操作。 缺点: - **单点故障**:事务的发起、提交还是取消,均是由协调者管理的,只要协调者宕机,那就凉凉了。 - **同步阻塞缺点**:从上面介绍以及例子可看出,我们的参与者系统中在没收到协调者的真正提交还是取消事务指令的时候,就是锁定当前的资源,并不真正的做些事务相关操作,所以,整个分布式系统环境就是阻塞的。 - **数据不一致缺点**:就是说在协调者向参与者们发送真正提交事务的时候,部分网路故障,造成部分系统没收到真正的指令,那么就会出现部分提交部分没提交,因此,这就会导致数据的不一致。 无法解决的问题 当协调者出错,同时参与者也出错时,两阶段无法保证事务执行的完整性。 考虑协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。 那么即使有了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。知道的人已经被灭口了。 ### 三阶段提交协议 采取强一致性,遵从ACID。 在二阶段上增加了:超时和预提交机制。 有这三个主阶段,canCommit、preCommit、doCommit这三个阶段 ![三阶段提交协议.png][2] 流程: 1.CanCommit阶段 3PC的CanCommit阶段其实和2PC的准备阶段很像。 协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。 2.PreCommit阶段 Coordinator根据Cohort的反应情况来决定是否可以继续事务的PreCommit操作。 根据响应情况,有以下两种可能。 A.假如Coordinator从所有的Cohort获得的反馈都是Yes响应,那么就会进行事务的预执行: 发送预提交请求。Coordinator向Cohort发送PreCommit请求,并进入Prepared阶段。 事务预提交。Cohort接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。 响应反馈。如果Cohort成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。 B.假如有任何一个Cohort向Coordinator发送了No响应,或者等待超时之后,Coordinator都没有接到Cohort的响应,那么就中断事务: 发送中断请求。Coordinator向所有Cohort发送abort请求。 中断事务。Cohort收到来自Coordinator的abort请求之后(或超时之后,仍未收到Cohort的请求),执行事务的中断。 3.DoCommit阶段 该阶段进行真正的事务提交: 执行提交 A.发送提交请求。Coordinator接收到所有Cohort发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有Cohort发送doCommit请求。 B.事务提交。Cohort接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。 C.响应反馈。事务提交完之后,向Coordinator发送ACK响应。 D.完成事务。Coordinator接收到所有Cohort的ACK响应之后,完成事务。 缺点: 如果进入PreCommit后,Coordinator发出的是abort请求,假设只有一个Cohort收到并进行了abort操作, 而其他对于系统状态未知的Cohort会根据3PC选择继续Commit,此时系统状态发生不一致性。 二阶段和三阶段的区别: 加了询问,增大成功概率。 对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败)。协调者挂了,参与者等待超时后,默认提交事务。有一丢进步。 如果参与者异常了,协调者也异常了,会造成其他参与者提交。 在2PC的准备阶段和提交阶段之间,插入预提交阶段,使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。 PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。 ### 基于消息的最终一致性形式 采取最终一致性,遵从BASE理论。 **BASE**:全称是,Basically Avaliable(基本可用),Soft state(软状态),Eventually consistent(最终一致性)三个短语的缩写,来自eBay的架构师提出。 - **Basically Avaliable:**就是在分布式系统环境中,允许牺牲掉部分不影响主流程的功能的不可用,将其降级以确保核心服务的正常可用。 - **Soft state:**就是指在事务中,我们允许系统存在中间状态,且并不影响我们这个系统。就拿数据库的主从复制来说,是完全允许复制的时候有延时的发生的。 - **Eventually consistent:**还是以数据库主从复制为例说,虽然主从复制有小延迟,但是很快最终就数据保持一致了。 分布式事务不可能100%解决,只能提高成功概率。两阶段之间时间,毫秒级别。 补救措施: 定时任务补偿。程序或脚本补偿。人工介入。 ### TCC 解决方案:TCC(Try、Confirm、Cancel),两阶段补偿型方案。 从名字可以看出,实现一个事务,需要定义三个API:预先占有资源,确认提交实际操作资源,取消占有。 如果后两个环节执行一半失败了,记录日志,补偿处理,通知人工。 2PC:是资源层面的分布式事务,一直会持有资源的锁。如果跨十几个库,一下锁这么多数据库,会导致,极度浪费资源。降低了吞吐量。 TCC:在业务层面的分布式事务,最终一致性,不会一直持有锁。将锁的粒度变小,每操作完一个库,就释放了锁。 都是相对的:如果每天只有一个请求,用2PC比TCC要性能高。因为tcc多了多次接口调用。而此时的2PC不怕占用资源,反正就一个调用。高并发场景下TCC优势要大。 ### 消息中间件实现 消息队列柔性事务 本地事务+定时任务+消息队列+事件表 创建事件表 ``` CREATE TABLE `tbl_order_event` ( `id` int(16) NOT NULL, `order_type` varchar(32) DEFAULT NULL COMMENT '事件类型(支付表支付完成,订单表修改状态)', `process` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '事件环节(new,published,processed)', `content` varchar(255) DEFAULT NULL COMMENT '事件内容,保存事件发生时需要传递的数据', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ``` ![消息队列柔性事务.png][3] 如图,支付服务与订单服务,在支付库中1,2,3步可以处于同一事务中,有支付操作,向支付表插入记录,并在支付库的事件表中,插入一条新纪录,事件环节为new,失败则回滚。4,5步处于同一事务,有一个定时任务不断从事件表中读取事件,写入消息队列,写入成功将事件环节改为published,失败回滚,等待下一次定时任务执行。6,7处于同一事务中,订阅消息队列,有消息时取得消息,并插入到订单库的事件表中,事务完成后才会给消息队列回复确认,失败的回滚相当于消息没有被消费,8,9,10处于同一事务,定时任务从事件表中读取事件,根据事件内容,更新订单表,更新之后将事件环节改成processed,失败回滚,等待下一次定时任务执行。也就是将一个事务拆分成多个子事务,该方式保证的最终一致性,但是失去了时效性。 ### seata框架 https://seata.io/zh-cn/docs/overview/what-is-seata.html ![seata组件功能示意图.png][4] seata相当于2pc的改版,分为TC:事务协调者,和RM:资源管理者也就是事件参与者,在此基础上增加了TM:事务管理者,也就是事务得到发起者,TM也是一个RM。 seata使用 下载seata server。 修改file.conf ``` service { # transaction service group mapping # 修改,可不改,my_test_tx_group随便起名字。 vgroup_mapping.my_test_tx_group = "default" # only support when registry.type=file, please don't set multiple addresses # 此服务的地址 default.grouplist = "127.0.0.1:8091" # disable seata disableGlobalTransaction = false } store { # store mode: file、db # 修改 mode = "db" # file store property file { # store location dir dir = "sessionStore" } # database store property # db信息修改 db { # the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "druid" # mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata-server?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai" user = "root" password = "root" } } ``` 修改registry.conf ``` registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa # 修改 type = "eureka" nacos { serverAddr = "localhost" namespace = "" cluster = "default" } # 修改 eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } } ``` 创建数据库,并建表 ``` 分支事务表 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) DEFAULT NULL, `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `status` tinyint(4) DEFAULT NULL, `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`branch_id`) USING BTREE, KEY `idx_xid` (`xid`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; 全局事务表 CREATE TABLE `global_table` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `timeout` int(11) DEFAULT NULL, `begin_time` bigint(20) DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`xid`) USING BTREE, KEY `idx_gmt_modified_status` (`gmt_modified`,`status`) USING BTREE, KEY `idx_transaction_id` (`transaction_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; 全局锁 CREATE TABLE `lock_table` ( `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `transaction_id` bigint(20) DEFAULT NULL, `branch_id` bigint(20) NOT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`row_key`) USING BTREE, KEY `idx_branch_id` (`branch_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; ``` 表的结构不能错。 接着在每个RM的数据库中增加undo_log表,用于事务的回滚。 ``` 用于RM回滚的。 CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; ``` 启动seata-server,会注册到eureka中。 java代码中的配置: 添加依赖 ``` <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>2.2.0.RELEASE</version> </dependency> ``` 在resource下面添加file.conf,registry.conf file.conf ``` service { # transaction service group mapping vgroup_mapping.fbs_tx_group = "default" # 与server中的一致 # only support when registry.type=file, please don't set multiple addresses default.grouplist = "127.0.0.1:8091" # disable seata disableGlobalTransaction = false } client { rm { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 retry.policy.branch-rollback-on-conflict = true } report.retry.count = 5 table.meta.check.enable = false report.success.enable = true } tm { commit.retry.count = 5 rollback.retry.count = 5 } undo { data.validation = true log.serialization = "jackson" log.table = "undo_log" } log { exceptionRate = 100 } support { # auto proxy the DataSource bean spring.datasource.autoproxy = false } } # transaction log store, only used in seata-server store { # store mode: file、db mode = "db" # file store property file { # store location dir dir = "sessionStore" } # database store property db { # the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "druid" # mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.cj.jdbc.Driver" driver-class-name = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata-server?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai" user = "root" password = "root" } } ``` 添加了client的配置项 registry.conf与server的一样,直接复制过来。 在方法上添加`@GlobalTransactional(rollbackFor = Exception.class)`注解。 流程: 当执行加了@GlobalTransactional注解的方法时,会在global_table中添加全局事务,每一个事务参与者会在branch_table表中添加各自的事务记录,并且分别在自己的undo_log表中加入一条记录,包含before_image,之前的镜像,也就是修改之前的状态,after_image,之后的镜像,也就是修改之后的状态,用以在失败时回滚数据。全局事务完成后,global_table,branch_table,undo_log中的记录都将被删除。 ![seata AT 模式.png][5] [1]: https://www.princelei.club/usr/uploads/2020/06/637161673.png [2]: https://www.princelei.club/usr/uploads/2020/06/3193754929.png [3]: https://www.princelei.club/usr/uploads/2020/06/2625048969.png [4]: https://www.princelei.club/usr/uploads/2020/06/1948966346.png [5]: https://www.princelei.club/usr/uploads/2020/06/881970547.png Last modification:June 20th, 2020 at 10:43 pm © 允许规范转载
选材新颖独特,通过细节描写赋予主题鲜活生命力。
技术伦理的探讨体现人文科技平衡意识。