一、大批量插入 废弃分页插入使用rewriteBatchedStatements

       搜索动态sql.xml文件<insert id="insertList">,里面包含<foreach>标签,最终MyBatis 会在底层把这几千个对象拼成一句极其庞大的 SQL 发给 MySQL。 如果使用saveBatch() + rewriteBatchedStatements=true,会分批打包成高效的二进制流,即不压跨内存且速度加快

<insert id="insertList" parameterType="list">
    insert into t_sample (
        id, sampleNo, sampleName, productDate, ...
    )
    values
    <foreach collection="list" item="item" separator=",">
        (
            #{item.id}, #{item.sampleNo}, #{item.sampleName}, #{item.productDate}, ...
        )
    </foreach>
</insert>

1、给application.properties数据库配置后添加 rewriteBatchedStatements=true

注意:如果你的 url 里原本没有问号 ?,你需要先加个 ? 再加这个参数。如果有别的参数,直接用 & 连起来即可

2、在serviceImpl里面删除传统的 getMapper().insertList(excelAllDataList)操作,换成this.saveBatch(excelAllDataList, 1000),使用 MyBatis-Plus 自带的 saveBatch,第二个参数 1000 就是“每满 1000 条发一次车”

二、大批量导入随机生成编码,唯一编号重复问题

uniqueNo(唯一码) = 年月日时分秒 (14位) + 两位随机数 (10-99) + 物品序号 i

但是大批量导入生产易发生重复。

处理办法1:设置流水号index,初始值为0,为每一个唯一码后添加index,且index++。但缺点是长度不稳定,高并发时两人同时操作会重复。

处理办法2:Redis 单线程原子自增,不可能重复

 Redis在内存操作,所以发号非常快。key为px:reagent:seq:20260325,有过期时间好处理。increment(redisKey) 为时间标签添加递增号码。

@Autowired
private StringRedisTemplate stringRedisTemplate;
private List<ReagentDetailBean> createDetailBeans(ReagentBean bean, UserBean loginUser) {
    // 获取当前时间:例如 20260325154907
    String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
    // 提取当天的日期作为 Redis 的 Key 后缀:20260325
    String dayStr = dateStr.substring(0, 8);
    // 1. 设计 Redis Key(项目名:模块名:业务:日期)
    String redisKey = "px:reagent:seq:" + dayStr; 
    List<ReagentDetailBean> list = new ArrayList<>();
    // 这里的 bean.getVcount() 是生成数量
    for (int i = 0; i < bean.getVcount(); i++) {
        ReagentDetailBean detailBean = new ReagentDetailBean();
        detailBean.setId(PxBeanUtils.newStrId());
        detailBean.setReagentId(bean.getId()); 
        // 2. 找 Redis 要一个绝对唯一的递增号码(1, 2, 3, 4...)
        Long seq = stringRedisTemplate.opsForValue().increment(redisKey);
        // 3. 兜底保护:如果是今天第一个生成的号码,给这个 Key 设置一个 24 小时的过期时间,防止内存泄漏
        if (seq != null && seq == 1L) {
            stringRedisTemplate.expire(redisKey, 24, TimeUnit.HOURS);
        }  
        // 4. 将数字格式化为固定长度的字符串(比如 4 位数,不足左侧补零:0001, 0002...)
        String formattedSeq = String.format("%04d", seq);
        // 5. 拼装:时间精确到秒 + 固定4位流水号
        detailBean.setUniqueNo(dateStr + formattedSeq);
        detailBean.setCreatedById(loginUser.getId());
        detailBean.setCreatedByName(loginUser.getRealName());
        detailBean.setDeptName(loginUser.getDeptName());
        list.add(detailBean);
    }
    return list;
}

处理办法3:雪花算法  

1 位符号位+ 41 位时间戳(毫秒级) + 10 位机器标识码(最高1024台) + 12 位序列号

直接调用 IdWorker.getIdStr()就行

import com.baomidou.mybatisplus.core.toolkit.IdWorker; // 引入 MyBatis-Plus 自带的雪花工具类

private List<ReagentDetailBean> createDetailBeans(ReagentBean bean, UserBean loginUser) {
    List<ReagentDetailBean> list = new ArrayList<>();
    
    // bean.getVcount() 是要生成的瓶数
    for (int i = 0; i < bean.getVcount(); i++) {
        ReagentDetailBean detailBean = new ReagentDetailBean();
        detailBean.setId(PxBeanUtils.newStrId()); // 原来的表主键
        detailBean.setReagentId(bean.getId());
        
        // 直接调用自带的雪花算法,生成一个 19 位的纯数字字符串(例如:1635821948512345678)
        // 完全不需要网络请求,纯内存计算,耗时几乎为 0 纳秒
        String uniqueNo = IdWorker.getIdStr();
        detailBean.setUniqueNo(uniqueNo);
        
        detailBean.setCreatedById(loginUser.getId());
        detailBean.setCreatedByName(loginUser.getRealName());
        detailBean.setDeptName(loginUser.getDeptName());
        list.add(detailBean);
    }
    return list;
}

雪花算法强依赖于机器时钟。如果发生时钟回拨(比如服务器时间不准,NTP 同步时把时间往回拨了 1 秒),就有极小概率生成重复 ID。所以在企业级部署时,通常会配合 MyBatis-Plus 的时钟回拨容忍机制,或者在发现回拨时抛出异常并降级使用 Redis 发号来兜底。

三、BlockAttackInnerInterceptor 防止全表攻击拦截器

防止忘加where误删整个表

@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加“防全表更新与删除”插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        // 如果还有分页插件,也可以继续往里加
        // interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));  
        return interceptor;
    }
}

四、 慢 SQL 拦截器

PerformanceInterceptor 计算出 SQL 执行耗费时间,如果超过则报错

@Configuration
public class MybatisConfig {

    @Bean
    // 性能监控只能在开发(dev)和测试(test)环境开启,若在生产环境(prod)开启,极其消耗性能
    @Profile({"dev", "test"}) 
    public PerformanceInterceptor performanceInterceptor() {
        PerformanceInterceptor interceptor = new PerformanceInterceptor();
        // 设置 SQL 执行的最大容忍时间为 1000 毫秒,超过直接报错
        interceptor.setMaxTime(1000); 
        // 格式化控制台打印的 SQL
        interceptor.setFormat(true); 
        return interceptor;
    }
}

但是这种监控方法拉完了,不够底层容易性能损耗,于是新方法诞生了

法1:系统自带的

执行以下配置命令

-- 1. 开启慢查询日志开关
SET GLOBAL slow_query_log = 'ON';

-- 2. 设置阈值:执行超过 1 秒的算作慢 SQL(通常设为 1 或 2)
SET GLOBAL long_query_time = 1;

-- 3. 连没有走索引的 SQL 也一起记下来,不管它快不快
SET GLOBAL log_queries_not_using_indexes = 'ON';

MySQL 会在它的安装目录里生成一个xxx-slow.log 的文件,用mysqldumpslow 工具去分析,抓出最慢的 SQL 。

法2:p6spy

之前直接把 SQL 发给 MySQL 驱动;现在p6spy 挡在中间。Java 把 SQL 发给 p6spy,p6spy 开始掐表计时,然后再发给 MySQL。等结果回来后,p6spy 掐停秒表,把带上真实参数的完整 SQL 和执行耗时,打印在 IDEA 控制台上。

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version> </dependency>
spring:
  datasource:
    # 之前是 com.mysql.cj.jdbc.Driver,现在换成间谍驱动
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    # 在原有的 jdbc:mysql 之间,强行插入一个 p6spy:
    url: jdbc:p6spy:mysql://127.0.0.1:3306/px_base_manage?...

src/main/resources 下建一个文件叫 spy.properties

# 使用标准输出打印到控制台
appender=com.p6spy.engine.spy.appender.StdoutLogger
# 启过滤规则
filter=true
# 只记录执行时间超过 1000 毫秒(1秒)的 SQL,执行很快的 CRUD 统统不打印
executionThreshold=1000

重启项目,IDEA 控制台就会精准打印出这条 SQL 消耗时间

适合本地开发和测试环境,如果发生大批量高并发情况下,p6spy是抗不住的,因此来了Druid

法3:Druid

<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid-spring-boot-starter</artifactId>
     <version>1.2.16</version> 
</dependency>
spring:
  datasource:
    #数据库基本信息
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/你的数据库名?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 你的密码

    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      #  初始化连接数,项目刚启动时和数据库建立几个连接
      initial-size: 5
      #  最小空闲连接数,保证突发请求不用现连数据库
      min-idle: 5
      #  最大活跃连接数,决定了你的系统最高并发量
      max-active: 20
      #  获取连接等待超时时间,单位毫秒。等 60 秒没桌子就报错退出
      max-wait: 60000

      #  检测被数据库偷偷断开的“僵尸连接”
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false

      #  开启监控大屏和慢 SQL 统计
      filter:
        stat:
          enabled: true
          # 开启慢 SQL 记录
          log-slow-sql: true
          # 设定阈值:执行时间超过 1000 毫秒(1秒)的 SQL,会被系统抓出来当做慢 SQL 
          slow-sql-millis: 1000
      
      #  配置浏览器访问监控大屏的账号密码
      stat-view-servlet:
        enabled: true
        # 登录大屏的账号,可以随便改
        login-username: admin
        # 登录大屏的密码
        login-password: 123456
        # 允许哪些 IP 访问这个监控大屏?(为空表示所有电脑都能访问)
        allow: ''

启动后访问  http://localhost:端口号/control/druid/login.html

Druid其他优势

1、连接池被挤爆时可查数据源,关掉优化闲置的慢 SQL

2、处理数据库连接泄漏,忘了close()还给连接池,发生GetConnectionTimeoutException,

      Druid 会把每一条借出去的连接打上标记 堆栈追踪

3、找到最慢的前端页面,调用一次 接口,底层到底触发了多少次 SQL 查询

4、智能拦截所有恶意的、违规的 SQL

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐