Redis内置分布式共享锁AcquireLock原理

最近公司公司站点被攻击,所以需求对于重量级的操作根据用户加锁

对于单站点的网站来说,.Net自带的Lock已经够用,但是对于分布式的网站,Lock却没有办法实现应用于不同服务器
所谓条条大路通罗马,即便是分布式,加锁的方式也是挺多,可以选择数据库,也可以用分布式锁,我这边用到的就是Redis自带的AcquireLock

之前并没有怎么接触Redis,所以先对这个锁进行了测试,线上代码

  public void Lock(Action a, string key, int TimeOut = 120)
        {
            IDisposable sr=null;
            try
            {
                sr = RedisManager.GetClient().AcquireLock(RedisManager.GetLockKey(key), TimeSpan.FromSeconds(TimeOut));
                a();
                sr.Dispose();
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                if(sr!=null)
                {
                    sr.Dispose();
                }
            }
        }
    class Program
    {
        static int Index = 0;
        static void Main(string[] args)
        {
            //Console.WriteLine("holle Word!");
            //Console.WriteLine(JsonConvert.SerializeObject(VerifyDataModel.GetVerifyDataModels()));
            //Console.WriteLine(JsonConvert.SerializeObject(VerifyDataModel.GetVerifyDataModels()));
            //RedisManager.Set("11", "nimeid");
            //Console.WriteLine(RedisManager.Get("11"));
            //Console.WriteLine(RedisManager.Get("11"));
            //Console.WriteLine("22222");
            Console.WriteLine("分布式锁!");
            for(int i=0;i<100;i++)
            {
                new Task(p=> { IndexAdd((int)p); },i).Start();
            }



            Console.Read();
        }

        static void IndexAddPrecss(int i)
        {
            Thread.Sleep(1000);
            Index++;
           // Console.WriteLine(DateTime.Now);
            Console.WriteLine("我是第" + i + "个测试");
        }

        static void IndexAdd(int i)
        {
            LockHelper.Lock(()=> { IndexAddPrecss(i); }, "test11211");
        }
    }

代码很简单 ,刚开始调用也没有问题

但是......我把超时时间设置短一点之后却出现

What ?????

怎么会有些成功有些失败呢?

后面我加了时间测试一下 结果就有点明显了

C#在做Task应该又有默认线程池启用的,不晓得做了什么限制,导致只有四个线程同时启用,其他线程要等四个线程结束后才启用,于是用了四个线程测试:

(Lock超时时间设置1S,线程执行2S)

由这个结果可以得知,当四个线程同时争锁的时候,一个成功,其他等待,当等待时间超过超时时间时,Redis会自动抛出异常

-----------------------------------------引入正题--------------------------------

为此我查看了redis封装的源代码,我直接把注释写在源代码上

public RedisLock(RedisClient redisClient, string key, TimeSpan? timeOut)
{
    Func<bool> action = null;
    this.redisClient = redisClient;
    this.key = key;
    if (action == null)
    {
        action = delegate {
            long num;
            Func<IRedisClient, bool> command = null;
            TimeSpan? nullable1 = timeOut;
            TimeSpan span = nullable1.HasValue ? nullable1.GetValueOrDefault() : new TimeSpan(0x16d, 0, 0, 0);
            DateTime dateTime = DateTime.UtcNow.Add(span);
            string lockString = (dateTime.ToUnixTimeMs() + 1L).ToString();
            //验证key存不存在,不存在新增,直接返回true
            if (redisClient.SetValueIfNotExists(key, lockString))
            {
                return true;
            }
            redisClient.Watch(new string[] { key });
            //验证key取回的值能否long格式,不能则返回false,出现这种情况最有可能key跟其他业务的key重叠了
            if (!long.TryParse(redisClient.Get<string>(key), out num))
            {
                redisClient.UnWatch();
                return false;
            }
            //验证key的值是否已经超时,未超时则返回false
            if (num > DateTime.UtcNow.ToUnixTimeMs())
            {
                redisClient.UnWatch();
                return false;
            }
            //key超时重新生成
            using (IRedisTransaction transaction = redisClient.CreateTransaction())
            {
                if (command == null)
                {
                    command = r => r.Set<string>(key, lockString);
                }
                transaction.QueueCommand(command);
                return transaction.Commit();
            }
        };
    }
    //当返回falase 这边会while重复执行
    ExecExtensions.RetryUntilTrue(action, timeOut);
}
public static void RetryUntilTrue(Func<bool> action, TimeSpan? timeOut)
{
    int i = 0;
    DateTime utcNow = DateTime.UtcNow;
//当等待时间超过超时时间,直接抛出异常
while (!timeOut.HasValue || ((DateTime.UtcNow - utcNow) < timeOut.Value)) { i++; if (action()) { return; } SleepBackOffMultiplier(i); } throw new TimeoutException(string.Format("Exceeded timeout of {0}", timeOut.Value)); }

前期绕了很多弯路,后面慢慢理解,懂得原理才有利于更好的开发

 

posted @ 2019-05-15 14:18  无心风雨的落叶  阅读(3786)  评论(0)    收藏  举报