In some distributed environment, multi-threaded concurrent programming, if the same resource read and write operations, an unavoidable problem is resource competition, by introducing the concept of distributed lock, can solve the problem of data consistency.

About the author: May Jun, Nodejs Developer, moOCnet certified author, love technology, love to share the post-90s youth, welcome to pay attention to Nodejs technology stack and Github open source project www.nodejs.red

Understand threads, processes, and distributed locks

Thread locking: One advantage of single-threaded programming is that requests are sequential, so you don’t have to worry about thread safety or resource contention, so you don’t have to worry about thread safety when you’re programming Node.js. In multithreaded programming, such as Java, you may be familiar with the word synchronized, which is often the simplest way to solve concurrent programming in Java. Synchronized ensures that only one thread is executing a method or block of code at a time.

Process locks: A service deployed on a server, open more than one process at the same time, the Node. Js programming to take advantage of operating system resources, according to the number of core CPU can open multiple processes mode, if at this time to a Shared resource operation or resource competition problems, moreover each process is independent of each other, have their own independent memory space. Process locks are also hard to solve with Java’s synchronized, which is only valid within the same JVM.

Distributed lock: No matter a service is in single-thread or multi-process mode, it still faces the same problem when it is deployed on multiple machines and operates on the same shared resource in a distributed environment. This is where the concept of distributed locking is introduced. As shown in the figure below, the SET operation is not an atomic operation since read first data is modified by the business logic. When multiple clients perform read and write operations on the same resource, concurrency problems will occur. In this case, distributed locking is introduced to solve the problem, which is usually a very broad solution.

Distributed lock based on Redis

There are many ways to implement distributed locks: database, Redis, Zookeeper. Here is mainly introduced through Redis to achieve a distributed lock, at least to ensure three features: security, deadlock, fault tolerance.

Security: the so-called radish a pit, the first thing to do is to lock, at any time to ensure that only one client holds the lock.

Deadlock: A deadlock may occur because the lock that should have been released is not released for some reason. Therefore, you can set the expiration time synchronously when locking the lock. If the lock is not released due to the client’s own reason, ensure that the lock can be automatically released.

Fault tolerance: Fault tolerance is considered in multi-node mode. As long as N/2+1 nodes are available, the client can successfully acquire and release locks.

Redis single instance distributed lock implementation

To implement a simple distributed lock in a single Node instance of Redis, we will use some simple Lua scripts to implement atomicity. For details, see the Redis Lua scripts in node.js

locked

The first step is to use the setnx command to trap the pit. To prevent deadlocks, an expire time is usually set after the pit, as shown below:

setnx key value
expire key seconds
Copy the code

The above command is not an atomic operation. The so-called atomic operation means that the command will not be interrupted by other threads or requests during execution. If the setnx command is executed successfully, the network expire command will not be executed, resulting in deadlock.

Maybe you can think of to use to solve things, but things have a characteristic, either success or failure, are carried out a sigh of relief, in the example above, we first according to the result of setnx expire is needed to determine whether you need to be set, obviously things won’t work here, the community also has a lot of libraries to solve this problem, Redis now supports the setnx and EXPIRE extension parameters in the set command, which can be executed in one go, as shown below:

  • Value: It is recommended to set the value to a random value. This will be explained when releasing locks
  • EX seconds: specifies the expiration time
  • PX milliseconds: Also sets the expiration time, in different units
  • NX | XX: NX with setnx effect is the same
set key value [EX seconds] [PX milliseconds] [NX|XX]
Copy the code

Release the lock

The process of releasing the lock is to delete the pit that was originally occupied, but it is not enough to delete the pit with the del key. It is easy to delete the lock of others. Why? For example, client A obtains A lock with key = name1 (within 2 seconds), and then processes its own business logic. However, when the blocking time of business logic processing exceeds the lock time, the lock will be automatically released. During this period, client B obtains the lock with key = name1. In this case, client A directly deletes client B’s lock by using the del key command after its own service processing. Therefore, only the locks that client A owns should be released when releasing the lock.

You are advised to set value to a random value during lock locking to secure lock release. Check whether the key exists and value is equal to the specified value before deleting the key. Determine and delete is not an atomic operation, so Lua scripts are still needed.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
Copy the code

Redis single instance distributed lock node.js practice

The Redis client using Node.js is ioredis, NPM install ioredis -s to install the package first.

Initialize a custom RedisLock

class RedisLock {
    /** * RedisLock @param {*} client * @param {*} options */
    constructor (client, options={}) {
        if(! client) {throw new Error('Client does not exist');
        }

        if(client.status ! = ='connecting') {
            throw new Error('Client is not connected properly');
        }

        this.lockLeaseTime = options.lockLeaseTime || 2; // The default lock expiration time is 2 seconds
        this.lockTimeout = options.lockTimeout || 5; // The default lock timeout is 5 seconds
        this.expiryMode = options.expiryMode || 'EX';
        this.setMode = options.setMode || 'NX';
        this.client = client; }}Copy the code

locked

Run the set command to pass the setnx and expire extension parameters to start locking the pit. If the lock fails to be locked, try again. If the lock is not obtained within the specified lockTimeout period, the lock fails to be obtained.

class RedisLock {
    
    / * * * unlocked * @ param * @ {*} key param {*} val * @ param expire * / {*}
    async lock(key, val, expire) {
        const start = Date.now();
        const self = this;

        return (async function intranetLock() {
            try {
                const result = await self.client.set(key, val, self.expiryMode, expire || self.lockLeaseTime, self.setMode);
        
                // The lock succeeded
                if (result === 'OK') {
                    console.log(`${key} ${val}Lock succeeded);
                    return true;
                }

                / / lock timeout
                if (Math.floor((Date.now() - start) / 1000) > self.lockTimeout) {
                    console.log(`${key} ${val}Lock retry timeout end ');
                    return false;
                }

                // Loop to wait for a retry
                console.log(`${key} ${val}Wait for retry ');
                await sleep(3000);
                console.log(`${key} ${val}Start retry ');

                return intranetLock();
            } catch(err) {
                throw new Error(err); }}) (); }}Copy the code

Release the lock

Releasing the lock executes the Redis Lua script we defined through redis.eval(script).

class RedisLock {
    @param {*} key * @param {*} val */
    async unLock(key, val) {
        const self = this;
        const script = "if redis.call('get',KEYS[1]) == ARGV[1] then" +
        " return redis.call('del',KEYS[1]) " +
        "else" +
        " return 0 " +
        "end";

        try {
            const result = await self.client.eval(script, 1, key, val);

            if (result === 1) {
                return true;
            }
            
            return false;
        } catch(err) {
            throw new Error(err); }}}Copy the code

test

The UUID is used to generate a unique ID, a random ID that can be used either way as long as it is unique.

const Redis = require("ioredis");
const redis = new Redis(6379."127.0.0.1");
const uuidv1 = require('uuid/v1');
const redisLock = new RedisLock(redis);

function sleep(time) {
    return new Promise((resolve) = > {
        setTimeout(function() {
            resolve();
        }, time || 1000);
    });
}

async function test(key) {
    try {
        const id = uuidv1();
        await redisLock.lock(key, id, 20);
        await sleep(3000);
        
        const unLock = await redisLock.unLock(key, id);
        console.log('unLock: ', key, id, unLock);
    } catch (err) {
        console.log('Lock failed', err);
    }  
}

test('name1');
test('name1');
Copy the code

Name1 26e02970-0532-11Ea-b978-2160dffafa30 lock key = name1; Name1 26E02970-0532-11EA-B978-2160dFFAFa30 locked successfully after two retries because the lock was automatically released after 3 seconds in the above test.

Name1 26e00260-0532-11Ea-b978-2160dffafa30 The lock succeeded name1 26e02970-0532-11Ea-b978-2160dffafa30 Wait and retry name1 26e02970-0532-11Ea-b978-2160dffafa30 Start Retry Name1 26e02970-0532-11Ea-b978-2160dffafa30 Wait retry unLock: name1 26e00260-0532-11ea-b978-2160dffafa30trueName1 26e02970-0532-11Ea-b978-2160dffafa30 Start Retry Name1 26e02970-0532-11Ea-b978-2160dffafa30 unLock: name1 26e02970-0532-11ea-b978-2160dffafa30true
Copy the code

The source address

https://github.com/Q-Angelo/project-training/tree/master/redis/lock/redislock.js
Copy the code

Redlock algorithm

The above is a simple implementation of Redis distributed locking using Node.js, which is available in a single instance. When we make an extension to the Redis Node, what happens under Sentinel, Redis Cluster?

The following is an example of automatic failover for Redis Sentinel. Suppose client A acquires the lock on primary node 192.168.6.128, and the primary node fails before it can synchronize information to the secondary node. Sentinel then elects another secondary node as the primary node. Then client B applies for the same lock at this time, the same lock will be held by multiple clients, and it is not good to have high requirements on the final consistency of data.

Redlock introduction

In view of these problems, Redis official website Redis. IO /topics/dist… Redlock provides a standardized algorithm using Redis to achieve distributed lock Redlock, Chinese translation version reference redis.cn/topics/dist…

Redlock is also described in the above documentation, so here’s a quick summary: Redlock provides strong protection in Redis single or multiple instances, with its own fault tolerance, From N instance using the same key, random value try to set the key value [EX seconds] [PX milliseconds] [NX | XX] command to get the lock, at least in the effective time N / 2 + 1 a Redis instance to lock, At this point, the lock is considered successful, otherwise the lock fails, in which case the client should unlock all Redis instances.

Apply Redlock to Node.js

Github.com/mike-marcac… Node.js version of Redlock implementation, easy to use, before starting to install ioredis, Redlock package.

npm i ioredis -S
npm i redlock -S
Copy the code

coding

const Redis = require("ioredis");
const client1 = new Redis(6379."127.0.0.1");
const Redlock = require('redlock');
const redlock = new Redlock([client1], {
    retryDelay: 200.// time in ms
    retryCount: 5});// Multiple Redis instances
// const redlock = new Redlock(
/ / [new Redis (6379), "127.0.0.1"), the new Redis (6379, "127.0.0.2"), the new Redis (6379, "127.0.0.3")].
// )

async function test(key, ttl, client) {
    try {
        const lock = await redlock.lock(key, ttl);

        console.log(client, lock.value);
        // do something ...

        // return lock.unlock();
    } catch(err) {
        console.error(client, err);
    }
}

test('name1'.10000.'client1');
test('name1'.10000.'client2');
Copy the code

test

Client2 cannot obtain the lock because client1 obtained the lock first. Error LockError is reported after 5 attempts: Exceeded 5 attempts to lock the resource “name1”.