我们将对接口的调用提出优化方案,让大家学会如何提升请求访问效率。
如何学会高并发接口设计?
如何掌握线程与线程池?
如何学会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 >= #{paramMap.commitDateStart}
</if>
<if test="paramMap.commitDateEnd != null">
AND c.commit_date <= #{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 企业相册维护与展示
上传相册![](https://i-blog.csdnimg.cn/direct/b0e503148b93485ab10a08d897d409bc.jpeg)
修改相册
需要注意,这里是一个拼接后的字符串,而不是多个上传记录,拼接的目的是可以减少数据库的存储记录数,其次没必要每个记录单独存储。
把企业相册的mapper拷贝过来,service可以不需要,借用企业的service即可。
显示相册照片![](https://i-blog.csdnimg.cn/direct/98f5356168e64395a122241d5824f931.jpeg)
![](https://i-blog.csdnimg.cn/direct/49490f0f6d9c497da756ee183760f1a8.jpeg)
测试相册修改(企业信息维护)
运营端以及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![](https://i-blog.csdnimg.cn/direct/8994221cf36d4b1eba5ac24e3208ee3c.jpeg)
由于thenRun是没有返回值的,所以本来要返回的,被处理了,则定义的参数就需要改为void了。
thenAccept![](https://i-blog.csdnimg.cn/direct/8b2b371a09b5427db3a796ef8a3f8a20.jpeg)
thenApply![](https://i-blog.csdnimg.cn/direct/e3dba48093d247b68f9528f4b4f56e01.jpeg)
和whenComplete与handle对比
then可以认为是一个线程中相关联的业务,可以影响最终返回的结果值。相当于是打boss的一整个主线任务,thenxxx的运行是会影响打boss的最终结果的。因为执行到最后一个一个的节点都是连窜的,是一个聚合体,我们可以控制结果的返回或者不返回。
而whenComplete与handle则不是,仅代表整个任务完成以后去做一件别的事,他是一个回调函数,是可以是任何业务,因为一开始的任务完成后,可以返回值,所做的其他业务也并不太会影响这个值。相当于打boss过程的支线任务,支线剧情,分支不太会影响打boss的最终结果。
其实也就是说,when面向的是结果(整个任务完成后),then面向的是过程(前一个任务执行后)。
2-4 CompletableFuture 双重任务组合![](https://i-blog.csdnimg.cn/direct/1440dc2142a849c7a19cf99c67a62940.jpeg)
- runAfterBoth: 两个CompletableFuture任务都完成以后,执行action的业务
- runAfterEither:其中一个CompletableFuture任务完成,则执行aciton
需要注意:这两个api都无法获得参数以及返回数据。
accept
thenAcceptBoth: 两个任务都执行完了,可以接受两个任务的参数,但是thenAcceptBoth不能返回值,是void类型
acceptEither:同理,另个中其中一个
applly(thenCombine)
thenCombine: 两个任务都执行完了,可以接受两个任务的参数,并且thenAcceptBoth可以返回值
applyToEither:同理,另个中其中一个
小节
所以以上的相关API完全可以把3个甚至多个任务组合起来进行执行甚至嵌套,如此可以更好的去管理多线程的执行顺序。
2-5 CompletableFuture 多重任务组合![](https://i-blog.csdnimg.cn/direct/14a4a139ed1f4cd487d368602fa777f7.jpeg)
- allOf: 等待所有任务完成
- anyOf: 所有任务中只要有一个完成
allOf![](https://i-blog.csdnimg.cn/direct/992f4cf6c9d1481286d431e8b10169b6.jpeg)
如果不加allOf.get()
这一行代码,则后续代码是异步进行的,不会阻塞。
anyOf![](https://i-blog.csdnimg.cn/direct/cfb8a1a7d72e4f2bbf631ec35bf10392.jpeg)
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 高并发测试查询性能![](https://i-blog.csdnimg.cn/direct/c65988369f2d4b579dc9a9b4735f54de.jpeg)
![](https://i-blog.csdnimg.cn/direct/4ae07020b0a64153b242320405c3d270.jpeg)
![](https://i-blog.csdnimg.cn/direct/d7ef07bea8444f38b0813c7ae7c8e4d1.jpeg)
![](https://i-blog.csdnimg.cn/direct/668ce6a416534fb6a8d9742a1a301391.jpeg)
通过对比,很明显,使用多线程要比普通查询更快。只不过本地测试的不是很明显,因为网络带宽不存在延迟。
2-8 完善HR信息维护
主要多了如下两栏:有个前端小bug改一下:
修改用户的BO中添加一下字段:
2-9 完善HR个人标签与签名![](https://i-blog.csdnimg.cn/direct/353545194e55410ab543a536ce866073.jpeg)
2-10 企业后台查询HR列表![](https://i-blog.csdnimg.cn/direct/8a47b6e02efe423bb04ad5a2750342dc.jpeg)
![](https://i-blog.csdnimg.cn/direct/2e2144fdae8a42a2b11cb348cbf36018.jpeg)
![](https://i-blog.csdnimg.cn/direct/13c83f812c8e4bf4be86dbb1f600ec5d.jpeg)
![](https://i-blog.csdnimg.cn/direct/9fba6670f728409facc7c45b81430557.jpeg)
测试如下:
2-11 代码最小成本方案 - 离职解绑![](https://i-blog.csdnimg.cn/direct/bc830d2628fa480f85effb8d065067f1.jpeg)
![](https://i-blog.csdnimg.cn/direct/8fda287792894c2984039fdc9ec2043d.jpeg)
![](https://i-blog.csdnimg.cn/direct/44104d776b9f4c9fba72d04e35657a4f.jpeg)
此处MP不够灵活,我们需要更新为null或者“”的时候,是无法执行的,因为我们加了配置 [not_empty]
解决方案:
- 可以在当前user的dto(pojo)针对字段做单独的设置,设置为忽略,如此可以更新为null或者“”,但是这么做会破坏dto(pojo),未来重新逆向生成不注意会覆盖这个类并且产生bug
- 对当前users的dto复制一份新的,忽略null以及“”进行保存,但是这么做会额外增加dto的冗余,没有必要,重新逆向生成可能会遗漏修改
- 手动写sql脚本
缺点:可以用,但是直接破坏当前代码 -
- 保持当前设置不变,对本字段设置字符串为“0”,0本身也没有任何意义,多表关联也查询不出任何信息。而且通过0也能够表示这个用户以前是HR,只是现在离职了。
缺点:虽然可以保证业务正常,但是难看了一点,对业务不了解的新人接触代码后可能会觉得莫名其妙或者吐槽代码
- 保持当前设置不变,对本字段设置字符串为“0”,0本身也没有任何意义,多表关联也查询不出任何信息。而且通过0也能够表示这个用户以前是HR,只是现在离职了。