🚀 SpringBoot 自定义系统健康检测:数据库、Redis、表统计、更新时长、系统性能全链路监控
在生产环境中,我们常常需要一个统一的健康检查接口,用来监控系统运行状态、关键依赖服务、以及各业务表的更新情况。
相比 Spring Actuator 的简单 "UP / DOWN",我们往往需要 更全面、更细粒度、更贴近业务场景的健康监控系统。
本文将基于以下大纲构建一个可直接落地的健康监控模块:
📌 文章大纲
✔ 数据库健康检测
✔ Redis 连通性检测
✔ 表统计(总量 / 今日新增 / 未更新天数)
✔ 每张表的最新更新时间
✔ 系统运行时长(uptime)、CPU、内存信息
✔ 自动格式化数字与时间(万、百万、亿 / 刚刚、几分钟前)
最终你将得到一个 完整的、可上线的健康监控接口。
🧱 一、健康检查核心结构设计
使用 SystemHealthVO 作为返回对象:
java
@Data
public class SystemHealthVO {
private boolean dbHealthy;
private boolean redisHealthy;
private boolean systemHealthy;
private List<String> healthDetails = new ArrayList<>();
private long uptimeDays;
private long jvmMemoryUsedMb;
private long jvmMemoryTotalMb;
private String cpuLoadPercent;
private List<TableStats> tableStats;
private String totalBusinessRows;
private long totalTodayChange;
private String totalTodayChangeStr;
private String latestUpdateAgo;
private String mostOutdatedTable;
private Integer mostOutdatedDays;
@Data
public static class TableStats {
private String tableName;
private long totalRows;
private String totalRowsStr;
private long todayIncrement;
private Integer daysNoUpdate;
private String lastUpdateAgo;
}
}
🗄 二、数据库健康检查(最严格方式)
最简单也最可靠的方式:查询一张必然存在的表。
java
private boolean checkDatabase(SystemHealthVO vo) {
try {
healthMapper.countTable("sys_user");
return true;
} catch (Exception e) {
vo.getHealthDetails().add("数据库连接异常:" + e.getMessage());
return false;
}
}
📦 三、Redis 连通性检测
写入 + 删除一个轻量 Key,即可验证。
java
private boolean checkRedis(SystemHealthVO vo) {
try {
stringRedisTemplate.opsForValue().set("health_check_key", "ok", 3, TimeUnit.SECONDS);
stringRedisTemplate.delete("health_check_key");
return true;
} catch (Exception e) {
vo.getHealthDetails().add("Redis 连接异常");
return false;
}
}
⚙ 四、系统性能信息:CPU、JVM、运行时长
java
private void fillSystemInfo(SystemHealthVO vo) {
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
vo.setUptimeDays(uptime / 86_400_000L);
Runtime r = Runtime.getRuntime();
vo.setJvmMemoryUsedMb((r.totalMemory() - r.freeMemory()) >> 20);
vo.setJvmMemoryTotalMb(r.totalMemory() >> 20);
var os = (com.sun.management.OperatingSystemMXBean)
ManagementFactory.getOperatingSystemMXBean();
double load = os.getProcessCpuLoad();
vo.setCpuLoadPercent(load >= 0 ? String.format("%.1f%%", load * 100) : "N/A");
}
📊 五、核心功能:业务表统计(总量 / 今日新增 / 未更新天数 / 最近更新时间)
定义表信息结构:
java
private static class TableInfo {
String tableName;
String displayName;
String timeColumn;
TableInfo(String tableName, String displayName, String timeColumn) {
this.tableName = tableName;
this.displayName = displayName;
this.timeColumn = timeColumn;
}
}
示例定义业务表常量:
java
private static final List<TableInfo> TABLES = List.of(
new TableInfo("order_info", "订单表", "update_time"),
new TableInfo("product", "商品表", "modify_time"),
new TableInfo("user_log", "用户日志", "create_time")
);
⭐ 核心统计逻辑
java
private RealTimeData loadTableStatistics() {
RealTimeData data = new RealTimeData();
data.stats = new ArrayList<>();
long totalRows = 0;
long todayTotal = 0;
Timestamp latestUpdate = null;
String mostOutdatedTable = null;
Integer mostOutdatedDays = null;
LocalDate today = LocalDate.now();
Date dayStart = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
Date dayEnd = Date.from(today.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
for (TableInfo info : TABLES) {
SystemHealthVO.TableStats s = new SystemHealthVO.TableStats();
s.setTableName(info.displayName);
try {
long cnt = Optional.ofNullable(healthMapper.countTable(info.tableName)).orElse(0L);
s.setTotalRows(cnt);
s.setTotalRowsStr(formatNumber(cnt));
totalRows += cnt;
Timestamp last = healthMapper.getMaxTime(info.tableName, info.timeColumn);
if (last != null) {
s.setLastUpdateAgo(formatTimeAgo(last));
LocalDate lastDay = last.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
int days = (int) ChronoUnit.DAYS.between(lastDay, today);
s.setDaysNoUpdate(days);
if (mostOutdatedDays == null || days > mostOutdatedDays) {
mostOutdatedDays = days;
mostOutdatedTable = info.displayName;
}
if (latestUpdate == null || last.after(latestUpdate)) {
latestUpdate = last;
}
}
long todayIncrement = Optional.ofNullable(
healthMapper.countToday(info.tableName, info.timeColumn, dayStart, dayEnd)
).orElse(0L);
s.setTodayIncrement(todayIncrement);
todayTotal += todayIncrement;
} catch (Exception e) {
log.error("统计表 {} 失败", info.tableName, e);
}
data.stats.add(s);
}
data.totalAllRows = totalRows;
data.totalTodayAll = todayTotal;
data.latestUpdateTime = latestUpdate;
data.mostOutdatedName = mostOutdatedTable;
data.mostOutdatedDays = mostOutdatedDays;
return data;
}
🔢 六、数字格式化(万 / 百万 / 亿)
java
private String formatNumber(long num) {
if (num < 10_000) return "" + num;
if (num < 100_000_000) return String.format("%.1f万", num / 10000.0).replace(".0万", "万");
if (num < 100_000_000_000L) return String.format("%.1f百万", num / 1e8).replace(".0百万", "百万");
return String.format("%.1f亿", num / 1e11).replace(".0亿", "亿");
}
⏳ 七、人性化时间格式(几分钟前 / 几天前)
java
private String formatTimeAgo(Timestamp ts) {
LocalDateTime t = ts.toLocalDateTime();
Duration d = Duration.between(t, LocalDateTime.now());
if (d.toMinutes() < 5) return "刚刚";
if (d.toHours() < 1) return d.toMinutes() + "分钟前";
if (d.toDays() < 1) return d.toHours() + "小时前";
if (d.toDays() < 30) return d.toDays() + "天前";
if (d.toDays() < 365) return (d.toDays() / 30) + "个月前";
return (d.toDays() / 365) + "年前";
}
🧩 八、最终健康检查接口
java
@Override
public SystemHealthVO getHealthInfo() {
SystemHealthVO vo = new SystemHealthVO();
vo.setHealthDetails(new ArrayList<>());
vo.setDbHealthy(checkDatabase(vo));
vo.setRedisHealthy(checkRedis(vo));
vo.setSystemHealthy(vo.isDbHealthy() && vo.isRedisHealthy() && vo.getHealthDetails().isEmpty());
fillSystemInfo(vo);
RealTimeData data = loadTableStatistics();
vo.setTableStats(data.stats);
vo.setTotalBusinessRows(formatNumber(data.totalAllRows));
vo.setTotalTodayChange(data.totalTodayAll);
vo.setTotalTodayChangeStr(data.totalTodayAll > 0 ? "+" + data.totalTodayAll : "" + data.totalTodayAll);
vo.setMostOutdatedTable(data.mostOutdatedName);
vo.setMostOutdatedDays(data.mostOutdatedDays);
vo.setLatestUpdateAgo(
data.latestUpdateTime == null ? "从未更新" : formatTimeAgo(data.latestUpdateTime)
);
return vo;
}