当前位置: 首页 > news >正文

Apache Seata分布式事务之Seata-Client原理及流程详解

本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。
本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。

前言

在分布式系统中,分布式事务是一个必须要解决的问题,目前使用较多的是最终一致性方案。自年初阿里开源了Fescar(四月初更名为Seata)后,该项目受到了极大的关注度,目前已接近8000Star。Seata以高性能和零侵入的方式为目标解决微服务领域的分布式事务难题,目前正处于快速迭代中,近期小目标是生产可用的Mysql版本。关于Seata的总体介绍,可以查看官方WIKI获得更多更全面的内容介绍。

本文主要基于spring cloud+spring jpa+spring cloud alibaba fescar+mysql+seata的结构,搭建一个分布式系统的demo,通过seata的debug日志和源代码,从client端(RM、TM)的角度分析说明其工作流程及原理。

文中代码基于fescar-0.4.1,由于项目刚更名为seata不久,例如一些包名、类名、jar包名称还都是fescar的命名,故下文中仍使用fescar进行表述。

示例项目:https://github.com/fescar-group/fescar-samples/tree/master/springcloud-jpa-seata

相关概念

  • XID:全局事务的唯一标识,由ip:port:sequence组成
  • Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
  • Transaction Manager (TM ):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
  • Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

分布式框架支持

Fescar使用XID表示一个分布式事务,XID需要在一次分布式事务请求所涉的系统中进行传递,从而向feacar-server发送分支事务的处理情况,以及接收feacar-server的commit、rollback指令。
Fescar官方已支持全版本的dubbo协议,而对于spring cloud(spring-boot)的分布式项目社区也提供了相应的实现

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-alibaba-fescar</artifactId><version>2.1.0.BUILD-SNAPSHOT</version>
</dependency>

该组件实现了基于RestTemplate、Feign通信时的XID传递功能。

业务逻辑

业务逻辑是经典的下订单、扣余额、减库存流程。
根据模块划分为三个独立的服务,且分别连接对应的数据库

  • 订单:order-server
  • 账户:account-server
  • 库存:storage-server

另外还有发起分布式事务的业务系统

  • 业务:business-server

项目结构如下图
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正常业务

  1. business发起购买请求
  2. storage扣减库存
  3. order创建订单
  4. account扣减余额

异常业务

  1. business发起购买请求
  2. storage扣减库存
  3. order创建订单
  4. account扣减余额异常

正常流程下2、3、4步的数据正常更新全局commit,异常流程下的数据则由于第4步的异常报错全局回滚。

配置文件

fescar的配置入口文件是registry.conf,查看代码ConfigurationFactory得知目前还不能指定该配置文件,所以配置文件名称只能为registry.conf

private static final String REGISTRY_CONF = "registry.conf";
public static final Configuration FILE_INSTANCE = new FileConfiguration(REGISTRY_CONF);

registry中可以指定具体配置的形式,默认使用file类型,在file.conf中有3部分配置内容

  1. transport
    transport部分的配置对应NettyServerConfig类,用于定义Netty相关的参数,TM、RM与fescar-server之间使用Netty进行通信
  2. service
	 service {#vgroup->rgroupvgroup_mapping.my_test_tx_group = "default"#配置Client连接TC的地址default.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disable是否启用seata的分布式事务disableGlobalTransaction = false}
  1. client
	client {#RM接收TC的commit通知后缓冲上限async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}}

数据源Proxy

除了前面的配置文件,fescar在AT模式下稍微有点代码量的地方就是对数据源的代理指定,且目前只能基于DruidDataSource的代理。
注:在最新发布的0.4.2版本中已支持任意数据源类型

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {DruidDataSource druidDataSource = new DruidDataSource();return druidDataSource;
}@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {return new DataSourceProxy(druidDataSource);
}

使用DataSourceProxy的目的是为了引入ConnectionProxy,fescar无侵入的一方面就体现在ConnectionProxy的实现上,即分支事务加入全局事务的切入点是在本地事务的commit阶段,这样设计可以保证业务数据与undo_log是在一个本地事务中。

undo_log是需要在业务库上创建的一个表,fescar依赖该表记录每笔分支事务的状态及二阶段rollback的回放数据。不用担心该表的数据量过大形成单点问题,在全局事务commit的场景下事务对应的undo_log会异步删除。

CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

启动Server

前往https://github.com/apache/incubator-seata/releases 下载与Client版本对应的fescar-server,避免由于版本的不同导致的协议不一致问题
进入解压之后的 bin 目录,执行

./fescar-server.sh 8091 ../data

启动成功输出

2019-04-09 20:27:24.637 INFO [main]c.a.fescar.core.rpc.netty.AbstractRpcRemotingServer.start:152 -Server started ... 

启动Client

fescar的加载入口类位于GlobalTransactionAutoConfiguration,对基于spring boot的项目能够自动加载,当然也可以通过其他方式示例化GlobalTransactionScanner

@Configuration
@EnableConfigurationProperties({FescarProperties.class})
public class GlobalTransactionAutoConfiguration {private final ApplicationContext applicationContext;private final FescarProperties fescarProperties;public GlobalTransactionAutoConfiguration(ApplicationContext applicationContext, FescarProperties fescarProperties) {this.applicationContext = applicationContext;this.fescarProperties = fescarProperties;}/*** 示例化GlobalTransactionScanner* scanner为client初始化的发起类*/@Beanpublic GlobalTransactionScanner globalTransactionScanner() {String applicationName = this.applicationContext.getEnvironment().getProperty("spring.application.name");String txServiceGroup = this.fescarProperties.getTxServiceGroup();if (StringUtils.isEmpty(txServiceGroup)) {txServiceGroup = applicationName + "-fescar-service-group";this.fescarProperties.setTxServiceGroup(txServiceGroup);}return new GlobalTransactionScanner(applicationName, txServiceGroup);}
}

可以看到支持一个配置项FescarProperties,用于配置事务分组名称

spring.cloud.alibaba.fescar.tx-service-group=my_test_tx_group

如果不指定服务组,则默认使用spring.application.name+ -fescar-service-group生成名称,所以不指定spring.application.name启动会报错

@ConfigurationProperties("spring.cloud.alibaba.fescar")
public class FescarProperties {private String txServiceGroup;public FescarProperties() {}public String getTxServiceGroup() {return this.txServiceGroup;}public void setTxServiceGroup(String txServiceGroup) {this.txServiceGroup = txServiceGroup;}
}

获取applicationId和txServiceGroup后,创建GlobalTransactionScanner对象,主要看类中initClient方法

private void initClient() {if (StringUtils.isNullOrEmpty(applicationId) || StringUtils.isNullOrEmpty(txServiceGroup)) {throw new IllegalArgumentException("applicationId: " + applicationId + ", txServiceGroup: " + txServiceGroup);}//init TMTMClient.init(applicationId, txServiceGroup);//init RMRMClient.init(applicationId, txServiceGroup);}

方法中可以看到初始化了TMClientRMClient,对于一个服务既可以是TM角色也可以是RM角色,至于什么时候是TM或者RM则要看在一次全局事务中@GlobalTransactional注解标注在哪。
Client创建的结果是与TC的一个Netty连接,所以在启动日志中可以看到两个Netty Channel,其中标明了transactionRole分别为TMROLERMROLE

2019-04-09 13:42:57.417  INFO 93715 --- [imeoutChecker_1] c.a.f.c.rpc.netty.NettyPoolableFactory   : NettyPool create channel to {"address":"127.0.0.1:8091","message":{"applicationId":"business-service","byteBuffer":{"char":"\u0000","direct":false,"double":0.0,"float":0.0,"int":0,"long":0,"readOnly":false,"short":0},"transactionServiceGroup":"my_test_tx_group","typeCode":101,"version":"0.4.1"},"transactionRole":"TMROLE"}
2019-04-09 13:42:57.505  INFO 93715 --- [imeoutChecker_1] c.a.f.c.rpc.netty.NettyPoolableFactory   : NettyPool create channel to {"address":"127.0.0.1:8091","message":{"applicationId":"business-service","byteBuffer":{"char":"\u0000","direct":false,"double":0.0,"float":0.0,"int":0,"long":0,"readOnly":false,"short":0},"transactionServiceGroup":"my_test_tx_group","typeCode":103,"version":"0.4.1"},"transactionRole":"RMROLE"}
2019-04-09 13:42:57.629 DEBUG 93715 --- [lector_TMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:RegisterTMRequest{applicationId='business-service', transactionServiceGroup='my_test_tx_group'}
2019-04-09 13:42:57.629 DEBUG 93715 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:RegisterRMRequest{resourceIds='null', applicationId='business-service', transactionServiceGroup='my_test_tx_group'}
2019-04-09 13:42:57.699 DEBUG 93715 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:version=0.4.1,extraData=null,identified=true,resultCode=null,msg=null,messageId:1
2019-04-09 13:42:57.699 DEBUG 93715 --- [lector_TMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:version=0.4.1,extraData=null,identified=true,resultCode=null,msg=null,messageId:2
2019-04-09 13:42:57.701 DEBUG 93715 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : com.alibaba.fescar.core.rpc.netty.RmRpcClient@3b06d101 msgId:1, future :com.alibaba.fescar.core.protocol.MessageFuture@28bb1abd, body:version=0.4.1,extraData=null,identified=true,resultCode=null,msg=null
2019-04-09 13:42:57.701 DEBUG 93715 --- [lector_TMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : com.alibaba.fescar.core.rpc.netty.TmRpcClient@65fc3fb7 msgId:2, future :com.alibaba.fescar.core.protocol.MessageFuture@9a1e3df, body:version=0.4.1,extraData=null,identified=true,resultCode=null,msg=null
2019-04-09 13:42:57.710  INFO 93715 --- [imeoutChecker_1] c.a.fescar.core.rpc.netty.RmRpcClient    : register RM success. server version:0.4.1,channel:[id: 0xe6468995, L:/127.0.0.1:57397 - R:/127.0.0.1:8091]
2019-04-09 13:42:57.710  INFO 93715 --- [imeoutChecker_1] c.a.f.c.rpc.netty.NettyPoolableFactory   : register success, cost 114 ms, version:0.4.1,role:TMROLE,channel:[id: 0xd22fe0c5, L:/127.0.0.1:57398 - R:/127.0.0.1:8091]
2019-04-09 13:42:57.711  INFO 93715 --- [imeoutChecker_1] c.a.f.c.rpc.netty.NettyPoolableFactory   : register success, cost 125 ms, version:0.4.1,role:RMROLE,channel:[id: 0xe6468995, L:/127.0.0.1:57397 - R:/127.0.0.1:8091]

日志中可以看到

  1. 创建Netty连接
  2. 发送注册请求
  3. 得到响应结果
  4. RmRpcClientTmRpcClient成功实例化

TM处理流程

在本例中,TM的角色是business-service,BusinessService的purchase方法标注了@GlobalTransactional注解

@Service
public class BusinessService {@Autowiredprivate StorageFeignClient storageFeignClient;@Autowiredprivate OrderFeignClient orderFeignClient;@GlobalTransactionalpublic void purchase(String userId, String commodityCode, int orderCount){storageFeignClient.deduct(commodityCode, orderCount);orderFeignClient.create(userId, commodityCode, orderCount);}
}

方法调用后将会创建一个全局事务,首先关注@GlobalTransactional注解的作用,在GlobalTransactionalInterceptor中被拦截处理

/*** AOP拦截方法调用*/
@Override
public Object invoke(final MethodInvocation methodInvocation) throws Throwable {Class<?> targetClass = (methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null);Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);//获取方法GlobalTransactional注解final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, GlobalTransactional.class);final GlobalLock globalLockAnnotation = getAnnotation(method, GlobalLock.class);//如果方法有GlobalTransactional注解,则拦截到相应方法处理if (globalTransactionalAnnotation != null) {return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);} else if (globalLockAnnotation != null) {return handleGlobalLock(methodInvocation);} else {return methodInvocation.proceed();}
}

handleGlobalTransaction方法中对TransactionalTemplate的execute进行了调用,从类名可以看到这是一个标准的模版方法,它定义了TM对全局事务处理的标准步骤,注释已经比较清楚了

public Object execute(TransactionalExecutor business) throws TransactionalExecutor.ExecutionException {// 1. get or create a transactionGlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();try {// 2. begin transactiontry {triggerBeforeBegin();tx.begin(business.timeout(), business.name());triggerAfterBegin();} catch (TransactionException txe) {throw new TransactionalExecutor.ExecutionException(tx, txe,TransactionalExecutor.Code.BeginFailure);}Object rs = null;try {// Do Your Businessrs = business.execute();} catch (Throwable ex) {// 3. any business exception, rollback.try {triggerBeforeRollback();tx.rollback();triggerAfterRollback();// 3.1 Successfully rolled backthrow new TransactionalExecutor.ExecutionException(tx, TransactionalExecutor.Code.RollbackDone, ex);} catch (TransactionException txe) {// 3.2 Failed to rollbackthrow new TransactionalExecutor.ExecutionException(tx, txe,TransactionalExecutor.Code.RollbackFailure, ex);}}// 4. everything is fine, commit.try {triggerBeforeCommit();tx.commit();triggerAfterCommit();} catch (TransactionException txe) {// 4.1 Failed to committhrow new TransactionalExecutor.ExecutionException(tx, txe,TransactionalExecutor.Code.CommitFailure);}return rs;} finally {//5. cleartriggerAfterCompletion();cleanUp();}
}

通过DefaultGlobalTransaction的begin方法开启全局事务

public void begin(int timeout, String name) throws TransactionException {if (role != GlobalTransactionRole.Launcher) {check();if (LOGGER.isDebugEnabled()) {LOGGER.debug("Ignore Begin(): just involved in global transaction [" + xid + "]");}return;}if (xid != null) {throw new IllegalStateException();}if (RootContext.getXID() != null) {throw new IllegalStateException();}//具体开启事务的方法,获取TC返回的XIDxid = transactionManager.begin(null, null, name, timeout);status = GlobalStatus.Begin;RootContext.bind(xid);if (LOGGER.isDebugEnabled()) {LOGGER.debug("Begin a NEW global transaction [" + xid + "]");}
}

方法开头处if (role != GlobalTransactionRole.Launcher)对role的判断有关键的作用,表明当前是全局事务的发起者(Launcher)还是参与者(Participant)。如果在分布式事务的下游系统方法中也加上@GlobalTransactional注解,那么它的角色就是Participant,会忽略后面的begin直接return,而判断是Launcher还是Participant是根据当前上下文是否已存在XID来判断,没有XID的就是Launcher,已经存在XID的就是Participant.
由此可见,全局事务的创建只能由Launcher执行,而一次分布式事务中也只有一个Launcher存在。

DefaultTransactionManager负责TM与TC通讯,发送begin、commit、rollback指令

@Override
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)throws TransactionException {GlobalBeginRequest request = new GlobalBeginRequest();request.setTransactionName(name);request.setTimeout(timeout);GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);return response.getXid();
}

至此拿到fescar-server返回的XID表示一个全局事务创建成功,日志中也反应了上述流程

2019-04-09 13:46:57.417 DEBUG 31326 --- [nio-8084-exec-1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : offer message: timeout=60000,transactionName=purchase(java.lang.String,java.lang.String,int)
2019-04-09 13:46:57.417 DEBUG 31326 --- [geSend_TMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : write message:FescarMergeMessage timeout=60000,transactionName=purchase(java.lang.String,java.lang.String,int), channel:[id: 0xa148545e, L:/127.0.0.1:56120 - R:/127.0.0.1:8091],active?true,writable?true,isopen?true
2019-04-09 13:46:57.418 DEBUG 31326 --- [lector_TMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:FescarMergeMessage timeout=60000,transactionName=purchase(java.lang.String,java.lang.String,int)
2019-04-09 13:46:57.421 DEBUG 31326 --- [lector_TMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:MergeResultMessage com.alibaba.fescar.core.protocol.transaction.GlobalBeginResponse@2dc480dc,messageId:1196
2019-04-09 13:46:57.421 DEBUG 31326 --- [nio-8084-exec-1] c.a.fescar.core.context.RootContext      : bind 192.168.224.93:8091:2008502699
2019-04-09 13:46:57.421 DEBUG 31326 --- [nio-8084-exec-1] c.a.f.tm.api.DefaultGlobalTransaction    : Begin a NEW global transaction [192.168.224.93:8091:2008502699]

全局事务创建后,就开始执行business.execute(),即业务代码storageFeignClient.deduct(commodityCode, orderCount)进入RM处理流程,此处的业务逻辑为调用storage-service的扣减库存接口。

RM处理流程

@GetMapping(path = "/deduct")
public Boolean deduct(String commodityCode, Integer count){storageService.deduct(commodityCode,count);return true;
}@Transactional
public void deduct(String commodityCode, int count){Storage storage = storageDAO.findByCommodityCode(commodityCode);storage.setCount(storage.getCount()-count);storageDAO.save(storage);
}

storage的接口和service方法并未出现fescar相关的代码和注解,体现了fescar的无侵入。那它是如何加入到这次全局事务中的呢?答案在ConnectionProxy中,这也是前面说为什么必须要使用DataSourceProxy的原因,通过DataSourceProxy才能在业务代码的本地事务提交时,fescar通过该切入点,向TC注册分支事务并发送RM的处理结果。

由于业务代码本身的事务提交被ConnectionProxy代理实现,所以在提交本地事务时,实际执行的是ConnectionProxy的commit方法

public void commit() throws SQLException {//如果当前是全局事务,则执行全局事务的提交//判断是不是全局事务,就是看当前上下文是否存在XIDif (context.inGlobalTransaction()) {processGlobalTransactionCommit();} else if (context.isGlobalLockRequire()) {processLocalCommitWithGlobalLocks();} else {targetConnection.commit();}
}private void processGlobalTransactionCommit() throws SQLException {try {//首先是向TC注册RM,拿到TC分配的branchIdregister();} catch (TransactionException e) {recognizeLockKeyConflictException(e);}try {if (context.hasUndoLog()) {//写入undologUndoLogManager.flushUndoLogs(this);}//提交本地事务,写入undo_log和业务数据在同一个本地事务中targetConnection.commit();} catch (Throwable ex) {//向TC发送RM的事务处理失败的通知report(false);if (ex instanceof SQLException) {throw new SQLException(ex);}}//向TC发送RM的事务处理成功的通知report(true);context.reset();
}private void register() throws TransactionException {//注册RM,构建request通过netty向TC发送注册指令Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),null, context.getXid(), null, context.buildLockKeys());//将返回的branchId存在上下文中context.setBranchId(branchId);
}

通过日志印证一下上面的流程

2019-04-09 21:57:48.341 DEBUG 38933 --- [nio-8081-exec-1] o.s.c.a.f.web.FescarHandlerInterceptor   : xid in RootContext null xid in RpcContext 192.168.0.2:8091:2008546211
2019-04-09 21:57:48.341 DEBUG 38933 --- [nio-8081-exec-1] c.a.fescar.core.context.RootContext      : bind 192.168.0.2:8091:2008546211
2019-04-09 21:57:48.341 DEBUG 38933 --- [nio-8081-exec-1] o.s.c.a.f.web.FescarHandlerInterceptor   : bind 192.168.0.2:8091:2008546211 to RootContext
2019-04-09 21:57:48.386  INFO 38933 --- [nio-8081-exec-1] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select storage0_.id as id1_0_, storage0_.commodity_code as commodit2_0_, storage0_.count as count3_0_ from storage_tbl storage0_ where storage0_.commodity_code=?
Hibernate: update storage_tbl set count=? where id=?
2019-04-09 21:57:48.673  INFO 38933 --- [nio-8081-exec-1] c.a.fescar.core.rpc.netty.RmRpcClient    : will connect to 192.168.0.2:8091
2019-04-09 21:57:48.673  INFO 38933 --- [nio-8081-exec-1] c.a.fescar.core.rpc.netty.RmRpcClient    : RM will register :jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false
2019-04-09 21:57:48.673  INFO 38933 --- [nio-8081-exec-1] c.a.f.c.rpc.netty.NettyPoolableFactory   : NettyPool create channel to {"address":"192.168.0.2:8091","message":{"applicationId":"storage-service","byteBuffer":{"char":"\u0000","direct":false,"double":0.0,"float":0.0,"int":0,"long":0,"readOnly":false,"short":0},"resourceIds":"jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false","transactionServiceGroup":"hello-service-fescar-service-group","typeCode":103,"version":"0.4.0"},"transactionRole":"RMROLE"}
2019-04-09 21:57:48.677 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false', applicationId='storage-service', transactionServiceGroup='hello-service-fescar-service-group'}
2019-04-09 21:57:48.680 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:version=0.4.1,extraData=null,identified=true,resultCode=null,msg=null,messageId:9
2019-04-09 21:57:48.680 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : com.alibaba.fescar.core.rpc.netty.RmRpcClient@7d61f5d4 msgId:9, future :com.alibaba.fescar.core.protocol.MessageFuture@186cd3e0, body:version=0.4.1,extraData=null,identified=true,resultCode=null,msg=null
2019-04-09 21:57:48.680  INFO 38933 --- [nio-8081-exec-1] c.a.fescar.core.rpc.netty.RmRpcClient    : register RM success. server version:0.4.1,channel:[id: 0xd40718e3, L:/192.168.0.2:62607 - R:/192.168.0.2:8091]
2019-04-09 21:57:48.680  INFO 38933 --- [nio-8081-exec-1] c.a.f.c.rpc.netty.NettyPoolableFactory   : register success, cost 3 ms, version:0.4.1,role:RMROLE,channel:[id: 0xd40718e3, L:/192.168.0.2:62607 - R:/192.168.0.2:8091]
2019-04-09 21:57:48.680 DEBUG 38933 --- [nio-8081-exec-1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : offer message: transactionId=2008546211,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false,lockKey=storage_tbl:1
2019-04-09 21:57:48.681 DEBUG 38933 --- [geSend_RMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : write message:FescarMergeMessage transactionId=2008546211,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false,lockKey=storage_tbl:1, channel:[id: 0xd40718e3, L:/192.168.0.2:62607 - R:/192.168.0.2:8091],active?true,writable?true,isopen?true
2019-04-09 21:57:48.681 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:FescarMergeMessage transactionId=2008546211,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false,lockKey=storage_tbl:1
2019-04-09 21:57:48.687 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:MergeResultMessage BranchRegisterResponse: transactionId=2008546211,branchId=2008546212,result code =Success,getMsg =null,messageId:11
2019-04-09 21:57:48.702 DEBUG 38933 --- [nio-8081-exec-1] c.a.f.rm.datasource.undo.UndoLogManager  : Flushing UNDO LOG: {"branchId":2008546212,"sqlUndoLogs":[{"afterImage":{"rows":[{"fields":[{"keyType":"PrimaryKey","name":"id","type":4,"value":1},{"keyType":"NULL","name":"count","type":4,"value":993}]}],"tableName":"storage_tbl"},"beforeImage":{"rows":[{"fields":[{"keyType":"PrimaryKey","name":"id","type":4,"value":1},{"keyType":"NULL","name":"count","type":4,"value":994}]}],"tableName":"storage_tbl"},"sqlType":"UPDATE","tableName":"storage_tbl"}],"xid":"192.168.0.2:8091:2008546211"}
2019-04-09 21:57:48.755 DEBUG 38933 --- [nio-8081-exec-1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : offer message: transactionId=2008546211,branchId=2008546212,resourceId=null,status=PhaseOne_Done,applicationData=null
2019-04-09 21:57:48.755 DEBUG 38933 --- [geSend_RMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : write message:FescarMergeMessage transactionId=2008546211,branchId=2008546212,resourceId=null,status=PhaseOne_Done,applicationData=null, channel:[id: 0xd40718e3, L:/192.168.0.2:62607 - R:/192.168.0.2:8091],active?true,writable?true,isopen?true
2019-04-09 21:57:48.756 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:FescarMergeMessage transactionId=2008546211,branchId=2008546212,resourceId=null,status=PhaseOne_Done,applicationData=null
2019-04-09 21:57:48.758 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:MergeResultMessage com.alibaba.fescar.core.protocol.transaction.BranchReportResponse@582a08cf,messageId:13
2019-04-09 21:57:48.799 DEBUG 38933 --- [nio-8081-exec-1] c.a.fescar.core.context.RootContext      : unbind 192.168.0.2:8091:2008546211
2019-04-09 21:57:48.799 DEBUG 38933 --- [nio-8081-exec-1] o.s.c.a.f.web.FescarHandlerInterceptor   : unbind 192.168.0.2:8091:2008546211 from RootContext
  1. 获取business-service传来的XID
  2. 绑定XID到当前上下文中
  3. 执行业务逻辑sql
  4. 向TC创建本次RM的Netty连接
  5. 向TC发送分支事务的相关信息
  6. 获得TC返回的branchId
  7. 记录Undo Log数据
  8. 向TC发送本次事务PhaseOne阶段的处理结果
  9. 从当前上下文中解绑XID

其中第1步和第9步,是在FescarHandlerInterceptor中完成的,该类并不属于fescar,是前面提到的spring-cloud-alibaba-fescar,它实现了基于feign、rest通信时将xid bind和unbind到当前请求上下文中。到这里RM完成了PhaseOne阶段的工作,接着看PhaseTwo阶段的处理逻辑。

事务提交

各分支事务执行完成后,TC对各RM的汇报结果进行汇总,给各RM发送commit或rollback的指令

2019-04-09 21:57:49.813 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Receive:xid=192.168.0.2:8091:2008546211,branchId=2008546212,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false,applicationData=null,messageId:1
2019-04-09 21:57:49.813 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.AbstractRpcRemoting    : com.alibaba.fescar.core.rpc.netty.RmRpcClient@7d61f5d4 msgId:1, body:xid=192.168.0.2:8091:2008546211,branchId=2008546212,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false,applicationData=null
2019-04-09 21:57:49.814  INFO 38933 --- [atch_RMROLE_1_8] c.a.f.core.rpc.netty.RmMessageListener   : onMessage:xid=192.168.0.2:8091:2008546211,branchId=2008546212,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false,applicationData=null
2019-04-09 21:57:49.816  INFO 38933 --- [atch_RMROLE_1_8] com.alibaba.fescar.rm.AbstractRMHandler  : Branch committing: 192.168.0.2:8091:2008546211 2008546212 jdbc:mysql://127.0.0.1:3306/db_storage?useSSL=false null
2019-04-09 21:57:49.816  INFO 38933 --- [atch_RMROLE_1_8] com.alibaba.fescar.rm.AbstractRMHandler  : Branch commit result: PhaseTwo_Committed
2019-04-09 21:57:49.817  INFO 38933 --- [atch_RMROLE_1_8] c.a.fescar.core.rpc.netty.RmRpcClient    : RmRpcClient sendResponse branchStatus=PhaseTwo_Committed,result code =Success,getMsg =null
2019-04-09 21:57:49.817 DEBUG 38933 --- [atch_RMROLE_1_8] c.a.f.c.rpc.netty.AbstractRpcRemoting    : send response:branchStatus=PhaseTwo_Committed,result code =Success,getMsg =null,channel:[id: 0xd40718e3, L:/192.168.0.2:62607 - R:/192.168.0.2:8091]
2019-04-09 21:57:49.817 DEBUG 38933 --- [lector_RMROLE_1] c.a.f.c.rpc.netty.MessageCodecHandler    : Send:branchStatus=PhaseTwo_Committed,result code =Success,getMsg =null

从日志中可以看到

  1. RM收到XID=192.168.0.2:8091:2008546211,branchId=2008546212的commit通知
  2. 执行commit动作
  3. 将commit结果发送给TC,branchStatus为PhaseTwo_Committed

具体看下二阶段commit的执行过程,在AbstractRMHandler类的doBranchCommit方法

/*** 拿到通知的xid、branchId等关键参数* 然后调用RM的branchCommit*/
protected void doBranchCommit(BranchCommitRequest request, BranchCommitResponse response) throws TransactionException {String xid = request.getXid();long branchId = request.getBranchId();String resourceId = request.getResourceId();String applicationData = request.getApplicationData();LOGGER.info("Branch committing: " + xid + " " + branchId + " " + resourceId + " " + applicationData);BranchStatus status = getResourceManager().branchCommit(request.getBranchType(), xid, branchId, resourceId, applicationData);response.setBranchStatus(status);LOGGER.info("Branch commit result: " + status);
}

最终会将branchCommit的请求调用到AsyncWorker的branchCommit方法。AsyncWorker的处理方式是fescar架构的一个关键部分,因为大部分事务都是会正常提交的,所以在PhaseOne阶段就已经结束了,这样就可以将锁最快的释放。PhaseTwo阶段接收commit的指令后,异步处理即可。将PhaseTwo的时间消耗排除在一次分布式事务之外。

private static final List<Phase2Context> ASYNC_COMMIT_BUFFER = Collections.synchronizedList( new ArrayList<Phase2Context>());/*** 将需要提交的XID加入list*/
@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {if (ASYNC_COMMIT_BUFFER.size() < ASYNC_COMMIT_BUFFER_LIMIT) {ASYNC_COMMIT_BUFFER.add(new Phase2Context(branchType, xid, branchId, resourceId, applicationData));} else {LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");}return BranchStatus.PhaseTwo_Committed;
}/*** 通过定时任务消费list中的XID*/
public synchronized void init() {LOGGER.info("Async Commit Buffer Limit: " + ASYNC_COMMIT_BUFFER_LIMIT);timerExecutor = new ScheduledThreadPoolExecutor(1,new NamedThreadFactory("AsyncWorker", 1, true));timerExecutor.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {try {doBranchCommits();} catch (Throwable e) {LOGGER.info("Failed at async committing ... " + e.getMessage());}}}, 10, 1000 * 1, TimeUnit.MILLISECONDS);
}private void doBranchCommits() {if (ASYNC_COMMIT_BUFFER.size() == 0) {return;}Map<String, List<Phase2Context>> mappedContexts = new HashMap<>();Iterator<Phase2Context> iterator = ASYNC_COMMIT_BUFFER.iterator();//一次定时循环取出ASYNC_COMMIT_BUFFER中的所有待办数据//以resourceId作为key分组待commit数据,resourceId是一个数据库的连接url//在前面的日志中可以看到,目的是为了覆盖应用的多数据源创建while (iterator.hasNext()) {Phase2Context commitContext = iterator.next();List<Phase2Context> contextsGroupedByResourceId = mappedContexts.get(commitContext.resourceId);if (contextsGroupedByResourceId == null) {contextsGroupedByResourceId = new ArrayList<>();mappedContexts.put(commitContext.resourceId, contextsGroupedByResourceId);}contextsGroupedByResourceId.add(commitContext);iterator.remove();}for (Map.Entry<String, List<Phase2Context>> entry : mappedContexts.entrySet()) {Connection conn = null;try {try {//根据resourceId获取数据源以及连接DataSourceProxy dataSourceProxy = DataSourceManager.get().get(entry.getKey());conn = dataSourceProxy.getPlainConnection();} catch (SQLException sqle) {LOGGER.warn("Failed to get connection for async committing on " + entry.getKey(), sqle);continue;}List<Phase2Context> contextsGroupedByResourceId = entry.getValue();for (Phase2Context commitContext : contextsGroupedByResourceId) {try {//执行undolog的处理,即删除xid、branchId对应的记录UndoLogManager.deleteUndoLog(commitContext.xid, commitContext.branchId, conn);} catch (Exception ex) {LOGGER.warn("Failed to delete undo log [" + commitContext.branchId + "/" + commitContext.xid + "]", ex);}}} finally {if (conn != null) {try {conn.close();} catch (SQLException closeEx) {LOGGER.warn("Failed to close JDBC resource while deleting undo_log ", closeEx);}}}}
}

所以对于commit动作的处理,RM只需删除xid、branchId对应的undo_log即可。

事务回滚

对于rollback场景的触发有两种情况

  1. 分支事务处理异常,即ConnectionProxy中report(false)的情况
  2. TM捕获到下游系统上抛的异常,即发起全局事务标有@GlobalTransactional注解的方法捕获到的异常。在前面TransactionalTemplate类的execute模版方法中,对business.execute()的调用进行了catch,catch后会调用rollback,由TM通知TC对应XID需要回滚事务
 public void rollback() throws TransactionException {//只有Launcher能发起这个rollbackif (role == GlobalTransactionRole.Participant) {// Participant has no responsibility of committingif (LOGGER.isDebugEnabled()) {LOGGER.debug("Ignore Rollback(): just involved in global transaction [" + xid + "]");}return;}if (xid == null) {throw new IllegalStateException();}status = transactionManager.rollback(xid);if (RootContext.getXID() != null) {if (xid.equals(RootContext.getXID())) {RootContext.unbind();}}
}

TC汇总后向参与者发送rollback指令,RM在AbstractRMHandler类的doBranchRollback方法中接收这个rollback的通知

protected void doBranchRollback(BranchRollbackRequest request, BranchRollbackResponse response) throws TransactionException {String xid = request.getXid();long branchId = request.getBranchId();String resourceId = request.getResourceId();String applicationData = request.getApplicationData();LOGGER.info("Branch rolling back: " + xid + " " + branchId + " " + resourceId);BranchStatus status = getResourceManager().branchRollback(request.getBranchType(), xid, branchId, resourceId, applicationData);response.setBranchStatus(status);LOGGER.info("Branch rollback result: " + status);
}

然后将rollback请求传递到DataSourceManager类的branchRollback方法

public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {//根据resourceId获取对应的数据源DataSourceProxy dataSourceProxy = get(resourceId);if (dataSourceProxy == null) {throw new ShouldNeverHappenException();}try {UndoLogManager.undo(dataSourceProxy, xid, branchId);} catch (TransactionException te) {if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;} else {return BranchStatus.PhaseTwo_RollbackFailed_Retryable;}}return BranchStatus.PhaseTwo_Rollbacked;
}

最终会执行UndoLogManager类的undo方法,因为是纯jdbc操作代码比较长就不贴出来了,可以通过连接到github查看源码,说一下undo的具体流程

  1. 根据xid和branchId查找PhaseOne阶段提交的undo_log
  2. 如果找到了就根据undo_log中记录的数据生成回放sql并执行,即还原PhaseOne阶段修改的数据
  3. 第2步处理完后,删除该条undo_log数据
  4. 如果第1步没有找到对应的undo_log,就插入一条状态为GlobalFinished的undo_log.
    出现没找到的原因可能是PhaseOne阶段的本地事务异常了,导致没有正常写入。
    因为xid和branchId是唯一索引,所以第4步的插入,可以防止PhaseOne阶段恢复后的成功写入,那么PhaseOne阶段就会异常,这样一来业务数据也就不会提交成功,数据达到了最终回滚了的效果

总结

本地结合分布式业务场景,分析了fescar client侧的主要处理流程,对TM和RM角色的主要源码进行了解析,希望能对大家理解fescar的工作原理有所帮助。

随着fescar的快速迭代以及后期的Roadmap规划,假以时日相信fescar能够成为开源分布式事务的标杆解决方案。

相关文章:

Apache Seata分布式事务之Seata-Client原理及流程详解

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 前言 在分布式系统中&#xff0c;分布式事务是一个必须要解决的问题&#xff0c;目前使用较多…...

Linux wget报未找到命令

wget报未找到命令需要安装wget 1、下载wget安装文件&#xff0c;本次于华为云资源镜像下载 地址&#xff1a;https://mirrors.huaweicloud.com/centos-vault/7.8.2003/os/x86_64/Packages/ 2、下载后上传到安装服务器/install_package&#xff0c;执行命令安装 rpm -ivh /i…...

38条Web测试经验分享

1. 页面链接检查 每一个链接是否都有对应的页面&#xff0c;并且页面之间切换正确。可以使用一些工具&#xff0c;如LinkBotPro、File-AIDCS、HTML Link Validater、Xenu等工具。 LinkBotPro不支持中文&#xff0c;中文字符显示为乱码&#xff1b;HTML Link Validater只能测…...

TCP报文校验和(checksum)计算

一. 原理 将TCP相关内容&#xff08;TCP伪头部TCP头部TCP内容&#xff09;转换成16比特的字符&#xff0c;然后进行累加&#xff0c;最后结果进行取反。TCP伪头部是固定的&#xff0c;下文有相关代码展示。 二. 源码 源码 #include <stdio.h> #include <stdlib.h&…...

【ue5】虚幻5同时开多个项目

正常开ue5项目我是直接在桌面点击快捷方式进入 只会打开一个项目 如果再想打开一个项目需要进入epic 再点击启动就可以再开一个项目了...

【Python实战因果推断】23_倾向分3

目录 Propensity Score Matching Inverse Propensity Weighting Propensity Score Matching 另一种控制倾向得分的常用方法是匹配估计法。这种方法搜索具有相似可观测特征的单位对&#xff0c;并比较接受干预与未接受干预的单位的结果。如果您有数据科学背景&#xff0c;您可…...

Qt源码解析之QObject

省去大部分virtual和public方法后&#xff0c;Qobject主要剩下以下成员&#xff1a; //qobject.h class Q_CORE_EXPORT Qobject{Q_OBJECTQ_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)Q_DECLARE_PRIVATE(QObject) public:Q_I…...

【算法专题】模拟算法题

模拟算法题往往不涉及复杂的数据结构或算法&#xff0c;而是侧重于对特定情景的代码实现&#xff0c;关键在于理解题目所描述的情境&#xff0c;并能够将其转化为代码逻辑。所以我们在处理这种类型的题目时&#xff0c;最好要现在演草纸上把情况理清楚&#xff0c;再动手编写代…...

分库分表真的适合你的系统吗?

曾几何时&#xff0c;“并发高就分库&#xff0c;数据大就分表”已经成了处理 MySQL 数据增长问题的圣经。 面试官喜欢问&#xff0c;博主喜欢写&#xff0c;候选人也喜欢背&#xff0c;似乎已经形成了一个闭环。 但你有没有思考过&#xff0c;分库分表真的适合你的系统吗&am…...

9 redis,memcached,nginx网络组件

课程目标: 1.网络模块要处理哪些事情 2.reactor是怎么处理这些事情的 3.reactor怎么封装 4.网络模块与业务逻辑的关系 5.怎么优化reactor? io函数 函数调用 都有两个作用:io检测 是否就绪 io操作 1. int clientfd = accept(listenfd, &addr, &len); 检测 全连接队列…...

【MySQL】事务四大特性以及实现原理

事务四大特性 原子性&#xff08;Atomicity&#xff09; 事务中的所有操作要么全部完成&#xff0c;要么全部不执行。如果事务中的任何一步失败&#xff0c;整个事务都会被回滚&#xff0c;以保持数据的完整性。 一致性&#xff08;Consistency&#xff09; 事务应确保数据库…...

【控制Android.bp的编译】

1.首先Android.bp的语法是不支持if 条件语句的 2.查到可以用enabled来控制Android.bp中的模块是否参与编译&#xff0c;但是并不能实现动态的控制&#xff0c;比如你需要根据获取到的安卓版本来控制一个Android.bp是否编译&#xff0c;是无法做到的。enabled只能是固定的true或…...

【车载开发系列】J-Link/JFlash 简介与驱动安装方法

【车载开发系列】J-Link/JFlash 简介与驱动安装方法 【车载开发系列】J-Link/JFlash 简介与驱动安装方法 【车载开发系列】J-Link/JFlash 简介与驱动安装方法一. 软件介绍二. 下载安装包二. 开始安装三. 确认安装四. J-Flash的使用 一. 软件介绍 J-Link是SEGGER公司为支持仿真…...

207 课程表

题目 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中 prerequisites[i] [ai, bi] &#xff0c;表示如果要学习课程 ai 则 必须 先学习课程 bi 。 …...

罗剑锋的C++实战笔记学习(一):const、智能指针、lambda表达式

1、const 1&#xff09;、常量 const一般的用法就是修饰变量、引用、指针&#xff0c;修饰之后它们就变成了常量&#xff0c;需要注意的是const并未区分出编译期常量和运行期常量&#xff0c;并且const只保证了运行时不直接被修改 一般的情况&#xff0c;const放在左边&…...

宁德时代天行发布,商用车超充时代来临

近日&#xff0c;宁德时代正式推出商用动力电池品牌——“宁德时代天行”&#xff0c;同时发布“宁德时代天行轻型商用车&#xff08;L&#xff09;-超充版”和“宁德时代天行轻型商用车&#xff08;L&#xff09;-长续航版”两款产品&#xff0c;可实现4C超充能力和500km的实况…...

硅纪元应用评测 | 弱智吧大战GPT4o和Claude 3.5 Sonnet

"硅纪元AI应用测评"栏目&#xff0c;深入解析和评测最新的人工智能应用&#xff0c;提供专业见解和实用建议。不论您是AI专家还是科技爱好者&#xff0c;都能找到权威、详尽的测评&#xff0c;帮助您在快速发展的AI领域中做出最佳选择。一起探索AI的真实潜力&#xf…...

注意力机制 attention Transformer 笔记

动手学深度学习 这里写自定义目录标题 注意力加性注意力缩放点积注意力多头注意力自注意力Transformer 注意力 注意力汇聚的输出为值的加权和 查询的长度为q&#xff0c;键的长度为k&#xff0c;值的长度为v。 q ∈ 1 q , k ∈ 1 k , v ∈ R 1 v {\bf{q}} \in {^{1 \times…...

开始尝试从0写一个项目--后端(二)

实现学生管理 新增学生 接口设计 请求路径&#xff1a;/admin/student 请求方法&#xff1a;POST 请求参数&#xff1a;请求头&#xff1a;Headers&#xff1a;"Content-Type": "application/json" 请求体&#xff1a;Body&#xff1a; id 学生id …...

【图解大数据技术】Hive、HBase

【图解大数据技术】Hive、HBase Hive数据仓库Hive的执行流程Hive架构数据导入Hive HBaseHBase简介HBase架构HBase的列式存储HBase建表流程HBase数据写入流程HBase数据读取流程 Hive Hive是基于Hadoop的一个数据仓库工具&#xff0c;Hive的数据存储在HDFS上&#xff0c;底层基于…...

composables 目录下的文件(web前端)

composables 目录通常用于存放可组合的函数或逻辑&#xff0c;这些函数或逻辑可以在不同的组件中复用。具体来说&#xff0c;composables 目录下的文件通常包含以下内容&#xff1a; 组合式函数 (Composable Functions)&#xff1a; 这些函数利用 Vue 3 的组合式 API&#xff0…...

使用Python绘制堆积柱形图

使用Python绘制堆积柱形图 堆积柱形图效果代码 堆积柱形图 堆积柱形图&#xff08;Stacked Bar Chart&#xff09;是一种数据可视化图表&#xff0c;用于显示不同类别的数值在某一变量上的累积情况。每一个柱状条显示多个子类别的数值&#xff0c;子类别的数值在柱状条上堆积在…...

DP:二维费用背包问题

文章目录 &#x1f3b5;二维费用背包问题&#x1f3b6;引言&#x1f3b6;问题定义&#x1f3b6;动态规划思想&#x1f3b6;状态定义和状态转移方程&#x1f3b6;初始条件和边界情况 &#x1f3b5;例题&#x1f3b6;1.一和零&#x1f3b6;2.盈利计划 &#x1f3b5;总结 &#x1…...

C语言标准库中的函数

由于C语言标准库中的函数非常多&#xff0c;我将按类别列出一些常见函数及其作用。请注意&#xff0c;这里不可能列出所有函数&#xff0c;但我会尽量覆盖主要的类别和函数。 ### 标准输入输出 - printf: 格式化输出到标准输出&#xff08;通常是屏幕&#xff09;。 - scanf: …...

Qt5.9.9 关于界面拖动导致QModbusRTU(QModbusTCP没有测试过)离线的问题

问题锁定 参考网友的思路&#xff1a; Qt5.9 Modbus request timeout 0x5异常解决 网友认为是Qt的bug&#xff0c; 我也认同&#xff1b;网友认为可以更新模块&#xff0c; 我也认同&#xff0c; 我也编译了Qt5.15.0的code并成功安装到Qt5.9.9中进行使用&#xff0c;界面拖…...

API的定义理解

前言 在程序员的日常工作中&#xff0c;“API”这个词在程序员的口中重复的次数&#xff0c;绝对是名列前茅的。 但是对刚开始工作的新人来说&#xff0c;API这个概念还是比较模糊。 确实&#xff0c;API这个概念是随着语义环境而不一样的&#xff0c;所以会让人迷惑。 下面…...

启航IT之旅:高考假期预习指南

标题&#xff1a;启航IT之旅&#xff1a;高考假期预习指南 随着高考的落幕&#xff0c;许多有志于IT领域的学子们即将踏上新的学习旅程。这个假期&#xff0c;是他们探索IT世界的黄金时期。本文将为准IT新生们提供一份全面的预习指南&#xff0c;帮助他们为未来的学习和职业生…...

HarmonyOS开发:循环渲染ForEach

需求&#xff1a; 创建多个列表组件&#xff0c;并实现点赞功能 语言&#xff1a; ArkTS 平台&#xff1a; DevEco Studio ForEach 接口描述 ForEach( arr: Array, itemGenerator: (item: Object, index: number) > void, keyGenerator?: (item: Object, index: number) &…...

构建工程化:多种不同的工程体系如何编写MakeFile

源码分析 核心MakeFile 这个 Makefile 是一个复杂的构建脚本&#xff0c;用于管理和构建一个大型项目。它包括多个目标、条件判断和递归调用 make 命令来处理多个子项目和子目录。让我们逐部分进行详细解析。 伪目标和变量定义 .PHONY: all clean install build test init.…...

聚焦从业人员疏散逃生避险意识能力提升,推动生产经营单位每年至少组织开展(疏散逃生演练,让全体从业人员熟知逃生通道、安全出口及应急处置要求,形成常态化机制。

聚焦从业人员疏散逃生避险意识能力提升&#xff0c;推动生产经营单位每年至少组织开展(疏散逃生演练&#xff0c;让全体从业人员熟知逃生通道、安全出口及应急处置要求&#xff0c;形成常态化机制。完整试题答案查看 A.三次B.两次C.一次 综合运用“四不两直”、明察暗访、 ()、…...

【手机取证】如何使用360加固助手给apk加固

文章关键词&#xff1a;手机取证、电子数据取证、数据恢复 一、前言 APP加固是对APP代码逻辑的一种保护。原理是将应用文件进行某种形式的转换&#xff0c;包括不限于隐藏&#xff0c;混淆&#xff0c;加密等操作&#xff0c;进一步保护软件的利益不受损坏&#xff0c;下面给…...

Vue的介绍

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…...

MySql数据库常用指令合集

MySql数据库常用指令合集 一、创建数据库db11.创建表 字段---表头 student_no,username,sex2.新增单条插入多条插入3.删除4.更新5.查询5.1.查询该表全部信息5.2.查询该表中username&#xff0c;并且要求名字为zhangsan性别女&#xff0c;还可以用&#xff08;or&#xff09; 6.…...

ArcGIS Pro SDK (七)编辑 13 注解

ArcGIS Pro SDK &#xff08;七&#xff09;编辑 13 注解 文章目录 ArcGIS Pro SDK &#xff08;七&#xff09;编辑 13 注解1 注释构建工具2 以编程方式启动编辑批注3 更新批注文本4 修改批注形状5 修改批注文本图形6 接地到网格 环境&#xff1a;Visual Studio 2022 .NET6 …...

模拟面试001-Java开发工程师+简历+问题+回答

模拟面试001-Java开发工程师简历问题回答 目录 模拟面试001-Java开发工程师简历问题回答面试简历面试官题问求职者回答1. 关于Java编程和技术栈2. 关于XX在线购物平台项目3. 关于XX企业资源规划系统项目4. 团队协作与项目管理5. 个人发展与职业规划 参考资料 面试简历 **个人信…...

微信小程序 ——入门介绍及简单的小程序编写

目录 一、小程序入门 1.1 什么是小程序 1.2 小程序的优点 1.3 小程序注册 1.4 安装开发工具 1.5 创建第一个小程序 二、小程序目录结构及入门案例 2.1 目录结构 2.2 入门案例 2.2.1 创建界面 2.2.2 设置标题 2.2.3 编写WXML文件 2.2.4 编写JS文件 2.2.5 编写WXSS…...

ubuntu20.04安装lio-sam

1、依赖功能包安装 sudo apt install ros-noetic-robot-state-publisher sudo apt-get install ros-noetic-robot-localization libmetis-dev 2、boost版本 boost版本查看&#xff1a;cat /usr/include/boost/version.hpp | grep "BOOST_LIB_VERSION" boost版本为1.…...

Kafka系列之Kafka知识超强总结

一、Kafka简介 Kafka是什么 Kafka是一种高吞吐量的分布式发布订阅消息系统&#xff08;消息引擎系统&#xff09;&#xff0c;它可以处理消费者在网站中的所有动作流数据。 这种动作&#xff08;网页浏览&#xff0c; 搜索和其他用户的行动&#xff09;是在现代网络上的许多社…...

第32讲:K8S集群与Cephfs文件系统集成

文章目录 1.在K8S环境下RBD与Cephfs的使用对比2.Cephfs环境介绍3.在Ceph集群中为K8S创建单独Cephfs文件系统和认证用户3.1.创建一个K8S使用的Cephfs文件系统3.2.将创建的Cephfs文件系统挂载到本地路径3.3.创建K8S连接Ceph集群使用的认证用户 4.K8S PV存储卷使用Cephfs文件系统4…...

服务器数据恢复—DS5300存储raid5阵列数据恢复案例

服务器存储数据恢复环境&#xff1a; 某单位一台某品牌DS5300存储&#xff0c;1个机头4个扩展柜&#xff0c;50块硬盘组建2组RAID5磁盘阵列&#xff08;一组raid5阵列有27块成员盘&#xff0c;存放Oracle数据库文件&#xff1b;另外一组raid5阵列有23块成员盘&#xff09;。存储…...

使用Ubuntu 22.04安装Frappe-Bench【二】

系列文章目录 第一章 使用VMware创建Ubuntu 22.04【一】 文章目录 系列文章目录前言什么是Frappe-Bench&#xff1f;使用安装ERPNext能实现什么效果&#xff1f; 官网给了一个说明 一、使用Ubuntu 22.04安装Frappe-Bench一、安装要求二、安装命令三、 可能出现问题 总结 前言 …...

MySQL增删改查

1.创建数据库&#xff1a; 使用CREATE DATABASE语句 CREATE DATABASE school;show databases; 列出MySQL数据库管理系统的数据库列表 2.切换数据库&#xff1a; 使用USE语句选择要操作的数据库 USE school&#xff1b;select database (); 当前所在库mysql> select…...

Java跳出循环的四种方式

1、continue,break,return continue&#xff1a;跳出当前层循环的当前语句&#xff0c;执行当前层循环的下一条语句。   continue标签 break&#xff1a;跳出当前层循环。 break标签&#xff1a;多层循环时&#xff0c;跳到具体某层循环。 return&#xff1a;结束所有循环…...

直播预告|飞思实验室暑期公益培训7月10日正式开启,报名从速!

01 培训背景 很荣幸地向大家宣布&#xff1a;卓翼飞思实验室将于7月10日正式开启为期两个月的暑期公益培训&#xff01;本次培训为线上直播&#xff0c;由中南大学计算机学院特聘副教授&#xff0c;RflySim平台总研发负责人戴训华副教授主讲。 培训将基于“RflySim—智能无人…...

3-2 梯度与反向传播

3-2 梯度与反向传播 主目录点这里 梯度的含义 可以看到红色区域的变化率较大&#xff0c;梯度较大&#xff1b;绿色区域的变化率较小&#xff0c;梯度较小。 在二维情况下&#xff0c;梯度向量的方向指向函数增长最快的方向&#xff0c;而其大小表示增长的速率。 梯度的计算 …...

【qt】如何获取本机的IP地址?

需要用到这个类QHostInfo和pro里面添加network模块 用这个类的静态函数forName()来获取该主机名的信息 返回的就是这个类 这个QHostInfo类就包括主机的IP地址信息 用静态函数addresses()来获取 返回的是一个QHostAddress的容器 QList<QHostAddress>addrList hostIn…...

芯片的PPA-笔记

写在前面&#xff1a;这个仅记录自己对芯片PPA的一些思考&#xff0c;不一定正确&#xff0c;还请各位网友思辨的看待&#xff0c;欢迎大家谈谈自己的想法。 1 此次笔记的起因 记录的原因&#xff1a;自己在整理这段时间的功耗总结&#xff0c;又看到工艺对功耗的影响&#x…...

2024阿里巴巴全球数学竞赛决赛中的数列题解析(分析与方程方向第4题)

早点关注我&#xff0c;精彩不错过&#xff01; 上周给大家聊了一道有LLM背景的阿里数赛题&#xff0c;详情请戳&#xff1a; 2023阿里巴巴全球数学竞赛决赛中的LLM背景题解析&#xff08;应用与计算数学部分第2题&#xff09; 看起来大家还比较喜欢看这种具体问题求解和思路分…...

学java的第3天 后端商城小程序工作

1.数据库的大坑 特殊字段名 ’我的图片表中有一个字段是描述我写成desc了&#xff0c;正好是mysql中的关键字 就不能使用了 2.后端编写 2.1可以把请求分开 在商品浏览页中 只显示商品的大致信息 当用户再点击其他按钮时在发出请求 2.2把请求合并 把数据整合到一起 利用ass…...

DevOps实战:使用GitLab+Jenkins+Kubernetes(k8s)建立CI_CD解决方案

一.系统环境 本文主要基于Kubernetes1.21.9和Linux操作系统CentOS7.4。 服务器版本docker软件版本Kubernetes(k8s)集群版本CPU架构CentOS Linux release 7.4.1708 (Core)Docker version 20.10.12v1.21.9x86_64CI/CD解决方案架构图:CI/CD解决方案架构图描述:程序员写好代码之…...