目录
1、Bug案发现场
2、捉虫过程过程
3、解决方案与代码
4、成果展现与总结 编辑
5、参考文章
1、Bug案发现场
最近参与帮助以前同事实际业务开发中业务,在一个业务场景之中;使用H5页面通过二维码收集小微企业/个体工商户贷款业务需求。其中在获得用户输入企业名称后,获得企业或者个体工商户信用代码后,使用CompletableFuture的.supplyAsync方法实现异步保存企业基本信息到数据库之中;但是此异步操作类默认使用的ForkJoinPool 作为线程池。于是发生一个奇怪的Bug,在测试环境(test)【阿里云的ECS Docker JDK21+MySQL8.0.36+Redis 7.0.5】可以很正常实现数据保存;但是在正式环境部署到山东某市客户服务器上【Docker JDK21+Redis 7.0.5+MySQL5.7.44】死活无法保存基本公司信息到数据库里面。
具体出错问题就是下面这行代码
redisTemplate.boundValueOps(CACHE_KEY_PREFIX + key).get()
非法参数异常,未找到类对应的类。具体错误信息如下所示 :
+---[39.81% 0.872861ms ] vip.xiaonuo.common.cache.CommonCacheOperator:get() #203 [throws Exception]`---throw:java.lang.IllegalArgumentException #881 [org.springframework.data.redis.core.BoundValueOperations referenced from a method is not visible from class loader: 'app']
2、捉虫过程过程
我们在这个过程之中,怀疑过JDK21版本问题,退回到JDK17;怀疑过Redis问题也搭建相同的版本;所有的验证都是徒劳的。并且比较奇怪的Test环境一切正常。生产环境就是报错。从业务角度来说也是需要使用异步的方式来分开处理业务需求。甚至网上找到github上一篇类似文章。
https://github.com/spring-projects/spring-data-redis/issues/2772 【国内可能无法打开 上有截图】
最后思考后继续寻找异常关键词语
throw:java.lang.IllegalArgumentException #881 [org.springframework.data.redis.core.BoundValueOperations referenced from a method is not visible from class loader
于是SegmentFault 思否 网站上找一篇类似文章,有相关异常描述。最后定位并确定就是因为以前使用 CompletableFuture.supplyAsync() 异步方法,默认使用的 ForkJoinPool 作为线程池所导致的。
在有上述思路后,今天去开发现场,使用自己定义线程池;替换了默认ForkJoinPool线程池;时间这个困扰1周左右的问题。并且实现了基本公司信息正确正常入库。
3、解决方案与代码
自定义线程池
@Configuration
public class ThreadPoolConfig {// 核心线程数private int corePoolSize = 5;// 最大线程数private int maxPoolSize = 10;// 队列大小private int queueSize = 40;// 线程最大空闲时间private int keepAliveSeconds = 40;/*** 自定义消费队列线程池* CallerRunsPolicy 这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。* AbortPolicy 对拒绝任务抛弃处理,并且抛出异常。* DiscardPolicy 对拒绝任务直接无声抛弃,没有异常信息。* DiscardOldestPolicy 对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。* @return*/@Bean(value = "customAsyncTaskThreadPool")public ThreadPoolTaskExecutor buildCustomAsyncTaskThreadPool() {ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();threadPool.setCorePoolSize(corePoolSize);threadPool.setMaxPoolSize(maxPoolSize);threadPool.setQueueCapacity(queueSize);threadPool.setThreadNamePrefix("customAsyncTaskThreadPool-");threadPool.setKeepAliveSeconds(keepAliveSeconds);threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//如果@Bean 就不需手动,会自动InitializingBean的afterPropertiesSet来调initialize//threadPool.initialize();return threadPool;}
@Resource
@Qualifier("customAsyncTaskThreadPool")
private Executor asyncTaskThreadPoolExecutor;private void saveCompanyBasicInfo(CompanyLoanDemandCreateParam companyLoanDemandCreateParam) {CompletableFuture.supplyAsync(() -> {try {String uscc = companyLoanDemandCreateParam.getUscc();String key = String.format(BusinessConstant.WE_COMPANY_CACHE_KEY, uscc);log.error("key "+key);Object data =null;try {data =commonCacheOperator.get(key);} catch (Exception e) {log.error(e.toString());}if (ObjectUtils.isEmpty(data)) {//处理业务内容}} catch (ParseException e) {log.error("saveCompanyBasicInfo error", e);}return null;},asyncTaskThreadPoolExecutor);
4、成果展现与总结
通过解决困扰我同事与我这个问题;让我明白一个道理,如同最近在看《毛选--->实践论与矛盾论》要解决实际问题只有深入实践并且注意矛盾的特殊性;因为每个开发场景可能与网上文章有相似【矛盾的普遍性】但是又有其自己开发与运行环境的【特殊性】;只有抓住特殊性并结合普遍性才能够找到解决问题的主要矛盾;并使得获得有效的方法论去解决实际问题。
5、参考文章
ForkJoinPool 导致类加载器加载类失败
springboot自定义线程池,CompletableFuture实现多线程并发
生产问题之CompletableFuture默认线程池踩坑,请务必自定义线程池
CompletableFuture使用自定义线程池实现多任务结果聚合返回