RedLock 实现分布式锁

作者: BeckJin | 来源:发表于2019-01-06 21:13 被阅读8次

    并发是程序开发中不可避免的问题,根据系统面向用户、功能场景的不同,并发的重视程度会有不同。从程序的角度来说,并发意味着相同的时间点执行了相同的代码,而有些情况是不被允许的,比如:转账、抢购占库存等,如果没有做好临界条件的验证,会带来非常严重的后果。追根结底是因为并发引起的数据不一致问题,面对并发,我们通常会采用锁来优化。

    场景模拟

    如下模拟抢购的示例代码(C#):

    // 有10个商品库存
    private static int stockCount = 10;
    
    public bool Buy()
    {
        // 模拟执行的逻辑代码花费的时间
        Thread.Sleep(new Random().Next(100,500));
        if (stockCount > 0)
        {
            stockCount--;
            return true;
        }
        return false;
    }
    
    var test = new Test();
    
    Parallel.For(1, 16, (i) =>
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var data = test.Buy();
        stopwatch.Stop();
        Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId}, Result:{data}, Time:{stopwatch.ElapsedMilliseconds}");
    });
    Console.ReadKey();
    

    模拟并行调用 Buy 方法 15 次(内部使用的是线程池,所以 ThreadId 会有重复),实际上只有 10 个库存,返回结果却显示 11 个请求都购买成功了。

    concurrent

    单机部署模式解决方案

    在单机部署模式下,我们只需要加 lock(){} 就可以解决问题:

    // 有10个商品库存
    private static int stockCount = 10;
    
    private static object obj = new object();
    
    public bool Buy()
    {
        lock (obj)
        {
            // 模拟执行的逻辑代码花费的时间
            Thread.Sleep(new Random().Next(100, 500));
            if (stockCount > 0)
            {
                stockCount--;
                return true;
            }
            return false;
        }
    }
    
    concurrent with lock

    从输出结果中可以看出,确实只有10个请求是显示购买成功,但同时发现部分请求的执行时间明显变长,这就是加锁带来的最直观影响,当某个线程获得锁之后,在没有释放之前,其他线程只能继续等待,并发越高,更多的线程需要等待轮流被处理。

    各种语言一般都提供了锁的实现,用法大同小异,语言本身实现的锁只能作用于当前进程内,所以在单机模式部署的系统中使用基本没什么问题。

    集群部署模式解决方案(分布式锁)

    在集群模式下,系统部署于多台机器(一个系统运行在多个进程中),语言本身实现的锁只能确保当前进程内有效(基于内存),多进程就没办法共享锁状态,这时我们就得考虑采用分布式锁,分布式锁可以采用 数据库ZooKeeperRedis 等来实现,最终都是为了达到在不同的进程、线程内能共享锁状态的目的。

    这里将介绍基于 Redis 的 RedLock.net 来解决分布式下的并发问题,RedLock.net 是 RedLock 分布式锁算法的 .NET 版实现 (大部分语言都有对应的实现,查看) ,RedLock 分布式锁算法是由 Redis 的作者提出。

    RedLock 简介

    RedLock 的思想是使用多台 Redis Master ,节点完全独立,节点间不需要进行数据同步,因为 Master-Slave 架构一旦 Master 发生故障时数据没有复制到 Slave,被选为 Master 的 Slave 就丢掉了锁,另一个客户端就可以再次拿到锁。锁通过 setNX(原子操作) 命令设置,在有效时间内当获得锁的数量大于 (n/2+1) 代表成功,失败后需要向所有节点发送释放锁的消息。

    获取锁:

    SET resource_name my_random_value NX PX 30000
    

    释放锁:

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

    RedLock.net 集成

    1. 创建 .NETCore API 项目

    2. Nuget 安装 RedLock.net

      Install-Package RedLock.net
      
    3. appsettings.json 添加 redis 配置

      {
        "RedisUrl": "127.0.0.1:6379", // 多个用,分割
        ...
      }
      
    4. 添加 ProductService.cs,模拟商品购买

      // 有10个商品库存,如果同时启动多个API服务进行测试,这里改成存数据库或其他方式
      private static int stockCount = 10;
      public async Task<bool> BuyAsync()
      {
          // 模拟执行的逻辑代码花费的时间
          await Task.Delay(new Random().Next(100, 500));
          if (stockCount > 0)
          {
              stockCount--;
              return true;
          }
          return false;
      }
      
    5. 修改 Startup.cs ,创建 RedLockFactory

      定义 RedLockFactory 变量:

      private RedLockFactory lockFactory;
      

      添加方法:

      private RedLockFactory GetRedLockFactory()
      {
          var redisUrl = Configuration["RedisUrl"];
          if (string.IsNullOrEmpty(redisUrl))
          {
              throw new ArgumentException("RedisUrl 不能为空");
          }
          var urls = redisUrl.Split(",").ToList();
          var endPoints = new List<RedLockEndPoint>();
          foreach (var item in urls)
          {
              var arr = item.Split(":");
              endPoints.Add(new DnsEndPoint(arr[0], Convert.ToInt32(arr[1])));
          }
          return RedLockFactory.Create(endPoints);
      }
      

      在 ConfigureServices 注入 IDistributedLockFactory:

      lockFactory = GetRedLockFactory();
      services.AddSingleton(typeof(IDistributedLockFactory), lockFactory);
      services.AddScoped(typeof(ProductService));
      

      修改 Configure,应用程序结束时释放 lockFactory :

      public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime)
      {
          ...
      
          lifetime.ApplicationStopping.Register(() =>
          {
              lockFactory.Dispose();
          });
      }
      
    6. 在 Controller 添加方法 DistributedLockTest

      private readonly IDistributedLockFactory _distributedLockFactory;
      private readonly ProductService _productService;
      
      public HomeController(IDistributedLockFactory distributedLockFactory,
          ProductService productService)
      {
          _distributedLockFactory = distributedLockFactory;
          _productService = productService;
      }
      
      [HttpGet]
      public async Task<bool> DistributedLockTest()
      {
          var productId = "id";
          // resource 锁定的对象
          // expiryTime 锁定过期时间,锁区域内的逻辑执行如果超过过期时间,锁将被释放
          // waitTime 等待时间,相同的 resource 如果当前的锁被其他线程占用,最多等待时间
          // retryTime 等待时间内,多久尝试获取一次
          using (var redLock = await _distributedLockFactory.CreateLockAsync(productId, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(20)))
          {
              if (redLock.IsAcquired)
              {
                  var result = await _productService.BuyAsync();
                  return result;
              }
              else
              {
                  Console.WriteLine($"获取锁失败:{DateTime.Now}");
              }
          }
          return false;
      }
      
    7. 调用接口测试

      Parallel.For(1, 16, (i) =>
      {
          var stopwatch = new Stopwatch();
          stopwatch.Start();
          var data = GetAsync($"http://localhost:5000/home/distributedLockTest").Result;
          stopwatch.Stop();
          Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId}, Result:{data}, Time:{stopwatch.ElapsedMilliseconds}");
      });
      
      redLock

    关于 RedLock 分布式锁算法的争议大家可以参考:
    How to do distributed locking
    Is Redlock safe?

    总结

    如果使用锁,必然对性能上会有一定影响,我们需要根据实际场景来判断是真正需要。在指定锁过期时间时要相对合理,避免出现锁已过期,但逻辑还没执行完成,这样就失去了锁的意义,当然这种情况下我们还可以考虑重入锁。

    最后推荐一下微软开源的一个基于 Actor 模型的分布式框架 Orleans,也可以达到分布式锁的效果。

    参考链接

    相关文章

      网友评论

        本文标题:RedLock 实现分布式锁

        本文链接:https://www.haomeiwen.com/subject/gzodrqtx.html