# NEVER 传播行为适合健康检查场景?
为什么健康检查要强制禁止在事务中执行?
# 1. 健康检查的特殊性
健康检查的核心目标是:快速、准确地检测系统状态
public class HealthCheckService {
@Transactional(propagation = Propagation.NEVER)
public HealthStatus checkDatabase() {
long startTime = System.currentTimeMillis();
// 场景1:检查数据库连接是否正常
boolean dbConnected = testDatabaseConnection();
// 场景2:检查数据库响应时间
long responseTime = measureQueryResponseTime();
// 场景3:检查是否有死锁
boolean hasDeadlock = checkForDeadlocks();
long totalTime = System.currentTimeMillis() - startTime;
return new HealthStatus(dbConnected, responseTime, hasDeadlock);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2. 为什么不能用事务?(4个关键原因)
# 原因1:避免"假健康"状态
// 错误做法:在事务中检查
@Transactional
public void businessMethod() {
updateData(); // 执行一些业务操作
// 在事务中检查连接
boolean isHealthy = healthCheckService.checkDatabase();
// 问题:事务可能还没提交,看到的可能是旧数据!
reportHealth(isHealthy); // 报告可能是错误的健康状态
}
// 正确做法:强制无事务
@Transactional(propagation = Propagation.NEVER)
public HealthStatus checkDatabase() {
// 可以看到最新的、已提交的数据状态
// 确保检查结果反映真实情况
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 原因2:避免事务锁影响检查结果
// 场景:并发环境下的问题
@Transactional
public void processOrder() {
// 业务方法持有行锁
orderDao.lockForUpdate(orderId);
// 如果在事务中调用健康检查
healthCheckService.checkDatabase();
// 可能因为锁等待超时而误报"不健康"
// 长时间处理...
Thread.sleep(5000);
}
@Transactional(propagation = Propagation.NEVER)
public void checkDatabase() {
// 强制无事务,不会等待其他事务释放锁
// 可以立即获取连接并检查
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 原因3:防止长时间事务占用连接
// 健康检查通常需要独立连接
public class ConnectionPoolMonitor {
@Transactional(propagation = Propagation.NEVER)
public ConnectionPoolStats checkConnectionPool() {
// 检查连接池状态
int activeConnections = getActiveConnections();
int idleConnections = getIdleConnections();
int waitingThreads = getWaitingThreads();
// 如果用了事务,会占用一个连接直到事务结束
// NEVER确保立即获取、立即释放连接
return new ConnectionPoolStats(activeConnections, idleConnections);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 原因4:确保检查的独立性
// 健康检查应该是一个独立的"探针"
public class SystemMonitor {
@Transactional(propagation = Propagation.NEVER)
public MonitoringResult fullSystemCheck() {
// 检查点1:数据库
HealthStatus dbStatus = checkDatabase();
// 检查点2:缓存
HealthStatus cacheStatus = checkRedis();
// 检查点3:外部服务
HealthStatus externalServiceStatus = checkExternalAPI();
// 每个检查都应该是独立的
// 如果A检查失败,不应该影响B检查
// NEVER传播确保每个检查都是独立的
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3. 实际项目中的具体场景
# 场景1:Kubernetes存活探针
@Component
@Slf4j
public class KubernetesLivenessProbe {
// Kubernetes会定期调用此端点
// 必须确保无事务,快速响应
@GetMapping("/health/liveness")
@Transactional(propagation = Propagation.NEVER)
public ResponseEntity<?> livenessProbe() {
try {
// 快速检查:数据库是否可连接
boolean dbAlive = executeQuickQuery("SELECT 1");
// 快速检查:关键表是否可访问
boolean criticalTableAccessible =
executeQuickQuery("SELECT COUNT(1) FROM critical_table");
if (dbAlive && criticalTableAccessible) {
return ResponseEntity.ok().build();
} else {
log.error("Liveness check failed!");
return ResponseEntity.status(503).build();
}
} catch (Exception e) {
log.error("Liveness probe exception", e);
return ResponseEntity.status(503).build();
}
}
private boolean executeQuickQuery(String sql) {
// 简单的查询,确保快速完成
// NEVER传播确保不会受其他事务影响
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 场景2:数据库连接池监控
@Service
public class DatabasePoolHealthService {
@Transactional(propagation = Propagation.NEVER)
public PoolHealthReport checkConnectionPoolHealth() {
// 监控连接池的关键指标
PoolHealthReport report = new PoolHealthReport();
// 1. 获取连接时间(必须无事务)
long start = System.currentTimeMillis();
Connection conn = dataSource.getConnection();
long acquisitionTime = System.currentTimeMillis() - start;
conn.close();
// 2. 检查连接泄漏(查看未关闭的连接)
int leakedConnections = findLeakedConnections();
// 3. 检查最大等待时间
int maxWaitCount = getConnectionsWaitingTooLong();
report.setAcquisitionTime(acquisitionTime);
report.setLeakedConnections(leakedConnections);
report.setMaxWaitCount(maxWaitCount);
return report;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 场景3:定时健康检查任务
@Component
public class ScheduledHealthChecker {
@Scheduled(fixedRate = 30000) // 每30秒执行一次
@Transactional(propagation = Propagation.NEVER)
public void scheduledHealthCheck() {
log.info("开始执行定时健康检查...");
// 检查1:数据库读写测试
HealthCheckResult dbCheck = performDatabaseCheck();
// 检查2:磁盘空间检查
HealthCheckResult diskCheck = performDiskSpaceCheck();
// 检查3:内存使用检查
HealthCheckResult memoryCheck = performMemoryCheck();
// 发送告警(如果有问题)
if (!dbCheck.isHealthy()) {
alertService.sendAlert("数据库健康检查失败", dbCheck.getMessage());
}
log.info("定时健康检查完成");
}
@Transactional(propagation = Propagation.NEVER)
private HealthCheckResult performDatabaseCheck() {
// 创建测试数据
String testId = "health-check-" + System.currentTimeMillis();
TestRecord record = new TestRecord(testId, "health-check");
try {
// 写入测试
testRecordRepository.save(record);
// 立即读取验证(NEVER确保看到已提交的数据)
TestRecord retrieved = testRecordRepository.findById(testId);
// 清理测试数据
testRecordRepository.deleteById(testId);
return HealthCheckResult.healthy("数据库读写正常");
} catch (Exception e) {
return HealthCheckResult.unhealthy("数据库异常: " + e.getMessage());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 4. 如果不使用NEVER会怎样?
// 危险的错误示例
public class DangerousHealthCheck {
// 假设这里用了默认的REQUIRED
@Transactional
public boolean checkDatabase() {
// 业务代码可能意外调用了这个健康检查
}
}
public class OrderService {
@Transactional
public void processComplexOrder() {
// 开始一个复杂的事务
orderDao.save(order);
// 这里可能因为某些原因调用了健康检查
boolean isHealthy = healthCheckService.checkDatabase();
// 问题1:健康检查看到了未提交的数据
// 问题2:如果健康检查失败抛出异常,整个订单会回滚!
// 问题3:健康检查占用了事务连接,可能锁表
// ... 更多业务逻辑(可能耗时)
// 事务提交(可能很晚才释放锁)
}
}
// 结果:一个小小的健康检查可能引发生产事故!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 5. 总结:为什么健康检查必须用NEVER
| 检查类型 | 为什么用NEVER | 可能的风险 |
|---|---|---|
| 存活检查 | 必须快速响应,不能等待 | 否则K8s会误杀Pod |
| 就绪检查 | 必须看到最新提交的数据 | 否则可能路由到不可用实例 |
| 性能检查 | 必须测量真实性能,不受事务影响 | 否则数据不准确 |
| 连接池检查 | 必须独立获取连接 | 否则检查的可能是"假连接" |
| 死锁检查 | 必须能查看所有锁状态 | 否则看不到被当前事务持有的锁 |
核心原则:健康检查应该像一个独立的探针,从外部观察系统状态,而不是作为系统内部的一部分参与事务。
所以,当你在写监控、健康检查、性能测试等代码时,记住:
- ✅ 用
@Transactional(propagation = Propagation.NEVER) - ❌ 不要用默认的事务传播
- ❌ 更不要在有业务逻辑的方法中混入健康检查
这样就能确保你的健康检查是准确、独立、可靠的!