Spring Boot多实例下如何配置仅单实例执行自定义Micrometer指标?
问题描述
我在配置了Prometheus的Spring Boot应用中实现了自定义Micrometer指标,Prometheus每15秒抓取一次指标。该自定义指标每分钟查询一次数据库,当前部署了2个服务实例,两个实例都会每分钟执行相同的查询。代码如下:
package com.test; import com.entity.Foo; import com.repo.FooRepository; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; import java.util.function.Supplier; @Component public class MonitoringService { private final MeterRegistry meterRegistry; private final Gauge fooCount; private final FooRepository<Foo> fooRepository; @Autowired public MonitoringService(final FooRepository<Foo> fooRepository, final MeterRegistry meterRegistry) { this.fooRepository = fooRepository; this.meterRegistry = meterRegistry; fooCount = Gauge.builder("foo_count", checkFooCount()) .description("Number of foo count") .register(meterRegistry); } @Scheduled(fixedDelayString = "PT1M", initialDelayString = "PT1M") public Supplier<Number> checkFooCount() { return ()-> fooRepository.getTotalFooCount(); } }
请问如何配置让仅其中一个实例执行该指标任务?
解决方案
首先需要修正原代码的一个问题:原Gauge绑定的Supplier会在Prometheus每次抓取时被调用,这会导致每个实例都触发数据库查询。正确的做法是用一个可变变量存储指标值,定时任务仅更新这个变量,Gauge只读取变量值。以下是几种实现单实例执行任务的方案:
方法1:Redis分布式锁(推荐)
如果项目已引入Redis依赖,用分布式锁确保只有一个实例能执行查询:
@Component public class MonitoringService { private final MeterRegistry meterRegistry; private final Gauge fooCount; private final FooRepository<Foo> fooRepository; private final StringRedisTemplate stringRedisTemplate; private static final String LOCK_KEY = "foo_count_metric_lock"; private static final long LOCK_EXPIRE = 70; // 锁超时时间,需大于任务执行时长 private final AtomicInteger currentFooCount = new AtomicInteger(0); @Autowired public MonitoringService(final FooRepository<Foo> fooRepository, final MeterRegistry meterRegistry, final StringRedisTemplate stringRedisTemplate) { this.fooRepository = fooRepository; this.meterRegistry = meterRegistry; this.stringRedisTemplate = stringRedisTemplate; // 初始化Gauge,绑定可变变量 fooCount = Gauge.builder("foo_count", currentFooCount, AtomicInteger::get) .description("Number of foo count") .register(meterRegistry); } @Scheduled(fixedDelayString = "PT1M", initialDelayString = "PT1M") public void updateFooCount() { // 尝试获取锁 Boolean lockAcquired = stringRedisTemplate.opsForValue() .setIfAbsent(LOCK_KEY, "locked", LOCK_EXPIRE, TimeUnit.SECONDS); if (Boolean.TRUE.equals(lockAcquired)) { try { // 仅拿到锁的实例执行查询并更新指标 int count = fooRepository.getTotalFooCount(); currentFooCount.set(count); } finally { // 释放锁 stringRedisTemplate.delete(LOCK_KEY); } } } }
方法2:配置标记控制
给实例配置专属标记,仅启用标记的实例执行任务:
- 给不同实例配置差异化属性:
# 实例1配置 app: metric-task: enabled: true # 实例2配置 app: metric-task: enabled: false
- 修改代码增加条件判断:
@Component public class MonitoringService { private final MeterRegistry meterRegistry; private final Gauge fooCount; private final FooRepository<Foo> fooRepository; @Value("${app.metric-task.enabled:false}") private boolean taskEnabled; private final AtomicInteger currentFooCount = new AtomicInteger(0); @Autowired public MonitoringService(final FooRepository<Foo> fooRepository, final MeterRegistry meterRegistry) { this.fooRepository = fooRepository; this.meterRegistry = meterRegistry; fooCount = Gauge.builder("foo_count", currentFooCount, AtomicInteger::get) .description("Number of foo count") .register(meterRegistry); } @Scheduled(fixedDelayString = "PT1M", initialDelayString = "PT1M") public void updateFooCount() { if (!taskEnabled) { return; } // 仅启用标记的实例执行查询 int count = fooRepository.getTotalFooCount(); currentFooCount.set(count); } }
缺点:执行任务的实例挂掉后,需要手动修改配置切换实例。
方法3:数据库分布式锁
无Redis时,利用数据库唯一约束实现锁:
- 创建锁表(示例):
CREATE TABLE task_lock ( lock_key VARCHAR(64) PRIMARY KEY, expire_time DATETIME NOT NULL );
- 修改代码:
@Component public class MonitoringService { private final MeterRegistry meterRegistry; private final Gauge fooCount; private final FooRepository<Foo> fooRepository; private final JdbcTemplate jdbcTemplate; private static final String LOCK_KEY = "foo_count_metric_lock"; private final AtomicInteger currentFooCount = new AtomicInteger(0); @Autowired public MonitoringService(final FooRepository<Foo> fooRepository, final MeterRegistry meterRegistry, final JdbcTemplate jdbcTemplate) { this.fooRepository = fooRepository; this.meterRegistry = meterRegistry; this.jdbcTemplate = jdbcTemplate; fooCount = Gauge.builder("foo_count", currentFooCount, AtomicInteger::get) .description("Number of foo count") .register(meterRegistry); } @Scheduled(fixedDelayString = "PT1M", initialDelayString = "PT1M") public void updateFooCount() { try { // 插入锁记录,唯一键约束确保仅一个实例成功 jdbcTemplate.update( "INSERT INTO task_lock(lock_key, expire_time) VALUES (?, ?)", LOCK_KEY, LocalDateTime.now().plusSeconds(70) ); // 获取锁成功,执行查询更新指标 int count = fooRepository.getTotalFooCount(); currentFooCount.set(count); // 释放锁 jdbcTemplate.update("DELETE FROM task_lock WHERE lock_key = ?", LOCK_KEY); } catch (DuplicateKeyException e) { // 其他实例已持有锁,跳过执行 return; } } }
内容的提问来源于stack exchange,提问作者DarkCrow
相关产品推荐
相关产品推荐

