Files
ccdi/doc/implementation/优化说明/中介黑名单导入唯一性校验优化说明_20260205.md
wkc 1cd87d2695 refactor: 重命名 ruoyi-ccdi 模块为 ruoyi-info-collection
- Maven 模块从 ruoyi-ccdi 重命名为 ruoyi-info-collection
- Java 包名从 com.ruoyi.ccdi 改为 com.ruoyi.info.collection
- MyBatis XML 命名空间同步更新
- 保留数据库表名、API URL、权限标识中的 ccdi 前缀
- 更新项目文档中的模块引用
2026-02-24 17:12:11 +08:00

10 KiB
Raw Blame History

中介黑名单导入唯一性校验优化说明

优化时间

2026-02-05

优化目的

优化批量导入中介黑名单数据时的唯一性校验性能解决N+1查询问题。

问题描述

原实现问题

在导入个人中介和实体中介数据时,原实现存在以下性能问题:

  1. N+1查询问题

    • 在循环中对每条记录调用 checkPersonIdUniquecheckSocialCreditCodeUnique
    • 导入1000条数据时产生1000次数据库查询
    • 代码位置:
      • CcdiIntermediaryServiceImpl.importIntermediaryPerson:291
      • CcdiIntermediaryServiceImpl.importIntermediaryEntity:409
  2. 重复查询问题

    • 唯一性校验查询一次1000次
    • 获取bizId再次批量查询一次1次
    • 总计1001次数据库查询
  3. 性能瓶颈

    • 大量数据导入时响应慢
    • 数据库连接占用时间长
    • 网络往返次数多

优化方案

核心思路

将"循环中逐条查询"改为"一次性批量查询,内存中快速判断"

优化实现

1. 个人中介导入优化importIntermediaryPerson

优化前:

// 第一轮:数据验证和分类
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);
    // ...
}

优化后:

// 第一轮收集所有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

优化后实现:

// 第一轮收集所有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. 确保唯一索引存在

-- 个人中介表确保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. 批量查询执行计划检查

-- 检查批量查询是否使用了索引
EXPLAIN SELECT biz_id, person_id
FROM ccdi_biz_intermediary
WHERE person_id IN ('id1', 'id2', 'id3', ...);

-- 期望结果type=range, key=uk_person_id

3. 批量插入优化

-- 确保批量插入使用优化器优化
SET optimizer_switch='batched_key_access=on';

测试验证

测试数据

  • 个人中介测试数据:doc/test-data/intermediary/个人中介黑名单测试数据_1000条_第1批.xlsx
  • 实体中介测试数据:doc/test-data/intermediary/机构中介黑名单测试数据_1000条_第1批.xlsx

测试方法

使用测试脚本验证导入功能和性能:

# 运行测试脚本
python doc/test-data/intermediary/test_import_performance.py

验证要点

  1. 功能正确性:新增和更新逻辑正确
  2. 唯一性校验:重复数据能正确识别
  3. 性能提升:导入时间明显缩短
  4. 数据完整性:所有数据正确导入
  5. 异常处理:错误信息正确返回

相关文件

后端文件

  • ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java:245-488

数据库表

  • ccdi_biz_intermediary - 个人中介表
  • ccdi_enterprise_base_info - 实体中介表

测试数据

  • doc/test-data/intermediary/ - 测试数据目录

后续优化建议

1. 异步导入

对于超大批量数据10万+),可以考虑:

  • 使用消息队列异步处理
  • 提供导入进度查询接口
  • 导入完成后通知用户

2. 分批导入

对于内存受限场景:

  • 将大数据集分批处理每批1000条
  • 使用事务保证每批数据的原子性
  • 失败时回滚当前批次

3. 并行处理

对于多核CPU环境

  • 使用线程池并行处理不同批次
  • 注意控制并发数,避免数据库连接耗尽

4. 缓存优化

对于频繁导入相同数据的场景:

  • 使用Redis缓存常用数据
  • 缓存失效策略TTL或主动更新

5. 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%以上。优化后的代码具有更好的可读性、可维护性和扩展性,为后续功能扩展奠定了良好基础。

优化核心思想:

  • 批量操作优于循环操作
  • 内存计算优于网络计算
  • 提前规划优于事后补救