C# Redis 分布式锁 续集(新增Mysql分布式锁[还有更多种姿势])

上个文章 《.Net Redis 实现分布式锁 》发出去之后,有很多大佬就有了自己的见解,让我获益良多,又学到了一些新姿势(知识)。

上篇主要是采用了 StackExchange.Redis 的两种方式来获取锁.

获取锁

  1. 1. IDatabase.StringSet(key, value, TimeSpan, When.NotExists,CommandFlags.DemandMaster);(可以补个最后的参数,只有主服务有效)

  2. 2. IDatabase.LockTake(key, value, TimeSpan, CommandFlags.DemandMaster);

其实,获取锁这部分,是没有啥子大问题的(其实这两个操作的内部代码是一样的),有问题的应该是,解锁的部分。

解锁

关于解锁部分

我写的大概逻辑如下,如果取得了锁,就直接删除锁的key,如果我没有获取到锁(异常或者过期了),那么,我查一下此锁存在不,如果存在,跟我当前一样,我就删除(扫尾工作)。

        /// <summary>
        /// 释放锁
        /// </summary>
        /// <returns></returns>
        private bool UnLock(bool lockState)
        {
            if (lockState)
            {
                IDatabase.KeyDelete(key);
            }
            else
            {
                var data = IDatabase.StringGet(key);
                if (data == value)
                {
                    IDatabase.KeyDelete(key);
                }
            }
            return true;
        }

问题

  1. 1. 如果正常业务的过期还在执行,第二个获取锁的任务就会获取锁,这个时候,只需要超时时间是合理的,或者执行期间,正常续时,也能解决问题,类似看门狗。

  2. 2. 就是删除的时候,我实现获取了key里面的值来对比,是否是相同的值,如果相同才会删除,假设此方法并发,则会出现,互相干扰的情况,可能查出来有,删的时候,就已经被另外一个删掉了,这个只能通过原子性操作来实现。

StackExchange LockRelease 的实现

为啥把它提出来,因为它是通过事务实现的,我想这也是解决原子性的一种可行方案,毕竟事务本身就包含了原子性。

以下是它的实现源码部分

       public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
        {
            if (value.IsNull) throw new ArgumentNullException(nameof(value));
            var tran = GetLockReleaseTransaction(key, value);
            if (tran != null) return tran.Execute(flags);

            // without transactions (twemproxy etc), we can't enforce the "value" part
            return KeyDelete(key, flags);
        } 
        
        private ITransaction? GetLockReleaseTransaction(RedisKey key, RedisValue value)
        {
            var tran = CreateTransactionIfAvailable(asyncState);
            if (tran is not null)
            {
                tran.AddCondition(Condition.StringEqual(key, value));
                tran.KeyDeleteAsync(key, CommandFlags.FireAndForget);
            }
            return tran;
        }

原子性操作问题。

其实就是让解锁的时候,查询key value和删除key value 原子操作,不分开。

业界现有的标准做法都是通过Lua来实现的

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

KEYS[1] 就是 key, ARGV[1] 就是 value。

这样就实现了原子性的解锁操作。

Redis 分布式锁 实现的库

其实,实现的库还挺多的,主要是根据官网文档介绍的Redlock 算法来的,我想这样标准的锁的算法逻辑和库都有了,我们就可以不用自己封了。直接用。

具体Redlock 文档参考 :
https://redis.io/docs/reference/patterns/distributed-locks/

可以看Redis官方对Redis分布式锁的详解,以及各种库的支持。

可以看到C# Redlock的库有

  1. 1. Redlock-cs

  2. 2. RedLock.net

  3. 3. ScarletLock

  4. 4. Redlock4Net

其中 RedLock.Net 可以来做一个案例

案例实现

安装 nuget包

Install-Package RedLock.net -Version 2.3.2

代码如下:

class Program
{
    static void Main(string[] args)
    {
        Console.Title = "redis redlock Demo by 蓝创精英团队";
        var connMultiplexer = ConnectionMultiplexer.Connect("127.0.0.1");
        Console.WriteLine($"redis连接状态:{connMultiplexer.IsConnected}");
        var multiplexers = new List<RedLockMultiplexer> { connMultiplexer };
        using var redlockFactory = RedLockFactory.Create(multiplexers);
        Test(redlockFactory);
        Console.WriteLine("redis redlock  案例!");
        Console.ReadLine();
    }
    public static void Test(RedLockFactory RedLockFactory)
    {
        var tasks = new List<Task>();
        for (int i = 0; i < 5; i++)
        {
            tasks.Add(Task.Run(() =>
            {
                var ID = Guid.NewGuid().ToString("N");
                using var redislock = RedLockFactory.CreateLock("key", TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15 * 10), TimeSpan.FromSeconds(1));
                if (redislock.IsAcquired)
                {
                    Console.WriteLine($"{DateTime.Now} - {ID}:申请到标志位!");
                    Console.WriteLine($"{DateTime.Now} - {ID}:处理自己的 事情!");
                    Thread.Sleep(5 * 1000);
                    Console.WriteLine($"{DateTime.Now} - {ID}:处理完毕!");
                }
            }));
        }
        Task.WaitAll(tasks.ToArray());
    }
}

可以看到,封装的更爽了,自动释放锁。

C# Redis 分布式锁 续集(新增Mysql分布式锁[还有更多种姿势])

效果一致,也挺好用的。

MySql 分布式锁

很多时候,可能服务没有redis服务,那我能不能直接通过mysql实现锁来用。

很高兴的是,我刚好发现一个强大的库,包含了所有锁的实现,爽歪歪,我们赶紧用起来。

这个库就是.Net 的 DistributedLock 库。

支持以下中间件的库

  1. 1. SqlServer

  2. 2. Postgres

  3. 3. MySql

  4. 4. Oracle

  5. 5. Redis

  6. 6. Azure

  7. 7. ZooKeeper

  8. 8. FileSystem

  9. 9. WaitHandles uses operating system global WaitHandles (Windows only)

支持这么多,也支持redis,其实它自己也实现了Redlock 分布式锁。

那就来一个mysql的分布式锁试试。

DistributedLock mysql

Install-Package DistributedLock -Version 2.3.0

代码如下:

static void Main(string[] args)
{
    Console.Title = "MySql Lock Demo by 蓝创精英团队";
    Test();
    Console.WriteLine("MySql Lock  案例!");
    Console.ReadLine();
}
public static void Test()
{
    var tasks = new List<Task>();
    for (int i = 0; i < 5; i++)
    {
        tasks.Add(Task.Run(async () =>
        {
            var ID = Guid.NewGuid().ToString("N");
            var @lock = new MySqlDistributedLock("key", "Server=127.0.0.1;Port=3306;Database=test;Uid=root;Pwd=123456;");
            await using (await @lock.AcquireAsync(TimeSpan.FromSeconds(15 * 10)))
            {
                Console.WriteLine($"{DateTime.Now} - {ID}:申请到标志位!");
                Console.WriteLine($"{DateTime.Now} - {ID}:处理自己的 事情!");
                Thread.Sleep(5 * 1000);
                Console.WriteLine($"{DateTime.Now} - {ID}:处理完毕!");
            }
        }));
    }
    Task.WaitAll(tasks.ToArray());
}

效果还是很不错的,跟上边一样的

C# Redis 分布式锁 续集(新增Mysql分布式锁[还有更多种姿势])

总结

学习东西就是否决自己,然后重新认识自己的过程,认知的壁垒不是那么容易打破的,好奇心是一把能打破它的钥匙。

代码地址

https://github.com/kesshei/RedlockDemo.git

https://gitee.com/kesshei/RedlockDemo.git