likes
comments
collection
share

无锁读取配置变量

作者站长头像
站长
· 阅读数 17

问题引入

一般来说,一个后端程序会有许许多多动态的配置,也就是说,不允许重启就要生效的变量。

为什么这些配置变量改变了,不允许重启呢?

这当然是因为重启一次所付出的代价很大,例如,至少这段时间内服务是停滞状态。(就算是高可用,部署了一堆服务,能不重启应该就不重启)。

所以就有一个问题,这些配置变量并不是只读的,而是随时都会根据需求变化。变化之后,应该就要生效。

而实实在在的服务程序,都是多线程的,这就相当于,有一个写操作和多个读操作同时存在。

不加锁?行不行呢?

Golang来搞一个例子

...
var MaxConn = 10000 // 最大连接数,允许10000
...

看上面的代码,我们有一个变量MaxConn,作用是限制本程序能够接受的最大连接数。

如果这个变量,在程序启动之后,不会变化,那么在读取这个变量的时候,就直接:

if (nowConn < MaxConn) {
    return true
}

这种代码是可以允许的。

但是,一旦我们说,这个变量在程序运行时要改变,那么就不行了。

var MaxConn = 10000
var MaxConnLock sync.Mutex // 这里要一把锁
// 读取时
MaxConnLock.Lock()
defer MaxConnLock.Unlock()
if (nowConn < MaxConn) {
    return true
}
// 写入时
MaxConnLock.Lock()
defer MaxConnLock.Unlock()
MaxConn = 20000

可以看出,确实要加一把锁,这样的写法才是对的。

解决方案一:原子操作

Golang里提供了很多对于变量的,原子操作。

原子操作是什么呢,就是多线程之间不会产生竞争的操作,是可以安全的多线程读写操作。

例如:

//  声明
var MaxConn int64 = 10000

// 读操作
maxConn = atomic.LoadInt64(&MaxConn)

// 写操作
atomic.StoreInt64(&MaxConn, 20000)

以上的代码,可以随时使用,不用加锁。

这种搞法,行。

但是看以下需求。

多个配置变量要保持同步

什么叫保持同步,比如说,我有两个配置变量:

var MaxUser = 10000 // 某业务最大用户数
var MinUser = 8000 // 某业务最小用户数

我们要保证 MaxUser > MinUser

原子操作能办到吗?

显然不能,原子操作,不能保证多个变量之间满足某种关系。

如果我设置新的

  • MaxUser = 20000
  • MinUser = 15000

如果,我先设置 MaxUser ,然后再设置 Minuser,这是没问题。

但是,如果我先设置 MinUser,再设置 MaxUser,就有那么一小会,

MinUser = 15000 // 已经设置成新的

MaxUser = 10000 // 尚未设置成新的

这是不符合需求的。

可能有人说了,我就先设置MaxUser不就行了。

行,如果你有100个变量,你必须要慢慢的缕一下,谁先谁后。恐怕人都要昏了。

解决多变量同步无锁读取

这里有一个简单的方案。

我们将所有变量放入一个struct中:


type ConfigVar struct {
    MaxConn int64
    MinConn int64
    ..
    ..
    ..
}

好了,这个ConfigVar,就是一个整体,在我们的需求里,我们必须保证,

  • 每个线程自己取值的时候,都是一个整体,而不分开。
  • 每次写入都是一个整体,不会分开。

要完成这两个操作,我们需要一些额外的变量。

我们可以一个ConfigVar的数组,长度是2

var configVarArr  = make([]*ConfigVar, 2)

我们知道,只要保证读取的时候,没有别人在写,我们就成功了。

看上面数组,拥有两个元素,我们只要保证,我们读取的下标,和写入的下标不一样就行了!

所以:

var configVarArr  = make([]*ConfigVar, 2)
var nowReadIndex int64 = 0

nowReadIndex这个变量的意思就是,当前读取用的下标是0

所以写入的时候,自然用下标1

我们只需要在写入完毕之后,将这个下标改变一下即可。

也就是说,如果这个下标是0,就将他变成1。

如果这个下标是1,就将他变成0。

这样一来,读和写就永远不会同时用到同样的内存。

这样就实现了无锁读写了。