前提:仅 InnoDB 和 MyISAM 存储引擎支持分区;
心跳记录表分区操作:
设备心跳每天新增数据量80万条;需要定时删除7天前的心跳数据,普通删除语句属于行级删除,即便使用到索引,速度依然慢;后改成使用存储过程分批次多次删除,虽然每个批次删除速度很快但是总耗时还是长;最后决定改成使用分区操作,对7天前的数据进行删除:速度快,毫秒级(直接删除那些需要清理的数据所在的分区)。
1.将分区字段beat_time(字段类型datetime)设置成主键(原主键Id,新增主键beat_time);
2.创建历史分区和兜底分区(分区规则是按照日期递增,可以跳跃(举例:创建p20251222分区,不创建p20251223分区,直接创建p20251224分区),但不能有数据重叠(举例:兜底分区存的是29号以后的数据,如果直接创建p20251230分区,sql就会报错,原因就是兜底分区和p20251230分区出现了重叠);必须创建兜底分区,防止出现数据插入时找不到对应分区,兜底分区就可以存这些数据):表里有无数据都可以进行分区操作,在navicat数据库管理软件中执行以下sql创建按天划分的 RANGE 分区
sql
ALTER TABLE kt_door_hearbeat_log PARTITION BY RANGE (TO_DAYS(beat_time))(
--22号及之前的数据都放到p20251222分区里
PARTITION p20251222
VALUES
LESS THAN (TO_DAYS('2025-12-23')),
--23号的数据都放到p20251223分区里
PARTITION p20251223
VALUES
LESS THAN (TO_DAYS('2025-12-24')),
--24号的数据都放到p20251224分区里
PARTITION p20251224
VALUES
LESS THAN (TO_DAYS('2025-12-25')),
--25号的数据都放到p20251225分区里
PARTITION p20251225
VALUES
LESS THAN (TO_DAYS('2025-12-26')),
--26号的数据都放到p20251226分区里
PARTITION p20251226
VALUES
LESS THAN (TO_DAYS('2025-12-27')),
PARTITION p20251227
VALUES
LESS THAN (TO_DAYS('2025-12-28')),
PARTITION p20251228
VALUES
LESS THAN (TO_DAYS('2025-12-29')),
PARTITION p20251229
VALUES
LESS THAN (TO_DAYS('2025-12-30')),
-- 兜底分区:存放2025-12-29之后的所有数据
PARTITION p_future
VALUES
LESS THAN MAXVALUE
);
3.定期(每天00:00:00秒执行定时任务)删除7天前分区数据:
1.DoorHearBeatLogServiceImpl方法
java
@Override
public void cleanUpDoorHearBeatLog() {
//25-12-26xg
List<String> partitions = doorHearBeatLogMapper.getPartitions();
try {
if (partitions != null && !partitions.isEmpty()) {
LocalDate now = LocalDate.now();
DateTimeFormatter ymdFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String partitionName = "p" + now.format(ymdFormatter);
LocalDate tomorrow = now.plusDays(1);
DateTimeFormatter dashYmdFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String tomorrowTime = tomorrow.format(dashYmdFormatter);
//必须保证兜底分区p_future存在,才能继续执行
if (!partitions.contains("p_future")) {
doorHearBeatLogMapper.addPFuture();
logger.info("原本p_future分区不存在已重新创建");
} else {
logger.info("原本已存在分区 p_future");
}
if (!partitions.contains(partitionName)) {
doorHearBeatLogMapper.reorganizeFuturePartition(partitionName, tomorrowTime);
partitions = doorHearBeatLogMapper.getPartitions();
//验证创建结果,非常重要
boolean pFutureExists = partitions.contains("p_future");
boolean targetPartitionExists = partitions.contains(partitionName);
if (!pFutureExists & !targetPartitionExists) {//两个都没创建成功
doorHearBeatLogMapper.addPartition(tomorrowTime, partitionName);
logger.warn("两个分区创建失败!重新创建" + partitionName + "分区和兜底分区");
} else if (!pFutureExists & targetPartitionExists) {//p_future没创建成功
doorHearBeatLogMapper.addPFuture();
logger.warn("兜底分区创建失败!重新创建兜底分区");
} else if (!targetPartitionExists & pFutureExists) {//partitionName没创建成功
doorHearBeatLogMapper.reorganizeFuturePartition(partitionName, tomorrowTime);
logger.warn(partitionName + "分区创建失败!重新原子性拆分旧兜底分区,并创建" + partitionName + "分区和新兜底分区");
}
} else {
logger.info(partitionName + "已经存在无需创建此分区 ");
}
partitions = doorHearBeatLogMapper.getPartitions();
logger.info("创建完后的所有历史分区:" + partitions);
List<String> partitionsDel = partitions.stream().filter(partition -> !"p_future".equals(partition)).sorted((o1, o2) -> o2.compareTo(o1)).skip(7).collect(Collectors.toList());
logger.info("要清除的历史分区:" + partitionsDel);
partitionsDel.forEach(partition -> doorHearBeatLogMapper.cleanUpDoorHearBeatLog(partition));
}
} catch (Exception e) {
logger.error("系统自动记录快件日志时异常:{}", e.getMessage());
}
}
2.DoorHearBeatLogMapper
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jsdye.project.kt.doorAccess.domain.pojo.DoorHearBeatLog;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.StatementType;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author fgz
* @date 2025/9/29 15:55
* @description 门禁心跳日志持久层
*/
@Mapper
public interface DoorHearBeatLogMapper extends BaseMapper<DoorHearBeatLog> {
//25.11.20添加批量删除门禁心跳日志
// @Delete("CALL batch_delete_door_heartbeat(#{beatTime},30000)")
// @Options(statementType = StatementType.CALLABLE)
// void cleanUpDoorHearBeatLog(@Param("beatTime") Date beatTime);
//25.12.26删除门禁心跳日志
@Delete({
"ALTER TABLE kt_door_hearbeat_log DROP PARTITION ${beatTime}"
})
void cleanUpDoorHearBeatLog(@Param("beatTime") String beatTime);
//25-12-26xz
@Select("SELECT PARTITION_NAME FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = 'kt' AND TABLE_NAME = 'kt_door_hearbeat_log'")
List<String> getPartitions();
//25-12-26xz
@Insert({"ALTER TABLE kt_door_hearbeat_log ADD PARTITION ( PARTITION ${partitionName} VALUES LESS THAN (TO_DAYS(#{currentTime})), PARTITION p_future VALUES LESS THAN MAXVALUE )"
})
void addPartition(@Param("currentTime") String currentTime, @Param("partitionName") String partitionName);
//25.12.29批量删除门禁心跳日志
// @Delete({
// "ALTER TABLE kt_door_hearbeat_log DROP PARTITION ${beatTime};",
// "ALTER TABLE kt_door_hearbeat_log ADD PARTITION (",
// " PARTITION ${partitionName} VALUES LESS THAN (TO_DAYS(#{currentTime})),",
// " PARTITION p_future VALUES LESS THAN MAXVALUE",
// ");"
// })
// void cleanAndAddPartition(@Param("beatTime") String beatTime,@Param("currentTime") String currentTime, @Param("partitionName") String partitionName);
/**25.12.29
* 原子性拆分兜底分区p_future(无空窗期,避免心跳数据丢失)
* @param newPartitionName 新历史分区名(如p20251229)
* @param partitionTime 分区截止时间(如2025-12-29 00:00:00,无需手动加单引号)
*/
@Update({
"ALTER TABLE kt_door_hearbeat_log ",
"REORGANIZE PARTITION p_future INTO (",
" PARTITION ${newPartitionName} VALUES LESS THAN (TO_DAYS(#{partitionTime})),",
" PARTITION p_future VALUES LESS THAN MAXVALUE",
");"
})
void reorganizeFuturePartition(
@Param("newPartitionName") String newPartitionName,
@Param("partitionTime") String partitionTime
);
/**25.12.29
* 添加兜底分区p_future
*/
@Insert( {"ALTER TABLE kt_door_hearbeat_log ADD PARTITION (PARTITION p_future VALUES LESS THAN MAXVALUE);"})
void addPFuture();
}