seata源码-全局事务提交 服务端源码

news2024/11/15 18:00:50

前面的博客中,我们介绍了,发起全局事务时,是如何进行全局事务提交的,这篇博客,主要记录,在seata分布式事务中,全局事务提交的时候,服务端是如何进行处理的

发起全局事务提交操作

事务发起者,在所有分支事务执行完毕之后,如果没有发生异常,会进行全局事务提交

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里就不做过多解释了,前面seata入门和全局事务begin的博客中,有介绍过这里入参的request对象的重要性

服务端接收到请求

前面全局事务begin的源码,介绍过,netty服务端接收到请求之后,是如何执行到这里的,在这里会根据request请求的类型,交给不同的handler来处理
在这里插入图片描述

io.seata.server.AbstractTCInboundHandler#handle(io.seata.core.protocol.transaction.GlobalCommitRequest, io.seata.core.rpc.RpcContext)

在这里插入图片描述

io.seata.server.coordinator.DefaultCoordinator#doGlobalCommit

在这里插入图片描述

上面也没有太多的业务逻辑,没什么好说的,我们直接来看core.commit()方法的逻辑

io.seata.server.coordinator.DefaultCore#commit


@Override
public GlobalStatus commit(String xid) throws TransactionException {
    /**
     * 1.获取全局session:globalSession
     */
    GlobalSession globalSession = SessionHolder.findGlobalSession(xid);
    if (globalSession == null) {
        return GlobalStatus.Finished;
    }
    /**
     * 2.给globalSession添加监听
     */
    globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
    // just lock changeStatus
    /**
     * 3.对globalSession进行一些处理
     */
    boolean shouldCommit = SessionHolder.lockAndExecute(globalSession, () -> {
        // Highlight: Firstly, close the session, then no more branch can be registered.
        /**
         * 3.1 globalSession.setActive(false);将全局session的active设置为false
         * 在clean()方法中会把lockTable中本次加锁的记录(分支事务相关锁信息)删除
         */
        globalSession.closeAndClean();
        if (globalSession.getStatus() == GlobalStatus.Begin) {
            /**
             * 对于AT模式,这里永远返回false
             * 对于AT模式,这里会执行if的逻辑,将globalSession的status设置为AsyncCommitting
             */
            if (globalSession.canBeCommittedAsync()) {
                globalSession.asyncCommit();
                return false;
            } else {
                /**
                 * 将globalSession的状态设置为committing状态
                 */
                globalSession.changeStatus(GlobalStatus.Committing);
                return true;
            }
        }
        return false;
    });

    /**
     * 4.通知分支事务进行提交,如果是AT模式,不会进入到这里执行,因为shouldCommit是false
     * 是通过一个异步线程来进行调用doGlobalCommit()方法的
     */
    if (shouldCommit) {
        boolean success = doGlobalCommit(globalSession, false);
        //If successful and all remaining branches can be committed asynchronously, do async commit.
        if (success && globalSession.hasBranch() && globalSession.canBeCommittedAsync()) {
            globalSession.asyncCommit();
            return GlobalStatus.Committed;
        } else {
            return globalSession.getStatus();
        }
    } else {
        return globalSession.getStatus() == GlobalStatus.AsyncCommitting ? GlobalStatus.Committed : globalSession.getStatus();
    }
}

上面贴的这段代码,是netty服务端接收到客户端全局事务请求之后,最核心的一个入口,我们拆开来看

commit

closeAndClean()

上面代码中注释1、2、3就不细看了,是一些不重要的逻辑,我们直接来看3.1这个注释对应的方法
在这个方法中,一个close()方法,一个clean()方法
在这里插入图片描述

close()方法中的逻辑比较简单,就是把globalSession的active属性设置为了false
在这里插入图片描述

我们接着来看clean()方法:
在这里插入图片描述

clean()内部调用的这个方法,如果看过前面全局事务回滚的代码,就会发现这个代码很眼熟,就是把分支事务对应的lockTable中指定的加锁的资源进行释放
在这里插入图片描述

shouldCommit()

我们接着来看shouldCommit参数的赋值逻辑,可以着重看下第三点的注释,这里的shouldCommit参数,如果是AT模式,永远false,原因是在这里
在这里插入图片描述
在canBeCommittedAsync方法中,下面两张截图我们结合起来看,如果是AT模式,canBeCommitedAsync()返回的一定是true;如果这里返回true,那上面截图,一定会进入到if()的逻辑中
在这里插入图片描述
在这里插入图片描述

所以,对于AT模式,shouldCommit一定是false,并且会调用globalSession.asyncCommit();
在这里插入图片描述

这段代码的整体逻辑,就是说,如果当前事务是允许异步处理的,那就给shouldCommit赋值为false,同时把globalSession的status修该为AsyncCommitting;这个状态很重要,这里的意思,我认为是说,当前事务是需要异步处理的,当前代码中,就不同步处理了,接来下的逻辑,可以证明
在这里插入图片描述

可以发现,当是AT模式的时候,直接执行了else的逻辑,那我们接下来看下,对于netty服务端,真正去处理分支事务的代码

init()

这段初始化的代码,和上面commit()有点关联,我们截止到这里,需要知道上面commit()方法,如果是AT模式的时候,只是把当前globalSession的状态改成了AsyncCommitting状态

io.seata.server.coordinator.DefaultCoordinator#init

这个方法,是在服务端启动的时候,会在这里初始化一批异步线程,其中有一个和本篇博客有关系
在这里插入图片描述

在这里插入图片描述

查找所有AsyncCommitting状态的globalSession

io.seata.server.storage.db.session.DataBaseSessionManager#allSessions

在这里插入图片描述
在这里插入图片描述
这里就不再继续往下贴代码了,逻辑比较简单,可以自己看下,简单来说,就是根据当前入参中的AsyncCommitting,从globalTable中,根据状态进行查询,然后找到所有待异步处理的globalSession

core.doGlobalCommit

io.seata.server.coordinator.DefaultCore#doGlobalCommit

这个方法,是服务端进行全局事务提交的处理逻辑,中间绕了这么一大圈,现在逻辑应该有点清晰明了了,其实就是在netty服务端接收到同步请求的时候,只会先把lockTable中加锁的数据删除,然后修改globalSession的状态,最后通过异步定时执行的线程池去执行全局事务提交的逻辑

public boolean doGlobalCommit(GlobalSession globalSession, boolean retrying) throws TransactionException {
    boolean success = true;
    // start committing event
    eventBus.post(new GlobalTransactionEvent(globalSession.getTransactionId(), GlobalTransactionEvent.ROLE_TC,
        globalSession.getTransactionName(), globalSession.getBeginTime(), null, globalSession.getStatus()));

    if (globalSession.isSaga()) {
        success = getCore(BranchType.SAGA).doGlobalCommit(globalSession, retrying);
    } else {
        for (BranchSession branchSession : globalSession.getSortedBranches()) {
            // if not retrying, skip the canBeCommittedAsync branches
            if (!retrying && branchSession.canBeCommittedAsync()) {
                continue;
            }

            BranchStatus currentStatus = branchSession.getStatus();
            /**
             * 如果二阶段分支事务状态是失败,就无需执行下面的逻辑,直接remove即可
             */
            if (currentStatus == BranchStatus.PhaseOne_Failed) {
                globalSession.removeBranch(branchSession);
                continue;
            }
            try {
                /**
                 * 这里是服务端在全局事务提交的时候,会通知RM去对本地的branch事务进行处理,是通过netty去交互的
                 * 1.如果RM删除成功,就将branchSession移除,并释放锁
                 * 2.如果删除失败
                 */
                BranchStatus branchStatus = getCore(branchSession.getBranchType()).branchCommit(globalSession, branchSession);

                switch (branchStatus) {
                    case PhaseTwo_Committed:
                        globalSession.removeBranch(branchSession);
                        continue;
                    case PhaseTwo_CommitFailed_Unretryable:
                        if (globalSession.canBeCommittedAsync()) {
                            LOGGER.error(
                                "Committing branch transaction[{}], status: PhaseTwo_CommitFailed_Unretryable, please check the business log.", branchSession.getBranchId());
                            continue;
                        } else {
                            SessionHelper.endCommitFailed(globalSession);
                            LOGGER.error("Committing global transaction[{}] finally failed, caused by branch transaction[{}] commit failed.", globalSession.getXid(), branchSession.getBranchId());
                            return false;
                        }
                    default:
                        if (!retrying) {
                            globalSession.queueToRetryCommit();
                            return false;
                        }
                        if (globalSession.canBeCommittedAsync()) {
                            LOGGER.error("Committing branch transaction[{}], status:{} and will retry later",
                                branchSession.getBranchId(), branchStatus);
                            continue;
                        } else {
                            LOGGER.error(
                                "Committing global transaction[{}] failed, caused by branch transaction[{}] commit failed, will retry later.", globalSession.getXid(), branchSession.getBranchId());
                            return false;
                        }
                }
            } catch (Exception ex) {
                StackTraceLogger.error(LOGGER, ex, "Committing branch transaction exception: {}",
                    new String[] {branchSession.toString()});
                if (!retrying) {
                    globalSession.queueToRetryCommit();
                    throw new TransactionException(ex);
                }
            }
        }
        //If has branch and not all remaining branches can be committed asynchronously,
        //do print log and return false
        if (globalSession.hasBranch() && !globalSession.canBeCommittedAsync()) {
            LOGGER.info("Committing global transaction is NOT done, xid = {}.", globalSession.getXid());
            return false;
        }
    }
    //If success and there is no branch, end the global transaction.
    /**
     * 这里的sessionHelper.encCommitted会将globalSession中的记录删除
     */
    if (success && globalSession.getBranchSessions().isEmpty()) {
        SessionHelper.endCommitted(globalSession);

        // committed event
        eventBus.post(new GlobalTransactionEvent(globalSession.getTransactionId(), GlobalTransactionEvent.ROLE_TC,
            globalSession.getTransactionName(), globalSession.getBeginTime(), System.currentTimeMillis(),
            globalSession.getStatus()));

        LOGGER.info("Committing global transaction is successfully done, xid = {}.", globalSession.getXid());
    }
    return success;
}

上面这段代码,是全局事务提交的逻辑,本质上,和全局事务回滚的逻辑,没太大的区别,只是底层一个调用的是commit,一个调用的是rollback;针对上面这段代码,我们需要着重关注的是:branchCommit这个方法,这个方法,我们下面单独说,先来看下这段代码的逻辑

  1. getCore(branchSession.getBranchType()).branchCommit(globalSession, branchSession); 通过这个方法,发起netty请求,通知各个分支事务,进行全局事务的提交
  2. 根据分支事务的返回状态,进行不同的处理;如果分支事务处理成功,在seata服务端这里,会把分支事务删除,并且把内存中的分支事务id删除(在seata服务端,每个全局事务都会维护一个集合,存储当前全局事务对应的所有分支事务)
  3. 在所有的分支事务,处理完毕之后,SessionHelper.endCommitted(globalSession); 通过这个方法,结束所有的逻辑,其实就是把globalSession从mysql数据库中删除

针对第二点和第三点,就不点进去细看了,其实本质上和全局事务回滚时,做的逻辑几乎上是一致的,我们主要来看下第一点,分支事务是如何处理分支事务提交的逻辑,因为这段逻辑和分支事务回滚的逻辑不一样

分支事务提交

io.seata.rm.datasource.AsyncWorker#branchCommit

这是rm这一端接收到分支事务提交的处理逻辑,但是我们会发现,这段代码很简单:
在这里插入图片描述
这里可以看到,只是将当前请求信息,塞到了一个queue中

io.seata.rm.datasource.AsyncWorker#init

这里可以看到,在rm启动的时候,会初始化一个定时执行的线程池,在这个线程池中,会定时的调用doBranchCommit()方法
在这里插入图片描述

io.seata.rm.datasource.AsyncWorker#doBranchCommits

private void doBranchCommits() {
    if (ASYNC_COMMIT_BUFFER.isEmpty()) {
        return;
    }

    /**
     * 1.mappedContexts存储的是从阻塞队列中获取到的要处理的分支事务
     */
    Map<String, List<Phase2Context>> mappedContexts = new HashMap<>(DEFAULT_RESOURCE_SIZE);
    List<Phase2Context> contextsGroupedByResourceId;
    while (!ASYNC_COMMIT_BUFFER.isEmpty()) {
        Phase2Context commitContext = ASYNC_COMMIT_BUFFER.poll();
        contextsGroupedByResourceId = CollectionUtils.computeIfAbsent(mappedContexts, commitContext.resourceId, key -> new ArrayList<>());
        contextsGroupedByResourceId.add(commitContext);
    }

    /**
     * 2.遍历mappedContexts
     */
    for (Map.Entry<String, List<Phase2Context>> entry : mappedContexts.entrySet()) {
        Connection conn = null;
        DataSourceProxy dataSourceProxy;
        try {
            try {
                DataSourceManager resourceManager = (DataSourceManager) DefaultResourceManager.get()
                    .getResourceManager(BranchType.AT);
                dataSourceProxy = resourceManager.get(entry.getKey());
                if (dataSourceProxy == null) {
                    throw new ShouldNeverHappenException("Failed to find resource on " + entry.getKey());
                }
                conn = dataSourceProxy.getPlainConnection();
            } catch (SQLException sqle) {
                LOGGER.warn("Failed to get connection for async committing on " + entry.getKey(), sqle);
                continue;
            }
            contextsGroupedByResourceId = entry.getValue();
            Set<String> xids = new LinkedHashSet<>(UNDOLOG_DELETE_LIMIT_SIZE);
            Set<Long> branchIds = new LinkedHashSet<>(UNDOLOG_DELETE_LIMIT_SIZE);
            /**
             * 3.判断要处理的分支事务数量是否达到了批量处理的阈值
             * 如果到了,就批量进行删除
             * 否则的话,就清空xids和branchIds 然后return
             */
            for (Phase2Context commitContext : contextsGroupedByResourceId) {
                xids.add(commitContext.xid);
                branchIds.add(commitContext.branchId);
                int maxSize = Math.max(xids.size(), branchIds.size());
                /**
                 * 并不是在每次全局事务提交的时候,就会执行下面的sql
                 * 而是在达到一定的阈值的时候,才会批量执行,阈值默认是1000
                 *
                 * 删除undoLog日志
                 */
                if (maxSize == UNDOLOG_DELETE_LIMIT_SIZE) {
                    try {
                        UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType()).batchDeleteUndoLog(
                            xids, branchIds, conn);
                    } catch (Exception ex) {
                        LOGGER.warn("Failed to batch delete undo log [" + branchIds + "/" + xids + "]", ex);
                    }
                    xids.clear();
                    branchIds.clear();
                }
            }

            if (CollectionUtils.isEmpty(xids) || CollectionUtils.isEmpty(branchIds)) {
                return;
            }

            try {
                UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType()).batchDeleteUndoLog(xids,
                    branchIds, conn);
            } catch (Exception ex) {
                LOGGER.warn("Failed to batch delete undo log [" + branchIds + "/" + xids + "]", ex);
            }

            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (Throwable e) {
            LOGGER.error(e.getMessage(), e);
            try {
                if (conn != null) {
                    conn.rollback();
                }
            } catch (SQLException rollbackEx) {
                LOGGER.warn("Failed to rollback JDBC resource while deleting undo_log ", rollbackEx);
            }
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException closeEx) {
                    LOGGER.warn("Failed to close JDBC resource while deleting undo_log ", closeEx);
                }
            }
        }
    }
}

上面这端代码,我们可以看下,没1000ms执行一次,如果这1000ms之内,queue中待指定分支事务commit的请求达到了一定的阈值(UNDOLOG_DELETE_LIMIT_SIZE),就会执行commit请求(UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType()).batchDeleteUndoLog(
xids, branchIds, conn););如果没有达到阈值,也会去执行commit的操作

UndoLogManagerFactory.getUndoLogManager 这里执行提交的逻辑,其实就是把undoLog数据给删除,因为对于全局事务提交时,其实在第一阶段,mysql已经真正执行了commit的操作,在第二阶段,只需要把undoLog给删除即可

总结

以上,就是全局事务提交的逻辑,整体看下来,我们可以发现,对于全局事务提交的时候,分支事务在处理的时候,是异步来处理的,这是和回滚逻辑有很大的区别,因为上篇博客中,我们有看到,全局事务回滚时,分支事务在第二阶段,是同步处理的,在接收到请求之后,会根据undoLog生成回滚sql,并执行,然后删除undoLog数据,但是对于全局事务提交的第二阶段,会发现,接收到请求之后,直接塞到了队列中,通过异步的请求,没1000ms执行一次提交的逻辑

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

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

相关文章

时间复杂度(超详解+例题)

全文目录引言如何衡量一个算法的好坏时间复杂度时间复杂度的定义时间复杂度的大O表示法实例test1test2test3test4test5总结引言 如何衡量一个算法的好坏 我们在写算法的时候&#xff0c;对于实现同样的作用的不同算法&#xff0c;我们如何判断这个算法的好坏呢&#xff1f; …

微前端知识点汇总

1、微前端的实现方案 基于 qiankun 的微前端实践 微前端&#xff08;Micro-Frontends&#xff09;是一种类似于微服务的架构&#xff0c;它将微服务的理念应用于浏览器端&#xff0c;即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。 微前端目标直指巨石…

【Kubernetes】【十一】Pod详解 Pod的生命周期

Pod生命周期 我们一般将pod对象从创建至终的这段时间范围称为pod的生命周期&#xff0c;它主要包含下面的过程&#xff1a; pod创建过程 运行初始化容器&#xff08;init container&#xff09;过程 运行主容器&#xff08;main container&#xff09; 容器启动后钩子&#…

陆拾伍- 如何通过数据影响决策

零、为何能影响 客观的表达其实不一定是客观&#xff0c;只要一被展示&#xff0c;就有可能被主观的意愿所影响。 如何通过客观的数据去展示以及放大主观的意愿&#xff0c;主要有以下几种方法&#xff1a; 一、图表内容顺序 原始数据展示 这种展示&#xff0c;对于 A、B店来…

基于微信小程序的青少年生理健康知识小助手

基于微信小程序的青少年生理健康知识小助手 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目…

Python生日系统

#免费源码见文末公众号# 录入生日 def write():keyvar1.get()valuevar2.get()with open(d:\\生日系统.pickle,rb) as file:dictspickle.load(file)dicts[key]valuewith open(d:\\生日系统.pickle,wb) as file:pickle.dump(dicts,file)file.close() 查询生日 def read():namev…

DDR4介绍01

DDR4&#xff08;第四代双倍数据率同步动态随机存储器SDRAM&#xff09; 关于内存方面知识&#xff0c;大部分人、包括我自己也不是很懂&#xff0c;希望此篇文章能起到点作用&#xff0c;做硬件的就得把相关专业知识学牢了&#xff0c;尤其是专业术语。 下面是DDR4知识做一次…

软考高级之信息系统案例分析七重奏-《7》

本文主要介绍信息系统管理师案例分析 项目立项管理 可行性研究内容一般应包括以下内容。 (1)投资必要性 (2)技术的可行性 (3)财务可行性 (4)组织可行性 (5) 经济可行性 (6) 社会可行性 (7) 风险因素及对策。 项目整体管理 1、项目章程应当包括以下内容 (1)项目目的或…

英伟达GPU中的Tnesor Cores数量多寡与显卡性能有什么关联?

前言 最近在调研常用显卡的参数&#xff0c;看到Nvidia Tensor Cores常用于其中作为对比&#xff0c;呈现在性能好的显卡比如A100比RTX 3060更多更全面&#xff0c;开始思考Tensor Cores细致的作用是什么&#xff1f; 英伟达GPU显卡的简要发展历程 GTX是英伟达过去显卡的型号…

智慧工地安全着装识别检测算法 python

智慧工地安全着装识别检测算法通过pythonopencv网络模型AI视频分析技术&#xff0c;人员安全着装识别检测算法对现场物体的不安全状态以及人员的不安全行为&#xff08;不按要求着装&#xff09;进行自动实时分析。由于Python 较为简单&#xff0c;一般无法进行复杂的后端搭建&…

【C语言】指针进阶

目录 一、字符指针 二、指针数组 三、数组指针 四、数组指针的使用 五、函数指针数组 六、指向函数指针数组的指针 七、回调函数 我们知道了指针的概念&#xff1a; 1. 指针就是个变量&#xff0c;用来存放地址&#xff0c;地址唯一标识一块内存空间。 2. 指针的大小是…

基于springboot+html汽车维修系统汽车维修系统的设计与实现

基于springboothtml汽车维修系统汽车维修系统的设计与实现 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1…

OR青年导师访谈特辑 | 香港理工大学助理教授 马玮:一次拉长的面试 一个交流的平台

OR青年计划 由【运筹OR帷幄】社区主办的【OR青年计划】&#xff0c;旨在帮助对运筹学应用有理想和追求的同学&#xff0c;近距离与学界、业界导师交流课题&#xff0c;深入了解运筹学的细分方向&#xff0c;为后续的深造、就业生涯打下坚实的基础&#xff01;更多内容请查看链…

springmvc图书馆自习室座位预约管理系统java ssm

红枫图书馆自习室座位预约系统的开发过程中&#xff0c;采用B / S架构&#xff0c;主要使用jsp技术进行开发&#xff0c;中间件服务器是Tomcat服务器&#xff0c;使用Mysql数据库和Eclipse开发环境。该红枫图书馆自习室座位预约系统包括学生、管理员。其主要功能包括个人中心、…

Go基础-Hello world

文章目录1 GoRoot、GoPath2 编写程序3 运行程序1 GoRoot、GoPath GoRoot 就是Go的安装目录中的bin目录 GoPath 用于存放第三方类库和自己编写的封装好的代码块 2 编写程序 go_basic/01_helloworld.go package mainimport "fmt"func main() {fmt.Println("He…

番外篇 | 20+ 种注意力机制及代码 适用于YOLOv5/v7/v8

前言 之前已经在此篇博文更新过YOLO系列算法添加注意力机制的教程,共计实现了 13 13 13 种注意力及代码,本篇博文继续追加 10 10 10 余种。与之前不同的是,此篇博文代码添加方式更加严谨更加鲁棒,使用更简单,针对不同种类注

(考研湖科大教书匠计算机网络)第四章网络层-第七节:IPv4数据报首部格式

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;IP数据报首部格式概述二&#xff1a;各字段作用概述&#xff08;1&#xff09;版本&#xff08;2&#xff09;首部长度和可选字段&#xff08;3&am…

WSL(ubuntu2204)xfce4语言支持报错及配置WSL服务自启

语言支持报错 在图形桌面或命令行打开语言支持报错&#xff1a;dbus.exceptions.DBusException: org.freedesktop.DBus.Error.FileNotFound: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory itboonelocalhost:/$ sudo /usr/bin/gnome-…

gRPC 非官方教程

一、 简介 gRPC的定义&#xff1a; 一个高性能、通用的开源RPC框架主要面向移动应用开发&#xff1a; gRPC提供了一种简单的方法来精确地定义服务和为iOS、Android和后台支持服务自动生成可靠性很强的客户端功能库。基于HTTP/2协议标准而设计&#xff0c;基于ProtoBuf(Protoc…

Visual Studio 高级调试-代码调试

概述编程圈子里隔三差五的就会有场“谁是最强IDE”之争&#xff0c;重要的是我们需要对使用的IDE有充分的了解&#xff0c;正所谓工欲善其事&#xff0c;必先利其器。本文主要讲述Visual Studio常用的调试技巧&#xff0c;包括多类型断点&#xff0c;数据监视&#xff0c;以及多…