Make writing a habit together! This is the fourth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.

Problem is introduced into

In general, a back-end program has a lot of dynamic configuration, that is, variables that are about to take effect are not allowed to be restarted.

Why are these configuration variables changed and not allowed to restart?

This is, of course, because restarting once is costly, for example, at least for a while. (Even if it is highly available and a bunch of services are deployed, they should not restart if they can).

The problem is that these configuration variables are not read-only, but can change at any time based on requirements. After the change, it should take effect.

The real servers are multithreaded, which means there’s one write operation and multiple reads at the same time.

Don’t lock? How about that?

withGolangLet’s do an example

.var MaxConn = 10000 // The maximum number of connections is 10000.Copy the code

Looking at the code above, we have a variable MaxConn that limits the maximum number of connections this program can accept.

If this variable does not change after the program is started, then when the variable is read, it reads directly:

if (nowConn < MaxConn) {
    return true
}
Copy the code

This kind of code is permissible.

But once we say that this variable is going to change while the program is running, we can’t.

var MaxConn = 10000
var MaxConnLock sync.Mutex // I need a lock here
Copy the code
/ / read
MaxConnLock.Lock()
defer MaxConnLock.Unlock()
if (nowConn < MaxConn) {
    return true
}
Copy the code
/ / writer
MaxConnLock.Lock()
defer MaxConnLock.Unlock()
MaxConn = 20000
Copy the code

You can see that you really need to add a lock, and that’s the right way to write it.

Solution one: atomic manipulation

Golang provides a lot of atomic manipulation of variables.

What is an atomic operation? It’s an operation that doesn’t compete with each other. It’s a safe multi-threaded read and write operation.

Such as:

/ / declare
var MaxConn int64 = 10000

/ / read operation
maxConn = atomic.LoadInt64(&MaxConn)

/ / write operations
atomic.StoreInt64(&MaxConn, 20000)

Copy the code

The above code can be used at any time without locking.

So this way, all right.

But look at the following requirements.

Multiple configuration variables should be kept in sync

What does it mean to keep in sync? For example, I have two configuration variables:

var MaxUser = 10000 // Maximum number of users of a service
var MinUser = 8000 // Minimum number of users of a service
Copy the code

We want to ensure that MaxUser > MinUser

Can atomic operations be done?

You can’t, of course, do an atomic operation and you can’t guarantee a relationship between variables.

If I set a new one

  • MaxUser = 20000
  • MinUser = 15000

If I set MaxUser first and then Minuser, that’s fine.

But if I set MinUser first and then MaxUser, for a little while,

MinUser = 15000 // It is set to a new one

MaxUser = 10000 // Not set to new

This is not in line with demand.

Maybe someone said, I’ll just set MaxUser.

Ok, so if you have 100 variables, you have to slowly count who comes first and who comes last. I’m afraid we’re going to faint.

Solve multivariable synchronous lockless reading

Here’s a simple solution.

We put all the variables into a struct:


type ConfigVar struct {
    MaxConn int64
    MinConn int64. . . }Copy the code

Okay, this ConfigVar is the whole package, and in our requirements, we have to make sure that,

  • Each thread values itself as a whole, not separate.
  • Each write is a whole, not separated.

To do both, we need some extra variables.

We can have an array of ConfigVar of length 2:

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

We know that if we just make sure that no one else is writing while we’re reading, we’re done.

Look at the array above, it has two elements, and we just have to make sure that the index we’re reading is different than the index we’re writing!

So:

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

The nowReadIndex variable means that the index currently being read is 0.

So it’s natural to write it with subscript 1.

We just need to change the subscript when we’re done writing.

In other words, if this sub is 0, make it 1.

If the subscript is 1, make it 0.

This way, reading and writing will never use the same memory.

This enables lockless reads and writes.