事务简介:
事务:访问并可能更新数据库中各种数据项的一个程序执行单元。在关系数据库中,一个事务由一组sql语句组成。事务具有的四个属性:原子性、一致性、隔离性、持久性。通常称之为ACID特性。
原子性(atomicity):事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。
隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommittted)、读已提交(read committed,解决脏读)、可重复度(repeatable read)、串行化(serializable,解决幻读)
持久性(durablity):持久性也称为永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。
传统的项目中,例如现在有门户网站、订单服务、库存服务
第一个需求 门户网站仅仅需要对订单服务进行操作,如果操作异常则需要进行事务回滚。称之为本地事务
第二个需求,门户网站需要对订单服务和库存服务同事操作,如果操作异常则需要进行整体事务回滚,则称之为分布式事务
本地事务(Local Transaction):
@Transatinal
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称为本地事务。本地事务的ACID特性是数据库直接提供支持。
分布式事务
seata:开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,seata将为用户提供AT、TCC、SAGA、XA事务模式。
AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service全局事务服务)
Seata的三大角色
seata的架构中,一共有三个角色:
TC(Transaction Coordinator)事务协调者:维护全局和分支事务的状态,驱动全局事务提交和回滚
TM(Transaction Manager)事务管理器:定义全局事务的范围:开始全局事务、提交和回滚全局事务
RM(Resource Manager)资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或者回滚
TC为单独部署的Server服务端,TM和RM为嵌入到应用中的Client客户端
常见分布式事务解决方案
1、seata阿里分布式事务框架
2、消息队列
3、saga
4、XA
他们都有一个共同点,就是“两阶段(2PC)”,“两阶段”指完成整个分布式事务,划分为两个步骤完成。
实际上,这四种常见的分布式事务解决方案,分别对应着分布式事务的四种模式:AT、TCC、saga、xa。
每个模式也都有自己的代表产品,也就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC)
分布式事务理论基础
解决分布式事务,也有相应的规范和协议。分布式事务相关的协议有2PC、3PC
由于三阶段提交协议3PC非常难实现,目前市面驻留的分布式事务解决方案都是2PC协议,这就是"两阶段"的内在原因。(3PC是在2PC基础上实现的)
比如分析2PC时,几乎都会用到TCC两阶段的例子,第一阶段是try第二阶段完成confirm或cancel。2PC具有普适性——协议一样的存在,目前绝大多数分布式解决方案都是以两阶段提交协议2PC为基础的。
TCC(Try-Confirm-Cancel)实际上是服务化的两阶段提交协议。
2PC(Two-Phase Commit)两阶段提交协议
分为两个阶段:Prepare和Commit
Prepare提交事务请求
1、询问 协调者向所有参与者(可能是数据库也可能是应用)发送事务请求,询问是否执行事务操作,然后等待各个参与者的响应。
2、执行 各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据表中的记录,并将undo和redo信息记录事务日志中)。
3、响应 如果参与者成功执行了事务并写入undo和redo信息,则向协调者返回YES响应,否则返回NO响应。当然,参与者也可能宕机,从而不会返回响应。
Commit:执行事务提交
1、commit请求 协调者向所有参与者发送Commit请求。
2、事务提交 参与者收到Commit请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。
3、反馈结果 参与者执行事务提交后向协调者发送ack响应。
4、完成事务 接收到所有参与者的ack响应后,完成事务提交。
中断事务
1、rollback请求 协调者向所有参与者发送Rollback请求。
2、事务回滚 参与者收到Rollback后,使用Prepare阶段的undo位置执行事务回滚,完成后释放事务执行占用的所有资源。
3、反馈结果 参与者执行事务回滚后向协调者发送Ack响应
4、中断事务 接受到所有参与者的Ack响应后,完成事务中断
2PC同事也存在着一些问题
1、同步阻塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中吗,参与者是无法进行其他操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。
2、单点在2PC中,一切请求都来自于协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。
如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者进行后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待Prepare响应的时长等),所以也无法顺利处理上一个事务。
3、数据一致性 Commit事务过程中Commit请求/Rollback请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么久导致了部分参与者没有收到Commit/Rollback请求,而其他参与者则正常收到执行了Commit/Rollback操作,没有收到请求的参与者则继续阻塞,这是参与者之间的数据就不再一致了。
当参与者执行Commit/Rollback后向协调者发送Ack操作,然而协调者不论是否收到所有的参与者的Ack,该事务也不会再有其他的补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成功”。
4、环境可靠性依赖 协调者Prepare请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在2PC中,协调者会等待一段时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有参与者都是处于阻塞的,这种机制对网络问题常见的现实环境来说太苛刻了。
下面介绍4种模式(AT、TCC、Saga、XA)的分布式事务实现
AT模式(auto transcation)
无侵入的分布式事务解决方案。(阿里的seata框架,实现了该模式)
在AT模式下,用户只需关注自己的“业务sql”,用户的“业务sql”作为一阶段,seata框架会自动生成事务的二阶段调教和回滚操作。
用户(APP)通过事务的协调者发送请求,事务的协调者通过两个阶段进行处理:一阶段和二阶段
AT模式如何做到对业务的无侵入:
- 一阶段:
在一阶段,Seata会拦截“业务sql”,首先解析sql语义,找到“业务sql”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务sql”跟心业务数据,在业务数据更新之后, 再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 二阶段提交
二阶段如果是提交的话,因为“业务sql”在一阶段的时候已经提交至数据库,所有Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
- 二阶段回滚:
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务sql”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前首先要校验脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
TCC模式
缺点:侵入性比较强,并且的自己实现相关事务控制逻辑
优点:在整个过程中基本没有锁,性能更强
TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作;事务发起方在一阶段执行Try方式,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法
但是在实际业务逻辑过程中订单服务调用库存服务才用的是异步调用,随机引入了mq的使用,也就是可靠消息最终一致性方案
MQ可靠消息最终一致性方案
下单服务的操作过程: 在订单服务中,首先进行的是一阶段sendMQ进行预备消息,二阶段confirm中进行修改订单的状态 并且发送sendcommit确认请求通过RocketMq向库存服务进行投递扣减库存操作,如若出现修改订单状态异常或者需要回滚操作 则执行cancel进行删除消息操作(deleteMq)
在此过程中,如果sendcommit()确认请求失败,则RocketMQ会进行会查状态,如果失败则会进行此次请求删除,如果投递过程中失败,则RocketMQ 有一个重试机制会进行重试操作,如果仍然失败则会发送警告给开发者,则需要手动进行修改。
Seata快速开始
@GloabTranscational
1.Seata Server(TC)环境搭建
https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
Server端存储模式(store.mode)现有三种:
- file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)。
- db:高可用模式,全局事务会话信息通过db共享,相应性能差些。
步骤一:
打开config/file.conf
步骤二:
修改mode="db"
步骤三:
修改数据库连接信息(URL/USERNAME/PASSWORD)
store {
mode = "db"
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
步骤四:
创建数据库seata
新建表:可以去seata提供的资源目录介绍中下载:点击查看
\script\server\db\mysql.sql
(并将script文件拷贝到seata文件夹下,后面会用到)
- redis:Seata-Server1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置。
资源目录:https://github.com/seata/seata/tree/1.3.0/script
- client 存放client端sql脚本,参数配置
- config-center 各个配置中心参数导入脚本,config.txt (包含server和client,原名necos-config.txt)为通用参数文件
- server server端数据库脚本及各个容器配置
2.db存储模式+Nacos(注册&配置中心)部署
事务参与者需要与协调者进行通讯,通过necos注册中心进行集群的管理。
步骤五:修改配置文件
配置seata数据库
修改conf下的file.conf
store {
mode = "db"
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
配置nacos注册中心,负责事务参与者(微服务)和TC通信
将Seata Server注册到Nacos,修改conf目录下的registry.conf配置
(为什么需要nacos注册中心?通过nacos注册中心合理的与seata协调者进行通讯
为什么需要nacos配置中心还要修改config.txt文件的配置?seata中默认的配置中心是file,修改完成以后需要将seata目录下\script\config-center\config.txt下的数据库完成以后注册到nacos中)
修改conf下的registry.conf
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
为了将seata中的配置加载到nacos中,修改script下的config.txt文件
store.mode=db #修改存储模式 并将关于file的配置删掉
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 #数据库的地址
store.db.user=root #用户名
store.db.password=123456 #密码
配置事务分组,要与客户端配置的事务分组一致
#my_test_tx_group需要与客户端保持一致,default需要跟客户端registry.conf中的registry中的cluster保持一致
客户端propertirs配置:spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
事务分组:解决异地机房容错机制
my_test_tx_group可以自定义,比如(guangzhou,shanghai),但同时客户端也需要去修改
步骤六:配置参数注册到到Nacos
若是配置的nacos为127.0.0.1 则可直接双击运行seata\script\config-center\nacos下的nacos-config.sh(因为该sh文件中很多的设置为127.0.0.1)
否则的话
采用shell命令
参数说明:
-h:host,默认值为localhost
-p:port,默认时8848
-g:配置分组,默认值是‘seata_group’
-t:租户信息,对应Nacos的命名空间为ID字段,默认值为空
步骤七:启动Seata server
启动Seata Server
- 源码启动:执行server模块下io.seata.server.Server.java的main方法
- 命令启动:bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
支持的启动参数
参数 | 全写 | 作用 | 备注 |
-h | --host | 指定在注册中心注册的ip | 不指定时获取当前的ip,外部访问部署在云环境和容器中的server建议指定 |
-p | --port | 指定server启动的端口 | 默认为8091 |
m | --storeMode | 事务日志存储方式 | 支持file,sb,redis,默认为file,注意redis时需要Seate Server1.3版本及以上 |
-n | --serverNode | 用于指定seata-server节点ID | 比如1,2.。。默认为1 |
-e | --seataEnv | 指定seate-server运行环境 | 比如dev,test等,服务启动时会使用regidtry-dev.conf这样的配置 |
启动Seata Server,集群的情况下可指定端口号
bin/seata-server.sh start -p 8091 -n 1
bin/seata-server.sh start -p 8092 -n 2
bin/seata-server.sh start -p 8093 -n 3
启动成功,默认端口号为8091
在nacos注册中心可以查看到seata注册成功
Seata Client快速开始
声明式事务实现(@GloabTransactional)
接入微服务应用
业务场景
用户下单,整个业务逻辑由三个微服务构成:
- 订单服务:根据采购需求创建订单。
- 库存服务:对给定的商品扣除库存数量。
1) 启动Seata Server端,Seata Server使用nacos作为配置中心和注册中心(上一步已经完成)
2)简单搭建一个项目seata,新建两个微服务alibaba-order-seata和alibaba-seata-stock
- alibaba-order-seata 下单服务:添加一条下单数据
- alibaba-seata-stock 库存服务:较少库存
在下单服务和库存服务中中添加nacos和feign依赖:
<!--nacos服务注册依赖 nacos-discovery依赖了ribbon 可以不在引入ribbon依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--集成feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
配置文件中application.yml中添加(注意层级)
application:
name: alibaba-stock-seata
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
在下单服务中使用feign集成库存服务:
@FeignClient(value = "alibaba-stock-seata",path = "/seata/stock")
public interface StockService {
@PostMapping("/update")
public String updateStock(@RequestParam(value = "productId") String productId);
}
启动类中添加@EnableFeignClients
3) 配置微服务整合seata(在下单服务可库存服务中均需添加)
第一步:添加pom依赖
<!--添加seata依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
第二步:各微服务对应数据库中添加undo_log表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
primary key (`id`),
UNIQUE KEY `ux_undo_log`(`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
第三步:配置客户端与协调者进行通讯的事务分组 以及 seata注册中心(要求查看同步到nacos的注册中心文件config.txt) 请注意空格 注意层级
spring:
alibaba:
seata:
tx-service-group: henan #配置事务分组
#seata配置
seata:
#配置seata的注册中心,Seata Client如何去访问Seata Server
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
application: seata-server #默认就是seata-server
group: SEATA_GROUP #默认就是SEATA_GROUP
#配置seata的配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
application: seata-server #默认就是seata-server
group: SEATA_GROUP #默认就是SEATA_GROUP
第四步:在业务逻辑代码中添加seata事务注解@GlobalTransactional
第五步:启动,测试 结果为如果出现异常 订单数据不会增加 并且库存数量不会减少
该项目请关注项目地址,该项目里的seata项目