Sentinel
雪崩问题
服务D出现了故障,服务A一直请求服务D,当tomcat资源耗尽,服务A也出现了问题
雪崩就是某个服务出现故障,导致整个微服务都不可用
解决方案1:超时处理
设定超时时间,如果超过一定的时间就返回错误信息,不会无休止等待
解决方案2:舱壁模式
限定每个业务能使用的线程数,避免消耗整个tomcat的资源, 也叫线程隔离
解决方案3:熔断降级
由断路器统计业务的执行异常比例
如果超出阈值则会熔断该业务,拦截访问该业务的一切请求(比较好)
解决方案4:流量控制
限制每秒钟处理请求的数量,限制访问的OPS,避免服务因为流量徒增而故障
- 如何避免因瞬间高并发流量导致的服务故障
- 流量控制
- 如何避免因服务故障引起的雪崩问题?
- 超时处理
- 线程隔离
- 降级熔断
特点:完备的实时监控,广泛的开源生态
启动Sentinel
下载Jar包,在此处打开cmd, 输入指令
java -jar sentinel-dashboard-1.8.1.jar
默认的账户和密码都是sentinel
更换端口举例:
java -jar sentinel-dashboard-1.8.1.jar -Dserver.port=8090
微服务整合sentinel
注册中心和网关启动后
步骤1:引入依赖(服务加)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置Sentinel信息
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # sentinel 地址
如果报错: Relying upon circular references is discouraged and they are prohibited by default.
原因:因为 sentinel 循环依赖 springboot2.6.2的冲突,依赖循环引用
spring:
main:
allow-circular-references: true
限流规则(簇点链路)
簇点链路:就是项目内的调用链路(就是先访问Controller,然后交给Service,然后Mapper,形成的链路)
Sentinel 默认会监控 springMVC 的每个断点(EndPoint)(资源)
下面就是给每个资源进行资源流控
新增资源流控
- QPS是并发量,后面点击阈值设置为1,表示每秒只能允许一个请求
关联限流
就是B触发了限流,对A进行限流
当write请求到达阈值时,对update限流
使用条件:
- 两个竞争关系的资源
- 一个优先级高,一个优先级低的
链路限流
只计算从 test进入update的请求
200 / 50 = 4 每秒4个
下面两个方法都会调用 queryOrder 方法
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderServiceImpl orderService;
@GetMapping("/update")
public String update(){
orderService.queryOrder();
return "更新成功";
}
@GetMapping("/query")
public String query(){
orderService.queryOrder();
return "查询成功";
}
}
想想,service层的方法被监控了吗?没有吧!那显然不可以配置规则
Sentinel 默认只标记Controller中的方法作为资源,要标记其他方法需要
@Service
public class OrderServiceImpl {
@SentinelResource("goods")
public void queryOrder(){
System.err.println("查询成功!");
}
}
但是 Sentinel 默认会将Controller做Context整合,导致链路流控失效,需要修改配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # sentinel 地址
web-context-unify: false # 关闭context整合
流控效果
就是当达到阈值时,采取的措施
快速失败:直接抛出异常
Warm up:预热模式,当抛出异常,阈值逐渐增大
排队等待:让所有请求依次等待,依次执行,两个请求不能小于指定等待时长。
预热模式
预热5秒,最大数量十个,逐渐每秒最大数量
细节:请求阈值初始值 = OPS(阈值) / coldFactor
coldFactor 默认是3
排队等候
后面请求会等待前面请求结束后执行,如果超时会抛出异常
预期等待:200ms,最大2000ms,如果超出直接抛出异常
热点参数限流
是更细粒度的限流
注意:热点参数限流对默认的SpringMVC资源无效,需要加上@SentinelResource("hot")
@SentinelResource("hot")
@GetMapping("/{orderId}")
public Order queryOrderById(@PathVariable Integer orderId, @RequestHeader(value = "Truth",required = false) String truth) {
return order;
}
给/order/{orderId}这个资源添加热点参数限流,规则如下:
默认的热点参数规则是每1秒请求量不超过2
给102这个参数设置例外:每1秒请求量不超过4
给103这个参数设置例外:每1秒请求量不超过10
注意:类型要一致,long与int不一样
总结:根据线程数量 / 总秒数
单机阈值 和 1
隔离和降级
FeignClient整合Sentinel
限流可以尽量避免高并发,但是还会有问题,要靠线程隔离或者熔断降级
开启feign的Sentinel功能
feign:
sentinel:
enabled: true # 开启feign的sentinel功能
给feignClient编写失败后的逻辑
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
首先创建类实现FallbackFactory(这是公共的)
package com.feign.fallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User findById(Integer userId) {
log.error("查询用户异常!",cause);
return new User();
}
};
}
}
将上面对象声明成 bean,要在导入的类里面声明
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
在之前的客户端上加
@FeignClient(value = "user1service1", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
Order表主启动类
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
遇到错误: Unexpected exception during bean creation; nested exception is java.lang.IllegalStateException: Incompatible fallbackFactory instance. Fallback/fallbackFactory of type class com.feign.fallback.UserClientFallbackFactory is not assignable to interface feign.hystrix.FallbackFactory for feign client user1service1
原因:因为导包岛错了 FallbackFactory的包应该导入这个
import feign.hystrix.FallbackFactory;
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
会发现这里的地址已经是Get开头了
线程隔离两种方式
信号量隔离(Sentinel默认采用)
- 服务A 依赖服务B、服务C,当请求来的时候
- 让从N个池子里取线程,再分别调用Feign的客户端发起远程调用
信号量隔离 请求的时候,计数器会减1,如果总共信号量是10,超过10会直接拒绝,当然完成一个会减少一个
线程池隔离 | |
---|---|
优点 | 缺点 |
支持主动超时 | 线程的额外开销比较大 |
支持异步调用 |
场景:低扇出,就是一个服务依赖其他服务比较少,不适合高扇出
信号量隔离 | |
---|---|
优点 | 缺点 |
轻量级,无额外开销 | 不支持主动超时 |
不支持异步调用 |
场景:高频调用,高扇出
实现线程隔离 这就是配置信号量最大值隔离
熔断降级
统计异常比例,如果超出阈值,则会拦截服务一切请求。
当服务恢复时会放行该服务。
- closed:达到失败阈值,自动转Open快速失败
- Open:有个熔断时间,当熔断时间结束转Half-Open
- Half-Open:尝试放一次请求,根据返回结果,转Closed或Open
熔断策略
慢调用
业务响应时间过长,超过指定时长,触发熔断
- 最大RT:超过这个时间都算慢调用
- 比例阈值:达到设定时间触发熔断
- 熔断时长:超过这个时间后,进入Half-Open
- 最小请求数:在统计时长内统计N次达到最大RT的,达到比例阈值,触发熔断,熔断时间开始
- 统计时长:统计多少时间内的
异常比例或异常数
- 指定时间内调用次数超过指定请求数
- 并且异常比例达到设定的阈值(或者超过指定异常数),触发熔断
授权规则(只允许从网关来的请求)
白名单:来源(origin)在白名单内的调用者允许访问
黑名单:来源(origin)在黑名单内的调用者不允许访问
只允许从网关来的请求
步骤1:判断是否从网关发起的请求
@Component
public class HeaderOriginParser implements RequestOriginParser {
// 1. 创建 sentinel包下 HeaderOriginParser 实现 RequestOriginParser
@Override
public String parseOrigin(HttpServletRequest request) {
// 1. 获取请求头
String origin = request.getHeader("origin");
if(StringUtils.isEmpty(origin)){
//设置默值
return origin = "blank";
}
return origin;
}
}
步骤2:给网关过来的请求新增请求头
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8845 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://user1service1 # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service
uri: lb://order1service
predicates:
- Path=/order/**
# 添加过滤条件,新增请求头
default-filters:
- AddRequestHeader=Truth,Itcast is freaking aowsome!
- AddRequestHeader=origin,gateway
步骤3:添加授权规则
自定义异常结果
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429; //限流都是429
if(e instanceof FlowException){
msg = "请求被限流";
}else if(e instanceof DegradeException){
msg = "请求被降级";
}else if(e instanceof ParamFlowException){
msg = "热点参数限流";
}else if(e instanceof AuthorityException){
msg = "请求没有权限";
status = 401;
}
//设置相应类型为JSON和编码设置
response.setContentType("application/json;charset=utf-8");
// 状态码
response.setStatus(status);
// 拼接JSON
response.getWriter().println("{"message": "" + msg + "", "status": " + status + "}");
}
}
规则持久化
重启服务,配置的规则都丢失了,肯定不能容忍,因为保存到了内存里
持久化规则
规则管理模式 | |
---|---|
原始模式 | Sentinel的默认模式,将规则保存在内存,重启丢失 |
pull模式 | |
pull模式
- 当我们编写规则时,会推送给Sentinel Dashboard 客户端
- 会更新内存中的规则,并且持久化到数据库中
- 如果另外的服务,也需要这个规则
- 微服务就会定时轮询这个数据库
- 监听到数据库或者文件内容发生变化,就知道规则更新
缺点:时效性太差,会导致数据不一致
push模式(推荐)
- push模式不会把规则推送给任何一个客户端
- 而是将配置推送到远程配置中心,例如Nacos
- 微服务去监听Nacos,发现变化,立即更新
实现Push模式
push模式实现最为复杂,依赖于nacos,并且需要改在Sentinel控制台源码。
步骤1:引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
步骤2:配置Nacos地址
spring:
application:
name: order1service
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: localhost:8845
config:
file-extension: yaml
enabled: false
sentinel:
datasource:
flow:
nacos:
server-addr: localhost:8845 # nacos地址
dataId: order1service-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow # 还可以是:degrade、authority、param-flow
degrade: #配置降级,这里不加
nacos:
server-addr: localhost:8845 # nacos地址
dataId: order1service-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade # 还可以是:degrade、authority、param-flow
分布式事务(Seata)
事务必须满足,ACID
一个微服务,可以多个事务同时
每个服务都是独立的,所以并不知道其他服务抛出异常了,不会回滚
分布式事务就是要保证所有分支事务最终状态一致
CAP:一致性,可用性,分区容错性
CAP | |
---|---|
Consistency(一致性) | |
Availability(可用性) | |
Partition tolerance (分区容错性) |
Eric Brewer 说,分布式系统无法同时满足这三个指标。
这个结论就叫做 CAP 定理。
一致性:用户访问分布式的任意节点,得到的数据必须一致
用户修改任意节点的数据,就必须实现节点数据的同步
可用性:用户访问集群中任意健康节点,必须得到回应,不能超时或者拒绝
Partition 分区:因为网络故障或其他原因,导致分布式系统部分节点与其他节点失去联系,形成独立分区
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
简述CAP理论
分布式系统节点通过网络连接,一定会出现分区问题(P)
当出现分区问题,系统的一致性和可用性就无法同时满足
思考:elasticsearch集群是CP还是AP?
ES集群出现分区时,故障节点会被剔除集群,数据分片会重新分配到其它节点,保证数据一致。
- 因此是低可用性,高一致性,属于CP
BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
Basically Available (基本可用)
- 分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态)
- 在一定时间内,允许出现中间状态,比如临时的不一致状态。
Eventually Consistent(最终一致性)
- 虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
AP模式:
各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
CP模式:
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
初始Seata
TC:维护全局和分支事务的状态,协调全局事务提交或回滚。
TM:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
RM:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
具体:
- TC:TC检查分支事务的状态,全成功才可以提交。
- TM:告诉TC,开始全局事务,调用分支事务,执行完毕,提高事务给TC
- RM:代理分支事务,向TC注册当前分支事务,执行业务。报告分支事务给TC
不同分布式事务的解决方案 | |
---|---|
XA模式 | 强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入 |
TCC模式 | 最终一致的分阶段事务模式,有业务侵入 |
AT模式 | 最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式 |
SAGA模式 | 长事务模式,有业务侵入 |
部署TC服务(java 17)
配置Seata
打开 模板配置,直接修改面信息
seata:
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8845
namespace:
group: DEFAULT_GROUP
username: nacos
password: nacos
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
data-id: seataServer.properties #配置文件
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: file
preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 127.0.0.1:8845
group: DEFAULT_GROUP
namespace:
cluster: default # 集群
username: nacos
password: nacos
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
在Nacos 控制台中新增配置文件 seataServer.properties
信息下方
# 数据存储方式,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
在刚刚指定的架构中创建表
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;
部署TC服务(java 8)
打开config 里面 registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
# tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8845"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8845"
namespace = ""
group = "DEFAULT_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
在Nacos 控制台中新增配置文件 seataServer.properties
表示安装成功
集成Seata
引入依赖(java 8)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
配置文件,会检查当前服务,需要设置GROUP
seata:
registry:
type: nacos
nacos:
server-addr: localhost:8845
# namespace: ""
group: DEFAULT_GROUP
application: seata-server #这个名字要和 Seata 配置文件中一样
username: nacos
password: nacos
tx-service-group: seata-demo # 项目名
service:
vgroup-mapping:
seata-demo: SH
XA模式(强一致性)
DTP标准
seata的XA模式进行了调整
- RM一阶段工作
- 注册事务到TC
- 执行分业务sql不提交
- 报告状态到TC
- TC二阶段工作
- TC检测分支事务执行状态
- 如果都成功,通知RM提交事务
- 如果有失败,通知所有RM回滚事务
- RM二阶段工作
- 接收TC指令,提交或回滚事务
XA模式的优点是什么 | |
---|---|
事务的强一致,满足ACID原则 | |
常用数据库都支持,实现简单,并且没有代码侵入 |
XA模式缺点是什么 | |
---|---|
因为一阶段需要锁定数据库,等到第二阶段结束才释放,性能较差 | |
依赖关系性数据库实现事务 |
1:开启XA:每一个参与事务的微服务都要添加
开启XA模式
seata:
data-source-proxy-mode: XA #开启数据源代理的XA模式
2:给发起全局事务的入口添加注解,替换以前的 Transactional
// 创建订单
@Override
@GlobalTransactional
public Integer insert(Order order) {
// 扣除余额
accountClient.updateAccount(order.getUserId(), order.getMoney().toString());
// 扣除库存
storageClient.updateCount(order.getCommodityCode(), order.getCount());
return orderDao.insert(order);
}
AT模式(最终一致性)
解决XA模式,资源模式锁定过长
阶段一RM:
- 注册分支事务
- 执行业务sql并
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可 阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
简述AT模式与XA模式最大的区别是什么? | |
---|---|
XA模式一阶段不提交事务,锁定资源 | AT模式一阶段直接提交,不锁定资源 |
XA模式依赖数据库机制回滚 | AT模式利用数据快照实现数据回滚 |
XA模式强一致 | AT模式最终一致 |
读写隔离问题?
在事务1释放db锁后,事务2拿到了锁,并对数据库进行了修改。这个时候事务1,发生了错误,产生了回滚。事务2的数据就等于无效更新
全局锁:由TC记录当前正在操作的某行数据的事务,该事务持有全局锁,具备执行权
事务隔离:
- 事务1,获取db锁,保存快照,执行业务,获取全局锁,提交事务释放db锁
- 事务2,获取db锁,保存快照,获取全局锁失败,因为事务1没释放。
- 事务1,获取db锁,但事务2事务没结束。无法获得,等待时间很长
- 事务2,每隔10毫秒时间会获取全局锁,最多30次300毫秒。
- 事务2,任务超时回滚业务释放锁
- 事务1,获取db锁,回滚业务
AT和XA性能不一样?
因为全局锁锁定的范围只是Seata的,db锁不释放,任何人都无法访问
如果非Seate管理的事务执行了,怎么办
- seate事务:获取db锁,保存快照,执行业务,获取全局锁,提交事务,释放db锁
- 非seate事务,获取db锁,执行业务,提交事务,释放db锁
快照有两个,一个是更新前, 一个是更新后
拿着更新后的快照和现在数据库的数据进行对比,会回滚
记录异常,发送警告,人工介入
ATM的优点 | ATM缺点 |
---|---|
一阶段直接提交事务,释放数据库资源性能比较快 | 两阶段之间属于软状态,属于最终一致 |
利用全局锁实现读写隔离 | 快照功能影响性能,但是比XA模式好 |
没有代码侵入,框架自动回滚和提交 |
- 创建表
在Seata的数据库中,创建表
-- ----------------------------
-- Records of undo_log
-- ----------------------------
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
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 NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
在微服务项目数据库中创建
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 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',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
- 实现AT,记得加全局事务哦~
seata:
data-source-proxy-mode: AT
TCC模式(性能最好)
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为try的反向操作。
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
幂等性:幂等性就是,无论调用多少次,不会重复调用出现问题,保证是数据一致
空回滚:当某分支事务阶段阻塞时,可能导致全局事务超时而触发二阶段Cancel操作,未执行try操作时先执行了cancel操作,就是空回滚
业务悬挂:就是已经执行了回滚业务,但是这个时候突然畅通了,执行try去了。但是这个时候没有阶段二了,无法confirm或cancel了,这就是业务悬挂。
TCC 业务分析 / 声明接口
为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务id和执行状态,为此我们设计了一张表:
业务表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
`state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;
SET FOREIGN_KEY_CHECKS = 1;
需要创建TCC接口和TCC业务层实现类,以及dao层,实体类
service 层
@LocalTCC // 告诉Seate这是TCC
public interface AccountTCCService {
// - @TwoPhaseBusinessAction 这个注解加在那个方法上,那个方法就是 try
// - name 指定当前方法名称
// - commitMethod 对应 confirm方法、
// - rollbackMethod 对应 cancel 方法
// - @BusinessActionContextParameter 用这个注解标注的参数,表示会放到上下文中,会利用 BusinessActionContext拿到参数
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
serviceImpl实现类
@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Resource
private AccountDao accountDao;
@Resource
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 全局获取事务 id
String xid = RootContext.getXID();
// 1. 判断是否是业务悬挂
AccountFreeze freeze1 = freezeMapper.selectById(xid);
if (freeze1 != null) {
System.out.println("拒绝try");
// 说明已经走过了 直接拒绝
return;
}
// 1.扣减可用余额
accountDao.deduct(userId, money);
// 2.记录冻结金额,事务状态
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
System.out.println("运行try");
}
@Override
public boolean confirm(BusinessActionContext context) {
// 1.获取事务id
String xid = context.getXid();
// 2.根据id删除冻结记录
int count = freezeMapper.deleteById(xid);
System.out.println("运行confirm");
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext context) {
// 0.查询冻结记录
String xid = context.getXid();
AccountFreeze freeze = freezeMapper.selectById(xid);
// 判断空回滚
if (freeze == null) {
String userId = context.getActionContext("userId").toString();
freeze = new AccountFreeze();
freeze.setXid(xid);
freeze.setUserId(userId);
freeze.setState(AccountFreeze.CANCEL);
freeze.setFreezeMoney(0);
System.out.println("空回滚");
return true;
}
// 判断是否是幂等
if(freeze.getState() == AccountFreeze.CANCEL){
System.out.println("幂等");
return true;
}
// 1.恢复可用余额
accountDao.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 2.将冻结金额清零,状态改为CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.CANCEL);
int count = freezeMapper.updateById(freeze);
System.out.println("运行回滚");
return count == 1;
}
}
Saga 模式
Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
没有隔离性,会有脏写的可能,没有全局锁,不冻结资源
一阶段:依次执行事务 如果有任何一个事务存在问题,会启动补偿逻辑。
Saga模式优点 | Saga模式优点 |
---|---|
事务参与者基于事件驱动实现异步调用,吞吐高 | 软状态持续时间不确定,时效性差 |
不用编写TCC中的三个阶段,实现简单 | 没有锁,没有事务隔离,会有脏写 |
高可用(搭建)
TC的异地多机房容灾架构
如果有服务挂了。咋整
异地,放在多个机房。上海!北京!杭州
微服务通过Nacos,namespace,和 group,服务名,找到服务
可是事务组,写在代码里,这怎么行。需要(热更新)
实现高可用,异地容灾
- 首先再打开一个seata,里面的 cluster = "HZ"
- 启动,可以看到seata.server有两个集群了
- 更改热更新,动态修改配置
新增配置组
配置内容如下
# 事务组映射关系
service.vgroupMapping.seata-demo=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
那怎么读取呢,新增配置,事务组可以不要
seata:
config:
type: nacos
nacos:
server-addr: localhost:8845 # 读取地址
username: nacos # 用户名和密码
password: nacos
group: SEATA_GROUP # 配置所在群组
data-id: client.properties # 配置文件名称
注意:seate配置文件中不要用默认的服务名
- 下图配置管理中要在一个分组中
- 服务也在一块