异步并发利器:实际项目中使用CompletionService提升系统性能
1|0场景
随着互联网应用的深入,很多传统行业也都需要接入到互联网。我们公司也是这样,保险核心需要和很多保险中介对接,比如阿里、京东等等。这些公司对于接口服务的性能有些比较高的要求,传统的核心无法满足要求,所以信息技术部领导高瞻远瞩,决定开发互联网接入服务,满足来自性能的需求。
2|0概念
CompletionService将Executor和BlockingQueue的功能融合在一起,将Callable任务提交给CompletionService来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时被封装为Future。对于更多的概念,请参阅其他网络文档。
线程池的设计,阿里开发手册说过不要使用Java Executors 提供的默认线程池,因此需要更接近实际的情况来自定义一个线程池,根据多次压测,采用的线程池如下:
说明:公司的业务为低频交易,对于单次调用性能要求高,但是并发压力根本不大,所以 阻塞队列已满且线程数达到最大值时所采取的饱和策略为调用者执行。
3|0实现
3|1业务
投保业务主要涉及这几个大的方面:投保校验、核保校验、保费试算
- 投保校验:最主要的是要查询客户黑名单和风险等级,都是千万级的表。而且投保人和被保人都需要校验
- 核保校验:除了常规的核保规则校验,查询千万级的大表,还需要调用外部智能核保接口获得用户的风险等级,投保人和被保人都需要校验
- 保费试算:需要计算每个险种的保费
3|2设计
根据上面的业务,如果串行执行的话,单次性能肯定不高,所以考虑多线程异步执行获得校验结果,再对结果综合判断
- 投保校验:采用一个线程(也可以根据投保人和被保人数量来采用几个线程)
- 核保校验:
- 常规校验:采用一个线程
- 外部调用:有几个用户(指投保人和被保人)就采用几个线程
- 保费计算:有几个险种就采用几个线程,最后合并得到整个的保费
3|3代码
以下代码是样例,实际逻辑已经去掉
先创建投保、核保(常规、外部调用)、保费计算4个业务服务类:
投保服务类:
InsuranceVerificationServiceImpl,假设耗时50ms
核保常规校验服务类:
UnderwritingCheckServiceImpl,假设耗时50ms
核保外部调用服务类:ExternalCallServiceImpl,假设耗时200ms
试算服务类:
TrialCalculationServiceImpl,假设耗时50ms
统一返回接口类:TaskResponseModel, 上面4个服务的方法统一返回TaskResponseModel
注:
- key为这次调用的唯一标识,由调用者传进来
- resultCode结果码,200为成功,400表示有异常
- resultMessage信息,表示不成功或者异常信息
- data业务处理结果,如果成功的话
- 这些服务类都是单例模式
要使用用CompletionService的话,需要创建实现了Callable接口的线程
投保Callable:
核保常规校验Callable:
核保外部调用Callable:
试算调用Callable:
注:
- 每一次调用,需要创建这4种Callable
- 返回统一接口TaskResopnseModel
异步执行的类:TaskExecutor
注:
- 为单例模式
- 接收参数为List<Callable<TaskResponseModel<Object>>>,也就是上面定义的4种Callable的列表
- 返回List<TaskResponseModel<Object>>,也就是上面定义4种Callable返回的结果列表
- 我们的业务是对返回结果统一判断,业务返回结果有因果关系
- 如果线程执行有异常,也返回List<TaskResponseModel>,这个时候列表中只有一个TaskResponseModel,key为error, 后续调用者可以通过这个来判断线程是否执行成功;
调用方:
CompletionServiceController
注:
1.为测试方便,提供两个接口调用:一个是串行执行,一个是异步并发执行
2.在异步并发执行函数asyncExecute中:
- 根据有多少个被保人,创建多少个外部调用的Callable实例,key值为EXTERNALCALL_KEY + insured.getIdcard(),在一次保单投保调用中,每一个被保人Callable的key是不一样的。
- 根据有多少个险种,创建多少个试算的Callable实例,key为TRIA_KEY + risk.getRiskcode(),在一次保单投保调用中,每一个险种的Callable的key是不一样的
- 创建投保校验的Callable实例,业务上只需要一个
- 创建核保校验的Callable实例,业务上只需要一个
- 将Callable列表传入到TaskExecutor执行异步并发调用
- 根据返回结果来判断,通过判断返回的TaskResponseModel的key值可以知道是哪类业务校验,分别进行判断,还可以交叉判断(公司的业务就是要交叉判断)
4|0验证
验证数据:
上面数据表明:有两个被保人,两个险种。按照我们上面的定义,会调用两次外部接口,两次试算,一次投保,一次核保。而在样例代码中,一次外部接口调用耗时为200ms, 其他都为50ms.
本地开发的配置为8C16G:
- 同步串行接口调用计算:2 * 200 + 2 * 50 + 50 + 50 = 600ms
- 多线程异步执行调用计算:按照多线程并发执行原理,取耗时最长的200ms
验证:同步接口
输出耗时:可以看到耗时601ms
验证:多线程异步执行接口
输出耗时:可以看到为204ms
结果:基本和我们的预期相符合。
5|0结束
这是将实际生产中的例子简化出来,具体生产的业务比较复杂,不便于展示。
实际情况下,原来的接口需要1000ms以上才能完成单次调用,有的需要2000-3000ms。现在的接口,在生产两台8c16g的虚拟机, 经过4个小时的简单压测能够支持2000用户并发,单次返回时长为350ms左右,服务很稳定,完全能够满足公司的业务发展需求。
__EOF__

本文链接:https://www.cnblogs.com/caicz/p/15097463.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人