# 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. 为什么不能用事务?(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:避免事务锁影响检查结果

// 场景:并发环境下的问题
@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

# 原因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

# 原因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

# 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:数据库连接池监控

@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

# 场景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

# 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

# 5. 总结:为什么健康检查必须用NEVER

检查类型 为什么用NEVER 可能的风险
存活检查 必须快速响应,不能等待 否则K8s会误杀Pod
就绪检查 必须看到最新提交的数据 否则可能路由到不可用实例
性能检查 必须测量真实性能,不受事务影响 否则数据不准确
连接池检查 必须独立获取连接 否则检查的可能是"假连接"
死锁检查 必须能查看所有锁状态 否则看不到被当前事务持有的锁

核心原则:健康检查应该像一个独立的探针,从外部观察系统状态,而不是作为系统内部的一部分参与事务。

所以,当你在写监控、健康检查、性能测试等代码时,记住:

  • ✅ 用 @Transactional(propagation = Propagation.NEVER)
  • ❌ 不要用默认的事务传播
  • ❌ 更不要在有业务逻辑的方法中混入健康检查

这样就能确保你的健康检查是准确、独立、可靠的!

Last Updated: 12/4/2025, 10:23:47 AM