scenario

In Redis, it often reads the value of a key, performs some business logic processing, calculates a new value according to the read value, and sets it in again.

If client A reads the key value and then client B changes the key value, then there is A concurrency security problem.

simulating

Suppose Redis Server has a key named test that contains a JSON array [1, 2, 3].

Let’s simulate the situation where both client A and client B access the modification at the same time. The code is as follows:

Client A:

class RedisClientA(username: String, password: String, host: String, port: Int) { val jedis: Jedis init { val pool = JedisPool(JedisPoolConfig(), host, port) jedis = pool.resource jedis.auth(username, password) } fun update(key: String) {val idStr = jedis.get(key) val idList = json.decodeFromString <MutableList<Int>>(idStr) Sleep (2L) idlist. add(4) println("new id list: $idList") jedis.set(key, Json.encodeToString(idList)) } fun getVal(key: String): String? { return jedis.get(key) } } fun main() { val key = "test" val redisClientA = RedisClientA("default", "123456", Redisclienta. update(key) val res = redisclientA. getVal(key) println("res: $res")}Copy the code

Client B:

class RedisClientB(username: String, password: String, host: String, port: Int) { val jedis: Jedis init { val pool = JedisPool(JedisPoolConfig(), host, port) jedis = pool.resource jedis.auth(username, password) } fun update(key: String) { val idStr = jedis.get(key) val idList = Json.decodeFromString<MutableList<Int>>(idStr) idList.add(5) println("new id list: $idList") jedis.set(key, Json.encodeToString(idList)) } fun getVal(key: String): String? { return jedis.get(key) } } fun main() { val key = "test" val redisClientB = RedisClientB("default", "123456", Redisclientb. update(key) val res = redisclientb. getVal(key) println("res: $res")}Copy the code

Client A blocked for 2 seconds to simulate the processing of time-consuming business logic. While processing, client B accesses “test” and adds ID :5.

When client A finishes processing the time-consuming service logic, ID :4 is added and ID :5 is overwritten.

The final contents of “test” are as follows:

CAS to ensure data consistency

The WATCH command provides check-and-set (CAS) behavior for Redis transactions. WATCH keys are monitored to see if they have been changed. If at least one monitored build is modified before EXEC executes, the entire transaction is cancelled and EXEC returns Null replay to indicate that the transaction failed. We just need to repeat the operation and hopefully there will be no new competition during this time frame. This form of locking is called optimistic locking, and it is a very powerful locking mechanism.

So how does the CAS approach work? We just need to change the code in RedisClientA’s update() method to look like this:

fun update(key: String) { var flag = true while (flag) { jedis.watch(key) val idStr = jedis.get(key) val idList = Json. DecodeFromString <MutableList<Int>>(idStr) // wait 2 seconds, Timeunit.seconds.sleep (2L) val Transaction = jedis.multi() idList.add(4) println("new ID list: $idList") transaction.set(key, Json.encodeToString(idList)) transaction.exec()?.let { flag = false } } }Copy the code

The final contents of “test” are as follows:

It can be seen that we use WATCH and TRANACTION commands to achieve data consistency by using CAS optimistic locking.