无锁读取配置变量
问题引入
一般来说,一个后端程序会有许许多多动态的配置,也就是说,不允许重启就要生效的变量。
为什么这些配置变量改变了,不允许重启呢?
这当然是因为重启一次所付出的代价很大,例如,至少这段时间内服务是停滞状态。(就算是高可用,部署了一堆服务,能不重启应该就不重启)。
所以就有一个问题,这些配置变量并不是只读的,而是随时都会根据需求变化。变化之后,应该就要生效。
而实实在在的服务程序,都是多线程的,这就相当于,有一个写操作和多个读操作同时存在。
不加锁?行不行呢?
用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。
这样一来,读和写就永远不会同时用到同样的内存。
这样就实现了无锁读写了。
转载自:https://juejin.cn/post/7082752962464284679