【数据库优化】大批量插入、编号重复、雪花算法回拨容忍、 p6spy、Druid慢SQL监控
一、大批量插入 废弃分页插入使用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
更多推荐
所有评论(0)