viper热加载保存会触发多次的问题

Viper是一个用于Go应用程序的完整且灵活的配置解决方案。它支持多种配置源,包括JSON、TOML、YAML、HCL以及Java属性配置文件,并且可以轻松地从远程配置中心(如Consul、Etcd等)读取配置

Viper 是可以实现配置热加载(配置文件更新后会自动重新加载,而不需要重启服务)功能的,通过viper.WatchConfig()viper.OnConfigChange实现

WatchConfig:启动一个协程来监控配置文件变化

OnConfigChange:当配置文件发生变化时,触发回调函数

一、问题描述

使用Viper的热加载功能,OnConfigChange回调函数在文件保存后会触发多次,导致配置文件被多次加载

测试记录:

  1. 环境:Windows

    编辑器:

    • goland:2次
    • vscode:2次
    • 记事本:1次
    • vim:3次
  2. 环境:Linux

    编辑器:

    • vim:2次
    • goland:3次
  3. 代码示例:

    func InitConfig(env string) error {
       cfgOnce.Do(func() {
          // 根据传参/环境变量设定配置文件路径
          err := setConfigPath(env)
          if err != nil {
                cfgERR = err
          }
          // yaml
          viper.SetConfigType(constant.ConfigType)
          
          if err := viper.ReadInConfig(); err != nil {
                cfgERR = err
          }
          
          // 首次加载配置
          if err := viper.Unmarshal(&cfg); err != nil {
                cfgERR = err
          }
          
          // 启动热更新监听器
          if err := startHotReload(); err != nil {
                cfgERR = err
          }
       })
          
       if cfgERR != nil {
          return cfgERR
       }
       
       return nil
    }
    
    func startHotReload() error {
       viper.WatchConfig()
       viper.OnConfigChange(func(e fsnotify.Event) {
           // 重载配置文件
           reloadConfig()
       })
       
       return nil
    }
    
    func reloadConfig() {
       if err := viper.Unmarshal(&cfg); err != nil {
            fmt.Printf("Error loading config: %v\n", err)
       } else {
            fmt.Println("Config reloaded successfully")
       }
    }
    

二、问题分析

通过Baidu、Google、还有各种AI:ChatGPT、编程助手Baidu Comate、豆包MarsCode等,给出的结果大概是几下几种:

  1. 文件系统的通知机制:

    操作系统(如Linux的inotify或Windows的ReadDirectoryChangesW)可能在文件修改时发送多个通知。这可能是因为文件首先被打开以进行修改,然后内容被写入,最后文件被关闭。每个步骤都可能触发一个通知

  2. 编辑器的保存行为:

    不同的编辑器在保存文件时可能会执行多个步骤,如备份旧文件、写入新内容、再检查文件权限等,每个步骤都可能触发文件系统通知。

  3. 电脑上装了类似加密软件之类的影响

  4. viper本身内部机制的问题

个人觉得,根据测试数据分析,第1、2种情况比较靠谱

三、解决方案

下边两种方案,亲测都可以解决此问题,也可以两种方案结合起来一起使用

  1. 延迟合并处理多次回调

    // ...省略其他代码
    const configChangeDelay = 20 * time.Millisecond 
    
    func startHotReload() {
        viper.WatchConfig()
        viper.OnConfigChange(func(e fsnotify.Event) {
            fmt.Println("config file changed:", e.Name)
            if configChangeTimer != nil {
                configChangeTimer.Stop()
            }
            // 延迟20ms,合并多次回调
            configChangeTimer = time.AfterFunc(configChangeDelay, func() {
                // 这里处理配置文件变化后的操作
                reloadConfig()
            })
        })
    }
    
  2. 使用MD5校验配置文件是否发生变化,只处理有变化的配置文件

    这种方式也可以解决另一个问题,就是没有进行修改文件内容,只保存一下,也会进行文件重载

    // ...省略其他代码
    func startHotReload(filePath string) (err error) {
       initialConfMD5, err := ReadFileMd5(filePath)
       if err != nil {
            return
       }
       
       viper.WatchConfig()
       viper.OnConfigChange(func(e fsnotify.Event) {
          // 过滤不是写入的操作或者keys为空的操作
          if e.Op != 2 || len(viper.AllKeys()) == 0 {
               return
          }
          
          currentConfMD5, err := ReadFileMd5(filePath)
          fmt.Println("initialConfMD5: ", initialConfMD5)
          fmt.Println("currentConfMD5: ", currentConfMD5)
          if err != nil {
               return
          }
          
          // 过滤 MD5值相同,则不执行后续逻辑
          if currentConfMD5 == initialConfMD5 {
               return
          }
          
          // 重载配置文件
          reloadConfig()
          
          // 更新MD5值以便下次比较
          initialConfMD5 = currentConfMD5
       })
       
       return nil
    }
    func ReadFileMd5(filepath string) (string, error) {
       file, err := os.Open(filepath)
       if err != nil {
            return "", err
       }
       defer file.Close()
       
       hasher := md5.New()
       if _, err := io.Copy(hasher, file); err != nil {
            return "", err
       }
       
       return hex.EncodeToString(hasher.Sum(nil)), nil
    }
    
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容