RedLock 实现分布式锁

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

场景模拟

如下模拟抢购的示例代码(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,也可以达到分布式锁的效果。

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容