[特殊字符] SpringBoot 自定义系统健康检测:数据库、Redis、表统计、更新时长、系统性能全链路监控

🚀 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;
}
相关推荐
深圳佛手1 小时前
Sharding-JDBC 和 Sharding-Proxy 区别
java
kk哥88991 小时前
inout参数传递机制的底层原理是什么?
java·开发语言
5***E6851 小时前
MySQL:drop、delete与truncate区别
数据库·mysql
记得记得就1511 小时前
【MySQL数据库管理】
数据库·mysql·oracle
小二·2 小时前
Spring框架入门:深入理解Spring DI的注入方式
java·后端·spring
避避风港2 小时前
转发与重定向
java·servlet
Austindatabases2 小时前
给PG鸡蛋里面挑骨头--杭州PostgreSQL生态大会
数据库·postgresql
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于springboot和协同过滤算法的线上点餐系统为例,包含答辩的问题和答案
java·spring boot·后端