【一】问题描述
一个需求,通过线程池执行一个指标预警触发的功能,如果指标触发了预警,就会批量添加触发记录,存储是通过Mybatis-plus实现的,PO实体类会集成BasePo,自动获取当前用户添加到创建人更新人字段中。然后就出现一个问题,如果通过接口直接调用预警触发的方法,可以正常跑通,但是如果将方法放进线程池中,就会在createBatch方法爆出空指针错误。
经过测试,接口调用这个方法和多线程跑这个方法,到了createBatch这一步,除了创建人修改人创建时间修改时间这些自动生成的数据意外,其他的字段都是有数据的,对象也是有值的,所以就不知道是哪里爆空指针。
【二】多线程代码
定义Runable子类
private class CheckIndexWarnRunnable implements Runnable {
SecurityUser securityUser;
IndIndexTaskInstance taskInstance;
public CheckIndexWarnRunnable(IndIndexTaskInstance taskInstance, SecurityUser securityUser) {
this.taskInstance = taskInstance;
this.securityUser = securityUser;
}
@Override
public void run() {
SecurityUserHelper.set(securityUser);
List<IndIndex> indIndexList = Lists.newArrayList();
if (ObjectUtil.isNotNull(taskInstance.getCusIndexDefId())) {
indIndexList = indIndexRepo.queryByCusIndexId(taskInstance.getCusIndexDefId());
} else {
IndIndex indIndex = indIndexRepo.queryById(Long.valueOf(taskInstance.getIndexId()));
indIndexList = ListUtil.of(indIndex);
}
for (IndIndex indIndex : indIndexList) {
log.info("指标:{},开始预警校验", indIndex.getIndexCode());
//获取物理表,默认数据源
IndexStorageTableVO indexStorageTable = indIndexService.queryIndexStorageTable(indIndex.getId());
Tuple3<String, IDatabaseTemplate, DataSourceResponse> databaseTemplate = DatabaseUtil.queryIDatabaseTemplate3(indexStorageTable.getLinkDataSrcId());
List<Column> columns = databaseTemplate.getV2().columns(indexStorageTable.getSchemaName().toUpperCase(), indexStorageTable.getTableName().toUpperCase());
columns.sort(Comparator.comparingInt(Column::getColumnId));
List<String> columnNames = columns.stream().map(Column::getColumnName).collect(Collectors.toList());
indexStorageTable.getColumnNames().addAll(columnNames);
DbTypeEnum dbTypeEnum = DbTypeEnum.getInstance(databaseTemplate.getV1());
IndexWarnContext indexWarnContext = new IndexWarnContext();
indexWarnContext.setTaskInstance(taskInstance);
indexWarnContext.setDatabaseTemplate(databaseTemplate.getV2());
indexWarnContext.setDataSourceResponse(databaseTemplate.getV3());
indexWarnContext.setStorageTable(indexStorageTable);
//indexWarnContext.setDomainTableName(domainTableName);
indexWarnContext.setDbTypeEnum(dbTypeEnum);
indexWarnContext.setIndIndex(indIndex);
indIndexWarnService.checkStoreIndexWarn(indexWarnContext);
log.info("指标:{},预警校验完成", indIndex.getIndexCode());
}
}
}
放进线程池
for (IndIndexTaskInstance beSubmitedTask : indexTaskInstances) {
String taskInstanceKey = getTaskInstanceKey(beSubmitedTask);
//对实例加锁
RLock instanceLock = redissonClient.getLock(taskInstanceKey);
FutureTask<Boolean> futureTask = new FutureTask<>(() -> instanceLock.tryLock());
new Thread(futureTask).start();
if (!futureTask.get()) {
log.info("指标固化获取锁失败{}", taskInstanceKey);
return;
}
submitedCount += 1;
IndexStorageTaskRunnable runnable = new IndexStorageTaskRunnable(beSubmitedTask, SecurityUserHelper.get(), instanceLock);
executorService.execute(runnable);
// 执行预警监控
log.info("开始触发预警监控");
CheckIndexWarnRunnable checkIndexWarnRunnable = new CheckIndexWarnRunnable(beSubmitedTask, SecurityUserHelper.get());
executorService.execute(checkIndexWarnRunnable);
log.info("完成触发预警监控");
}
【三】问题排查
MybatisPlus是怎么做到自动填充创建人的呢,答案是通过拦截器
@Configuration
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
private void fillUserId(MetaObject metaObject, String fieldName, SecurityUser securityUser) {
if (metaObject.hasGetter(fieldName)) {
Object userId = this.getFieldValByName(fieldName, metaObject);
if (userId == null) {
this.setFieldValByName(fieldName, securityUser.getId(), metaObject);
}
}
}
}
通过拦截器获取securityUser对象,然后获取当前用户。
这个时候想到学习ThreadLocal的时候,使用场景就是在多线程异步的时候再线程本地存一份用户信息或者时间处理器对象。
所以想到可能是获取用户的securityUser对象在多线程情况下是获取不到的,在线程池外面创建的对象,在线程池里面并不可用
最后排查出来时因为没有把securityUser传进来导致的
【四】问题知识点
1-多线程
2-ThreadLocal
3-拦截器