Principle and implementation of Redis distributed lock

When operating on the same resource, a single cache read is no problem, but what to do when concurrency exists. In order to avoid data inconsistency, we need to perform a "lock" operation before operating the shared resource.

We are developing many business scenarios that use locks, such as inventory control, lucky draw, second kill, etc. Generally, we will use memory lock to ensure linear execution.

But now most sites will use distributed deployment, so multiple servers must use the same target to determine the lock. The biggest difference between distributed and stand-alone is that it is not multi-threaded, but multi process.

Figure 1: memory locks used by distributed sites

Figure 2: distributed sites using distributed locks

Of course, we can't use such a complex scenario for the time being. We can simply access redis.

Design (pessimistic lock / optimistic lock)

Pessimistic locking mode (I think there will be problems during operation, so I lock them all)

Pessimistic lock, as the name suggests, is very pessimistic. Every time I go to get the data, I think others will modify it,

So every time you get the data, it will be locked, so that others will block the data until it gets the lock.

Many such locking mechanisms are used in traditional relational databases, such as row lock, table lock, read lock and write lock, which are locked before operation.

Optimistic locking method (when you think there will be no problem, so you don't lock it. Query and judge when updating, and then check whether anyone has modified this data during this period.)

Optimistic lock, as the name suggests, is very optimistic. Every time I go to get the data, I think others will not modify it,

Therefore, it will not be locked, but when updating, it will judge whether others have updated this data during this period. You can use mechanisms such as version number.

Optimistic locking is applicable to multi read application types, which can improve throughput. For example, if a database is provided, it is similar to write_ In fact, the condition mechanism provides optimistic locks.

Two types of locks have their own advantages and disadvantages. One cannot be considered better than the other. For example, optimistic locks are applicable to the case where there are few writes, that is, when conflicts really rarely occur, which can save the cost of locks,

The whole throughput of the system is increased.

However, if conflicts often occur, the upper layer application will constantly retry, which will reduce the performance. Therefore, pessimistic locking is more appropriate in this case.

Redis three commands

1,SETNX

SETNX key value: if and only if the key does not exist, set a string whose key is val and return 1; If the key exists, do nothing and return 0.

2,expire

expire key timeout: set a timeout for the key, with the unit of second. After this time, the lock will be automatically released to avoid deadlock.

3,delete

delete key: deletes a key

These three commands are mainly used when Redis is used to implement distributed locks.

Proposition: inventory spike of a commodity.

If we want to hold a second kill for a commodity, we have stored the inventory data 100 in redis in advance. Now we need to make inventory deduction.

Figure 3: schematic diagram of locking request

code implementation

We are based on servicestack Redis} operation

We create a console application (. NET Framework) named RedisLock. Note that if you create a net core application, the servicestack Redis will choose the core.

Then install servicestack in NuGet Redis.

Redis connection pool

       //Redis connection pool (configure connection address, read / write connection address, etc.)
        public static PooledRedisClientManager RedisClientPool = CreateManager();
        private static PooledRedisClientManager CreateManager()
        {
            //Write node (master node)
            List<string> writes = new List<string>();
            writes.Add("10.17.3.97:6379");  
            //Read node
            List<string> reads = new List<string>();
            reads.Add("10.17.3.97:6379");

            //Configure connection pool and read / write classification
            return new PooledRedisClientManager(writes, reads, new RedisClientManagerConfig()
            {
                MaxReadPoolSize = 50, //Number of read nodes
                MaxWritePoolSize = 50,//Number of write nodes
                AutoStart = true,
                DefaultDb = 0
            });
        }

Use the SetNX command of Redis to implement locking

        /// <summary>
        ///Lock (using the SetNX command of Redis to lock)
        /// </summary>
        ///< param name = "key" > lock key < / param >
        ///< param name = "selfmark" > self marking < / param >
        ///< param name = "lockexpiryseconds" > automatic lock expiration time [default 10] (s) < / param >
        ///< param name = "waitlockmilliseconds" > wait lock time (ms) < / param >
        /// <returns></returns>
        public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue)
        {
            DateTime begin = DateTime.Now;
            selfMark = Guid.NewGuid().ToString("N");//Mark by yourself. It will be used when releasing the lock. The lock you add can only be opened by yourself unless it expires
            using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
            {
                
                string lockKey = "Lock:" + key;
                while (true)
                {
                    string script = string.Format("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE',KEYS[1],{0}) return 1 else return 0 end", lockExpirySeconds * 1000);
                    //Cyclic acquisition and lock removal
                    if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1)
                    {
                        return true;
                    }

                    //Return without waiting for lock
                    if (waitLockMilliseconds == 0)
                    {
                        break;
                    }

                    //If the waiting time is exceeded, no longer wait
                    if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds)
                    {
                        break;
                    }
                    Thread.Sleep(100);
                }

                return false;
            }
        }

Because servicestack The SetNX method provided by redis does not provide a method to set the expiration time, and the locking business cannot be executed separately (the permanent deadlock caused by the failure to set the expiration time if the locking is successful), so it is implemented by script to solve the deadlock problem under abnormal conditions

If it is set to 0, it is an optimistic locking mechanism. If the lock cannot be obtained, it will directly return the lock not obtained

The default value is the maximum value of long. It is a pessimistic locking mechanism, which is about many, many days. It can be understood as waiting all the time

Release lock

     /// <summary>
        ///Release lock
        /// </summary>
        ///< param name = "key" > lock key < / param >
        ///< param name = "selfmark" > self marking < / param >
        public static void UnLock(string key, string selfMark)
        {
            using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
            {
                string lockKey = "Lock:" + key;
                var script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                redisClient.ExecLuaAsString(script, new[] { lockKey }, new[] { selfMark });
            }
        }

Business call (we use multithreading to simulate the scenario of multi-user second kill)

     //Business: pessimistic locking method
        public static void PessimisticLock()
        {
            int num = 10;  //Total quantity
            string lockkey = "xianseng";

            //Pessimistic lock opened, 20 people took the baby at the same time
            for (int i = 0; i < 20; i++)
            {
                Task.Run(() =>
                {
                    string selfmark = ""; 
                    try
                    {
                        if (Lock(lockkey, out selfmark))
                        {
                            if (num > 0)
                            {
                                num--;
                                Console.WriteLine($"I got the baby: Baby surplus{num}individual\t\t{selfmark}");
                            }
                            else
                            {
                                Console.WriteLine("Baby, it's gone");
                            }
                            Thread.Sleep(100);
                        }
                    }
                    finally
                    {
                        UnLock(lockkey, selfmark);
                    }
                });
            }

            Console.ReadLine();
        }

        //Business: optimistic lock mode
        public static void OptimisticLock()
        {
            int num = 10; //Total quantity
            string lockkey = "xianseng";

            //Optimistic lock opens 10 threads, and each thread takes it 5 times
            for (int i = 0; i < 10; i++)
            {
                var lineOn = "thread " + (i + 1);
                Task.Run(() =>
                {
                    for (int j = 0; j < 5; j++)
                    {
                        string selfmark = "";
                        try
                        {
                            if (Lock(lockkey, out selfmark, 10, 0))
                            {
                                if (num > 0)
                                {
                                    num--;
                                    Console.WriteLine($"{lineOn} The first{(j+1)}Time I got the baby: Baby surplus{num}individual\t\t{selfmark}");
                                }
                                else
                                {
                                    Console.WriteLine($"{lineOn} The first{(j + 1)}The baby is gone");
                                }

                                Thread.Sleep(1000);
                            }
                            else
                            {
                                Console.WriteLine($"{lineOn} The first{(j+1)}I didn't get it this time. I don't want to wait");
                            }
                        }
                        finally
                        {
                            UnLock(lockkey, selfmark);
                        }
                    }
                });
            }

            Console.ReadLine();
        }

Then call in the main function to view the display effect

       static void Main(string[] args)
        {
            Call: pessimistic locking mode (I think there will be problems when I operate, so I lock them all)
            PessimisticLock();

            ///Call: optimistic locking method (if you think there will be no problem, you don't lock it. Query and judge when updating, and then check whether anyone has modified this data during this period.)
            //OptimisticLock();
        }

This simply implements the Redis distribution lock function. Go and have a try.

Original link:
http://www.cnblogs.com/xiongze520/p/15176559.html

If this article is helpful, you can pay attention to my official account "w's programming diary" for more Java data.

Keywords: Database Redis Programmer Distribution

Added by rdub on Sat, 11 Dec 2021 14:22:07 +0200