2026-02-05 13:33:27 +08:00
|
|
|
|
# 中介黑名单导入唯一性校验优化说明
|
|
|
|
|
|
|
|
|
|
|
|
## 优化时间
|
|
|
|
|
|
2026-02-05
|
|
|
|
|
|
|
|
|
|
|
|
## 优化目的
|
|
|
|
|
|
优化批量导入中介黑名单数据时的唯一性校验性能,解决N+1查询问题。
|
|
|
|
|
|
|
|
|
|
|
|
## 问题描述
|
|
|
|
|
|
|
|
|
|
|
|
### 原实现问题
|
|
|
|
|
|
在导入个人中介和实体中介数据时,原实现存在以下性能问题:
|
|
|
|
|
|
|
|
|
|
|
|
1. **N+1查询问题**
|
|
|
|
|
|
- 在循环中对每条记录调用 `checkPersonIdUnique` 或 `checkSocialCreditCodeUnique`
|
|
|
|
|
|
- 导入1000条数据时,产生1000次数据库查询
|
|
|
|
|
|
- 代码位置:
|
|
|
|
|
|
- `CcdiIntermediaryServiceImpl.importIntermediaryPerson:291`
|
|
|
|
|
|
- `CcdiIntermediaryServiceImpl.importIntermediaryEntity:409`
|
|
|
|
|
|
|
|
|
|
|
|
2. **重复查询问题**
|
|
|
|
|
|
- 唯一性校验查询一次(1000次)
|
|
|
|
|
|
- 获取bizId再次批量查询一次(1次)
|
|
|
|
|
|
- 总计1001次数据库查询
|
|
|
|
|
|
|
|
|
|
|
|
3. **性能瓶颈**
|
|
|
|
|
|
- 大量数据导入时响应慢
|
|
|
|
|
|
- 数据库连接占用时间长
|
|
|
|
|
|
- 网络往返次数多
|
|
|
|
|
|
|
|
|
|
|
|
## 优化方案
|
|
|
|
|
|
|
|
|
|
|
|
### 核心思路
|
|
|
|
|
|
**将"循环中逐条查询"改为"一次性批量查询,内存中快速判断"**
|
|
|
|
|
|
|
|
|
|
|
|
### 优化实现
|
|
|
|
|
|
|
|
|
|
|
|
#### 1. 个人中介导入优化(importIntermediaryPerson)
|
|
|
|
|
|
|
|
|
|
|
|
**优化前:**
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 第一轮:数据验证和分类
|
|
|
|
|
|
for (int i = 0; i < list.size(); i++) {
|
|
|
|
|
|
// 检查唯一性 - 每次循环都查询数据库
|
|
|
|
|
|
if (!checkPersonIdUnique(excel.getPersonId(), null)) { // ❌ N+1查询
|
|
|
|
|
|
// ...
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二轮:批量处理
|
|
|
|
|
|
if (!updateList.isEmpty()) {
|
|
|
|
|
|
// 再次查询已存在记录的bizId - 重复查询
|
|
|
|
|
|
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
|
|
|
|
|
List<CcdiBizIntermediary> existingList = bizIntermediaryMapper.selectList(wrapper);
|
|
|
|
|
|
// ...
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**优化后:**
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 第一轮:收集所有personId
|
|
|
|
|
|
for (CcdiIntermediaryPersonExcel excel : list) {
|
|
|
|
|
|
if (StringUtils.isNotEmpty(excel.getPersonId())) {
|
|
|
|
|
|
personIds.add(excel.getPersonId());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二轮:批量查询已存在的记录 - 只查询一次 ✅
|
|
|
|
|
|
java.util.Map<String, String> personIdToBizIdMap = new java.util.HashMap<>();
|
|
|
|
|
|
if (!personIds.isEmpty()) {
|
|
|
|
|
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
|
wrapper.select(CcdiBizIntermediary::getBizId, CcdiBizIntermediary::getPersonId);
|
|
|
|
|
|
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
|
|
|
|
|
List<CcdiBizIntermediary> existingList = bizIntermediaryMapper.selectList(wrapper);
|
|
|
|
|
|
|
|
|
|
|
|
// 建立personId到bizId的映射
|
|
|
|
|
|
for (CcdiBizIntermediary existing : existingList) {
|
|
|
|
|
|
personIdToBizIdMap.put(existing.getPersonId(), existing.getBizId());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第三轮:数据验证和分类 - 使用Map快速判断
|
|
|
|
|
|
for (int i = 0; i < list.size(); i++) {
|
|
|
|
|
|
// 使用Map快速判断是否存在 - O(1)复杂度,不查询数据库 ✅
|
|
|
|
|
|
String existingBizId = personIdToBizIdMap.get(excel.getPersonId());
|
|
|
|
|
|
if (existingBizId != null) {
|
|
|
|
|
|
// 记录已存在
|
|
|
|
|
|
if (updateSupport) {
|
|
|
|
|
|
person.setBizId(existingBizId); // 直接使用缓存中的bizId
|
|
|
|
|
|
updateList.add(person);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
insertList.add(person);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第四轮:批量处理 - 直接插入和更新,无需额外查询 ✅
|
|
|
|
|
|
bizIntermediaryMapper.insertBatch(insertList);
|
|
|
|
|
|
bizIntermediaryMapper.updateBatch(updateList);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 2. 实体中介导入优化(importIntermediaryEntity)
|
|
|
|
|
|
|
|
|
|
|
|
**优化后实现:**
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 第一轮:收集所有socialCreditCode
|
|
|
|
|
|
for (CcdiIntermediaryEntityExcel excel : list) {
|
|
|
|
|
|
if (StringUtils.isNotEmpty(excel.getSocialCreditCode())) {
|
|
|
|
|
|
socialCreditCodes.add(excel.getSocialCreditCode());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二轮:批量查询已存在的记录 - 只查询一次 ✅
|
|
|
|
|
|
java.util.Map<String, CcdiEnterpriseBaseInfo> existingEntityMap = new java.util.HashMap<>();
|
|
|
|
|
|
if (!socialCreditCodes.isEmpty()) {
|
|
|
|
|
|
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
|
wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, socialCreditCodes);
|
|
|
|
|
|
List<CcdiEnterpriseBaseInfo> existingList = enterpriseBaseInfoMapper.selectList(wrapper);
|
|
|
|
|
|
|
|
|
|
|
|
// 建立socialCreditCode到实体的映射
|
|
|
|
|
|
for (CcdiEnterpriseBaseInfo existing : existingList) {
|
|
|
|
|
|
existingEntityMap.put(existing.getSocialCreditCode(), existing);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第三轮:数据验证和分类 - 使用Map快速判断 ✅
|
|
|
|
|
|
for (int i = 0; i < list.size(); i++) {
|
|
|
|
|
|
CcdiEnterpriseBaseInfo existingEntity = existingEntityMap.get(excel.getSocialCreditCode());
|
|
|
|
|
|
if (existingEntity != null) {
|
|
|
|
|
|
// 记录已存在
|
|
|
|
|
|
if (updateSupport) {
|
|
|
|
|
|
updateList.add(entity);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
insertList.add(entity);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 优化技巧
|
|
|
|
|
|
|
|
|
|
|
|
1. **批量查询**
|
|
|
|
|
|
- 使用 `wrapper.in()` 一次性查询所有待校验的键值
|
|
|
|
|
|
- 减少数据库往返次数
|
|
|
|
|
|
|
|
|
|
|
|
2. **内存映射**
|
|
|
|
|
|
- 使用 `HashMap` 存储查询结果
|
|
|
|
|
|
- O(1)时间复杂度的快速查找
|
|
|
|
|
|
|
|
|
|
|
|
3. **查询优化**
|
|
|
|
|
|
- 使用 `wrapper.select()` 只查询需要的字段
|
|
|
|
|
|
- 减少数据传输量
|
|
|
|
|
|
|
|
|
|
|
|
4. **提前收集**
|
|
|
|
|
|
- 在第一轮循环中收集所有待校验的键值
|
|
|
|
|
|
- 避免在循环中查询数据库
|
|
|
|
|
|
|
|
|
|
|
|
## 性能对比
|
|
|
|
|
|
|
|
|
|
|
|
### 数据库查询次数对比
|
|
|
|
|
|
|
|
|
|
|
|
| 导入数据量 | 优化前查询次数 | 优化后查询次数 | 性能提升 |
|
|
|
|
|
|
|----------|-------------|-------------|---------|
|
|
|
|
|
|
| 100条 | 100+1=101次 | 1次 | 99% |
|
|
|
|
|
|
| 500条 | 500+1=501次 | 1次 | 99.8% |
|
|
|
|
|
|
| 1000条 | 1000+1=1001次 | 1次 | 99.9% |
|
|
|
|
|
|
| 5000条 | 5000+1=5001次 | 1次 | 99.98% |
|
|
|
|
|
|
|
|
|
|
|
|
### 响应时间对比(预估)
|
|
|
|
|
|
|
|
|
|
|
|
| 导入数据量 | 优化前响应时间 | 优化后响应时间 | 性能提升 |
|
|
|
|
|
|
|----------|------------|------------|---------|
|
|
|
|
|
|
| 100条 | ~5秒 | ~0.5秒 | 90% |
|
|
|
|
|
|
| 500条 | ~25秒 | ~1秒 | 96% |
|
|
|
|
|
|
| 1000条 | ~50秒 | ~2秒 | 96% |
|
|
|
|
|
|
| 5000条 | ~250秒 | ~8秒 | 96.8% |
|
|
|
|
|
|
|
|
|
|
|
|
> 注:响应时间受网络延迟、数据库性能、服务器配置等因素影响,以上为保守预估值
|
|
|
|
|
|
|
|
|
|
|
|
### 资源消耗对比
|
|
|
|
|
|
|
|
|
|
|
|
| 指标 | 优化前 | 优化后 | 改善 |
|
|
|
|
|
|
|--------------|------------------|-------------------|-----------|
|
|
|
|
|
|
| 数据库连接占用时间 | 长时间占用 | 短暂占用 | 减少90%+ |
|
|
|
|
|
|
| 网络往返次数 | N+1次 | 1-2次 | 减少99%+ |
|
|
|
|
|
|
| 内存占用 | 基本占用 | 额外占用HashMap(很小) | 略微增加(可忽略) |
|
|
|
|
|
|
| CPU使用 | 循环+数据库等待 | 批量查询+内存判断 | 优化 |
|
|
|
|
|
|
|
|
|
|
|
|
## 优化效果
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 性能提升
|
|
|
|
|
|
- **查询次数减少99%+**:从N+1次降低到1次
|
|
|
|
|
|
- **响应时间减少90%+**:大幅提升用户体验
|
|
|
|
|
|
- **数据库压力降低**:减少数据库连接占用
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 代码质量提升
|
|
|
|
|
|
- **逻辑更清晰**:四阶段流程(收集→查询→分类→处理)
|
|
|
|
|
|
- **可维护性更好**:职责分明,易于理解和修改
|
|
|
|
|
|
- **扩展性更强**:易于添加其他批量校验逻辑
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 资源利用优化
|
|
|
|
|
|
- **数据库连接池压力减轻**:减少连接占用时间
|
|
|
|
|
|
- **网络带宽节省**:减少网络往返次数
|
|
|
|
|
|
- **服务器吞吐量提升**:可支持更多并发导入请求
|
|
|
|
|
|
|
|
|
|
|
|
## MySQL层面优化建议
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 确保唯一索引存在
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 个人中介表:确保personId有唯一索引
|
|
|
|
|
|
ALTER TABLE ccdi_biz_intermediary
|
|
|
|
|
|
ADD UNIQUE INDEX uk_person_id (person_id);
|
|
|
|
|
|
|
|
|
|
|
|
-- 实体中介表:确保socialCreditCode有唯一索引
|
|
|
|
|
|
ALTER TABLE ccdi_enterprise_base_info
|
|
|
|
|
|
ADD UNIQUE INDEX uk_social_credit_code (social_credit_code);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 批量查询执行计划检查
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 检查批量查询是否使用了索引
|
|
|
|
|
|
EXPLAIN SELECT biz_id, person_id
|
|
|
|
|
|
FROM ccdi_biz_intermediary
|
|
|
|
|
|
WHERE person_id IN ('id1', 'id2', 'id3', ...);
|
|
|
|
|
|
|
|
|
|
|
|
-- 期望结果:type=range, key=uk_person_id
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 批量插入优化
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 确保批量插入使用优化器优化
|
|
|
|
|
|
SET optimizer_switch='batched_key_access=on';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 测试验证
|
|
|
|
|
|
|
|
|
|
|
|
### 测试数据
|
|
|
|
|
|
- 个人中介测试数据:`doc/test-data/intermediary/个人中介黑名单测试数据_1000条_第1批.xlsx`
|
|
|
|
|
|
- 实体中介测试数据:`doc/test-data/intermediary/机构中介黑名单测试数据_1000条_第1批.xlsx`
|
|
|
|
|
|
|
|
|
|
|
|
### 测试方法
|
|
|
|
|
|
使用测试脚本验证导入功能和性能:
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 运行测试脚本
|
|
|
|
|
|
python doc/test-data/intermediary/test_import_performance.py
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 验证要点
|
|
|
|
|
|
1. ✅ 功能正确性:新增和更新逻辑正确
|
|
|
|
|
|
2. ✅ 唯一性校验:重复数据能正确识别
|
|
|
|
|
|
3. ✅ 性能提升:导入时间明显缩短
|
|
|
|
|
|
4. ✅ 数据完整性:所有数据正确导入
|
|
|
|
|
|
5. ✅ 异常处理:错误信息正确返回
|
|
|
|
|
|
|
|
|
|
|
|
## 相关文件
|
|
|
|
|
|
|
|
|
|
|
|
### 后端文件
|
2026-02-24 17:12:11 +08:00
|
|
|
|
- `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java:245-488`
|
2026-02-05 13:33:27 +08:00
|
|
|
|
|
|
|
|
|
|
### 数据库表
|
|
|
|
|
|
- `ccdi_biz_intermediary` - 个人中介表
|
|
|
|
|
|
- `ccdi_enterprise_base_info` - 实体中介表
|
|
|
|
|
|
|
|
|
|
|
|
### 测试数据
|
|
|
|
|
|
- `doc/test-data/intermediary/` - 测试数据目录
|
|
|
|
|
|
|
|
|
|
|
|
## 后续优化建议
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 异步导入
|
|
|
|
|
|
对于超大批量数据(10万+),可以考虑:
|
|
|
|
|
|
- 使用消息队列异步处理
|
|
|
|
|
|
- 提供导入进度查询接口
|
|
|
|
|
|
- 导入完成后通知用户
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 分批导入
|
|
|
|
|
|
对于内存受限场景:
|
|
|
|
|
|
- 将大数据集分批处理(每批1000条)
|
|
|
|
|
|
- 使用事务保证每批数据的原子性
|
|
|
|
|
|
- 失败时回滚当前批次
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 并行处理
|
|
|
|
|
|
对于多核CPU环境:
|
|
|
|
|
|
- 使用线程池并行处理不同批次
|
|
|
|
|
|
- 注意控制并发数,避免数据库连接耗尽
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 缓存优化
|
|
|
|
|
|
对于频繁导入相同数据的场景:
|
|
|
|
|
|
- 使用Redis缓存常用数据
|
|
|
|
|
|
- 缓存失效策略:TTL或主动更新
|
|
|
|
|
|
|
|
|
|
|
|
### 5. SQL进一步优化
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- 使用INSERT ON DUPLICATE KEY UPDATE(如果业务允许)
|
|
|
|
|
|
INSERT INTO ccdi_biz_intermediary (biz_id, person_id, ...)
|
|
|
|
|
|
VALUES (?, ?, ...)
|
|
|
|
|
|
ON DUPLICATE KEY UPDATE
|
|
|
|
|
|
name = VALUES(name),
|
|
|
|
|
|
mobile = VALUES(mobile),
|
|
|
|
|
|
...;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
|
|
本次优化通过**批量查询 + 内存映射**的方式,成功将唯一性校验的数据库查询次数从N+1次降低到1次,性能提升99%以上。优化后的代码具有更好的可读性、可维护性和扩展性,为后续功能扩展奠定了良好基础。
|
|
|
|
|
|
|
|
|
|
|
|
优化核心思想:
|
|
|
|
|
|
- **批量操作优于循环操作**
|
|
|
|
|
|
- **内存计算优于网络计算**
|
|
|
|
|
|
- **提前规划优于事后补救**
|