Seata 源码篇之AT模式启动流程 - 下 - 04

news2024/12/26 17:39:06

Seata 源码篇之AT模式启动流程 - 下 - 04

  • 全局事务提交
  • 分支事务全局提交
  • 全局事务回滚
  • 分支事务全局回滚
  • 小结


本系列文章:

  • Seata 源码篇之核心思想 - 01
  • Seata 源码篇之AT模式启动流程 - 上 - 02
  • Seata 源码篇之AT模式启动流程 - 中 - 03

上一篇文章,我们看了Seata AT模式一阶段提交流程,本文我们来看看AT模式的二阶段流程和全局事务提交回滚逻辑的实现。


全局事务提交

当某个分支事务执行完本地业务SQL语句后,下一步就进入全局事务提交环节了,此处我们可以回顾TransactionalTemplate模版类的execute方法,如下所示:

    public Object execute(TransactionalExecutor business) throws Throwable {
        // 1. 获取当前全局事务的上下文信息
        TransactionInfo txInfo = business.getTransactionInfo();
        ...
        // 1.1 判断当前是否已经存在一个全局事务
        GlobalTransaction tx = GlobalTransactionContext.getCurrent();

        // 1.2 根据不同的全局事务传播行为进行处理
        Propagation propagation = txInfo.getPropagation();
        SuspendedResourcesHolder suspendedResourcesHolder = null;
        try {
            switch (propagation) {
                 ...
            }

            // 1.3 将当前全局锁配置设置到本地线程缓存中,然后返回先前的配置
            GlobalLockConfig previousConfig = replaceGlobalLockConfig(txInfo);

            try {
                // 2. 如果当前线程是全局事务的发起者,即TM,则给TC发送一个开启全局事务的请求,否则只是简单回调相关钩子方法
                beginTransaction(txInfo, tx);

                Object rs;
                try {
                    // 3. 执行当前分支事务对应的本地事务
                    rs = business.execute();
                } catch (Throwable ex) {
                    // 4. 分支事务执行发生异常,判断对应异常是否需要回滚,如果需要则回滚当前全局事务
                    completeTransactionAfterThrowing(txInfo, tx, ex);
                    throw ex;
                }

                // 5. 当前分支事务执行正常,由TM发送提交全局事务的请求
                commitTransaction(tx, txInfo);

                return rs;
            } finally {
                // 6. 资源清理和恢复,同时触发钩子回调
                resumeGlobalLockConfig(previousConfig);
                triggerAfterCompletion();
                cleanUp();
            }
        } finally {
            // 7. 如果存在被挂起的全局事务,则进行恢复
            if (suspendedResourcesHolder != null) {
                tx.resume(suspendedResourcesHolder);
            }
        }
    }

本节我们来看一下全局事务提交的commitTransaction方法实现:

    private void commitTransaction(GlobalTransaction tx, TransactionInfo txInfo)
            throws TransactionalExecutor.ExecutionException, TransactionException {
        // 1. 判断全局事务是否超时
        if (isTimeout(tx.getCreateTime(), txInfo)) {
            // business execution timeout
            Exception exx = new TmTransactionException(TransactionExceptionCode.TransactionTimeout,
                String.format("client detected transaction timeout before commit, so change to rollback, xid = %s", tx.getXid()));
            rollbackTransaction(tx, exx);
            return;
        }

        try {
        // 2. 触发回调埋点,同时指向事务提交动作
            triggerBeforeCommit();
            tx.commit();
        // 3. 记录事务执行提交动作后的状态
            GlobalStatus afterCommitStatus = tx.getLocalStatus();
            TransactionalExecutor.Code code = TransactionalExecutor.Code.Unknown;
            switch (afterCommitStatus) {
                case TimeoutRollbacking:
                    code = TransactionalExecutor.Code.Rollbacking;
                    break;
                case TimeoutRollbacked:
                    code = TransactionalExecutor.Code.RollbackDone;
                    break;
                case Finished:
                    code = TransactionalExecutor.Code.CommitFailure;
                    break;
                default:
            }
        // 4. 如果事务提交失败或者超时,则抛出对应的异常信息
            Exception statusException = null;
            if (GlobalStatus.isTwoPhaseHeuristic(afterCommitStatus)) {
                statusException = new TmTransactionException(TransactionExceptionCode.CommitHeuristic,
                    String.format("Global transaction[%s] not found, may be rollbacked.", tx.getXid()));
            } else if (GlobalStatus.isOnePhaseTimeout(afterCommitStatus)) {
                statusException = new TmTransactionException(TransactionExceptionCode.TransactionTimeout,
                    String.format("Global transaction[%s] is timeout and will be rollback[TC].", tx.getXid()));
            }
            if (null != statusException) {
                throw new TransactionalExecutor.ExecutionException(tx, statusException, code);
            }
        //  5. 正常提交后,触发对应的回调埋点
            triggerAfterCommit();
        } catch (TransactionException txe) {
            // 4.1 Failed to commit
            throw new TransactionalExecutor.ExecutionException(tx, txe,
                    TransactionalExecutor.Code.CommitFailure);
        }
    }

DefaultGlobalTransaction 的 commit 方法负责完成全局事务的提交,当然全局事务提交由TM执行,如果当前分支事务角色是RM,这里直接返回,啥也不干:

    @Override
    public void commit() throws TransactionException {
        // 1. 如果是RM角色,直接返回
        if (role == GlobalTransactionRole.Participant) {
            return;
        }
        // 2. 如果是TM角色,则尝试执行全局事务提交,如果提交失败了,则进行多轮尝试
        int retry = COMMIT_RETRY_COUNT <= 0 ? DEFAULT_TM_COMMIT_RETRY_COUNT : COMMIT_RETRY_COUNT;
        try {
            while (retry > 0) {
                try {
                    retry--;
                    status = transactionManager.commit(xid);
                    break;
                } catch (Throwable ex) {
                    if (retry == 0) {
                        throw new TransactionException("Failed to report global commit", ex);
                    }
                }
            }
        } finally {
            // 3. TM全局事务提交成功后,执行XID解绑
            if (xid.equals(RootContext.getXID())) {
                suspend(true);
            }
        }
        ...
    }

DefaultTransactionManager 在Seata中的职责主要作为防腐层存在,负责屏蔽与TC的通信过程,下面我们看看其commit方法实现:

    @Override
    public GlobalStatus commit(String xid) throws TransactionException {
        GlobalCommitRequest globalCommit = new GlobalCommitRequest();
        globalCommit.setXid(xid);
        GlobalCommitResponse response = (GlobalCommitResponse) syncCall(globalCommit);
        return response.getGlobalStatus();
    }

当TM完成全局事务提交后,下面便是由TC通知其他分支事务执行全局提交了,也就是二阶段提交,二阶段提交的主要任务就是异步删除本地的undo日志。


分支事务全局提交

各个分支事务的全局提交由TC异步回调通知完成,如下图所示:

在这里插入图片描述

具体代码存在于DefaultRMHandler的handle方法中:

    @Override
    public BranchCommitResponse handle(BranchCommitRequest request) {
        MDC.put(RootContext.MDC_KEY_XID, request.getXid());
        MDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(request.getBranchId()));
        // 利用分支事务类型,获取对应的处理器,然后调用处理器的handle方法,处理分支事务提交请求
        return getRMHandler(request.getBranchType()).handle(request);
    }

这里获取的处理器类型为RMHandlerAT,所以最终会调用RMHandlerAT的handle方法处理分支事务提交请求:

public abstract class AbstractRMHandler extends AbstractExceptionHandler
    implements RMInboundHandler, TransactionMessageHandler {

    @Override
    public BranchCommitResponse handle(BranchCommitRequest request) {
        BranchCommitResponse response = new BranchCommitResponse();
        exceptionHandleTemplate(new AbstractCallback<BranchCommitRequest, BranchCommitResponse>() {
            @Override
            public void execute(BranchCommitRequest request, BranchCommitResponse response)
                throws TransactionException {
                // 真正执行分支事务提交的方法
                doBranchCommit(request, response);
            }
        }, request, response);
        return response;
    }
    
    ...
}
    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();
        // 调用资源管理器的branchCommit方法,完成分支事务提交
        BranchStatus status = getResourceManager().branchCommit(request.getBranchType(), xid, branchId, resourceId,
            applicationData);
        response.setXid(xid);
        response.setBranchId(branchId);
        response.setBranchStatus(status);
    }

由于这里我们使用的是AT模式,所以最终会调用DataSourceManager的branchCommit方法完成分支事务的提交:

    @Override
    public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                                     String applicationData) throws TransactionException {
        return asyncWorker.branchCommit(xid, branchId, resourceId);
    }

asyncWorker 通过名字可以猜到,此处采用的是异步提交方式,所以下面我们来看看asyncWorker是如何进行异步提交的:

    public BranchStatus branchCommit(String xid, long branchId, String resourceId) {
        // 准备两阶段提交上下文信息,然后加入分支事务提交队列中
        Phase2Context context = new Phase2Context(xid, branchId, resourceId);
        addToCommitQueue(context);
        return BranchStatus.PhaseTwo_Committed;
    }

不难看出,此处采用的是典型的生产者-消费者模式,下面看看具体是谁会从提交队列中取出任务执行:

    private void addToCommitQueue(Phase2Context context) {
        // 直接将任务加入队列中去,然后返回
        if (commitQueue.offer(context)) {
            return;
        }
        // 如果队列满了,则立即让线程池处理一波任务,然后再尝试将当前任务加入队列
        // 此处的thenRun方法是异步执行的
        CompletableFuture.runAsync(this::doBranchCommitSafely, scheduledExecutor)
                .thenRun(() -> addToCommitQueue(context));
    }

队列和定时任务线程池初始化过程可以在AsyncWorker类的构造函数中寻见:

    public AsyncWorker(DataSourceManager dataSourceManager) {
        this.dataSourceManager = dataSourceManager;
        // 默认队列大小为10000 
        commitQueue = new LinkedBlockingQueue<>(ASYNC_COMMIT_BUFFER_LIMIT);
        // 启动定时任务线程池,每秒执行一次任务
        ThreadFactory threadFactory = new NamedThreadFactory("AsyncWorker", 2, true);
        scheduledExecutor = new ScheduledThreadPoolExecutor(2, threadFactory);
        scheduledExecutor.scheduleAtFixedRate(this::doBranchCommitSafely, 10, 1000, TimeUnit.MILLISECONDS);
    }

下面可以来看看具体执行的是什么样的任务:

    void doBranchCommitSafely() {
        doBranchCommit();
    }
 
    private void doBranchCommit() {
        if (commitQueue.isEmpty()) {
            return;
        }

        // 1. 取出队列中所有任务
        List<Phase2Context> allContexts = new LinkedList<>();
        commitQueue.drainTo(allContexts);
        // 2. 按照资源ID对分支事务提交任务进行分组
        Map<String, List<Phase2Context>> groupedContexts = groupedByResourceId(allContexts);
        // 3. 依次处理每个任务
        groupedContexts.forEach(this::dealWithGroupedContexts);
    }  

    private void dealWithGroupedContexts(String resourceId, List<Phase2Context> contexts) {
        ...
        // 1. 通过资源ID获取对应的数据源代理器
        DataSourceProxy dataSourceProxy = dataSourceManager.get(resourceId);
        // 2. 删除undo日志
        Connection conn = null;
        conn = dataSourceProxy.getPlainConnection();
        ...
        UndoLogManager undoLogManager = UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType());
        List<List<Phase2Context>> splitByLimit = Lists.partition(contexts, UNDOLOG_DELETE_LIMIT_SIZE);
        for (List<Phase2Context> partition : splitByLimit) {
            deleteUndoLog(conn, undoLogManager, partition);
        }
        ...
    } 

    private void deleteUndoLog(final Connection conn, UndoLogManager undoLogManager, List<Phase2Context> contexts) {
        Set<String> xids = new LinkedHashSet<>(contexts.size());
        Set<Long> branchIds = new LinkedHashSet<>(contexts.size());
        contexts.forEach(context -> {
            xids.add(context.xid);
            branchIds.add(context.branchId);
        });
       ...
       // 删除undo_log表中的undo_log日志
       // 这里提交的事务是为了确保批量删除undo_log日志这一过程的原子性 
       undoLogManager.batchDeleteUndoLog(xids, branchIds, conn);
       if (!conn.getAutoCommit()) {
            conn.commit();
       }
       ...
    } 

可以看到分支事务全局提交逻辑很简单,就是借助asyncWorker完成undo日志的异步删除。


全局事务回滚

全局事务回滚逻辑存在于TransactionalTemplate的completeTransactionAfterThrowing方法中:

    private void completeTransactionAfterThrowing(TransactionInfo txInfo, GlobalTransaction tx, Throwable originalException)
            throws TransactionalExecutor.ExecutionException, TransactionException {
        // 如果需要对当前异常执行回滚,则执行全局事务回滚操作,否则还是执行全局事务提交
        if (txInfo != null && txInfo.rollbackOn(originalException)) {
            rollbackTransaction(tx, originalException);
        } else {
            // not roll back on this exception, so commit
            commitTransaction(tx, txInfo);
        }
    }
    private void rollbackTransaction(GlobalTransaction tx, Throwable originalException) throws TransactionException, TransactionalExecutor.ExecutionException {
        try {
            // 执行全局事务回滚逻辑和前后回调埋点
            triggerBeforeRollback();
            tx.rollback();
            triggerAfterRollback();
        }
        ...
    }

DefaultGlobalTransaction 的 rollback 方法负责完成全局事务的回滚,当然全局事务回滚也由TM执行,如果当前分支事务角色是RM,这里直接返回,啥也不干:

   @Override
    public void rollback() throws TransactionException {
        // 1. 如果当前分支事务的角色是RM,则直接返回
        if (role == GlobalTransactionRole.Participant) {
            ...
            return;
        }
        ...
        // 2. 如果当前分支事务角色是TM,则通知TC负责发起全局事务回滚请求
        int retry = ROLLBACK_RETRY_COUNT <= 0 ? DEFAULT_TM_ROLLBACK_RETRY_COUNT : ROLLBACK_RETRY_COUNT;
        try {
            while (retry > 0) {
                try {
                    retry--;
                    status = transactionManager.rollback(xid);
                    break;
                } catch (Throwable ex) {
                    ...
                }
            }
        } finally {
            if (xid.equals(RootContext.getXID())) {
                suspend(true);
            }
        }
        ...
    }

DefaultTransactionManager 在Seata中的职责主要作为防腐层存在,负责屏蔽与TC的通信过程,下面我们看看其rollback方法实现:

    @Override
    public GlobalStatus rollback(String xid) throws TransactionException {
        GlobalRollbackRequest globalRollback = new GlobalRollbackRequest();
        globalRollback.setXid(xid);
        GlobalRollbackResponse response = (GlobalRollbackResponse) syncCall(globalRollback);
        return response.getGlobalStatus();
    }

分支事务全局回滚

各个分支事务的本地由TC异步回调通知完成,如下图所示:

在这里插入图片描述

具体代码存在于DefaultRMHandler的handle方法中:

    @Override
    public BranchRollbackResponse handle(BranchRollbackRequest request) {
        MDC.put(RootContext.MDC_KEY_XID, request.getXid());
        MDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(request.getBranchId()));
         // 利用分支事务类型,获取对应的处理器,然后调用处理器的handle方法,处理分支事务提交请求
        return getRMHandler(request.getBranchType()).handle(request);
    }

这里获取的处理器类型为RMHandlerAT,所以最终会调用RMHandlerAT的handle方法处理分支事务提交请求:

public abstract class AbstractRMHandler extends AbstractExceptionHandler
    implements RMInboundHandler, TransactionMessageHandler {

    @Override
    public BranchRollbackResponse handle(BranchRollbackRequest request) {
        BranchRollbackResponse response = new BranchRollbackResponse();
        exceptionHandleTemplate(new AbstractCallback<BranchRollbackRequest, BranchRollbackResponse>() {
            @Override
            public void execute(BranchRollbackRequest request, BranchRollbackResponse response)
                throws TransactionException {
                // 执行全局回滚
                doBranchRollback(request, response);
            }
        }, request, response);
        return response;
    }

    ...
}
    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();
        // 调用资源管理器的branchRollback方法,完成分支事务回滚
        BranchStatus status = getResourceManager().branchRollback(request.getBranchType(), xid, branchId, resourceId,
            applicationData);
        response.setXid(xid);
        response.setBranchId(branchId);
        response.setBranchStatus(status);
    }

由于这里我们使用的是AT模式,所以最终会调用DataSourceManager的branchRollback方法完成分支事务的回滚:

    @Override
    public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId,
                                       String applicationData) throws TransactionException {
        DataSourceProxy dataSourceProxy = get(resourceId);
        ...
        // 利用undo日志完成回滚
        UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType()).undo(dataSourceProxy, xid, branchId);
        ...
        return BranchStatus.PhaseTwo_Rollbacked;
    }

关于如何利用undo日志完成回滚,这块内容将在本系列后面进行讲解,本文暂时不做深究。


小结

到目前为止,我们大体浏览了Seata AT模式整体实现流程。后面,我们将深入Server模块进行研究,以及Seata的RPC模块,感兴趣的童鞋可以持续关注。

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

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

相关文章

全志ARM926 Melis2.0系统的开发指引⑧

全志ARM926 Melis2.0系统的开发指引⑧ 编写目的12.5. 应用程序编写12.5.1. 简单应用编写12.5.1.1. 注册应用12.5.1.2. 创建管理窗口12.5.1.3. 实现管理窗口消息处理回调函数12.5.1.4. 创建图层12.5.1.5. 创建 framewin12.5.1.6. 实现 framewin 消息处理回调函数 -. 全志相关工具…

JavaScript系列从入门到精通系列第十五篇:JavaScript中函数的实参介绍返回值介绍以及函数的立即执行

文章目录 一&#xff1a;函数的参数 1&#xff1a;形参如何定义 2&#xff1a;形参的使用规则 二&#xff1a;函数的返回值 1&#xff1a;函数返回值如何定义 2&#xff1a;函数返回值种类 三&#xff1a;实参的任意性 1&#xff1a;方法可以作为实参 2&#xff1a;将匿…

Next.js 入门笔记

前言 之前初步体验了 React 的魅力, 又看文档理解了一下 useState 和 useEffect, 目前初步理解的概念是: useState 用来声明在组件中使用并且需要修改的变量 useEffect 用来对 useState 声明的变量进行初始化赋值 可能理解的不太准确, 不过大概差不多是这么个意思. 但是再往后…

Qt之显示PDF文件

之前使用过mupdf库&#xff0c;能够成功显示pdf&#xff0c;但是我用着有BUG&#xff0c;不太理解它的代码&#xff0c;搞了好久都不行。后面又试了其他库&#xff0c;如pdfium、popler、下载了很多例程&#xff0c;都跑不起来&#xff01;后面偶然得知xpdf库&#xff0c;看起来…

【C++设计模式之建造者模式:创建型】分析及示例

简介 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它将复杂对象的构建过程与其表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 描述 建造者模式通过将一个复杂对象的构建过程拆分成多个简单的部分&#xff0c;并由不同…

华为云云耀云服务器L实例评测|部署个人音乐流媒体服务器 navidrome

华为云云耀云服务器L实例评测&#xff5c;部署个人音乐流媒体服务器 navidrome 一、云耀云服务器L实例介绍1.1 云服务器介绍1.2 产品规格1.3 产品优势1.4 支持镜像 二、云耀云服务器L实例配置2.1 重置密码2.2 服务器连接2.3 安全组配置 三、部署 navidrome3.1 navidrome 介绍3.…

全志ARM926 Melis2.0系统的开发指引⑦

全志ARM926 Melis2.0系统的开发指引⑦ 编写目的11. 调屏11.1. 调屏步骤简介11.1.1. 判断屏接口。11.1.2. 确定硬件连接。11.1.3. 配置显示部分 sys_config.fex11.1.3.1. 配置屏相关 IO 11.1.4. Lcd_panel_cfg.c 初始化文件中配置屏参数11.1.4.1. LCD_cfg_panel_info11.1.4.2. L…

网课搜题 小猿题库多接口微信小程序源码 自带流量主

多接口小猿题库等综合网课搜题微信小程序源码带流量主&#xff0c;网课搜题小程序, 可以开通流量主赚钱 搭建教程1, 微信公众平台注册自己的小程序2, 下载微信开发者工具和小程序的源码3, 上传代码到自己的小程序 源码下载&#xff1a;https://download.csdn.net/download/m0_…

【C语言经典100例题-70】求一个字符串的长度(指针)

代码 使用指针来遍历字符串&#xff0c;直到遇到字符串结尾的空字符\0为止&#xff0c;统计字符数量即为字符串长度。 #include<stdio.h> #define n 20 int getlength(char *a) {int len 0;while(*a!\0){len;a;}return len; } int main() {char *arr[n] { 0 };int l…

软技能继续挑战网络安全领域

根据 ISACA 的一份新报告&#xff0c;新的网络安全调查结果指出了网络安全专家缺乏的领域&#xff0c;其中人际技能、云计算和安全措施是网络安全专家最突出的技能缺陷。 59% 的网络安全领导者表示他们的团队人手不足。50% 的受访者表示有非入门级职位的职位空缺&#xff0c;而…

多媒体应用设计师

1.多媒体技术基础 1.1.媒体与技术 1.1.媒体 维基百科&#xff1a;传播信息载体 国际电信联盟&#xff08;ITU-T&#xff09;&#xff1a;感知、表示、存储和传输的手段和方法。 两层含义&#xff1a;存储信息的实体&#xff0c;媒质。传递信息载体&#xff0c;媒介。 1.2.国…

使用Python优雅的绘制甘特图

简介 Gantt图表是一种条形图&#xff0c;用于描绘项目进度。图表在垂直轴上列出要执行的任务&#xff0c;在水平轴上列出时间间隔。图中水平条的宽度显示每个活动的持续时间。在Python中&#xff0c;我们可以使用Plotly库来创建和展示Gantt图表。 基础的Gantt图表 首先&…

beego-简单项目写法--路径已经放进去了

Beego案例-新闻发布系统 1.注册 后台代码和昨天案例代码一致。,所以这里面只写一个注册的业务流程图。 **业务流程图 ** 2.登陆 业务流程图 登陆和注册业务和我们昨天登陆和注册基本一样&#xff0c;所以就不再重复写这个代码 但是我们遇到的问题是如何做代码的迁移&…

计算机专业毕业设计项目推荐12-志愿者管理系统(Spring+Js+Mysql)

志愿者管理系统&#xff08;SpringJsMysql&#xff09; **介绍****各部分模块实现** 介绍 本系列(后期可能博主会统一为专栏)博文献给即将毕业的计算机专业同学们,因为博主自身本科和硕士也是科班出生,所以也比较了解计算机专业的毕业设计流程以及模式&#xff0c;在编写的过程…

[NISACTF 2022]join-us - 报错注入无列名注入

点击登录&#xff0c;找到注入点 这种框&#xff0c;可以直接爆破关键字&#xff0c;看是否拦截&#xff0c;也可以手动尝试&#xff0c;发现、union、and、or、substr、database等关键字都拦截了 1、学到了&#xff1a;可以用数据库中不存在的表名或者不存在的自定义函数名爆…

面试高频手撕算法 - 01背包系列

1. 前言 为什么要专门去搞一下这个背包问题呢 ? 因为作者已经在两场面试中吃了这个亏, 尤其是在面深信服的测开岗的时候, 一面的难度适中, 加上面试官也没为难我, 侥幸让我过了. (以下是一面问题) 二面的时候, 主要问了项目和手撕算法. 当时项目个人觉得面的还不错, 因为本人是…

Mac下docker安装MySQL8.0.34

学习并记录一下如何用docker部署MySQL 在Docker中搜索并下载MySQL8.0.x的最新版本 下载好后&#xff0c;在Images中就可以看到MySQL的镜像了 通过下面的命令也可以查看docker images启动镜像&#xff0c;使用下面的命令就可以启动镜像了docker run -itd --name mysql8.0.34 -…

java项目log4j2单独为某个类配置日志文件

在项目中&#xff0c;一般都是把日志记录到一个日志文件中。 对应的log4j2.xml内容如下图所示&#xff1a;只有一个RollingFile节点&#xff0c;整个系统只会生成一个log日志文件。 生成的日志文件如下图&#xff1a; 当系统不断扩大&#xff0c;业务越来越复杂&#xff0c;所…

spark on hive

需要提前搭建好hive&#xff0c;并对hive进行配置。 1、将hive的配置文件添加到spark的目录下 cp $HIVE_HOME/conf/hive-site.xml $SPARK_HOME/conf2、开启hive的hivemetastore服务 提前创建好启动日志存放路径 mkdir $HIVE_HOME/logStart nohup /usr/local/lib/apache-hi…

阶段五-Day02-jQuery

一、jQuery入门 1. 定义和特点 目前最流行的JavaScript函数库之一&#xff0c;对JavaScript进行了封装。 并不是一门新语言&#xff0c;而是将常用的、复杂的JavaScript操作进行函数化封装&#xff0c;封装后可以直接调用&#xff0c;大大降低了使用JavaScript的难度&#xf…