You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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. 给不同实例配置差异化属性:
# 实例1配置
app:
  metric-task:
    enabled: true

# 实例2配置
app:
  metric-task:
    enabled: false
  1. 修改代码增加条件判断:
@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时,利用数据库唯一约束实现锁:

  1. 创建锁表(示例):
CREATE TABLE task_lock (
    lock_key VARCHAR(64) PRIMARY KEY,
    expire_time DATETIME NOT NULL
);
  1. 修改代码:
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 17:50:43