【工作实录】我终于一次性刷新了所有服务的内存数据

765 阅读8分钟

前言

目前我在开发公司的后台系统,有个需求是后台改完系统配置后需要刷新配置,当时看到这需求的我:啥玩意???

情况是这样的:我们项目的系统配置在数据库表里,但用到系统配置的地方那么多,不可能每次需要配置的时候都要查一遍 MySQL,而且修改的频率也不高,就完全没必要……于是我们的配置信息就会在服务启动的时候去数据库读一遍,加载到内存里,后面需要用到配置的地方就直接去内存里那就行了。

在没有后台之前,咱是这么做的……直接请求服务的刷新内存接口,因为是微服务会部署多个实例,你的请求又会通过负载均衡分发到其中一个实例,这次到了哪台服务器我也不知道,所以就直接拿 Postman 重复发多次请求,不出意外每个服务都会走一遍了。

简直

下面我所用到的解决方案不难,但还是因为踩坑(就是太菜)实际耗了我一下午的时间,当时冷汗都出来了😰,做的时候已经11.10下午了,说是要双十一前要上线…

配置信息加载

系统配置的存储(服务启动的时候将系统配置加载到实例内存里)

@Component
public class InitApplicationRunner {

    @Autowired
    private SystemConfigService systemConfigService;

    public static Map<StringSystemConfigSYSTEM_CONFIG = new HashMap<>();

    @PostConstruct
    public void init() {
        // 保存到内存
        SYSTEM_CONFIG = systemConfigService.listSystemConfigs();
    }
}

每次有请求需要读系统配置的时候,就都会去 InitApplicationRunner 类里拿 SYSTEM_CONFIG,然后取需要的配置。

实现思路

通过 RabbitMQ 发消息的方式告诉我的服务:你要给我刷新配置了

通常 RabbitMQ 的使用方式是服务充当消费者监听某个队列,队列来消息了就投递给消费者:我在叫你做事

好家伙!MQ 也是消息到队列后,默认轮询机制将消息投给其中一个消费者,并不是投递给监听的所有服务。也就是说,这次消息队列投给了服务1,下次就给服务2,再下次才给服务3,再服务4……

突破口:每个服务绑定一个队列,有几个服务那就有几个队列,再把交换机改成广播 fanout 类型,消息一来,交换机把消息统一分发给所有队列,这样所有的服务也就顺理成章收到消息了。

好的,那就开干?

具体实现

导依赖

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置

spring:
  rabbitmq:
    host: localhost
    username: guest
    password: guest

消费者声明队列与交换机绑定

由于我们每个服务都需要一个单独的队列和它绑定,所以代码中不能把队列名写死,要是变化的,既然代码中不能写死,那就让服务启动的时候给它动态生成一段就好了。

我们在服务启动的时候进行队列交换机绑定时,队列名后面加上一段随机数,这样我们的每个消费者就会独享一个队列。


可是,想着挺简单,真正写起来的时候,智能的 IDE 跟你说:连我这关都通不过!!!

好家伙!直接编译报错,这个队列名 name 的值需要是个定值,我们再点进注解里面看……

String 可不就是个定值嘛😂

要是只能写死,我要你有何用?

等会儿……让我们再看一眼注释the queue name or "" for a generated queue name (default).

意思就是:这个值用作你的队列名,如果是 "" 空字符串的话,将会生成一个队列名,而它默认值正好就是 ""

(说实话,这个是我在写这篇文章的时候才发现的…哭了)

好吧,继续造!

直接把 name 声明去掉

@RabbitListener(
        bindings = @QueueBinding(
                exchange = @Exchange(name = "admin.ex", type = "fanout", autoDelete = "true"),
                value = @Queue(durable = "true", autoDelete = "true")
        )
)

交换机:名称为 admin.ex,类型为 fanout, autoDelete = "true" 表示为自动删除的交换机(一旦所有队列和它解绑,交换机将自动删除),在我们这场景下,确实也没太大必要是 false

因为交换机没有存储数据的能力,如果交换机没有绑定任何队列,那么发送给交换机的数据就会丢失; 如果交换机都没有,投递消息的时候好歹会有错误日志,说交换机不存在。

队列:名称不主动声明,随机生成。durable = "true" 表示为持久化队列,在这场景下无所谓,习惯性加上了。

autoDelete = "true" 表示为自动删除的队列,一旦所有消费者解除了监听后,就会自动删除,在我们当前的需求上也是有必要的。

启动多个消费者

服务多开也不用复制模块,稍加操作就好。

Allow parallel run ,再运行启动类就不是重启了,而是另开一个服务。我们只需要在启动前改下端口名,这样就会有多个实例在跑了。

我们的两个队列也都创建好了,两个名字都是 Spring 给随机生成的。

生产者发送消息

@Autowired
private RabbitTemplate rabbitTemplate;

public void refreshConfig() {
    // fanout没有路由键不能填null,要是空字符串,消息直接带个空字符串当过去就行
    rabbitTemplate.convertAndSend("admin.ex""""");
}

消费者成功接收消息

好了,消费者都成功收到消息刷新配置。

害,这个写法我也是刚刚才发现的……那我再把另一种方式码出来吧

Java 原生写法

导依赖

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.7.3</version>
</dependency>

消费者声明队列与交换机绑定

@Slf4j
@Component
public class MqListener {
    
    @Autowired
    private SystemConfigService systemConfigService;

    /**
     * 定义交换机名称
     */
    private static final String EXCHANGE_NAME = "admin.ex";

    @PostConstruct
    public void msgHandler() throws IOException, TimeoutException {
        // 1.连接RabbitMQ
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        // 2.获得channel对象
        Channel channel = connection.createChannel();

        // 3.声明交换器类型(fanout),要求要与生产者一致
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        // 4.*声明随机队列
        String queueName = channel.queueDeclare().getQueue();
        // 5.进行队列交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        // 6.获得消息
        channel.basicConsume(queueName, truenew DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                log.info("队列[{}]刷新配置", queueName);
                InitApplicationRunner.SYSTEM_CONFIG = systemConfigService.listSystemConfigs();
            }
        });
        // 7.不能关闭连接(因为需要保持监听)
    }
    
}

在消费者实例启动时加载类 MqListener ,用于与 MQ 进行连接(不断开),保持监听的状态。

启动多个消费者

方式与上面一样,不再赘述。

启动后队列也都创建好了,名字也是随机生成的。

生产者发送消息

public void refreshConfig() throws IOException, TimeoutException {
    // 1.连接RabbitMQ
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    factory.setPort(5672);
    factory.setUsername("guest");
    factory.setPassword("guest");
    Connection connection = factory.newConnection();
    // 2.获得channel对象
    Channel channel = connection.createChannel();
    
    // 3.声明交换机名和类型(fanout)
    String exchangeName = "admin.ex";
    channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT);
    // 这里没有指明队列,队列由消费者指定
    
    // 4.发布消息,直接空字符串当带过去就行
    channel.basicPublish(exchangeName, ""null"".getBytes("UTF-8"));
    // 5.关闭连接和通道
    channel.close();
    connection.close();
}

消费者成功接收消息

不说了,还是 Spring Boot 更香一点,我去改代码了

最后

关于项目的系统配置加载大家还有什么更好的办法可以在评论区进行交流啊~

如果本文对你有帮助的话不妨点个👍。

分享技术,稳住,我们能赢💪!