本地事务
数据库事务(简称:事务),Transactional是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
起初,事务仅限于对单一数据库资源的访问控制,架构服务话以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及到一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
事务特性
原子性(Atomicity):事务作为一个整体去执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态下不能被观察到(这层语义也应该属于原子性)。
**隔离性(lsolation):**多个事务并发执行时,一个事务的执行不应影响其他事物的执行,如同只有这一个操作在被数据库所执行一样。
执行性(Durability):已被提交的事物对数据库的修改应该是永久性的保存在数据库中。在事务结束后,此操作不可逆转。
分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
对于上面介绍的分布式事务应用架构,尽管—个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:
如果将上面这两种场景(—个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。
分布式事务相关理论
CAP定理
CAP定理是在1998年加州大学计算机科学家EricBrewer提出的,分布式系统有3个指标
- Consistency 一致性
- Availiability 可用性
- Partition tolerance 分区容错性
这三个指标首字母加起来就是CAP,但是这不可能同时做到CAP,这个结论就叫做CAP定理。
分区容错性
大多数分布式系统都分不在多个子网络中,每个子网络就叫做一个区。分区容错性的意思就是,区间通信可能失败。比如,一台服务器放在中国,一台放在美国,这就是两个区,她们之间可能无法通信。
G1和G2两台跨区服务器。G1向G2发送一条消息,G2可能无法收。系统设计时,必须考虑该情况。
一般来说,分区容错无法避免,因此认为,CAP的P总是成立的,C和A不能同时做到。
可用性
可用性,只要接收到用户的请求,服务器就必须给出回应。
用户可以选择向G1或G2发起读操作,不管是哪台服务器,只要收到请求,就必须告诉用户,到底是V0还是V1,否则就不满足可用性。但是这样可能会导致数据不准确。
一致性
写操作之后的读操作,必须返回该值。
- 强一致性,要求更新过的数据能被后续的访问都能看到
- 弱一致性,能容忍后续的部分或者全部访问不到
- 最终一致性,经过一段时间之后,要求能访问到更新后的数据
CAP中的一致性是指强一致性。
eg:某条记录是V0,用户向G1发起一个读操作,将其改为V1.
用户有可能向G2发起读操作,由于G2没有发生变化,因此返回的还是V0。
G1和G2的读操作结果不一致,这就不满足一致性了。
为了让G1也能变为V1,就要在G1写操作时,让G1发送一条消息给G2,要求G2也改为V1
但是同步数据时,可能网络故障,数据就不能保持一致性,要么就是等待服务器恢复后再去请求,但是这样就不能保证可用性了。
- AP:保证可用性,放弃一致性,返回的数据有可能不准确。(12306)
- CP:保证一致性,放弃可用性,等待网络恢复后,再同步返回数据,保证数据准确。(ZooKeeper),与钱相关的一般都是保证强一致性。
BASE理论
BASE:全称: Basically Available(基本可用),Soft state(软状态) 和 Eventuallv consistent (最终一致性)三个短语的缩写,来自ebpy 的架构师提出。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:
即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务待点,采用适当的方式来使系统达到最终一致性 (Eventual consistency)
eg:类似于12306,查看票时,有票但是一下单就没票了,这样可以保证可用性的同时保证一致性。
Basically Available(基本可用)
什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:
1,响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给用户结果,而基本可用的搜索引擎可以在1秒左右返回结果。
2. 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。到证了基本可用。
Soft state (软状态)
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent (最终—致性)
系统能路保证在没有其他新的更新操作的情况下,致据地终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能获取到最新的值。
分布式事务解决方案
基于XA协议两阶段提交
XA规范中分布式事务有AP、RM、TM组成:
- 应用程序AP:定义事务边界(定义事务的开始和结束)并访问事务边界内的资源。
- 资源管理器RM:RM管理计算机的共享资源,许多软件都可以去访问这些资源,资源包含比如数据库、问价系统、打印机服务等。
- 事务管理器TM:负责全局事务,分配全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、会滚、失败恢复等等。
第一阶段:TM要求所有的RM准备自己对应的事务分支,询问每个RM能否成功提交当前的事务分支,RM根据自己的情况返回TM状态,失败返回NO,RM就会对这个分支就行会滚,当前的这个分支就没了
第二阶段:TM根据第一阶段提交的结果就行处理,是提交还是会滚,所有的RM都返回成功,TM会通知所有的RM提交事务,只要有一个RM返回NO,则通知所有的RM会滚事务。
优点:尽量保证了数据的强一致性,适合对数据强一致要求很高的关键领域,(其实也不能100%保证强一致性)
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发场景。
TCC事务补偿机制
TCC其实就是采用补偿机制,其核心思想是:针对每个操作,都要注册一个与其对一个的确认和补偿(撤销)操作,分为3阶段:
- Try阶段主要是对业务系统做检测及资源预留
- Confirm阶段主要是对业务系统做确认提交,try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的,只要try成功,confirm一定成功
- Cancel阶段主要是在业务执行错误,需要会滚的状态下执行的业务取消,预留资源释放
举例:A和B转账
我们有一个本地方法,里面依次调用
1、首先在try阶段,要先调用远程接口把B和A的钱冻结起来。
2、在confirm阶段,执行远程调用的转账操作,转账成功解冻。
3、在执行第2步成功时,那么转账成功,如果执行失败,则调用远程接口对应的解冻方法(Cancel)
流程
1、Try接口,先查看A账户余额是否充足,充足则冻结金额,并扣减。B账户需要检查账户是否可用
2、刚才的操作都没问题,就需要去走Confirm接口:A:刚才冻结的金额,解冻并真正扣减;B:把对应的钱加起来。这样事务就提交成功了。
3、如果Try接口中任何一方有异常,就不会走Confirm接口,走Cancel接口,A:把金额解冻回来,并且增加回来,将数据进行会滚操作。
- 优点:相比两阶段提交,可用性比较强
- 缺点:数据的一致性要差些,TCC属于应用层面的一种补偿机制,所以需要程序员在实现的时候,多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不好定义及处理。代码侵入性很强。
消息最终一致性
消息最终一致性核心思想是将分布式事务拆成本地事务进行处理,这种思想来源ebay,示意图:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
- 优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。
- 缺点:消息表会男合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理
分布式事务问题简介
跨数据库、多数据源的统一调度。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立应用,分别使用三个独立的数据源,
业务操作需要调用3个服务来完成。此时每个服务内部的数据一致性由本地事物保证,但是全局的数据一致性问题无法得到保证!
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
示例:
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供技术支持:
- 仓储服务:对给定的商品扣除仓储数量
- 订单服务:根据采购需要创建订单
- 账户服务:从用户账户中扣除余额
架构图:
Seata简介
官网地址
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务解决方案
Seata术语
Transcation ID XID-全局事物ID
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
分布式事务处理过程
处理过程
1、TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
2、XID在微服务调用链路的上下文中传播;
3、RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖;
4、TM向TC发起针对XID的全局提交或回滚决议;
5、TC调度XID下管辖的全局分支事务完成提交或回滚请教;
示意图
Seata-Server安装
发布地址
1.0.0tar下载地址
GitHub官网下载换缓慢,可以通过github官网下载加速器加速下载
上传tar到服务器
解压
tar -zxvf seata-server-1.0.0.tar.gz
修改配置文件
修改conf\file.conf文件
先备份,再做修改
cp file.conf file.conf.bak
主要修改自定义事务组名称+事务日志存储模式为DB+数据库连接信息
事务日志存储模式默认为文件,修改为存储到DB中
修改service模块
原厂默认配置:
service {
#transaction service group mapping
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
}
修改vgroup_mapping.my_test_tx_group的value,自定义即可
service {
#transaction service group mapping
vgroup_mapping.my_test_tx_group = "seata_tx_group"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#disable seata
disableGlobalTransaction = false
}
修改store模块
## 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 = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "xxxxx"
}
}
修改详情:
建数据库
由于我们将存储模式从文件修改为了db,所以我们需要创建对应的数据库
-- 创建seata库
CREATE DATABASE IF NOT EXISTS seata;
0.9版本建表SQL存储在db_store.sql在seata目录下的conf目录里面
但是我的1.0.0版本conf目录下,并没有对应的sql文件,查看官网:
去github上获取对应的sql语句
1.0.0版本SQL
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
guthub:https://github.com/seata/seata/blob/1.0.0/script/server/db/mysql.sql
将此脚本直接执行即可生成对应的表文件
修改registry文件
先做备份
cp registry.conf registry.conf.bak
原文件:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
...
}
修改为nacos,并配置nacos的信息
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
...
}
启动
先启动Nacos,等Nacos启动完成,再启动Seata
负责会启动报错:
启动Nacos
sh /root/development/nacos/bin/startup.sh -m standalone
启动Seata
sh /root/development/seata/bin/seata-server.sh
查看启动结果:
Seata分布式交易
分布式业务说明
创建3个微服务,一个订单服务,一个库存服务,一个账户服务
当用户下订单时,会在订单服务中创建一个订单,然后通过远程调用来库存来扣件下单商品库存,
再通过远程调用账户服务来扣件用户账户里的余额。
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,存在分布式事务问题。
下订单---->减库存---->扣余额---->改状态
业务库准备
创建业务库
准备订单、库存、账户业务数据库准备
- seata_order:存储订单的数据库
- seata_storage:存储账户的数据库
- seata_account:存储账户信息的数据库
对应的建库语句:
CREATE DATABASE IF NOT EXISTS `seata_order`;
CREATE DATABASE IF NOT EXISTS `seata_storage`;
CREATE DATABASE IF NOT EXISTS `seata_account`;
业务表:
USE seata_order;
CREATE TABLE IF NOT EXISTS t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
USE seata_storage;
CREATE TABLE IF NOT EXISTS t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`userd` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- 添加测试数据
INSERT INTO t_storage VALUES(NULL,1,100,0,100);
USE seata_account;
CREATE TABLE IF NOT EXISTS t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` INT(11) DEFAULT NULL COMMENT '总额度',
`userd` INT(11) DEFAULT NULL COMMENT '已用余额',
`residue` INT(11) DEFAULT NULL COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- 添加测试数据
INSERT INTO t_account VALUES(NULL,1,1000,0,1000);
创建对应的回滚日志表
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
`id` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of undo_log
-- ----------------------------
创建结果: