seata-2阶段提交-笔记3

本文属于B站图灵课堂springcloud笔记系列。

前面整理过2篇:seata 2阶段提交实现代码-笔记1-CSDN博客   扫描@GlobalTransactional注解

seata 2阶段提交实现代码-笔记2-CSDN博客 TC生成XID,并保存到global_table表。

本篇继续整理 执行业务逻辑,提交本地事务部分。

目前已经通过beginTransaction(txInfo, tx)获取到了全局事务ID,并记录到global_table全局事务表中,接下来会执行 business.execute():进入业务代码,触发点在执行业务逻辑第一篇里面,TransactionalTemplate.execute()。

demo :order调用逻辑就是调用库存服务扣减库存、调用账户服务扣减余额、生成订单。

以库存服务为例:

@Override
	@Transactional
	public void reduceStock(String commodityCode, Integer count)
			throws BusinessException {
		logger.info("[reduceStock] current XID: {}", RootContext.getXID());
		logger.info("扣减库存");

		checkStock(commodityCode, count);

		Timestamp updateTime = new Timestamp(System.currentTimeMillis());
		int updateCount = storageMapper.reduceStock(commodityCode, count, updateTime);
		if (updateCount == 0) {
			throw new BusinessException("deduct stock failed");
		}
	}

只是扣减库存,但是执行过程中我们会观察到库存库中表undo_log 会自动新增一条记录。

原因就是seata的数据源代理,用户是无感知的。前面篇1 也提到了GlobalTransactionScanner 是扫描有注解的bean做AOP增强。

数据源代理

  

DataSourceProxy 构造器会调用io.seata.rm.datasource.DataSourceProxy#init

private void init(DataSource dataSource, String resourceGroupId) {
        this.resourceGroupId = resourceGroupId;
        try (Connection connection = dataSource.getConnection()) {
            jdbcUrl = connection.getMetaData().getURL();
            dbType = JdbcUtils.getDbType(jdbcUrl);
            if (JdbcConstants.ORACLE.equals(dbType)) {
                userName = connection.getMetaData().getUserName();
            } else if (JdbcConstants.MYSQL.equals(dbType)) {
                validMySQLVersion(connection);
                checkDerivativeProduct();
            }
        } catch (SQLException e) {
            throw new IllegalStateException("can not init dataSource", e);
        }
        if (JdbcConstants.SQLSERVER.equals(dbType)) {
            LOGGER.info("SQLServer support in AT mode is currently an experimental function, " +
                    "if you have any problems in use, please feedback to us");
        }
        initResourceId();
        DefaultResourceManager.get().registerResource(this);
        TableMetaCacheFactory.registerTableMeta(this);
        //Set the default branch type to 'AT' in the RootContext.
        RootContext.setDefaultBranchType(this.getBranchType());
    }

DataSourceProxy.init的主要功能:

设置资源组ID,默认是DEFAULT、初始化ResourceId

向资源管理器DefaultResourceManager注册本类

加缓存:TableMetaCacheFactory 表元数据信息。还有隐含生成了代理连接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);
    }
public class ConnectionProxy extends AbstractConnectionProxy {

ConnectionProxy 继承了AbstractConnectionProxy,这个抽象连接代理,封装了很多通用功能。比如获取连接等。关注下StatementProxy、PreparedStatementProxy

  public Statement createStatement() throws SQLException {
        Statement targetStatement = getTargetConnection().createStatement();
        return new StatementProxy(this, targetStatement);
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        String dbType = getDbType();
        // support oracle 10.2+
        PreparedStatement targetPreparedStatement = null;
        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);
        }
        return new PreparedStatementProxy(this, targetPreparedStatement, sql);
    }

2.0版本里面StatementProxy、PreparedStatementProxy 继承类 不一样。都封装了几个SQL执行方法。SQL执行:

    @Override
    public boolean execute() throws SQLException {
        return ExecuteTemplate.execute(this, (statement, args) -> statement.execute());
    }

    @Override
    public ResultSet executeQuery() throws SQLException {
        return ExecuteTemplate.execute(this, (statement, args) -> statement.executeQuery());
    }

    @Override
    public int executeUpdate() throws SQLException {
        return ExecuteTemplate.execute(this, (statement, args) -> statement.executeUpdate());
    }

在这些方法中都调用了 ExecuteTemplate.execute(),接下来看看这个方法:

  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/oracle
        String dbType = statementProxy.getConnectionProxy().getDbType();
        if (CollectionUtils.isEmpty(sqlRecognizers)) {
            //sql 解析器,通过它可以获取sql的表名、列名、类型等信息,解析出sql表达式
            //PlainExecutor直接使用原生的Statment对象执行SQL
            sqlRecognizers = SQLVisitorFactory.get(
                    statementProxy.getTargetSQL(),
                    dbType);
        }
        Executor<T> executor;
        if (CollectionUtils.isEmpty(sqlRecognizers)) {
            //没找到sql 解析器,就使用PlainExecutor
            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:
                        if (JdbcConstants.SQLSERVER.equalsIgnoreCase(dbType)) {
                            executor = new SqlServerUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        } else {
                            executor = new UpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        }
                        break;
                    case DELETE:
                        if (JdbcConstants.SQLSERVER.equalsIgnoreCase(dbType)) {
                            executor = new SqlServerDeleteExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        } else {
                            executor = new DeleteExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        }
                        break;
                    case SELECT_FOR_UPDATE:
                        if (JdbcConstants.SQLSERVER.equalsIgnoreCase(dbType)) {
                            executor = new SqlServerSelectForUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        } else {
                            executor = new SelectForUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                        }
                        break;
                    case INSERT_ON_DUPLICATE_UPDATE:
                        switch (dbType) {
                            case JdbcConstants.MYSQL:
                                executor =
                                        new MySQLInsertOnDuplicateUpdateExecutor(statementProxy, statementCallback, sqlRecognizer);
                                break;
                            case JdbcConstants.MARIADB:
                                executor =
                                        new MariadbInsertOnDuplicateUpdateExecutor(statementProxy, statementCallback, sqlRecognizer);
                                break;
                            case JdbcConstants.POLARDBX:
                                executor = new PolarDBXInsertOnDuplicateUpdateExecutor(statementProxy, statementCallback, sqlRecognizer);
                                break;
                            default:
                                throw new NotSupportYetException(dbType + " not support to INSERT_ON_DUPLICATE_UPDATE");
                        }
                        break;
                    case UPDATE_JOIN:
                        switch (dbType) {
                            case JdbcConstants.MYSQL:
                                executor = new MySQLUpdateJoinExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                                break;
                            case JdbcConstants.MARIADB:
                                executor = new MariadbUpdateJoinExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                                break;
                            case JdbcConstants.POLARDBX:
                                executor = new PolarDBXUpdateJoinExecutor<>(statementProxy, statementCallback, sqlRecognizer);
                                break;
                            default:
                                throw new NotSupportYetException(dbType + " not support to " + SQLType.UPDATE_JOIN.name());
                        }
                        break;
                    default: //默认原生
                        executor = new PlainExecutor<>(statementProxy, statementCallback);
                        break;
                }
            } else {//批量
                executor = new MultiExecutor<>(statementProxy, statementCallback, sqlRecognizers);
            }
        }
        T rs;
        try {//使用上面返回的执行器,执行execute
            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;
    }

Seata提供了几种执行器也就是我们代码 case 中(INSERTUPDATEDELETESELECT_FOR_UPDATE,INSERT_ON_DUPLICATE_UPDATE,UPDATE_JOIN 少见),这些执行器的父类都是AbstractDMLBaseExecutor。以UpdateExecutor为例

然后我们看 executor.execute(args); 最终执行的方法

    @Override
    public T execute(Object... args) throws Throwable {
        String xid = RootContext.getXID();//获取xid,此前RootContext已经填充
        if (xid != null) {//与数据库连接绑定,注意是ConnectionProxy
            statementProxy.getConnectionProxy().bind(xid);
        }
        //设置是否需要全局锁
        statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
        return doExecute(args);//执行
    }

接下来看io.seata.rm.datasource.exec.AbstractDMLBaseExecutor#doExecute

    @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(() -> {
                //调用手动提交方法,得到分支执行的最终结果(并准备undo_log的内容,设置前置镜像 和 后置镜像)
                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);
        }
    }

再看下关键的executeAutoCommitFalse()

    protected T executeAutoCommitFalse(Object[] args) throws Exception {
        try {//获取前镜像
            TableRecords beforeImage = beforeImage();
            //执行目标sql
            T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
            //后镜像
            TableRecords afterImage = afterImage(beforeImage);
            //准备undo_log
            prepareUndoLog(beforeImage, afterImage);
            return result;
        } catch (TableMetaException e) {
            LOGGER.error("table meta will be refreshed later, due to TableMetaException, table:{}, column:{}",
                e.getTableName(), e.getColumnName());
            statementProxy.getConnectionProxy().getDataSourceProxy().tableMetaRefreshEvent();
            throw e;
        }
    }

此时本地事务未提交,再回到executeAutoCommitTrue中看看提交.ConnectionProxy.commit()

    public void commit() throws SQLException {
        try {
            lockRetryPolicy.execute(() -> {
                doCommit();//提交
                return null;
            });
        } catch (SQLException e) {
            if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
                rollback();//回滚
            }
            throw e;
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }

io.seata.rm.datasource.ConnectionProxy#doCommit()


    private void doCommit() throws SQLException {
        if (context.inGlobalTransaction()) {
            processGlobalTransactionCommit();//执行全局事务的提交逻辑
        } else if (context.isGlobalLockRequire()) {//如果需要全局事务锁
            processLocalCommitWithGlobalLocks();
        } else {//非全局事务,直接提交
            targetConnection.commit();
        }
    }

作为分布式事务,看第一个。

    private void processGlobalTransactionCommit() throws SQLException {
        try {//注册分支事务( RM 向TC发请求,TC注册分支事务)
            register();
        } catch (TransactionException e) {
            recognizeLockKeyConflictException(e, context.buildLockKeys());
        }
        try {
            //生成 undo_log 回滚日志:插入到undo_log表
            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();
    }

其中,生成undo_log,底层调用了io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager#insertUndoLog

代码不贴了。对应表就是业务库undo_log,

register RM想TC注册分支。

TC处理逻辑入口在

    @Override
    protected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response,
                                    RpcContext rpcContext) throws TransactionException {
        MDC.put(RootContext.MDC_KEY_XID, request.getXid());
        response.setBranchId(
                core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(),
                        request.getXid(), request.getApplicationData(), request.getLockKey()));
    }

底层核心逻辑调用了io.seata.server.coordinator.AbstractCore#branchRegister

@Override
    public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid,
                               String applicationData, String lockKeys) throws TransactionException {
        //根据xid 获取GlobalSession
        GlobalSession globalSession = assertGlobalSessionNotNull(xid, false);
        return SessionHolder.lockAndExecute(globalSession, () -> {
            globalSessionStatusCheck(globalSession);
            //创建分支事务 branchSession
            BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId,
                    applicationData, lockKeys, clientId);
            MDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(branchSession.getBranchId()));
            //获取全局锁 底层会存储到lock_table
            branchSessionLock(globalSession, branchSession);
            try {//加入到globalSession 底层会保存到branch_table 表
                globalSession.addBranch(branchSession);
            } catch (RuntimeException ex) {
                branchSessionUnlock(branchSession);
                throw new BranchTransactionException(FailedToAddBranch, String
                        .format("Failed to store branch xid = %s branchId = %s", globalSession.getXid(),
                                branchSession.getBranchId()), ex);
            }
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Register branch successfully, xid = {}, branchId = {}, resourceId = {} ,lockKeys = {}",
                        globalSession.getXid(), branchSession.getBranchId(), resourceId, lockKeys);
            }
            return branchSession.getBranchId();
        });
    }

最终会在seata库的 全局加锁会存储到lock_table表中,branch_table表中插一条记录。

这里不再展开,后面待整理。

流程很长,只看原码容易乱。网上找了个大佬画的图比较清晰,原文地址:墨滴社区

引用下,这个图可能跟你在看版本有所差异,我看2.0就是这样。

小结:

篇1:seata 2阶段提交实现代码-笔记1-CSDN博客

扫描@GlobalTransactional注解,TM向TC发请求,获取全局事务XID

篇2:seata 2阶段提交实现代码-笔记2-CSDN博客
TC生成全局事务XID,并存储到全局事务表global_table中
本篇:使用数据源代理
准备前置镜像
执行目标sql,执行但未提交
准备后置镜像,组装undo_log
向TC注册分支事务,TC端获取全局事务锁,涉及到seata库lock_table表中,把分支事务信息存储到branch_table表,
RM端提交undo_log信息,在业务库下的 undo_log表中,用于事务回滚。
RM端提交本地事务

真是博大精深啊,才看了个皮毛。

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

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

相关文章

Docker如何运行一个Java的jar包程序

Docker如何运行一个Java的jar包程序 1、jar包程序 2、start.sh运行jar包脚本 #!/bin/bash #进入目录 cd /app #1.下载SDK并安装 java -jar SDKDown1.4.jar #2.加载环境变量 export LD_LIBRARY_PATH/opt/casb/CipherSuiteSdk_linux/lib echo $LD_LIBRARY_PATH #3.执行SDK java …

Pycharm访问MongoDB数据库

MongoDB的基础操作 1. 创建连接 #导入pymongo中的用于操作数据库的客户端 from pymongo import MongoClient #创建客户端对象&#xff0c;连接MongoDB服务器 client MongoClient(mongodb://admin:admin123456localhost:27017) 2. 数据的增删改查 2.1 数据的写入 from mon…

【Python】编写一个函数,将指定的罗马字符转换为数字的形式。

#编写一个函数&#xff0c;将指定的罗马字符转换为数字的形式。R2N {I:1, V:5, X:10, L:50, C:100, D:500, M:1000}def roman2num(s):r 0n len(s)for i, ch in enumerate(s):v R2N[ch]if i < n-1 and v < R2N[s[i1]]:r - velse:r vreturn r;s input("请输入一…

【深度学习总结】使用PDF构建RAG:结合Langchain和通义千问

【深度学习总结】使用PDF构建RAG&#xff1a;结合Langchain和通义千问 使用平台&#xff1a;趋动云&#xff0c;注册送算力 前言 在大型语言模型&#xff08;LLMs&#xff09;应用领域&#xff0c;我们面临着大量挑战&#xff0c;从特定领域知识的匮乏到信息准确性的窘境&am…

GB28181系列三:GB28181流媒体服务器ZLMediaKit

我的音视频/流媒体开源项目(github) GB28181系列目录 目录 一、ZLMediaKit介绍 二、 ZLMediaKit安装、运行(Ubuntu) 1、安装 2、运行 3、配置 三、ZLMediaKit使用 一、ZLMediaKit介绍 ZLMediaKit是一个基于C11的高性能运营级流媒体服务框架&#xff0c;项目地址&#xf…

React 第十七节 useMemo用法详解

概述 useMemo 是React 中的一个HOOK&#xff0c;用于根据依赖在每次渲染时候缓存计算结果&#xff1b; 大白话就是&#xff0c;只有依赖项发生变化时候&#xff0c;才会重新渲染为新计算的值&#xff0c;否则就还是取原来的值&#xff0c;有点类似 vue 中的 computed 计算属性…

若依前后端分离版集成ShardingSphere-补充版代码演示

拉取项目&#xff1a;https://gitee.com/y_project/RuoYi-Vue。前后端分离版本新建数据库&#xff0c;字符集选择utf8mb4。导入mysql文件。 主pom文件中引入依赖 <!-- 分库分表引擎 --><dependency><groupId>org.apache.shardingsphere</groupId><…

Postman接口测试:全局变量/接口关联/加密/解密

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 全局变量和环境变量 全局变量&#xff1a;在postman全局生效的变量&#xff0c;全局唯一 环境变量&#xff1a;在特定环境下生效的变量&#xff0c;本环境内唯一 …

基于PHP的民宿预订管理系统

有需要请加文章底部Q哦 可远程调试 基于PHP的民宿预订管理系统 一 介绍 此民宿预订管理系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。(附带配套设计文档) 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册…

Elasticsearch:使用 Open Crawler 和 semantic text 进行语义搜索

作者&#xff1a;来自 Elastic Jeff Vestal 了解如何使用开放爬虫与 semantic text 字段结合来轻松抓取网站并使其可进行语义搜索。 Elastic Open Crawler 演练 我们在这里要做什么&#xff1f; Elastic Open Crawler 是 Elastic 托管爬虫的后继者。 Semantic text 是 Elasti…

NVM:安装配置使用(详细教程)

文章目录 一、简介二、安装 nvm三、配置 nvm 镜像四、配置环境变量五、使用教程5.1 常用命令5.2 具体案例 六、结语 一、简介 在实际的开发和学习中可能会遇到不同项目的 node 版本不同&#xff0c;而出现的兼容性问题。 而 nvm 就可以很好的解决这个问题&#xff0c;它可以在…

【HarmonyOS】HarmonyOS 和 Flutter混合开发 (一)之鸿蒙Flutter环境安装

【HarmonyOS】HarmonyOS 和 Flutter混合开发 &#xff08;一&#xff09;之鸿蒙Flutter环境安装 一、前言 flutter作为开源适配框架方案&#xff0c;已经在Android&#xff0c;IOS&#xff0c;Web&#xff0c;Window四大平台进行了适配&#xff0c;一套代码&#xff0c;可以同…

期权懂|期权新手入门知识:个股期权标的资产的作用

锦鲤三三每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 期权新手入门知识&#xff1a;个股期权标的资产的作用 个股期权标的资产的作用主要体现在以下几个方面‌&#xff1a; &#xff08;1&#xff09;基本面影响‌&#xff1a; 标的资…

Unity超优质动态天气插件(含一年四季各种天气变化,可用于单机局域网VR)

效果展示&#xff1a;https://www.bilibili.com/video/BV1CkkcYHENf/?spm_id_from333.1387.homepage.video_card.click 在你的项目中设置enviro真的很容易&#xff01;导入包裹并按照以下步骤操作开始的步骤&#xff01; 1. 拖拽“EnviroSky”预制件&#xff08;“environme…

【算法】【优选算法】链表

目录 一、链表常用技巧与操作总结二、2.两数相加三、24.两两交换链表中的节点3.1 迭代3.2 递归 四、143.重排链表五、23.合并K个升序链表5.1 堆5.2 分治5.3 暴力枚举 六、25.K个⼀组翻转链表 一、链表常用技巧与操作总结 技巧&#xff1a; 画图解题。使用虚拟头结点。像有插入…

【面试】Redis 常见面试题

一、介绍一下什么是 Redis&#xff0c;有什么特点? Redis 是一个高性能的 key-value 内存数据库。 不同于传统的 MySQL 这样的关系型数据库&#xff0c;Redis 主要使用内存存储数据&#xff08;当然也支持持久化存储到硬盘上&#xff09;&#xff0c;并非是使用 “表” 这样…

【Linux】NET9运行时移植到低版本GLIBC的Linux纯内核板卡上

背景介绍 自制了一块Linux板卡(基于全志T113i) 厂家给的SDK和根文件系统能够提供的GLIBC的版本比较低 V2.25/GCC 7.3.1 这个版本是无法运行dotnet以及dotnet生成的AOT应用的 我用另一块同Cortex-A7的板子运行dotnet的报错 版本不够&#xff0c;运行不了 而我的板子是根本就识…

MySQL Explain 分析SQL语句性能

一、EXPLAIN简介 使用EXPLAIN关键字可以模拟优化器执行SQL查询语句&#xff0c;从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。 &#xff08;1&#xff09; 通过EXPLAIN&#xff0c;我们可以分析出以下结果&#xff1a; 表的读取顺序数据读取…

vue3实现商城系统详情页(前端实现)

目录 写在前面 预览 实现 图片部分 详情部分 代码 源码地址 总结 写在前面 笔者不是上一个月毕业了么&#xff1f;找工作没找到&#xff0c;准备在家躺平两个月。正好整理一下当时的毕业设计&#xff0c;是一个商城系统。还是写篇文章记录下吧 预览 商品图片切换显示…

uniapp 微信小程序 功能入口

单行单独展示 效果图 html <view class"shopchoose flex jsb ac" click"routerTo(要跳转的页面)"><view class"flex ac"><image src"/static/dyd.png" mode"aspectFit" class"shopchooseimg"&g…