事务理论基础
分布式锁保证多线程下数据库操作安全保障 分布式事务发生异常可以回滚.
使用postman发送请求插入一条新订单。
然后现在库存只剩下8个商品,如果买10个的话应该统一失败。
CAP定理
假如node03在独立时将所有请求都堵塞并等待恢复和其余节点的连接的话以此保证数据一致性,就不满足可用性了。
BASE理论
初识Seata
Seata的架构
部署TC服务
docker 部署
1.拉取镜像
docker pull seataio/seata-server:1.5.0
2.创建自定义文件映射数据卷
mkdir -p /docker_volume/seata
3.运行一个容器并拷贝数据卷目录出来
docker run --name seata-server -p 8091:8091 -d seataio/seata-server:1.5.0
docker cp seata-server:/seata-server /docker_volume/seata
现在可以对application.yml配置文件进行自定义的修改配置。
3.删除已有容器
docker stop seata-server
docker rm seata-server
4.重新创建并运行容器
docker run --name seata-server -p 8091:8091 -p 7091:7091 -v /docker_volume/seata/seata-server:/seata-server -e SEATA_IP=192.168.80.180 -e SEATA_PORT=8091 seataio/seata-server:1.5.0
接下来的配置之后再研究。
windows部署
1.使用提供的安装解压部署
2.解压后目录如下
3.修改配置文件
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"
}
}
4.在nacos添加配置
为使tc集群可以共享配置。使用nacos作为配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。
其中的数据库地址、用户名、密码都需要修改成自己的数据库信息
# 数据存储方式,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=123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=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
5.创建数据库表
特别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。
新建一个名为seata的数据库,运行课前资料提供的sql文件:
这些表主要记录全局事务、分支事务、全局锁信息:
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;
6.启动TC服务
微服务集成Seata
1.引入依赖
<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: # tc
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server # tc服务在nacos中的服务名称
cluster: SH
tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
service:
vgroup-mapping: # 事务组与TC服务cluster的映射关系
seata-demo: SH
为三个服务分别配置之后重启服务。
然后可以看见对应的服务被seata发现了。
这里有个问题,就是用jdk17会报错,必须把idea配置的项目jdk变成jdk8,不是pom文件里面的jdk8.
动手实践
XA模式
第一阶段只是执行业务并返回结果并不提交,第二阶段才是提交。
如果第一阶段有的业务执行失败就会回滚之前提交的业务恢复状态。
seata比起传统xa多了个TM做整个事务的注册和管理。
xa模式具有强一致性,但是第一阶段所有业务都不提交而是一直等待别的业务,会一直占用着锁,性能较差,可用性较低。
实现
这里还要再pom文件里把mysql依赖版本改成8.0.11才可以,不然会报错。并且还要再配置文件里面配置数据库连接时加上&serverTimezone=GMT%2B8(三个服务都要改)
执行一次正常下单之后,还剩下6个库存和600余额。
然后执行一次失败下单。这次抛出了异常。并且库存没有减少,余额也没有变化。
看见扣款服务里面是有执行扣款的,但是却回滚了。
AT模式
与xa模式不同,at模式执行完sql之后会直接提交。因为没有等待,所以没有了资源锁定。
这种已经提交的数据库无法进行回滚,为了回退状态,会由RM记录数据快照用于将来回滚。
然后这里的二阶段再所有子事务都成功之后就只需要通知rn删除快照即可。存在子事务失败的话就基于快照进行回滚。
AT模式的脏写问题.
AT模式的阶段一和阶段二不是一个整体的锁定状态,会导致存在丢失更新的问题。
脏写问题解决方案
这个全局锁会记录是哪一个事务在操作哪一张表的哪一行数据,然后会进行锁定。
在解决方案中,事务1在获取全局锁之后只要不释放全局锁,别的事务2就无法获取来进行修改。会一直进行重试。
然后事务1要进行回滚要获取DB锁,但是这种形成了互相等待。seata的获取全局锁的重试最多等待时间为300ms,最终,事务2先释放了DB锁。
虽然这种解决方案感觉又梦回xa模式,但是XA的锁是数据库锁。AT的锁是TC记录的锁。两个锁的力度有很大差别。
TC锁是行锁并且对于其他不受seata管理的事务是不受影响。
所以在极端情况下可能有别的事务也要来修改同一行数据。但是几率极低。
原因:
1.转账业务下99.9%的情况在第二阶段都是成功,不会进行回滚。
2.这种分布式事务本身业务耗时长,并发较低。
虽然概率低,AT模式还是对这种情况做了处理。
脏写问题解决方案的解决方案
seata在保存快照时保存了两个快照。一个是更新前,一个是更新后。
在进行回滚恢复的时候会拿更新后的快照和当前数据库数据进行对比,一样则说明没有别的事务修改,如果不一样seata就会判断无法恢复并发出警告。
AT模式代码实现
注意:数据库连接池如果使用的是druid要使用druid-starter,不要使用druid。否则不生成undolog 无法回滚
在TC服务用的数据库seata里面执行上面的sql文件新建lock_table表和undo_log表。
postman测试
发起一次失败的请求.
扣款完后发现库存不足,然后进行回滚,回滚完就删除快照。 ubdo_log里的数据用完就删,所以不打断点是看不见的。
TCC模式
XA模式和AT模式都会加锁,都会存在性能的损耗。
AT模式损耗服务器性能,TCC模式损耗我的性能,给爷整笑了。
每个不同的事务在阶段一冻结的是不同资源,后面阶段二里提交或者回滚也都是动用先前冻结的资源.
TCC模式代码实现
这里只需要改成扣款的服务,其订单服务不适合TCC改造。扣减库存的也可以用TCC进行改造。
这里要先在数据库里添加可用金额的字段。
幂等性:一个接口调用多少次效果都是一致的。
空回滚和业务悬挂
两个子事务,一个try成功,一个阻塞超时了,TM发出回滚的命令,但是其中一个都没有try,回滚会有问题。如果报错的话,seata会重试cancel。
在空回滚之后业务恢复了,继续执行try,但是此时已经没有阶段二了。
因此try和cancel要做互相的判断再执行。
因此需要在数据库里记录事务的当前状态。
还要使用给的sql文件进行表格的创建.
@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
//0.获取事务id
String xid = RootContext.getXID();
//1.判断freeze中是否有冻结记录,如果有,一定是CANCEL执行过,要拒绝业务。
AccountFreeze oldFreeze = freezeMapper.selectById(xid);
if(oldFreeze==null){
//CANCEL执行过,要拒绝业务
return;
}
//1.扣减可用金额
accountMapper.deduct(userId,money);
//2.记录冻结金额,事务状态
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
//1.获取事务id
String xid = ctx.getXid();
//2.根据id删除冻结记录
int count = freezeMapper.deleteById(xid);
return count==1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
//0.查询冻结记录
String xid = ctx.getXid();
String userId = ctx.getActionContext("userId").toString();
AccountFreeze freeze = freezeMapper.selectById(xid);
//1.空回滚的判断,判断freeze是否为null,为null证明try没执行,需要空回滚。
if(freeze==null){
//证明try没执行,需要空回滚
freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
return true;
}
//2.幂等判断
if(freeze.getState()==AccountFreeze.State.CANCEL)
{
//已经处理一次CANCEl了,无需重复处理
return true;
}
//1.恢复可用余额
accountMapper.refund(freeze.getUserId(),freeze.getFreezeMoney());
//2.将冻结金额清零,状态给为CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count==1;
}
}
上面就是代码实现,测试的时候要在controller层里面修改调用的service接口
正常情况
异常情况
下单数量远超库存,此时业务会失败,集体回滚,数据库数据都不变.
freeze表里也会有一条记录。
控制台也显示用的TCC模式
SAGA模式
总结对比
高可用
这个就不做了,感觉以后没什么机会做这个。