【多线程】多线程接口并行对数据字典的查询优化

我们将对接口的调用提出优化方案,让大家学会如何提升请求访问效率。
如何学会高并发接口设计?
如何掌握线程与线程池?
如何学会CompletableFuture异步编排?
如何学会线程有序与并行?
如何学会线程任务、多任务组组合?

1-2 企业审核 - 查询企业列表

运营端查询企业,由于并发量不会像前端那么大,所以直接查数据库就行。
controller

service:

自定义mapper:

<select id="queryCompanyList"
        resultType="com.imooc.pojo.vo.CompanyInfoVO"
        parameterType="Map">

    SELECT
        c.id AS companyId,
        c.company_name AS companyName,
        c.short_name AS shortName,
        c.logo AS logo,
        c.address AS address,
        c.nature AS nature,
        c.people_size AS peopleSize,
        c.industry AS industry,
        c.financ_stage AS financStage,
        c.review_status AS reviewStatus,
        c.commit_date AS commitDate,
        u.real_name AS commitUser
    FROM
        company c
    LEFT JOIN
        users u
    ON
        c.commit_user_id = u.id
    WHERE
        1 = 1
        <if test="paramMap.companyName != null and paramMap.companyName != ''">
            AND c.company_name LIKE '%${paramMap.companyName}%'
        </if>
        <if test="paramMap.commitUser != null and paramMap.commitUser != ''">
            AND u.real_name LIKE '%${paramMap.commitUser}%'
        </if>
        <if test="paramMap.reviewStatus != null and paramMap.reviewStatus >= 0">
            AND c.review_status = #{paramMap.reviewStatus}
        </if>
        <if test="paramMap.commitDateStart != null">
            AND c.commit_date &gt;= #{paramMap.commitDateStart}
        </if>
        <if test="paramMap.commitDateEnd != null">
            AND c.commit_date &lt;= #{paramMap.commitDateEnd}
        </if>
    ORDER BY c.commit_date DESC

</select>

1-3 企业审核 - 获得企业详情

controller:service:自定义mapper:

<select id="getCompanyInfo"
            resultType="com.imooc.pojo.vo.CompanyInfoVO"
            parameterType="Map">

    SELECT
        c.id AS companyId,
        c.company_name AS companyName,
        c.short_name AS shortName,
        c.logo AS logo,
        c.province AS province,
        c.city AS city,
        c.district AS district,
        c.address AS address,

        c.nature AS nature,
        c.people_size AS peopleSize,
        c.industry AS industry,
        c.financ_stage AS financStage,

        c.work_time AS workTime,
        c.introduction AS introduction,
        c.advantage AS advantage,
        c.benefits AS benefits,
        c.bonus AS bonus,
        c.subsidy AS subsidy,

        c.review_status AS reviewStatus,
        c.review_replay AS reviewReplay,

        c.commit_date AS commitDate,
        c.commit_user_id AS commitUserId,
        u.real_name AS commitUser,
        c.commit_user_mobile AS commitMobile,

        c.legal_representative AS legalRepresentative,
        c.regist_capital AS registCapital,
        c.regist_place AS registPlace,
        c.build_date AS buildDate,

        c.auth_letter AS authLetter,
        c.biz_license AS bizLicense
    FROM
      company c
    LEFT JOIN
      users u
    ON
      c.commit_user_id = u.id
    WHERE
      c.id = #{paramMap.companyId}

</select>

1-4 企业审核 - 执行审核

更新企业状态

controller:service:

修改用户角色

controller:service:

feign:

1-5 审核流程的优化思考

拓展:针对审核流程的思考以及优化?

目前审核,其实我们是把审核的状态和审核的驳回信息放在了企业表:

其实这么做也不太好,因为用户第一次入驻的时候审核没问题,但是一旦企业审核通过,如果有人需要再次入驻,申请成为这家企业的HR,那么这个时候企业的状态又会重新变更为审核状态。

考虑一下怎么优化:

  • 新增审核表,审核企业的时候需要做三表关联查询,也就是用户表+企业表+审核表。这样做可以看到更多信息,也可以有每次的审核记录,现有的做法是无法看到历史审核记录的。而且客服审核的时候只需要关注用户就行,用户的信息以及授权书没问题就审核通过了。
  • 当然这么做,也会有缺点,缺点是,三表关联查询很多企业是不可以使用的,甚至有的公司禁止三表,只允许到两表关联,一般来说基本上3表是上限,超过3表查询一定要拆分组装。因为高并发下一定会有性能影响。
    所以关于这个拓展大家可以课后去实现一些,作为第二种的审核记录的模式。

那么目前,我们这里做的是针对单表企业表查询并且审核:
优点是单表查询更快更便捷,而且从业务上来说,如果企业的营业执照发生变更,我们也可以重新进行审核。缺点是没有HR提交的审核记录。但是可以一定程度上对企业进行重新审查,因为审核字段是在企业表里的,所以在进行新HR入驻的时候,可以再次对企业信息做一个核查,这其实也可以。

所以说,各自有各自的优缺点,业务规范和流程要按照自身情况去决定,不要为了技术而技术,当你能够考虑的更多的时候,你就成长了,毕竟我们未来是要向架构师成长的嘛。

如果说,在这个地方,一定要满足性能的情况之下,也要有审核记录呢?那么可以使用mongodb,审核记录插入到mongo,因为这是非重要数据,从业务上来说,允许丢失,而且mongo的并发能力比数据库高。所以可以使用。但是mongodb也不支持多表关联或者说多表查询不太好,所以可以做数据的冗余,把审核相关字段,提交者的相关信息,企业信息,全部写到一个表里就行了。

时间匆促,所以以最短的业务流程来做,当然用最优的方式是更好的。所以,如果你在一个公司开发相关功能的时候,给你的时间很少,但是要把相关的业务走通,这个时候,你要选择最短最有路径,而不是一整个完备的技术方案,前期先上线,满足现有需求,后期在进行功能的演变与改进,这才是一个合格的高级开发甚至技术经理的取巧之道。

1-6 SAAS端企业基本信息查询

controller 简单做一个查询即可

1-7 SAAS端企业详情展示

controller:

1-8 HR维护企业信息

app端的企业信息目前接口没有,先做一个,然后再做其他修改的地方:此处接口使用的是根据企业id做的查询,所以这里新增一个接口即可。

企业的相关信息修改在此页面:

修改企业信息

修改企业信息我们只需要一个接口就行,不需要太多。

controller:

service:

最后根据每个修改的字段进行断点调试来进行测试即可。

1-9 企业维护测试

1-10 企业相册维护与展示

上传相册

修改相册

需要注意,这里是一个拼接后的字符串,而不是多个上传记录,拼接的目的是可以减少数据库的存储记录数,其次没必要每个记录单独存储。

把企业相册的mapper拷贝过来,service可以不需要,借用企业的service即可。

显示相册照片

测试相册修改(企业信息维护)

运营端以及saas端的相册展示

1-11 企业亮点数据字典列表展示

数据字典的logo显示
controller:service:

测试网页端

登录运营端和saas端,测试企业详情的展示

1-12 多线程的初始化方式

目前我们在查询数据字典的时候,是分为了4次查询,这4个查询,本质上并没有任何关联,也没有任何依赖的顺序,只不过他们是同步进行的,我们可以考虑对这个接口进行优化,因为不仅仅是当前这个接口,其实有时候我们在处理数据的时候,一个方法里的几个任务,都需要进行异步的并行的执行,以此提高效率,所以,完全可以使用多线程的方式来做。

所以后续几节课的内容,我们会通过学习多线程来优化咱们的接口(需要注意,不仅仅是这个接口,很多业务都可以结合多线程来做,甚至一些架构底层的代码也是)

多线程

咱们课程的同学可能有部分没有接触过多线程,没有关系,我们一步一步会带你学习并且来实现多线程,而且这是非常重要的一个项目经验。

多线程的初始化方式:

  • 继承Thread
  • 实现Runnalbe接口
  • 实现callable接口(从命名就能看出,这是一个回调,我们是可以有回调通知的)
  • 线程池(使用居多,我们自己就是用的线程池,包括定时任务也是)

区别:
第一种第二种平时就是简单玩玩的,而且不能发挥计算的结果,使用场景覆盖不全。
第三种是可以获得计算结果,但是对于线程的资源不好控制,可能会占用大量的服务器资源,从而导致其他中间件崩溃。
第四种是比较好也是比较通用的方式,各方面都可以兼顾到。

四种线程实现方式 - 继承Thread

四种线程实现方式 - 实现Runnalbe接口

四种线程实现方式 - 实现callable接口

FutureTask可以传入callable,所以直接通过FutureTask来运行即可:

1-13 线程池的运行方式

四种线程实现方式 - 线程池

前面的3中方式,我们都是通过new Thread().start()来执行的。这其实不太好,是有问题的,会造成资源的浪费。

举个例子:我去食堂打饭,我要打一份五花肉,一份糖醋排骨,一个番茄蛋花汤,一份干锅花菜,还有一份饭,这个时候,我需要5个碗,没有碗我就得去买碗吧。于是我每次吃饭每打一个菜,就买一个碗,一个学期下来,我宿舍的柜子堆满了碗,放不下了,溢出了,我还是继续买碗浪费占用宿舍的空间,很显然,我发神经了对吧?

这是不符合常理的,我们应该有效的利用空余的碗,而不是用完了就放着,这样就是占用咱们的空间了。同理线程也是一个意思,每次运行都要new一个线程,那么开了太多的线程,最终会把内存给占用,空闲的线程在那边啥事都不干,这不就是浪费资源嘛?所以我们应该有效的利用线程,把线程管理起来,需要利用线程池。

也就是说,我现在吃饭用碗,我就只限定打5个菜,必须吃完了,盘子饭碗空了才能继续去打菜盛饭,如此一来,我就更有效的对饭碗进行了资源控制,不会浪费,哪怕我不吃饭了,那么空余的开销也就是5个空盆子,也不会占用我过多的空间。所以这就是线程池的概念,线程池里只有5个线程,每个线程忙完了才会去执行下一个任务,如果没有则空闲。

所以需要线程池来进行更好的管理,才是更有效的方式。

使用线程池

通过Executors可以初始化线程池进行配置,如下参考代码补全:

线程池可以共享共性任务,所以独立出一个类也行:submit可以有返回值,execute则没有,这个需要注意。
测试使用:

如此一来,哪怕有1万个任务进来,那么我们也只会有3个线程分批去消费任务,是每3个一个批次,而不是直接new一万个线程去执行了,更加保护了我们的内存,可以让系统更加稳定的运行。

其他可用的线程池:通过以上对线程的实现,相信大家会对线程有很高的接触,如果你是第一次接触,一定要好好理解,如果你接触过并且写过线程,这里可以直接作为复习内容观赏一下即可。

1-14 深入浅出线程池

上一节课,Executors可以用于创建线程池,除此以外,还可以通过ThreadPoolExecutor来进行创建和使用。线程池很重要,这些参数务必理解:定义线程池如下:

任务队列(workQueue):

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  • DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  • SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  • LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
  • LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

本节课务必理解通透,面试的时候大概率会被问,不问线程池也是变着法来结合中间件给你提问的。

小节

简单小节一下,平时开发过程中使用线程池的情况是很多的,使用线程池当然也有很多优势:

  • 可以减少过多资源的损耗,因为可以重复在线程池中任务结束的那些线程。
  • 提高业务调用的速度,因为异步线程可以更高的效率执行,任务进来直接丢给线程即可,哪怕再多也有阻塞队列。
  • 易于管理,因为比普通线程的使用更方便,避免额外的系统资源开销,也保证了整体系统平台的稳定性。

2-1 多线程异步任务编排CompletableFuture

提问

有一个场景,就是我们一个异步线程运行之后需要依赖另外一个异步线程,不同的异步线程也是有先后顺序的,比如AB线程同时运行,但是C线程依赖A,D线程依赖C运行后,所以这个时候怎么办呢?参考如下:

如此,我们其实可以通过CompletableFuture来实现对任务的编排,按照我们所想的来进行线程运行即可。

CompletableFuture是JDK8以后的产物,JDK8之前,我们使用的多线程,主要是通过Thread+Runnable来实现,但是这种方式有个弊端就是没有返回值。如果想要返回值怎么办呢,大多数人就会想到 Callable + Thread 的方式来获取到返回值,但是如此会阻塞主线程。所以就非常尴尬。

CompletableFuture是可以让我们对异步任务进行编排的,它提供了更强大的异步能力,可以帮我们解决痛点。本质上,他就是一个编排工具,如果你现在实在不理解也没关系,后面我们通过编码你就懂了,现阶段,你只需要理解他是类似于禅道或者说工作流的这么一个工具,可以协助我们更好的管理任务和任务之间的组合依赖,谁先谁后。

这4个方法可以运行,其中run是无返回,supply是有返回值。

初始化CompletableFuture

初始化CompletableFuture的runAsync方式:

初始化CompletableFuture的supplyAsync方式:

由于传入的本身是一个接口,所以直接使用lambda的箭头函数即可:

2-2 CompletableFuture 完成后回调

CompletableFuture在完成后,可以执行业务回调的方法,其实这与前端js的promise是类似的,可以起到编排的效果:

whenComplete: 字面意思,当完成了,则执行xxx业务异常后重设兜底数据如下不能获得异常的效果

handle 返回结果处理

whenComplete 的使用,是void无返回的:如果这个时候,还是需要处理返回,那么则需要使用到handle:

2-3 CompletableFuture 异步任务的顺序执行

上一节的whenComplete是任务完成后执行,是一个回调,但是如果需要多个异步任务,都是按照顺序去执行,那么则需要使用到thenXxx

  • thenRun: 任务完成后执行后续任务,无参,void
  • thenAccept: 任务完成后执行后续任务,可以接受到上一个任务返回的值作为入参,void
  • thenApply: 任务完成后执行后续任务,可以接受到上一个任务返回的值作为入参,并且再返回一个值

thenRun

由于thenRun是没有返回值的,所以本来要返回的,被处理了,则定义的参数就需要改为void了。

thenAccept

thenApply

和whenComplete与handle对比

then可以认为是一个线程中相关联的业务,可以影响最终返回的结果值。相当于是打boss的一整个主线任务,thenxxx的运行是会影响打boss的最终结果的。因为执行到最后一个一个的节点都是连窜的,是一个聚合体,我们可以控制结果的返回或者不返回。

而whenComplete与handle则不是,仅代表整个任务完成以后去做一件别的事,他是一个回调函数,是可以是任何业务,因为一开始的任务完成后,可以返回值,所做的其他业务也并不太会影响这个值。相当于打boss过程的支线任务,支线剧情,分支不太会影响打boss的最终结果。

其实也就是说,when面向的是结果(整个任务完成后),then面向的是过程(前一个任务执行后)。

2-4 CompletableFuture 双重任务组合

  • runAfterBoth: 两个CompletableFuture任务都完成以后,执行action的业务
  • runAfterEither:其中一个CompletableFuture任务完成,则执行aciton
    需要注意:这两个api都无法获得参数以及返回数据。

accept

thenAcceptBoth: 两个任务都执行完了,可以接受两个任务的参数,但是thenAcceptBoth不能返回值,是void类型

acceptEither:同理,另个中其中一个

applly(thenCombine)

thenCombine: 两个任务都执行完了,可以接受两个任务的参数,并且thenAcceptBoth可以返回值

applyToEither:同理,另个中其中一个

小节

所以以上的相关API完全可以把3个甚至多个任务组合起来进行执行甚至嵌套,如此可以更好的去管理多线程的执行顺序。

2-5 CompletableFuture 多重任务组合

  • allOf: 等待所有任务完成
  • anyOf: 所有任务中只要有一个完成

allOf

如果不加allOf.get()这一行代码,则后续代码是异步进行的,不会阻塞。

anyOf

2-6 多线程完善数据字典查询接口

创建自定义线程池配置:

放入spring容器管理:

@Autowired
private ThreadPoolExecutor threadPoolExecutor;
    
@PostMapping("app/getItemsByKeys")
public GraceJSONResult getItemsByKeys(@RequestBody QueryDictItemsBO itemsBO) throws Exception {

    CompletableFuture<List<DataDictionary>> advantageFuture = CompletableFuture.supplyAsync(() -> {
        String advantage[] = itemsBO.getAdvantage();
        List<DataDictionary> advantageList = dictionaryService.getItemsByKeys(advantage);
        return advantageList;
    }, threadPoolExecutor);

    CompletableFuture<List<DataDictionary>> benefitsFuture = CompletableFuture.supplyAsync(() -> {
        String benefits[] = itemsBO.getBenefits();
        List<DataDictionary> benefitsList = dictionaryService.getItemsByKeys(benefits);
        return benefitsList;
    }, threadPoolExecutor);

    CompletableFuture<List<DataDictionary>> bonusFuture = CompletableFuture.supplyAsync(() -> {
        String bonus[] = itemsBO.getBonus();
        List<DataDictionary> bonusList = dictionaryService.getItemsByKeys(bonus);
        return bonusList;
    }, threadPoolExecutor);

    CompletableFuture<List<DataDictionary>> subsidyFuture = CompletableFuture.supplyAsync(() -> {
        String subsidy[] = itemsBO.getSubsidy();
        List<DataDictionary> subsidyList = dictionaryService.getItemsByKeys(subsidy);
        return subsidyList;
    }, threadPoolExecutor);


    CompletableFuture allOf = CompletableFuture.allOf(advantageFuture,
                                                      benefitsFuture,
                                                      bonusFuture,
                                                      subsidyFuture);
    allOf.get();

    // FIXME 思考:如下代码是不是放在各自任务中更好?
    CompanyPointsVO list = new CompanyPointsVO();
    list.setAdvantageList(advantageFuture.get());
    list.setBenefitsList(benefitsFuture.get());
    list.setBonusList(bonusFuture.get());
    list.setSubsidyList(subsidyFuture.get());

    return GraceJSONResult.ok(list);
}

2-7 apipost 高并发测试查询性能

通过对比,很明显,使用多线程要比普通查询更快。只不过本地测试的不是很明显,因为网络带宽不存在延迟。

2-8 完善HR信息维护

主要多了如下两栏:有个前端小bug改一下:修改用户的BO中添加一下字段:

2-9 完善HR个人标签与签名2-10 企业后台查询HR列表测试如下:2-11 代码最小成本方案 - 离职解绑

此处MP不够灵活,我们需要更新为null或者“”的时候,是无法执行的,因为我们加了配置 [not_empty]
解决方案:

  • 可以在当前user的dto(pojo)针对字段做单独的设置,设置为忽略,如此可以更新为null或者“”,但是这么做会破坏dto(pojo),未来重新逆向生成不注意会覆盖这个类并且产生bug
  • 对当前users的dto复制一份新的,忽略null以及“”进行保存,但是这么做会额外增加dto的冗余,没有必要,重新逆向生成可能会遗漏修改
  • 手动写sql脚本
    缺点:可以用,但是直接破坏当前代码
    1. 保持当前设置不变,对本字段设置字符串为“0”,0本身也没有任何意义,多表关联也查询不出任何信息。而且通过0也能够表示这个用户以前是HR,只是现在离职了。
      缺点:虽然可以保证业务正常,但是难看了一点,对业务不了解的新人接触代码后可能会觉得莫名其妙或者吐槽代码

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值