Seata源码学习(四)-数据源代理

news2025/7/19 5:43:39

Seata源码分析-数据源代理

上节课我们分析了整体的Seata-AT模式的2PC执行流程,那么这节课我们要分析的就是在AT模式中的关键点,数据源代理

AT模式的核心点:

  1. 获取全局锁、开启全局事务
  2. 解析SQL并写入undolog

那么上节课其实我们已经把第一步分析清楚了,那么这节课我们就要分析的是AT模式如何解析SQL并写入undolog,首先我们要先明确实际上Seata其中采用了数据源代理的模式。

那么这个就需要我们在回顾一下GlobalTransactionScanner这个类型,在这个类型中继承了一些的接口和抽象类,比较关键的几个:

  • AbstractAutoProxyCreator
  • InitializingBean
  • ApplicationContextAware
  • DisposableBean

这里给大家回顾一下:

  1. 继承ApplicationContextAware类型以后,需要实现对应的方法:

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException

    当spring启动完成后,会自动调用这个类型,把ApplicationContext给bean。也就是说,GlobalTransactionScanner天然能拿到Spring的环境。

  2. 继承了InitializingBean接口,需要实现一个方法:

    void afterPropertiesSet() throws Exception;

    凡是继承该接口的类,在初始化bean的时候,当所有properties都设置完成后,会执行该方法。

  3. 继承DisposableBean,需要实现一个方法:

    void destroy() throws Exception;

    和InitializingBean接口相反,这个是在销毁的时候会调用这个方法。

  4. AbstractAutoProxyCreator就比较复杂了,它Spring实现AOP的一种方式。本质上是一个BeanPostProcessor,他在bean初始化之前,调用内部的createProxy方法,创建一个bean的AOP代理bean并返回,对Bean的增强。

总结一下:总体的逻辑就是,GlobalTransactionScanner扫描有注解的bean,做AOP增强。

数据源代理

关于数据源代理这里我们全局事务拦截成功后最终还是执行了业务方法的,但是由于Seata对数据源做了代理,所以sql解析与undolog入库操作是在数据源代理中执行的,箭头处的代理就是Seata对DataSource,Connection,Statement做的代理封装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MJdbp5jy-1676380176847)(image-20220226142501746.png)]

数据源代理是非常重要的一个环节。我们知道,在分布式事务运行过程中,undo log等的记录、资源的锁定等,都是用户无感知的,因为这些操作都在数据源的代理中完成了。

数据源代理DataSourceProxy

DataSourceProxy的主要功能为,它在构造方法中调用了一个自定义的init方法,主要做了以下能力的增强:

  1. 为每个数据源标识了资源组ID

  2. 如果配置打开,会有一个定时线程池定时更新表的元数据信息并缓存到本地

  3. 生成代理连接ConnectionProxy

那我们先来看init方法

private void init(DataSource dataSource, String resourceGroupId) {
    //资源组ID,默认是“default”这个默认值
    this.resourceGroupId = resourceGroupId;
    try (Connection connection = dataSource.getConnection()) {
        //根据原始数据源得到JDBC连接和数据库类型
        jdbcUrl = connection.getMetaData().getURL();
        dbType = JdbcUtils.getDbType(jdbcUrl);
        if (JdbcConstants.ORACLE.equals(dbType)) {
            userName = connection.getMetaData().getUserName();
        }
    } catch (SQLException e) {
        throw new IllegalStateException("can not init dataSource", e);
    }
    DefaultResourceManager.get().registerResource(this);
    if (ENABLE_TABLE_META_CHECKER_ENABLE) {
        //如果配置开关打开,会定时线程池不断更新表的元数据信息
        /**
        *每分钟查询一次数据源的表结构信息并缓存,在需要查询数据库结构时会用到,不然每次去数据库查询结构效率会很低。
        */
        tableMetaExcutor.scheduleAtFixedRate(() -> {
            try (Connection connection = dataSource.getConnection()) {
                TableMetaCacheFactory.getTableMetaCache(DataSourceProxy.this.getDbType())
                    .refresh(connection, DataSourceProxy.this.getResourceId());
            } catch (Exception ignore) {
            }
        }, 0, TABLE_META_CHECKER_INTERVAL, TimeUnit.MILLISECONDS);
    }

    //Set the default branch type to 'AT' in the RootContext.
    RootContext.setDefaultBranchType(this.getBranchType());
}

这3个增强里面,前两个都比较容易理解,第三是最重要的。我们知道在AT模式里面,会自动记录undo log、资源锁定等等,都是通过ConnectionProxy完成的。

另外,DataSourceProxy重写了几个方法。

重点是getConnection,此时会返回一个ConnectionProxy,而不是原生的Connection

@Override
public ConnectionProxy getConnection() throws SQLException {
    Connection targetConnection = targetDataSource.getConnection();
    return new ConnectionProxy(this, targetConnection);
}

@Override
public ConnectionProxy getConnection(String username, String password) throws SQLException {
    Connection targetConnection = targetDataSource.getConnection(username, password);
    return new ConnectionProxy(this, targetConnection);
}

ConnectionProxy分析

ConnectionProxy继承了AbstractConnectionProxy。一看到Abstract,就知道它的父类封装了很多通用工作。它的父类里面还使用了PreparedStatementProxy、StatementProxy、DataSourceProxy。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0bWlEDSg-1676380176848)(image-20220226172114629.png)]

我们先来分析AbstractConnectionProxy

AbstractConnectionProxy

在这个抽象连接对象中,定义了很多通用的逻辑,所以在这其中我们要关注的主要在于PreparedStatementProxy和StatementProxy,其实这里的通用逻辑就是数据源连接的步骤,获取连接,创建执行对象等等这些

@Override
public Statement createStatement() throws SQLException {
    //调用真实连接对象获得Statement对象
    Statement targetStatement = getTargetConnection().createStatement();
    //创建Statement的代理
    return new StatementProxy(this, targetStatement);
}

@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
    //数据库类型,比如mysql、oracle等
    String dbType = getDbType();
    // support oracle 10.2+
    PreparedStatement targetPreparedStatement = null;
    //如果是AT模式且开启全局事务,那么就会进入if分支
    if (BranchType.AT == RootContext.getBranchType()) {
        List<SQLRecognizer> sqlRecognizers = SQLVisitorFactory.get(sql, dbType);
        if (sqlRecognizers != null && sqlRecognizers.size() == 1) {
            SQLRecognizer sqlRecognizer = sqlRecognizers.get(0);
            if (sqlRecognizer != null && sqlRecognizer.getSQLType() == SQLType.INSERT) {
                //得到表的元数据
                TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(dbType).getTableMeta(getTargetConnection(),
                                                                                                   sqlRecognizer.getTableName(), getDataSourceProxy().getResourceId());
                //得到表的主键列名
                String[] pkNameArray = new String[tableMeta.getPrimaryKeyOnlyName().size()];
                tableMeta.getPrimaryKeyOnlyName().toArray(pkNameArray);
                targetPreparedStatement = getTargetConnection().prepareStatement(sql,pkNameArray);
            }
        }
    }
    if (targetPreparedStatement == null) {
        targetPreparedStatement = getTargetConnection().prepareStatement(sql);
    }
    // 创建PreparedStatementProxy代理
    return new PreparedStatementProxy(this, targetPreparedStatement, sql);
}

分布式事务SQL执行

在这两个代理对象中,执行SQL语句的关键方法如下:

@Override
public ResultSet executeQuery(String sql) throws SQLException {
    this.targetSQL = sql;
    return ExecuteTemplate.execute(this, (statement, args) -> statement.executeQuery((String) args[0]), sql);
}

@Override
public int executeUpdate(String sql) throws SQLException {
    this.targetSQL = sql;
    return ExecuteTemplate.execute(this, (statement, args) -> statement.executeUpdate((String) args[0]), sql);
}

@Override
public boolean execute(String sql) throws SQLException {
    this.targetSQL = sql;
    return ExecuteTemplate.execute(this, (statement, args) -> statement.execute((String) args[0]), sql);
}

其他执行SQL语句的方法与上面三个方法都是类似的,都是调用ExecuteTemplate.execute方法,下面来看一下ExecuteTemplate类:

/**
 * The type Execute template.
 *
 * @author sharajava
 */
public class ExecuteTemplate {

    /**
     * Execute t.
     *
     * @param <T>               the type parameter
     * @param <S>               the type parameter
     * @param statementProxy    the statement proxy
     * @param statementCallback the statement callback
     * @param args              the args
     * @return the t
     * @throws SQLException the sql exception
     */
    public static <T, S extends Statement> T execute(StatementProxy<S> statementProxy,
                                                     StatementCallback<T, S> statementCallback,
                                                     Object... args) throws SQLException {
        return execute(null, statementProxy, statementCallback, args);
    }

    /**
     * Execute t.
     *
     * @param <T>               the type parameter
     * @param <S>               the type parameter
     * @param sqlRecognizers    the sql recognizer list
     * @param statementProxy    the statement proxy
     * @param statementCallback the statement callback
     * @param args              the args
     * @return the t
     * @throws SQLException the sql exception
     */
    public static <T, S extends Statement> T execute(List<SQLRecognizer> sqlRecognizers,
                                                     StatementProxy<S> statementProxy,
                                                     StatementCallback<T, S> statementCallback,
                                                     Object... args) throws SQLException {
        // 如果没有全局锁,并且不是AT模式,直接执行SQL
        if (!RootContext.requireGlobalLock() && BranchType.AT != RootContext.getBranchType()) {
            // Just work as original statement
            return statementCallback.execute(statementProxy.getTargetStatement(), args);
        }

        // 得到数据库类型 ->MySQL
        String dbType = statementProxy.getConnectionProxy().getDbType();
        if (CollectionUtils.isEmpty(sqlRecognizers)) {
            //sqlRecognizers为SQL语句的解析器,获取执行的SQL,通过它可以获得SQL语句表名、相关的列名、类型的等信息,最后解析出对应的SQL表达式
            sqlRecognizers = SQLVisitorFactory.get(
                    statementProxy.getTargetSQL(),
                    dbType);
        }
        Executor<T> executor;
        if (CollectionUtils.isEmpty(sqlRecognizers)) {
            //如果seata没有找到合适的SQL语句解析器,那么便创建简单执行器PlainExecutor,
            //PlainExecutor直接使用原生的Statement对象执行SQL
            executor = new PlainExecutor<>(statementProxy, statementCallback);
        } else {
            if (sqlRecognizers.size() == 1) {
                SQLRecognizer sqlRecognizer = sqlRecognizers.get(0);
                switch (sqlRecognizer.getSQLType()) {
                    //下面根据是增、删、改、加锁查询、普通查询分别创建对应的处理器
                    case INSERT:
                        executor = EnhancedServiceLoader.load(InsertExecutor.class, dbType,
                                new Class[]{StatementProxy.class, StatementCallback.class, SQLRecognizer.class},
                                new Object[]{statementProxy, statementCallback, sqlRecognizer});
                        break;
                    case UPDATE:
                        executor = new UpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        break;
                    case DELETE:
                        executor = new DeleteExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        break;
                    case SELECT_FOR_UPDATE:
                        executor = new SelectForUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        break;
                    default:
                        executor = new PlainExecutor<>(statementProxy, statementCallback);
                        break;
                }
            } else {
                // 此执行器可以处理一条SQL语句包含多个Delete、Update语句
                executor = new MultiExecutor<>(statementProxy, statementCallback, sqlRecognizers);
            }
        }
        T rs;
        try {
            // 执行器执行
            rs = executor.execute(args);
        } catch (Throwable ex) {
            if (!(ex instanceof SQLException)) {
                // Turn other exception into SQLException
                ex = new SQLException(ex);
            }
            throw (SQLException) ex;
        }
        return rs;
    }

}

从ExecuteTemplate中可以看到,seata将SQL语句的执行委托给了不同的执行器。seata提供了6个执行器(模板模式),所有执行器的父类型为AbstractDMLBaseExecutor。

  • UpdateExecutor 执行update语句
  • InsertExecutor 执行insert语句
  • DeleteExecutor 执行delete语句
  • SelectForUpdateExecutor 执行select for update语句
  • PlainExecutor 执行普通查询语句
  • MultiExecutor 复合执行器,在一条SQL语句中执行多条语句

关系结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bRe4GR2s-1676380176848)(image-20220314203616110.png)]

那我们继续向下看,这里我们要分析的就是executor.execute(args);方法,自然这里调用的就是父类的方法

@Override
public T execute(Object... args) throws Throwable {
    String xid = RootContext.getXID();
    if (xid != null) {
        // 获取xid
        statementProxy.getConnectionProxy().bind(xid);
    }
    // 设置全局锁
    statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
    return doExecute(args);
}

向下来看doExecute()方法,AbstractDMLBaseExecutor重写的方法

@Override
public T doExecute(Object... args) throws Throwable {
    AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
    if (connectionProxy.getAutoCommit()) {
        return executeAutoCommitTrue(args);
    } else {
        return executeAutoCommitFalse(args);
    }
}

首先我们都清楚,数据库本身都是自动提交

@Override
public T doExecute(Object... args) throws Throwable {
    AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
    if (connectionProxy.getAutoCommit()) {
        return executeAutoCommitTrue(args);
    } else {
        return executeAutoCommitFalse(args);
    }
}

进入executeAutoCommitTrue()方法中

protected T executeAutoCommitTrue(Object[] args) throws Throwable {
    ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
    try {
        // 更改为手动提交
        connectionProxy.changeAutoCommit();
        return new LockRetryPolicy(connectionProxy).execute(() -> {
            // 调用手动提交方法 得到分支业务最终结果
            T result = executeAutoCommitFalse(args);
            // 执行提交
            connectionProxy.commit();
            return result;
        });
    } catch (Exception e) {
        // when exception occur in finally,this exception will lost, so just print it here
        LOGGER.error("execute executeAutoCommitTrue error:{}", e.getMessage(), e);
        if (!LockRetryPolicy.isLockRetryPolicyBranchRollbackOnConflict()) {
            connectionProxy.getTargetConnection().rollback();
        }
        throw e;
    } finally {
        connectionProxy.getContext().reset();
        connectionProxy.setAutoCommit(true);
    }
}

然后我们查看connectionProxy.changeAutoCommit();更改为手动提交

protected T executeAutoCommitFalse(Object[] args) throws Exception {
    if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && isMultiPk()) {
        throw new NotSupportYetException("multi pk only support mysql!");
    }
    // 前镜像
    TableRecords beforeImage = beforeImage();
    // 执行具体业余
    T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
    // 后镜像
    TableRecords afterImage = afterImage(beforeImage);
    // 暂存UndoLog,为了在Commit的时候保存到数据库
    prepareUndoLog(beforeImage, afterImage);
    return result;
}

然后我们再回到executeAutoCommitTrue这个方法中向下看connectionProxy.commit();

@Override
public void commit() throws SQLException {
    try {
        LOCK_RETRY_POLICY.execute(() -> {
            // 具体执行
            doCommit();
            return null;
        });
    } catch (SQLException e) {
        if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
            rollback();
        }
        throw e;
    } catch (Exception e) {
        throw new SQLException(e);
    }
}

进入到doCommit方法中

private void doCommit() throws SQLException {
    //判断是否存在全局事务
    if (context.inGlobalTransaction()) {
        processGlobalTransactionCommit();
    } else if (context.isGlobalLockRequire()) {
        processLocalCommitWithGlobalLocks();
    } else {
        targetConnection.commit();
    }
}

此时很明显我们存在全局事务,所以进入到processGlobalTransactionCommit方法中

private void processGlobalTransactionCommit() throws SQLException {
    try {
        // 注册分支
        register();
    } catch (TransactionException e) {
        recognizeLockKeyConflictException(e, context.buildLockKeys());
    }
    try {
        //写入数据库undolog
        UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
        //执行原生提交
        targetConnection.commit();
    } catch (Throwable ex) {
        LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
        report(false);
        throw new SQLException(ex);
    }
    if (IS_REPORT_SUCCESS_ENABLE) {
        report(true);
    }
    context.reset();
}

其中的register方法就是注册分支事务的方法,同时还有把undolog写入数据库和执行提交的操作

// 注册分支事务,生成分支事务id
private void register() throws TransactionException {
    if (!context.hasUndoLog() || !context.hasLockKey()) {
        return;
    }
    // 注册分支事务
    Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
                                                                null, context.getXid(), null, context.buildLockKeys());
    context.setBranchId(branchId);
}

接下来我们就具体看看写入数据库的方法flushUndoLogs

@Override
public void flushUndoLogs(ConnectionProxy cp) throws SQLException {
    ConnectionContext connectionContext = cp.getContext();
    if (!connectionContext.hasUndoLog()) {
        return;
    }

    String xid = connectionContext.getXid();
    long branchId = connectionContext.getBranchId();

    BranchUndoLog branchUndoLog = new BranchUndoLog();
    branchUndoLog.setXid(xid);
    branchUndoLog.setBranchId(branchId);
    branchUndoLog.setSqlUndoLogs(connectionContext.getUndoItems());

    UndoLogParser parser = UndoLogParserFactory.getInstance();
    byte[] undoLogContent = parser.encode(branchUndoLog);

    CompressorType compressorType = CompressorType.NONE;
    if (needCompress(undoLogContent)) {
        compressorType = ROLLBACK_INFO_COMPRESS_TYPE;
        undoLogContent = CompressorFactory.getCompressor(compressorType.getCode()).compress(undoLogContent);
    }

    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Flushing UNDO LOG: {}", new String(undoLogContent, Constants.DEFAULT_CHARSET));
    }
    // 写入数据库具体位置
    insertUndoLogWithNormal(xid, branchId, buildContext(parser.getName(), compressorType), undoLogContent, cp.getTargetConnection());
}

具体写入方法,此时我们使用的是MySql,所以执行的是MySql实现类

@Override
protected void insertUndoLogWithNormal(String xid, long branchId, String rollbackCtx, byte[] undoLogContent,
                                       Connection conn) throws SQLException {
    insertUndoLog(xid, branchId, rollbackCtx, undoLogContent, State.Normal, conn);
}

@Override
protected void insertUndoLogWithGlobalFinished(String xid, long branchId, UndoLogParser parser, Connection conn) throws SQLException {
    insertUndoLog(xid, branchId, buildContext(parser.getName(), CompressorType.NONE), parser.getDefaultContent(), State.GlobalFinished, conn);
}

// 具体写入
private void insertUndoLog(String xid, long branchId, String rollbackCtx, byte[] undoLogContent,
                           State state, Connection conn) throws SQLException {
    try (PreparedStatement pst = conn.prepareStatement(INSERT_UNDO_LOG_SQL)) {
        pst.setLong(1, branchId);
        pst.setString(2, xid);
        pst.setString(3, rollbackCtx);
        pst.setBytes(4, undoLogContent);
        pst.setInt(5, state.getValue());
        pst.executeUpdate();
    } catch (Exception e) {
        if (!(e instanceof SQLException)) {
            e = new SQLException(e);
        }
        throw (SQLException) e;
    }
}
                     State state, Connection conn) throws SQLException {
try (PreparedStatement pst = conn.prepareStatement(INSERT_UNDO_LOG_SQL)) {
    pst.setLong(1, branchId);
    pst.setString(2, xid);
    pst.setString(3, rollbackCtx);
    pst.setBytes(4, undoLogContent);
    pst.setInt(5, state.getValue());
    pst.executeUpdate();
} catch (Exception e) {
    if (!(e instanceof SQLException)) {
        e = new SQLException(e);
    }
    throw (SQLException) e;
}

}






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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【目标检测】K-means和K-means++计算anchors结果比较(附完整代码,全网最详细的手把手教程)

写在前面: 首先感谢兄弟们的订阅,让我有创作的动力,在创作过程我会尽最大努力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 一、介绍 YOLO系列目标检测算法中基于anchor的模型还是比较多的,例如YOLOv3、YOLOv4、YOLOv5等,我们可以随机初始化a…

简约精美电商小程序【源码好优多】

简介 一款开源的电商系统&#xff0c;包含微信小程序和H5端&#xff0c;为大中小企业提供移动电子商务优秀的解决方案。 后台采用Thinkphp5.1框架开发&#xff0c;执行效率、扩展性、稳定性值得信赖。并且Jshop小程序商城上手难度低&#xff0c;可大量节省定制化开发周期。 功…

设计模式(适配器模式)

设计模式&#xff08;适配器模式&#xff09; 第二章 设计模式之适配器模式&#xff08;Adapter&#xff09; 一、Adapter模式介绍 适配器模式位于实际情况和需求之间&#xff0c;填补两者之间的差距。 二、示例程序1&#xff08;使用继承的适配器&#xff09; 1.示例程序示…

操作系统(五):编译过程,静态链接,目标文件,动态链接

文章目录一、程序的编译过程二、静态链接三、目标文件四、动态链接一、程序的编译过程 在linux上编译一个c文件hello.c的命令为: g -o hello hello.c 整个过程大致要经过一下四步: 预处理 &#xff1a; 处理以#开头的预处理命令编译 &#xff1a; 翻译成汇编文件汇编 &…

HTTPS基础原理和配置-3

书接上文&#xff1a;HTTPS 基础原理和配置 - 2&#xff0c;接下来介绍&#xff1a; 配置 NGINX后端 HTTPS检查配置配置 HSTSOCSP Stapling 重要部分来了。如何使用这些选项并配置NGINX? 一、NGINX 的 HTTPS 配置 这里有一些基本的原语&#xff08;或叫做指令&#xff09;…

GEE学习笔记八十八:在自己的APP中使用绘制矢量(上)

在GEE中尤其是自己的APP中调用绘制的矢量图形方法之前没有合适的方法&#xff0c;但是现在可以通过ui.Map.DrawingTools(...)以及ui.Map.GeometryLayer(...)结合来做。具体的API如下图&#xff1a; 在这一篇中我先通过一个简单的例子来展示一下使用这些API后可以实现什么效果&a…

C#底层库--日期扩展类(上周、本周、明年、前年等)

系列文章 C#底层库–记录日志帮助类 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/124187709 C#底层库–数据库访问帮助类&#xff08;MySQL版&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/126886379 …

axios中的GET POST PUT PATCH,发送请求时params和data的区别

axios 中 get/post请求方式 1. 前言 最近突然发现post请求可以使用params方式传值&#xff0c;然后想总结一下其中的用法。 2.1 分类 经过查阅资料&#xff0c;get请求是可以通过body传输数据的&#xff0c;但是许多工具类并不支持此功能。 在postman中&#xff0c;选择get请…

以before为例 完成一个aop代理强化方法案例

观看本文 首先 您需要做好Spring aop的准备工作 具体可以参考我的文章 java Spring aop入门准备工作 首先 我们创建一个包 我这里叫 Aop 然后在Aop包下创建一个类 叫 User 参考代码如下 package Aop;public class User {public void add(){System.out.println("add....…

android kotlin 协程(三) 理解挂起,恢复以及job

android kotlin 协程(三) 理解挂起,恢复以及job 前言: 通过上两篇的基础入门,相信大家对协程api已经有了一个基本的影响,本篇开始尝试理解挂起于恢复. 本篇不涉及源码, 通过很多应用案例,来理解挂起于恢复! 协程执行流程理解 还是老套路,先来看原始效果 参考图1 在这里我们知…

电子技术——目录

电子技术——目录 第一章——信号与放大器 第二章——运算放大器 第三章——半导体器件 第四章——二极管 第五章——金属氧化物场效应晶体管 目录&#xff1a; MOS管的物理结构MOS管的CV特性MOS放大器基础MOS管的小信号模型基本MOS放大器配置MOS放大器的DC偏置分立MOS放…

传奇开服方法教程:传奇开服在哪些网站打广告?传奇发布站打广告技巧

传奇开服方法教程&#xff1a;传奇开服在哪些网站打广告&#xff1f;传奇发布站打广告技巧 纯分享经验以及方法&#xff1a;开传奇sf&#xff0c;成本最高的就是广告费用了&#xff0c;为了让服人气更高&#xff0c;主播和发布站相信你们都已经尝试了&#xff0c;上人效果如何你…

全网详解 .npmrc 配置文件:比如.npmrc的优先级、命令行,如何配置.npmrc以及npm常用命令等

文章目录1. 文章引言2. 简述.npmrc3. 配置.npmrc3.1 .npmrc配置文件的优先级3.2 .npmrc设置的命令行3.3 如何设置.npmrc4. 配置发布组件5. npm常用命令6. 重要备注6.1 yarn6.2 scope命名空间6.3 镜像出错1. 文章引言 今天在某低代码平台开发项目时&#xff0c;看到如下编译配置…

Java实现调用ChatGPT的相关接口(附详细思路)

目录1.0.简单版2.0.升级版2-1.call.timeout()怎么传入新的超时值2-2.timeout(10, TimeUnit.SECONDS)两个参数的意思&#xff0c;具体含义3.0.进阶版3-1.java.net.SocketTimeoutException: 超时如何解决4.0.终极版1.0.简单版 以下是一个使用 Java 实际请求 ChatGPT 的简单示例代…

EasyExcel详解-写Excel

一、注解使用注解很简单&#xff0c;只要在对应的实体类上面加上注解即可。ExcelProperty用于匹配excel和实体类的匹配,参数如下&#xff1a;名称默认值描述value空用于匹配excel中的头&#xff0c;必须全匹配,如果有多行头&#xff0c;会匹配最后一行头orderInteger.MAX_VALUE…

硬件系统工程师宝典(6)-----如何减小信号串扰?

各位同学大家好&#xff0c;欢迎继续做客电子工程学习圈&#xff0c;今天我们继续来讲这本书&#xff0c;硬件系统工程师宝典。 上篇我们说到描述信号传输的过程的需要做好阻抗匹配&#xff0c;以减小信号的反射。今天我们来看看传输线之间的噪声干扰&#xff0c;那就是信号的…

rdma rocev2报文格式总结

格式如下&#xff1a; wireshark抓包&#xff0c;soft-roce(基于rocev2实现的)格式如下&#xff1a; 其实这里看着有点奇怪&#xff0c;ICRC是跟在Payload头后面的&#xff0c;不知道为什么抓包看到的却是在BTH头后面的&#xff0c;还有就是看不到FCS?? 其中BTH头的格式有…

极兔一面:Dockerfile如何优化?注意:千万不要只说减少层数

说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;面试题是一个非常、非常高频的交流话题。 最近&#xff0c;有小伙伴面试极兔时&#xff0c;遇到一个面试题&#xff1a; 如果优化 Dockerfile&#xff1f; 小伙伴没有回答好&#xff0c;只是提到了减少镜像层数。…

01背包问题 AcWing(JAVA)

有 N件物品和一个容量是 V的背包。每件物品只能使用一次。 第 i件物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整数&#xff0c;N&#xff…

数据结构与算法(一):概述

数据结构学了有一年左右的时间了&#xff0c;但是一直没有详细地总结一下&#xff0c;现在回想起来&#xff0c;感觉有些内容忘记了。所以接下来一段时间我将重新归纳总结一下&#xff0c;算是温故而知新了。 一、数据结构 1、定义 数据结构是计算机存储、组织数据的方式。在…