系列文章:
【轻松入门SpringBoot】从0到1搭建web 工程(上)-使用SpringBoot框架
【轻松入门SpringBoot】从0到1搭建web 工程(中) -使用Spring框架
【轻松入门SpringBoot】从0到1搭建web 工程(下)-在实践中对比SpringBoot和Spring框架
【轻松入门SpringBoot】actuator健康检查(上)
目录
[1、建表:city 表](#1、建表:city 表)
前言:
上篇文章介绍了 Springboot框架用来做健康检查功能的actuator,actuator 支持从多个维度检查系统的健康程度,并且支持自定义,帮助开发者或维护者快速感知系统异常,及早控制影响。actuator 默认能检查数据库、磁盘、ping 是否正常,后面我们看看actuator自定义能还能检查哪些?
计划:
检查本地缓存是否加载成功?
检查 Redis连接是否正常?
监控线程池是否正常?
其他
准备:
因为前面几篇文章使用的 Springboot:2.0.4.RELEASE 版本,版本比较低,没有actuator 官网说的"支持从"存活"和"就绪"的维度提供安全检查"功能、健康组功能(可以对监控项分组,分为核心组、非核心组,使用者可以对不同组的异常采取不同的措施)等,所以我将 SpringBoot 升级到了3.3.0 版本,Java 版本升级到 17。
多版本并行问题:我的工作项目使用的 Java8,但 demo 需要 Java17,所以电脑上需要Java多版本并行,这里最后使用SDKMAN实现的。SDKMAN(全称 Software Development Kit Manager,曾用名 GVM)是一款开源、轻量的命令行工具,专为类 Unix 系统(macOS、Linux、WSL)设计,核心用于一键安装、切换、管理多版本 JVM 生态 SDK 与开发工具,自动配置环境变量,大幅简化开发环境维护SDKMAN!。非常好操作,按照豆包整理的教程很快就能安好了。一开始遇到了一些网络问题,怎么都下载不下来,借助豆包最后也很快解决了,有多版本并行需求的可以试试。
实现:
检查本地缓存是否加载成功:项目启动时加载数据库 city 表数据缓存到本地
1、建表:city 表
sql
CREATE TABLE `city` (
`city_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '城市ID(主键)',
`city_name` VARCHAR(50) NOT NULL COMMENT '城市名称',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` TINYINT UNSIGNED DEFAULT 0 COMMENT '是否删除(0=未删,1=已删)',
PRIMARY KEY (`city_id`) USING BTREE, -- 主键索引(InnoDB默认聚簇索引)
UNIQUE KEY `uk_city_name` (`city_name`) USING BTREE, -- 城市名称唯一索引(避免重复)
KEY `idx_is_deleted` (`is_deleted`) USING BTREE -- 软删除字段索引(查询优化)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市基础信息表';
2、缓存类
CityCacheService
java
package com.example.springbootuniq.cache;
import com.example.springbootuniq.entity.City;
import com.example.springbootuniq.repository.CityRepository;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class CityCacheService {
// 本地缓存 <city,cityName>
private final Cache<Integer, String> cityCache;
// 缓存加载标记(用于健康检查)
private volatile boolean cacheLoaded = false;
// 缓存加载时间戳
private long loadTimestamp;
private final CityRepository cityRepository;
// 构造器注入 cityRepository,初始化 caffeine 缓存
public CityCacheService(CityRepository cityRepository) {
this.cityRepository = cityRepository;
this.cityCache = Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
}
// 项目启动后自动加载数据到缓存(bean初始化后执行)
@PostConstruct
public void loadCityCache() {
try {
// 1.从数据库中查询所有城市
List<City> cityList = cityRepository.findAll();
if(CollectionUtils.isEmpty(cityList)){
cacheLoaded = false;
return;
}
// 2.加载缓存到本地
cityList.forEach(city -> cityCache.put(city.getCityId(), city.getCityName()));
cacheLoaded = true;
loadTimestamp = System.currentTimeMillis();
System.out.println("缓存加载完成,共加载:" + cityList.size() + "条数据");
} catch (Exception e) {
cacheLoaded = false;
System.out.println("缓存加载失败:" + e.getMessage());
e.printStackTrace();
}
}
// 对外提供查询接口
public String getCityName(Integer cityId) {
if (!cacheLoaded) {
throw new RuntimeException("缓存未加载完成"); // 缓存未加载完成,抛出异常
}
return cityCache.getIfPresent(cityId);
}
// 供健康检查调用:加载缓存加载状态
public boolean isCacheLoaded() {
return cacheLoaded;
}
// 供健康检查调用:获取缓存大小
public long getCacheSize(){
return cityCache.estimatedSize();
}
// 供健康检查调用:获取缓存加载时间
public long getLoadTimestamp() {
return loadTimestamp;
}
}
3、健康检查
CityCacheHealthIndicator
java
package com.example.springbootuniq.config.health;
import com.example.springbootuniq.cache.CityCacheService;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class CityCacheHealthIndicator implements HealthIndicator {
private final CityCacheService cityCacheService;
public CityCacheHealthIndicator(CityCacheService cityCacheService) {
this.cityCacheService = cityCacheService;
}
@Override
public Health health() {
boolean isLoaded = cityCacheService.isCacheLoaded();
long cacheSize = cityCacheService.getCacheSize();
if(isLoaded && cacheSize > 0){
// 缓存加载成功 -> 健康状态 up
return Health.up().withDetail("提示", "城市缓存配置加载成功")
.withDetail("缓存大小", cacheSize)
.withDetail("加载时间(时间戳)", cityCacheService.getLoadTimestamp())
.withDetail("查询示例(城市 Id=1)",cityCacheService.getCityName(1)) // 模拟调用
.build();
}else{
return Health.down().withDetail("提示", "城市缓存配置加载失败")
.withDetail("缓存大小", cacheSize)
.withDetail("缓存加载状态", isLoaded)
.withDetail("缓存数据量",cacheSize)
.build();
// return Health.up().withDetail("提示", "城市缓存配置加载失败")
// .withDetail("缓存大小", cacheSize)
// .withDetail("缓存加载状态", isLoaded)
// .withDetail("缓存数据量",cacheSize)
// .withDetail("factStatus","OUT_OF_SERVICE")
// .build();
}
}
}
配置:
XML
# Actuator 配置(与 spring 平级,统一 2 个空格缩进)
management:
endpoints:
web:
exposure:
include: health, info, livenessstate, readinessstate
endpoint:
health:
show-details: always
# 3.3.0 新格式:健康组直接在 management.health.group 下定义
health:
primary-group: core # 强制默认组为 core
include: core
# 定义健康组(3.3.0 新写法)
group:
core:
# 核心组包含的组件
include: db,diskSpace,ping
# 核心组的状态优先级
status:
order: DOWN, OUT_OF_SERVICE, UP, UNKNOWN
non-core:
# 非核心组包含的组件
include: cityCache
# 非核心组的状态优先级
status:
order: DOWN, OUT_OF_SERVICE, UP, UNKNOWN
status:
order: DOWN, OUT_OF_SERVICE, UP, UNKNOWN
4、运行结果
http://localhost:8080/actuator/health
java
{
"status": "UP",
"components": {
"cityCache": {
"status": "UP",
"details": {
"提示": "城市缓存配置加载成功",
"缓存大小": 5,
"加载时间(时间戳)": 1766929107938,
"查询示例(城市 Id=1)": "南京"
}
},
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 994662584320,
"free": 835691266048,
"threshold": 10485760,
"path": "/spring-boot-uniq/.",
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}
从结果可以看出,缓存已经加载完成,最外层status 是 up。
等缓存失效,再请求:返回结果
java
{
"status": "DOWN",
"components": {
"cityCache": {
"status": "DOWN",
"details": {
"提示": "城市缓存配置加载失败",
"缓存大小": 0,
"缓存加载状态": true,
"缓存数据量": 0
}
},
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 994662584320,
"free": 835683192832,
"threshold": 10485760,
"path": "/spring-boot-uniq/.",
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}
5、调试中
我想给检查项目划分一些级别,就像 autuator 中的描述:DOWN, OUT_OF_SERVICE, UP, UNKNOWN。级别高的有问题时,status=down,服务启动失败;级别低的抛出异常,但服务正常启动。比如上面的 city 缓存允许在服务启动时不加载完成,等定时任务同步,所以需要用到group-组的功能:cityCache 属于非核心组,当 cityCache 的 status 是 down 时,最外层的 status 也是 up.但目前运行的结果是 down,还在调试中。。。
总结:
这篇我们学习了使用 actuator 自定义健康检查-加载本地缓存,升级了 SpringBoot 和 Java 版本,并使用sdkman 实现多版本并行管理。分组的问题还在排查中,希望能在下周顺利解决掉。
这篇文章内容有点稀疏,一开始本来想先试 Redis 缓存的,但电脑的域名解析异常,搞了半天没搞好。改成先写本地缓存,想试试分组的概念,因为"一刀切"对复杂的生产环境不友好,而且Springboot也提供了这样的功能。于是开始进行版本升级,虽然没怎么踩坑,但版本升级这件事本身是需要花时间的,需要把报错的低版本的引用挨个替换掉。升级了 SpringBoot 版本,又发现 Java 版本不够,又开始升级 Java 版本,解决多版本并行问题。。。总之,都是成长。
bye~