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
    }
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容