type
status
date
slug
summary
tags
category
icon
password

第一个问题:SetConfigType()真的有用吗?

问题引入

当你使用如下方式读取配置时,viper会从./conf目录下查找任何以config为文件名的配置文件,如果同时存在./conf/config.json./conf/config.yaml两个配置文件的话,viper会从哪个配置文件加载配置呢?

复现

下面的 demo 代码模拟这种情况
发现输出的是 Name: Serendipity_json, Age: 20
之后我们尝试加上 viper.SetConfigType("yaml") 这一行代码,运行发现结果并没有因为加上这一行而改变。
运行结果
notion image

分析

首先来看一下为什么在没有指明文件类型的时候会读取到 json 文件。
  • viper 会按照文件系统的顺序查找文件,在你设置的路径下依次尝试加载 config.jsonconfig.yamlconfig.toml 等文件格式。
  • 默认情况下,.json 文件会被优先加载,如果同时存在 config.jsonconfig.yamlviper 会加载 config.json 文件。
明白了这一点后我们再来看为什么加上 SetConfigType("yaml") 后结果依旧不变。
我们来看一下 Viper 中 viper.ReadInConfig() 的源码
notion image
可以看到,只有在 stringInSlice() 中用到过 v.getConfigType(),也就是获取文件的种类,在读取文件的时候并没有做文件名称和种类的拼接,导致这个 SetConfigType() 并没有起到实质性确定文件类型的作用。

解决方案

第二个问题:热更新配置一致性问题

问题引入

假设我们当前服务中有一条流水线操作,需要分别调用三个接口A、B、C才能完成对应的功能,其中配置文件存储了调用的接口名称。
现在我们需要将流水线要执行的流程从ABC换成DEF,在替换过程中就可能出现热更新冲突的问题。

复现

  1. 业务协程开始从配置文件读取接口A的配置,读取完成后调用接口A。
  1. 在接口A调用尚未返回时,WatchConfig监听到配置文件变化,触发热更新OnConfigChange,配置文件变化如下:
    1. 此时协程继续按照流水线流程读取配置,读取到下一个要执行的接口是E,这里就破坏了流程的完整性,与我们理想状态下的ABC或者DEF的执行流程不一致,可能导致无法预估的错误出现。
    以下demo程序模拟了这种情况
    运行结果:
    notion image

    分析

    我们先来看一下 WatchConfig() 的源码
    notion image
    简单来说就是通过 WatchConfig() 先初始化一个 viper 对象,完成后开始进行文件变更事件的监听,OnConfigChange() 负责将用户定义的回调逻辑赋值给 viper 对象的 onConfigChange(),此时文件如果发生对应的变更,则会触发对应的回调逻辑。
    因此我们可以得出产生热更新冲突点原因:
    1. 并发读写未同步 viper 默认未对内存中的配置数据加锁,当多个 goroutine 同时读写配置时,会引发竞争。
    1. 配置存储非原子性配置文件写入中途被读取(如文件未完全写入),会导致读取到损坏或不完整的数据。

    解决方案

    方案一:加读写锁

    实现思路
    1. 全局配置对象的建立:为了便于管理多个系统的共享配置资源,我们将所有系统的相关配置集中存储在一个全局的配置对象中。通过这种设计,可以避免因同一配置对象被不同部分重复读取导致的操作异常。
    1. 线程安全机制的实现:在对全局配置进行更新时,直接修改原对象可能会引发多线程竞争和不一致性问题。为此,我们采用“读写锁“的方式,确保对配置对象的所有操作均需通过锁进行互斥处理。这种机制能够在任何时间点确保只有一个线程对配置对象进行修改。
    1. 防止更新阻塞的技术:为了避免因配置更新导致的线程阻塞问题以及确保数据一致性,我们在全局配置发生更新后采取以下措施:首先,在相关组件中触发回调机制,以通知其获取最新的配置信息;其次,启动一个协程来执行更新操作,这种方式可以有效避免因单一操作引发的资源阻塞,并确保其他协程线程能够正常读取和处理数据。
    源码
    运行结果:
    notion image

    方案二:原子操作

    实现思路
    1. 全局配置对象:通过全局配置对象的Store`方法,在初始化及更新阶段获取最新的配置信息。
    1. 复制读取:Load方法以读取复制一份当前的状态。这种方式确保了每次读取的数据都是最新版本的副本,避免数据不一致的风险。
    1. 原子性操作:由于这些操作采用的是原子性机制,避免了显式的锁管理,因此在性能上具有显著优势。相比传统的加锁方式,这种做法能够有效减少资源竞争和同步开销,从而提升了系统的整体效率。
    源码
    运行结果:
    notion image
    换成多个协程不同时运看看效果
    notion image

    第三个问题:AddConfigPath()路径究竟怎写?

    问题引入

    正常启动项目,main 函数,都使用同位置可以正常读取配置,但是在单元测试调用初始化函数时,出现了无法找到配置文件的问题。

    复现

    分析

    列出当前关键目录
    尝试在测试测试函数中输出当前路径,发现输出结果为 Muxi-Micro-Layout/conf/conf ,这显然不符合我们的预期。
    主要原因是对 ./ 有误解,我一直以为 ./ 的意思是当前项目的根目录,在 go 中 ./ 是基于执行命令的目录的,也就是说在不同的目录下调用 Init()./ 所代表的意义不同。

    解决方案

    因为是直接获取的 config.go 文件的绝对目录,所以无论在哪里调用配置初始化函数,都不会出现找不到文件的问题了

    参考链接: 官网:https://github.com/spf13/viper
     
    板子Go语言发送邮件
    Serendipity
    Serendipity
    From CCNU
    Announcement
    type
    status
    date
    slug
    summary
    tags
    category
    icon
    password
    本网站部署于国外服务器,国内访问较慢。多刷新或挂梯子。