Seata客户端的启动过程 学习记录

Seata客户端的启动过程

1.自动装配4个配置类

image-20230611160505833

将在SpringBoot启动时往容器中添加4个类

1. 自动配置类 SeataAutoConfiguration

SeataAutoConfiguration将会往容器中添加两个bean

  1. failureHandler 事务处理失败执行器
  2. globalTransactionScanner

failureHandler

failureHandler 实现了 FailureHandler 接口,将在事务处理出现异常时,执行相应阶段失败时的方法,我们可以自定类实现此接口,

在失败时执行对应的方法,比如发送邮件通知。但是要注意的是,自定义的事务失败处理器的beanName必须叫failureHandler,否则GlobalTransactionScanner将不会添加到容器中。

image-20230611161812868

public interface FailureHandler {
    void onBeginFailure(GlobalTransaction tx, Throwable cause);

    void onCommitFailure(GlobalTransaction tx, Throwable cause);

    void onRollbackFailure(GlobalTransaction tx, Throwable originalException);

    void onRollbackRetrying(GlobalTransaction tx, Throwable originalException);

    void onTimeoutRollback(GlobalTransaction tx, Throwable originalException);
}

failureHandler 并且会启动周期任务,去检查事务状态是不是正常完成,否则将继续打印警告信息

 public void onBeginFailure(GlobalTransaction tx, Throwable cause) {
        LOGGER.warn("Failed to begin transaction. ", cause);
    }

    public void onCommitFailure(GlobalTransaction tx, Throwable cause) {
        LOGGER.warn("Failed to commit transaction[" + tx.getXid() + "]", cause);
        this.timer.newTimeout(new CheckTimerTask(tx, GlobalStatus.Committed), 10L, TimeUnit.SECONDS);
    }

    public void onRollbackFailure(GlobalTransaction tx, Throwable originalException) {
        LOGGER.warn("Failed to rollback transaction[" + tx.getXid() + "]", originalException);
        this.timer.newTimeout(new CheckTimerTask(tx, GlobalStatus.Rollbacked), 10L, TimeUnit.SECONDS);
    }

    public void onRollbackRetrying(GlobalTransaction tx, Throwable originalException) {
        StackTraceLogger.warn(LOGGER, originalException, "Retrying to rollback transaction[{}]", new String[]{tx.getXid()});
        this.timer.newTimeout(new CheckTimerTask(tx, GlobalStatus.RollbackRetrying), 10L, TimeUnit.SECONDS);
    }

    public void onTimeoutRollback(GlobalTransaction tx, Throwable originalException) {
        StackTraceLogger.warn(LOGGER, originalException, "Transaction timeout rollback[{}]", new String[]{tx.getXid()});
    }

globalTransactionScanner

实现了4个接口,继承了一个父类

image-20230611162354082

  1. AbstractAutoProxyCreator 创建代理类
  2. ConfigurationChangeListener 监听配置文件变更
  3. initializingBean 初始化bean执行回调方法
  4. ApplicationContextAware 获取spring容器
  5. DisposableBean 可丢弃bean

首先看initializingBean 的afterPropertiesSet()回调方法

image-20230611162628536

disableGlobalTransaction 默认是false,走 initClient方法

private void initClient() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Initializing Global Transaction Clients ... ");
        }
        if (DEFAULT_TX_GROUP_OLD.equals(txServiceGroup)) {
            LOGGER.warn("the default value of seata.tx-service-group: {} has already changed to {} since Seata 1.5, " +
                    "please change your default configuration as soon as possible " +
                    "and we don't recommend you to use default tx-service-group's value provided by seata",
                    DEFAULT_TX_GROUP_OLD, DEFAULT_TX_GROUP);
        }
        if (StringUtils.isNullOrEmpty(applicationId) || StringUtils.isNullOrEmpty(txServiceGroup)) {
            throw new IllegalArgumentException(String.format("applicationId: %s, txServiceGroup: %s", applicationId, txServiceGroup));
        }
        //init TM
        TMClient.init(applicationId, txServiceGroup, accessKey, secretKey);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Transaction Manager Client is initialized. applicationId[{}] txServiceGroup[{}]", applicationId, txServiceGroup);
        }
        //init RM
        RMClient.init(applicationId, txServiceGroup);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Resource Manager is initialized. applicationId[{}] txServiceGroup[{}]", applicationId, txServiceGroup);
        }

        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Global Transaction Clients are initialized. ");
        }
        registerSpringShutdownHook();

    }

其中完成了TM与RM的初始化

首先看TM的初始化

image-20230611163340797

创建了TmNettyRemotingClient实列,而TmNettyRemotingClient 的成员变量 instance 则是一个TmNettyRemotingClient 对象,用于客户端与服务端通信的netty客户端,消息处理器为io.seata.core.rpc.netty.AbstractNettyRemotingClient.ClientHandler,同样也是一个双向消息处理器

    @Sharable
    class ClientHandler extends ChannelDuplexHandler {

        @Override
        public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
            if (!(msg instanceof RpcMessage)) {
                return;
            }
            processMessage(ctx, (RpcMessage) msg);
        }

        @Override
        public void channelWritabilityChanged(ChannelHandlerContext ctx) {
            synchronized (lock) {
                if (ctx.channel().isWritable()) {
                    lock.notifyAll();
                }
            }
            ctx.fireChannelWritabilityChanged();
        }
        
        ......
        
        }

调用了TmNettyRemotingClient.init方法

   @Override
    public void init() {
        // registry processor
        registerProcessor();
        if (initialized.compareAndSet(false, true)) {
            super.init();
            if (io.seata.common.util.StringUtils.isNotBlank(transactionServiceGroup)) {
                getClientChannelManager().reconnect(transactionServiceGroup);
            }
        }
    }

registerProcessor() 注册消息处理器,同样封装成Pair放入map中

super.registerProcessor(MessageType.TYPE_SEATA_MERGE_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_GLOBAL_BEGIN_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_GLOBAL_COMMIT_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_GLOBAL_REPORT_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_GLOBAL_ROLLBACK_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_GLOBAL_STATUS_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_REG_CLT_RESULT, onResponseProcessor, null);
        super.registerProcessor(MessageType.TYPE_BATCH_RESULT_MSG, onResponseProcessor, null);

getClientChannelManager().reconnect(transactionServiceGroup)与服务端建立连接

void reconnect(String transactionServiceGroup) {
        List<String> availList = null;
        try {
            availList = getAvailServerList(transactionServiceGroup);
        } catch (Exception e) {
            LOGGER.error("Failed to get available servers: {}", e.getMessage(), e);
            return;
        }
        if (CollectionUtils.isEmpty(availList)) {
            RegistryService registryService = RegistryFactory.getInstance();
            String clusterName = registryService.getServiceGroup(transactionServiceGroup);

            if (StringUtils.isBlank(clusterName)) {
                LOGGER.error("can not get cluster name in registry config '{}{}', please make sure registry config correct",
                        ConfigurationKeys.SERVICE_GROUP_MAPPING_PREFIX,
                        transactionServiceGroup);
                return;
            }

            if (!(registryService instanceof FileRegistryServiceImpl)) {
                LOGGER.error("no available service found in cluster '{}', please make sure registry config correct and keep your seata server running", clusterName);
            }
            return;
        }
        Set<String> channelAddress = new HashSet<>(availList.size());
        try {
            for (String serverAddress : availList) {
                try {
                    acquireChannel(serverAddress);
                    channelAddress.add(serverAddress);
                } catch (Exception e) {
                    LOGGER.error("{} can not connect to {} cause:{}", FrameworkErrorCode.NetConnect.getErrCode(),
                        serverAddress, e.getMessage(), e);
                }
            }
        } finally {
            if (CollectionUtils.isNotEmpty(channelAddress)) {
                List<InetSocketAddress> aliveAddress = new ArrayList<>(channelAddress.size());
                for (String address : channelAddress) {
                    String[] array = address.split(":");
                    aliveAddress.add(new InetSocketAddress(array[0], Integer.parseInt(array[1])));
                }
                RegistryFactory.getInstance().refreshAliveLookup(transactionServiceGroup, aliveAddress);
            } else {
                RegistryFactory.getInstance().refreshAliveLookup(transactionServiceGroup, Collections.emptyList());
            }
        }
    }

seata服务收到TM注册消息后,会调用io.seata.core.rpc.processor.server.RegTmProcessor#onRegTmMessage,并给客户端返回注册成功的消息

    private void onRegTmMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) {
        RegisterTMRequest message = (RegisterTMRequest) rpcMessage.getBody();
        String ipAndPort = NetUtil.toStringAddress(ctx.channel().remoteAddress());
        Version.putChannelVersion(ctx.channel(), message.getVersion());
        boolean isSuccess = false;
        String errorInfo = StringUtils.EMPTY;
        try {
            if (null == checkAuthHandler || checkAuthHandler.regTransactionManagerCheckAuth(message)) {
                ChannelManager.registerTMChannel(message, ctx.channel());
                Version.putChannelVersion(ctx.channel(), message.getVersion());
                isSuccess = true;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("TM checkAuth for client:{},vgroup:{},applicationId:{} is OK",
                        ipAndPort, message.getTransactionServiceGroup(), message.getApplicationId());
                }
            } else {
                if (LOGGER.isWarnEnabled()) {
                    LOGGER.warn("TM checkAuth for client:{},vgroup:{},applicationId:{} is FAIL",
                            ipAndPort, message.getTransactionServiceGroup(), message.getApplicationId());
                }
            }
        } catch (Exception exx) {
            isSuccess = false;
            errorInfo = exx.getMessage();
            LOGGER.error("TM register fail, error message:{}", errorInfo);
        }
        RegisterTMResponse response = new RegisterTMResponse(isSuccess);
        if (StringUtils.isNotEmpty(errorInfo)) {
            response.setMsg(errorInfo);
        }
        remotingServer.sendAsyncResponse(rpcMessage, ctx.channel(), response);
        if (isSuccess && LOGGER.isInfoEnabled()) {
            LOGGER.info("TM register success,message:{},channel:{},client version:{}", message, ctx.channel(),
                message.getVersion());
        }
    }

RM的初始化

    public static void init(String applicationId, String transactionServiceGroup) {
        //实例化RM的netty客户端
        RmNettyRemotingClient rmNettyRemotingClient = RmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup);
        //设置RM资源管理
        rmNettyRemotingClient.setResourceManager(DefaultResourceManager.get());
        //设置RM事务管理器
        rmNettyRemotingClient.setTransactionMessageHandler(DefaultRMHandler.get());
        rmNettyRemotingClient.init();
    }

ResourceManager 用于管理数据库资源

TransactionMessageHandler用来处理RM事务消息

rmNettyRemotingClient.init()方法中注册消息处理器,需要注意的是,此时并不会与seata服务器建立连接,因为此时还比没有管理数据库资源

    public void init() {
        // registry processor
        registerProcessor();
        if (initialized.compareAndSet(false, true)) {
            super.init();

            // Found one or more resources that were registered before initialization
            if (resourceManager != null
                    && !resourceManager.getManagedResources().isEmpty()
                    && StringUtils.isNotBlank(transactionServiceGroup)) {
                getClientChannelManager().reconnect(transactionServiceGroup);
            }
        }
    }

遍历四种模式下的资源管理器,获取其所管理的资源(此时全为空),因此并不会与seata服务端在此时建立连接。 RM与TC建立连接可以看后面创建代理数据源的过程。RM是用来管理代理数据源,当没有数据源的时候,也没必要建立连接,真正向TC注册RM的时机在初始化代理数据源后。image-20230611173320067

seata服务端io.seata.core.rpc.processor.server.RegRmProcessor#onRegRmMessage 返回RM注册成功消息

    private void onRegRmMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) {
        RegisterRMRequest message = (RegisterRMRequest) rpcMessage.getBody();
        String ipAndPort = NetUtil.toStringAddress(ctx.channel().remoteAddress());
        boolean isSuccess = false;
        String errorInfo = StringUtils.EMPTY;
        try {
            if (null == checkAuthHandler || checkAuthHandler.regResourceManagerCheckAuth(message)) {
                ChannelManager.registerRMChannel(message, ctx.channel());
                Version.putChannelVersion(ctx.channel(), message.getVersion());
                isSuccess = true;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("RM checkAuth for client:{},vgroup:{},applicationId:{} is OK", ipAndPort, message.getTransactionServiceGroup(), message.getApplicationId());
                }
            } else {
                if (LOGGER.isWarnEnabled()) {
                    LOGGER.warn("RM checkAuth for client:{},vgroup:{},applicationId:{} is FAIL", ipAndPort, message.getTransactionServiceGroup(), message.getApplicationId());
                }
            }
        } catch (Exception exx) {
            isSuccess = false;
            errorInfo = exx.getMessage();
            LOGGER.error("RM register fail, error message:{}", errorInfo);
        }
        RegisterRMResponse response = new RegisterRMResponse(isSuccess);
        if (StringUtils.isNotEmpty(errorInfo)) {
            response.setMsg(errorInfo);
        }
        remotingServer.sendAsyncResponse(rpcMessage, ctx.channel(), response);
        if (isSuccess && LOGGER.isInfoEnabled()) {
            LOGGER.info("RM register success,message:{},channel:{},client version:{}", message, ctx.channel(),
                message.getVersion());
        }
    }

2.自动配置类 SeataDataSourceAutoConfiguration

此自动配置类只会往容器中添加一个Bean :SeataAutoDataSourceProxyCreator,来自动创建数据源代理对象

   @Bean({"seataAutoDataSourceProxyCreator"})
    @ConditionalOnMissingBean({SeataAutoDataSourceProxyCreator.class})
    public static SeataAutoDataSourceProxyCreator seataAutoDataSourceProxyCreator(SeataProperties seataProperties) {
        return new SeataAutoDataSourceProxyCreator(seataProperties.isUseJdkProxy(), seataProperties.getExcludesForAutoProxying(), seataProperties.getDataSourceProxyMode());
    }

SeataDataSourceAutoConfiguration继承org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator 抽象类,并且重写了父类的wrapIfNecessary方法

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
				//判断当前bean是否是数据源类,不是不需要代理,直接返回
        if (!(bean instanceof DataSource)) {
            return bean;
        } else {
            DataSource origin;
            //判断是否是数据源代理类,如果是,则不需要er 
            if (!(bean instanceof SeataDataSourceProxy)) {
            		//获取缓存代理
                Object enhancer = super.wrapIfNecessary(bean, beanName, cacheKey);
                //如果当前bean就是缓存代理,直接返回
                if (bean == enhancer) {
                    return bean;
                } else {
                		//创建代理数据源
                    origin = (DataSource)bean;
                    SeataDataSourceProxy proxy = this.buildProxy(origin, this.dataSourceProxyMode);
                    DataSourceProxyHolder.put(origin, proxy);
                    return enhancer;
                }
            } else {
                LOGGER.warn("Manually register SeataDataSourceProxy(or its subclass) bean is discouraged! bean name: {}", beanName);
                SeataDataSourceProxy proxy = (SeataDataSourceProxy)bean;
                origin = proxy.getTargetDataSource();
                Object originEnhancer = super.wrapIfNecessary(origin, beanName, cacheKey);
                if (origin == originEnhancer) {
                    return origin;
                } else {
                    DataSourceProxyHolder.put(origin, proxy);
                    return originEnhancer;
                }
            }
        }
    }

调用buildProxy方法将当前数据源封装到DataSourceProxy对象中

    SeataDataSourceProxy buildProxy(DataSource origin, String proxyMode) {
        if (BranchType.AT.name().equalsIgnoreCase(proxyMode)) {
            return new DataSourceProxy(origin);
        } else if (BranchType.XA.name().equalsIgnoreCase(proxyMode)) {
            return new DataSourceProxyXA(origin);
        } else {
            throw new IllegalArgumentException("Unknown dataSourceProxyMode: " + proxyMode);
        }
    }

并且跟进 构造,找到init方法

 private void init(DataSource dataSource, String resourceGroupId) {
        this.resourceGroupId = resourceGroupId;

        try {
            Connection connection = dataSource.getConnection();
            Throwable var4 = null;

            try {
                this.jdbcUrl = connection.getMetaData().getURL();
                this.dbType = JdbcUtils.getDbType(this.jdbcUrl);
                if ("oracle".equals(this.dbType)) {
                    this.userName = connection.getMetaData().getUserName();
                } else if ("mariadb".equals(this.dbType)) {
                    this.dbType = "mysql";
                }

                this.version = this.selectDbVersion(connection);
            } catch (Throwable var14) {
                var4 = var14;
                throw var14;
            } finally {
                if (connection != null) {
                    if (var4 != null) {
                        try {
                            connection.close();
                        } catch (Throwable var13) {
                            var4.addSuppressed(var13);
                        }
                    } else {
                        connection.close();
                    }
                }

            }
        } catch (SQLException var16) {
            throw new IllegalStateException("can not init dataSource", var16);
        }

        this.initResourceId();
        //向TC注册RM
        DefaultResourceManager.get().registerResource(this);
        if (ENABLE_TABLE_META_CHECKER_ENABLE) {
            this.tableMetaExecutor.scheduleAtFixedRate(() -> {
                try {
                    Connection connection = dataSource.getConnection();
                    Throwable var3 = null;

                    try {
                        TableMetaCacheFactory.getTableMetaCache(this.getDbType()).refresh(connection, this.getResourceId());
                    } catch (Throwable var13) {
                        var3 = var13;
                        throw var13;
                    } finally {
                        if (connection != null) {
                            if (var3 != null) {
                                try {
                                    connection.close();
                                } catch (Throwable var12) {
                                    var3.addSuppressed(var12);
                                }
                            } else {
                                connection.close();
                            }
                        }

                    }
                } catch (Exception var15) {
                }

            }, 0L, TABLE_META_CHECKER_INTERVAL, TimeUnit.MILLISECONDS);
        }

        RootContext.setDefaultBranchType(this.getBranchType());
    }

可以看到此时完成了RM向TC的注册

3.自动配置类 SeataHttpAutoConfiguration

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication
@ConditionalOnMissingBean({SeataWebMvcConfigurer.class})
@ConditionalOnProperty(
    prefix = "seata.client.http",
    name = {"interceptor-enabled"},
    havingValue = "true",
    matchIfMissing = true
)
@AutoConfigureOrder(Integer.MAX_VALUE)
public class SeataHttpAutoConfiguration {
    public SeataHttpAutoConfiguration() {
    }

    @Bean
    @ConditionalOnClass(
        name = {"jakarta.servlet.http.HttpServletRequest"}
    )
    public JakartaSeataWebMvcConfigurer jakartaSeataWebMvcConfigurer() {
        return new JakartaSeataWebMvcConfigurer();
    }

    @Bean
    @ConditionalOnMissingBean({JakartaSeataWebMvcConfigurer.class})
    public SeataWebMvcConfigurer seataWebMvcConfigurer() {
        return new SeataWebMvcConfigurer();
    }
}
public class SeataWebMvcConfigurer implements WebMvcConfigurerAdapter {
    public SeataWebMvcConfigurer() {
    }

    public void addInterceptors(InterceptorRegistry registry) {
    		//添加拦截器
        registry.addInterceptor(new TransactionPropagationInterceptor());
    }
}

主要是为了在传递xid

image-20230611203840479

4.自动配置类SeataSagaAutoConfiguration

SeataSagaAutoConfiguration主要是为了出来saga模式下的分布式事务,基本不用跳过。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/28384.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DBA 抓包神器 tshark 测评

想窥探神秘的网络世界的奥秘&#xff0c;tshark 助你一臂之力&#xff01; 作者&#xff1a;赵黎明 爱可生 MySQL DBA 团队成员&#xff0c;熟悉 Oracle、MySQL 等数据库&#xff0c;擅长数据库性能问题诊断、事务与锁问题的分析等&#xff0c;负责处理客户 MySQL 及我司自研 D…

计算机组成原理(六)指令系统

一、指令的基本格式 1.1机器指令的相关概念 指令集(Instruction Set) 某机器所有机器指令的集合 *定长指令集 指令集中的所有指令长度均相同!取指令控制简单*不定长指令集 指令集中的所有指令长度有长、有短 操作码 (1)长度固定 用于指令字长较长的情况RISC 如IBM370操作码8位…

2023 年前端 Web 发展趋势

虽然就个人观点&#xff0c;我觉得 Web 开发在最近几年都没什么进展&#xff08;2016 年至 2021 年&#xff09;&#xff0c;但在刚刚过去的 2022 年中确实又出现了一些新的技术。在本文中&#xff0c;我想跟大家聊聊自己看到的最新 Web 开发的发展趋势。相信这波浪潮会继续激发…

RHCE shell 作业一

1. 设置邮箱 [rootserver ~]# yum install s-nail -y [rootserver ~]# vim /etc/s-nail.rc 编写脚本 [rootserver ~]# vim homework1.sh 设置定时任务 [rootserver ~]# vim /etc/crontab 2. [rootserver ~]# vim homework2.sh 测试&#xff1a; 3. [rootserve…

一文让你了解appium自动化的工作原理

目录 前言&#xff1a; 一、Appium加载的过程图解 二、初步认识appium工作过程 三、bootstrap介绍 四、所使用的技术 五、Capabilities 六、自我理解的工作原理 前言&#xff1a; Appium是一个流行的开源自动化测试框架&#xff0c;支持移动应用程序的自动化测试。 一…

进程管道:父进程和子进程

在接下来的对pipe调用的研究中&#xff0c;我们将学习如何在子进程中运行一个与其父进程完全不同的另外一个程序&#xff0c;而不是仅仅运行一个相同程序。我们用exec调用来完成这一工作。这里的一个难点是&#xff0c;通过exec调用的进程需要知道应该访问哪个文件描述符。在前…

MMPretrain

title: mmpretrain实战 date: 2023-06-07 16:04:01 tags: [image classification,mmlab] mmpretrain实战 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ccTl9bOl-1686129437336)(null)] 主要讲解了安装,还有使用教程.安装教程直接参考官网.下面讲…

pikachu靶场-../../(目录遍历)

目录遍历, 也叫路径遍历, 由于web服务器或者web应用程序对用户输入的文件名称的安全性验证不足而导致的一种安全漏洞&#xff0c;使得攻击者通过利用一些特殊字符就可以绕过服务器的安全限制&#xff0c;访问任意的文件 (可以是web根目录以外的文件&#xff09;&#xff0c;甚至…

pytorch深度学习框架—torch.nn模块(二)

pytorch深度学习框架—torch.nn模块&#xff08;二&#xff09; 激活函数 pytorch中提供了十几种激活函数&#xff0c;常见的激活函数通常为S形激活函数&#xff08;Sigmoid&#xff09;双曲正切激活函数(Tanh) 和线性修正单元&#xff08;ReLu&#xff09;激活函数等 层对应的…

Linux笔记

版本用的是CentOS7最min版 安装JDK&#xff1a;安装上传工具包&#xff1a;自动安装 yum install lrzsz -y 上传本地文件&#xff1a; rz -be 解压jdk&#xff1a; tar -zxvf jdk-8u371-linux-x64.tar.gz -z 用gzip来压缩/解压缩文件&#xff0c;加上该选项后可以将档案…

关于 vue2 后台管理系统构建 vue2+mock.js 的经典案例

一&#xff0c;初识 Mock.js 1.什么是 mock.js: 主要是模拟数据生成器&#xff0c;可以生成随机数据&#xff0c;拦截器 Ajax 请求 2.为什么要使用 mock.js 由于很多学生在学习过程中&#xff0c;后端还没有做好接口&#xff0c;写好接口文档&#xff0c;有了mock.js 前端就…

如何识别二叉树的“亲戚”?——探秘判断子树的奥妙

本篇博客会讲解力扣“572. 另一棵树的子树”的解题思路&#xff0c;这是题目链接。先来审题&#xff1a; 本题的思路是&#xff1a;使用递归&#xff0c;把大问题化作小问题。 先来思考&#xff1a;如何判断q是不是p的子树呢&#xff1f; q是p的子树有3种情况&#xff0c;分别…

MyBatis操作数据库(查询功能)

目录 一、MyBatis的概念 二、配置MyBits环境 三、 MyBatis连接数据库查询操作&#xff08;示例&#xff09; 创建MySQL数据库表 配置MyBatis 配置连接数据库和MyBatis xml文件 ​编辑 四、添加业务代码 实体类entity 数据持久层mapper 创建接口类 创建xml文件 服务层…

Spring Security--会话管理

就像登录qq一样&#xff0c;一个手机登录会将另外一个手机挤下线&#xff0c;这个就叫会话管理。 这个东西非常简单&#xff0c;在默认情况下可以登录n多次&#xff0c;一旦开启&#xff0c;就不允许登录多个。 什么是一个会话。 我们简单理解就是一个浏览器的同一个用户算一…

汉明码(Hamming Code)底层原理

汉明码&#xff08;Hamming Code&#xff09;底层原理 3Blue1Brown&#xff1a;Hamming Code【Part1】 3Blue1Brown&#xff1a;Hamming Code【Part2】 Hamming Code如何检查错误和定位错误&#xff1f; 检查错误通过奇校验或偶校验确定是否发生错误 定位错误通过依次对行和列…

将递归函数转成非递归函数的通用方法

看到过一道非常不错的面试题&#xff1a;不支持递归的程序语言如何实现递归程序&#xff1f; 之所以说这道题好&#xff0c;是因为&#xff1a; 首先&#xff0c;它不是纯粹考概念和死记硬背&#xff0c;求职者在回答问题之前需要进行一定的思考&#xff1b; 其次&#xff0c…

Debezium系列之:记录一次生产环境SQLServer数据库删除日志文件造成debezium connector数据不采集的解决方法

Debezium系列之:记录一次生产环境SQLServer数据库删除日志文件造成debezium connector数据不采集的解决方法 一、背景二、快速定位问题三、详细的解决步骤四、确认debezium connector恢复对数据库的数据采集五、经验总结一、背景 SQLServer数据库的日志把磁盘打满了,需要删除…

JAVA 实现 Redis 发布订阅

Redis 发布订阅 发布订阅&#xff1a;消息发布者发布消息 和 消息订阅者接收消息&#xff0c;两者之间通过某种媒介联系起来 例如订杂志&#xff0c;当自己订阅了爱格杂志&#xff0c;每个月会发刊一本。到发布的时候派送员将杂志送到自己手上就能看到杂志内容。只有我们订阅了…

C语言之结构体讲解

目录 结构体类型的声明 结构体初始化 结构体成员访问 结构体传参 对于上期指针初阶&#xff08;2&#xff09;我们后期还会讲数组指针是什么&#xff1f;大家可以先思考一下&#xff0c;后期我们会讲 1.结构体的声明 结构是一些值的集合&#xff0c;这些值被称为成员变量&am…

第二类曲线积分

文章目录 第二类曲线积分一、向量场是什么&#xff1f;二、向量场可视化三、计算1. 计算方式一2. 计算方式二 第二类曲线积分 因为之前学习第二类曲线的时候&#xff0c;不是很理解&#xff1b;所以最近看了mit的多元微积分课程&#xff0c;做一些课程笔记。 一、向量场是什么…