【SpringCloud】10.Spring Cloud Alibaba Sentinel——分布式服务
官网:https://seata.apache.org/zh-cn/
下载地址:https://seata.apache.org/zh-cn/download/seata-server
一般来说,在多个微服务的环境下,不可能只有一个数据源吧?——如果只有,那么就不需要用到seta-Server了。
目前,关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多的技术手段来实现。seta-Server就是解决这些问题的。
概述
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
发展历程
阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。
2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案:
2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
2019 年 fescar(全称fast easy commit and rollback****) 被重命名为了seata(simple extensiable autonomous transaction architecture)。TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。
Seata已经被Apache基金会收购。2023年10月,为了更好地通过社区驱动技术的演进,阿里巴巴和蚂蚁集团正式将Seata捐赠给Apache基金会,并通过了Apache基金会的投票决议,以全票通过的优秀表现加入Apache孵化器项目 。
工作流简介
纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。
Seata三个概念:
TC(Transaction Coordinator)-事务协调者,就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
TM(Transaction Manager)-事务管理者,标注全局@GlobalTransactional
,启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护全局事务和分支事务状态,作出开始事务、提交事物、回滚事物的决议。
RM(Resource Manager)-资源管理者,。注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
分布式事务执行流程总结:
- TM向TC申请一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
- XID在微服务调用链路的上下文中传播。
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成或提交回滚请求。
各种事务模式:https://seata.apache.org/zh-cn/docs/user/mode/at
Seata AT模式;Seata TCC模式;Seata Saga模式;Seata XA模式。
Seta-Server 2.0.0 安装
-
下载seta,载地址:https://seata.apache.org/zh-cn/release-history/seata-server
-
新建seata库,用于存储seata的全局信息。
打开mysql数据库,新建库seata库的全局信息。
CREATE DATABASE seata;USE seata;
seata运行mysql库:{home}\script\server\db\mysql.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_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 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(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
-
更改模板配置:
application.yml
。(修改前记得先备份哦)位置:D:\seata\seata-2.0.0\conf\application.yaml。将seata的配置库,配置到数据库文件中。console: user: username: seata password: seata # 修改内容开始 seata: config: # support: nacos, consul, apollo, zk, etcd3 type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP username: nacos password: nacos registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP namespace: cluster: default username: nacos password: nacos store: # support: file 、 db 、 redis 、 raft mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:13306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true user: root password: root min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 1000 max-wait: 5000 # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000' # 修改内容结束 security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
注:用户名和密码、mysql地址写自己的。
-
启动nacos 8848(
startup.cmd -m standalone
)。 -
打开{home}\bin路径。双击
seata-server.bat
命令执行后,查看http://localhost7091 。输入用户名和密码。查看配置知道,用户名和密码均为seata
。 -
再次查看nacos,服务列表已经添加seata-server服务。
Seata案例实战——数据库和表准备
案例:我们准备3个业务数据:订单+ 库存+ 账户的MySql数据库准备。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
再通过远程调用账户服务来扣减用户账户里面的余额,
最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单→减库存→扣余额→改(订单状态)
步骤如下:
- 建立数据库
- 建立undo_log回滚日志表(AT模式专用)
- 3个库分表建立业务表
建立数据库,需要建立的库说明:
库名称 | 说明 |
---|---|
seata_order | 存储订单的数据库 |
seata_storage | 存储库存的数据库 |
seata_account | 存储账户信息的数据库 |
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
建立undo_log回滚日志表(AT模式专用)
Seata是一种非入侵式的解决方案,需要为不同的库插入undo_log 日志 ,检查全局锁等。eata TCC模式;Seata Saga模式;Seata XA模式不需要。
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) 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',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
3个库分别创建业务表
存储订单库(seata_order)数据表
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`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;
SELECT * FROM t_order;
存储库存数据库(seata_storage)数据表
CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
存储账户信息数据库(seata_order)数据表
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;
微服务编码落地
业务数据库已经准备完毕,接着是编码落地。需求简单来说就是:
下订单→减库存→扣余额→改(订单状态)
步骤:
- 一键生成代码
- 修改公共方法
- 新建Order微服务
- 新建Storage微服务
- 新建Account微服务
一键生成代码
接下来,我们会为以上数据库分表一键生成上述三个库的代码。这里使用的是Mybatis的Mapper4生成。(https://www.cnblogs.com/luyj00436/p/18683355)。
修改公共方法,cloud-api-commons
新增库存(StorageFeignApi)和账户(AccountFeign)的两个Feign。
库存Feign接口
/**
* 库存接口
* @Author:lyj
* @Date:2025/1/22 09:29
*/
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi {
/**
* 扣减库存
* @param productId
* @param count
* @return
*/
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
账户Feign接口
/**
* 账户余额接口
* @Author:lyj
* @Date:2025/1/22 09:51
*/
@FeignClient(value="seata-account-service")
public interface AccountFeignApi {
/**
* 扣减账户月
* @param userId
* @param money
* @return
*/
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
新建Order 微服务
接下来,我们需要分别新建3个微服务。新建Order微服务,名称为:seata-order-service-2001
。
改POM,写业务类
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
写YAML,写配置。
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:13306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: root
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
主启动
/**
* @Author:lyj
* @Date:2025/1/22 10:50
*/
@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient // 服务注册和发现
@EnableFeignClients
public class SeataOrderMain2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain2001.class, args);
}
}
业务类
复制之前生成的Order业务类。(Order.java
、OrderMapper.java
、OrderMapper.xml
)。
新建Service及实现。
/**
* @Author:lyj
* @Date:2025/1/22 10:59
*/
public interface OrderService {
/***
* 创建订单
* @param order
*/
void create(Order order);
}
/**
* @Author:lyj
* @Date:2025/1/22 11:00
* 下订单→减库存→扣余额→改订单
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper; // 订单
@Resource
private StorageFeignApi storageFeignApi; // 库存
@Resource
private AccountFeignApi accountFeignApi; // 账户
/**
* 创建订单
* @param order
*/
@Override
@GlobalTransactional(name = "sentinel-create-order", rollbackFor = Exception.class) //@GlobalTransactional ,事务的发起者(比如订单模块)
public void create(Order order) {
// xid 检查
String xid = RootContext.getXID();
// 1. 创建订单
log.info("=================开始创建订单" + "\t" + "xid_order:" + xid);
// 订单创建状态 status : (0:创建中; 1:已完结)
order.setStatus(0);
int result = orderMapper.insertSelective(order);
// 插入订单成功后,获得mysql的实体对象
Order orderFromDB = null;
if (result > 1) {
orderFromDB = orderMapper.selectOne(order);
log.info("-------> 新建订单成功,orderFromDB info:" + orderFromDB);
System.out.println();
// 2. 扣减库存
log.info("------> 订单微服务开始使用Storage库存,做扣减count");
storageFeignApi.decrease(orderFromDB.getProductId(),orderFromDB.getCount());
System.out.println();
// 3. 扣减账号月
log.info("------> 订单微服务开始使用Account庄户,做扣减money");
accountFeignApi.decrease(orderFromDB.getProductId(),orderFromDB.getMoney());
System.out.println();
// 4. 修改订单状态(0:创建中,1:已完结)
log.info("------->修改订单状态");
orderFromDB.setStatus(1);
Example whereCondition = new Example(Order.class);
Example.Criteria criteria = whereCondition.createCriteria();
criteria.andEqualTo("userId", orderFromDB.getUserId());
criteria.andEqualTo("status", 0);
int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
log.info("------->修改订单状态完成" + "\t" + updateResult);
}
System.out.println();
log.info("结束新建订单\t" + "xid_order:" + xid);
}
}
创建接口
/**
* @Author:lyj
* @Date:2025/1/22 11:35
*/
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public ResultData create(Order order) {
orderService.create(order);
return ResultData.success(order);
}
}
创建库存、账户微服务
库存微服务,模块名称 : seata-storge-service2002
。其他与常规创建模块方法一致,略。
账户微服务,模块名称:seata-account-service2003
。其他与常规创建模块方法一致,略。
Seata案例测试
测试前准备
- 启动Nacos
- 启动Seata
- 启动微服务2001
- 启动微服务2002
- 启动微服务2003
生成下单请求:GET http://localhost:2001/order/create? userId=1&productId=1&count=1&money=1
此时,发现数据库完成了一个订单,两个个扣减。