1.背景

总是会听到分布式锁、并发等问题。但是个人很容易混淆,不易深刻理解这个锁对于并发的控制。

本文是个人专门用来记录对以上这些问题的理解过程。

在C#中有个关键字Lock.我们一般会用来给临界区或者共享资源来加锁使用,实现同一时间只有1个线程来访问。ps:lock是编译器的语法糖,底层是Monitor.Enter+Monitor.Exit等功能。但是lock只能保证同一进程下多个线程的并发访问。如果这时开了多个客户端来访问,就会出现并发问题。所以lock只适合单节点下的并发情况。

基于上面的问题,根据Redis来实现的Redis分布式锁就应允而生。因为Redis够快、支持事务、单命令支持原子性等这些特性,保证了Redis分布式锁的出色性能。

Redis分布式锁是什么了?其实就是一把锁,在分布式环境下,多个客户端并发访问的情况下,保证共享资源的串行有序访问,控制并发。Redis分布式锁底层采用setnx+expire命令组合来实现加锁,释放锁时,根据身份标识去释放各自创建的锁,各删各的锁。

本文会以秒杀为案例,简单记录下不同场景下的并发。

2.场景分析

2.1 同步方式执行秒杀

程序如下:

 /// <summary>
 /// 同步的方式执行秒杀
 /// </summary>
 private static void TestSeckillSync()
 {
     //模拟线程数
     var thredNumber = 20;
     //秒杀库存数
     var stockNumber = 3;

     //秒杀成功队列key
     var key = "order_queue";

     var csredis = new CSRedisClient(redisConnectionStr);
     csredis.Del(key);
     var isEnd = false;

     // 创建秒杀执行信号量集合
     List<Task> taskList = new List<Task>();
     // 添加计时器
     Stopwatch stopwatch = new Stopwatch();
     // 开启
     stopwatch.Start();

     for (int i = 0; i < thredNumber; i++)
     {
         int number = i;

         Thread.Sleep(50);

         if (isEnd)
         {
             Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
         }

         var len = csredis.LLen(key);
         //库存不足
         if (len >= stockNumber)
         {
             isEnd = true;
             stopwatch.Stop();
             Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
         }
         else
         {
             var value = $"线程{Thread.CurrentThread.ManagedThreadId}-用户{number}";
             csredis.LPush(key, value);
             Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀成功。");
         }
     }
     // 等待所有秒杀列表中任务结束
     var lenALL = csredis.LLen(key);
     Console.WriteLine($"\r\n秒杀成功人数:{lenALL} 人,用时:{stopwatch.ElapsedMilliseconds} 毫秒.");
     Console.WriteLine($"\r\n是否超售:{(lenALL > stockNumber ? "是" : "否")}");
     Console.WriteLine("\r\n秒杀成功人员名单:");
     for (int i = 0; i < stockNumber; i++)
     {
         Console.WriteLine(csredis.RPop(key));
     }
 }

程序执行后的结果:

2.2 并发方式执行秒杀

  /// <summary>
  /// 并发方式执行秒杀
  /// </summary>
  private static void TestSeckillTask()
  {
      //模拟线程数
      var thredNumber = 20;
      //秒杀库存数
      var stockNumber = 3;

      //秒杀成功队列key
      var key = "order_queue";

      var csredis = new CSRedisClient(redisConnectionStr);
      csredis.Del(key);
      var isEnd = false;

      // 创建秒杀执行信号量集合
      List<Task> taskList = new List<Task>();
      // 添加计时器
      Stopwatch stopwatch = new Stopwatch();
      // 开启
      stopwatch.Start();

      for (int i = 0; i < thredNumber; i++)
      {
          int number = i;
          taskList.Add(Task.Run(() =>
          {
              Thread.Sleep(50);

              if (isEnd)
              {
                  Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
              }

              var len = csredis.LLen(key);
              //库存不足
              if (len >= stockNumber)
              {
                  isEnd = true;
                  stopwatch.Stop();
                  Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
              }
              else
              {
                  var value = $"线程{Thread.CurrentThread.ManagedThreadId}-用户{number}";
                  csredis.LPush(key, value);
                  Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀成功。");
              }
          }));
      }
      // 等待所有秒杀列表中任务结束
      Task.WaitAll(taskList.ToArray());

      var lenALL = csredis.LLen(key);
      Console.WriteLine($"\r\n秒杀成功人数:{lenALL} 人,用时:{stopwatch.ElapsedMilliseconds} 毫秒.");
      Console.WriteLine($"\r\n是否超售:{(lenALL > stockNumber ? "是" : "否")}");
      Console.WriteLine("\r\n秒杀成功人员名单:");
      for (int i = 0; i < stockNumber; i++)
      {
          Console.WriteLine(csredis.RPop(key));
      }
  }

2.3 并发方式执行秒杀 加Lock

  private static readonly object fileLock = new object();

  /// <summary>
  /// 并发方式执行秒杀+加Lock(该方式仅仅对单节点有用 控制并发)
  /// </summary>
  private static void TestSeckillCodeLock()
  {
      //模拟线程数
      var thredNumber = 20;
      //秒杀库存数
      var stockNumber = 3;

      //秒杀成功队列key
      var key = "order_queue";

      var csredis = new CSRedisClient(redisConnectionStr);
      csredis.Del(key);
      var isEnd = false;

      // 创建秒杀执行信号量集合
      List<Task> taskList = new List<Task>();
      // 添加计时器
      Stopwatch stopwatch = new Stopwatch();
      // 开启
      stopwatch.Start();

      for (int i = 0; i < thredNumber; i++)
      {
          Thread.Sleep(50);

          int number = i;
          taskList.Add(Task.Run(() =>
          {
              lock (fileLock)
              {
                  if (isEnd)
                  {
                      Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
                  }

                  var len = csredis.LLen(key);
                  //库存不足
                  if (len >= stockNumber)
                  {
                      isEnd = true;
                      stopwatch.Stop();
                      Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
                  }
                  else
                  {
                      var value = $"线程{Thread.CurrentThread.ManagedThreadId}-用户{number}";
                      csredis.LPush(key, value);
                      Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀成功。");
                  }
              }
          }));
      }
      // 等待所有秒杀列表中任务结束
      Task.WaitAll(taskList.ToArray());

      var lenALL = csredis.LLen(key);
      Console.WriteLine($"\r\n秒杀成功人数:{lenALL} 人,用时:{stopwatch.ElapsedMilliseconds} 毫秒.");
      Console.WriteLine($"\r\n是否超售:{(lenALL > stockNumber ? "是" : "否")}");
      Console.WriteLine("\r\n秒杀成功人员名单:");
      for (int i = 0; i < stockNumber; i++)
      {
          Console.WriteLine(csredis.RPop(key));
      }
  }

2.4 并发方式执行秒杀+加Redis分布式锁

 /// <summary>
 /// 并发方式执行秒杀+加Redis分布式锁(单、多节点下控制并发)
 /// </summary>
 private static void TestSeckillRedisLock()
 {
     //模拟线程数
     var thredNumber = 20;
     //秒杀库存数
     var stockNumber = 3;

     //秒杀成功队列key
     var key = "order_queue";
     var lockKey = "orderlock";

     var csredis = new CSRedisClient(redisConnectionStr);
     csredis.Del(key);
     var isEnd = false;

     // 创建秒杀执行信号量集合
     List<Task> taskList = new List<Task>();
     // 添加计时器
     Stopwatch stopwatch = new Stopwatch();
     // 开启
     stopwatch.Start();

     for (int i = 0; i < thredNumber; i++)
     {
         int number = i;
         taskList.Add(Task.Run(() =>
         {
             Thread.Sleep(50);

             //尝试加锁  如果成功 再设置 过期时间
             //setnx+expire 采用Lua保证命令的原子性
             //也可以考虑一条命令执行 set xx xxx nx ex 1000
             //设置客户端的标识,用于解锁
             var nxSelfMarkvalue = $"thred{Thread.CurrentThread.ManagedThreadId}_user{number}";
             //加锁
             var isGetLock = csredis.RedisLock(lockKey, nxSelfMarkvalue, 1000);
             if (isGetLock)
             {
                 if (isEnd)
                 {
                     Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
                 }

                 var len = csredis.LLen(key);
                 //库存不足
                 if (len >= stockNumber)
                 {
                     isEnd = true;
                     stopwatch.Stop();
                     Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
                 }
                 else
                 {
                     var value = $"线程{Thread.CurrentThread.ManagedThreadId}-用户{number}";
                     csredis.LPush(key, value);
                     //解锁 nxSelfMarkvalue,防止误解锁
                     csredis.RedisUnLock(lockKey, nxSelfMarkvalue);
                     Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀成功。");
                 }
             }
             else
             {
                 Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 系统繁忙,请稍后再试。 秒杀失败。");
             }
         }));
     }
     // 等待所有秒杀列表中任务结束
     Task.WaitAll(taskList.ToArray());

     var lenALL = csredis.LLen(key);
     Console.WriteLine($"\r\n秒杀成功人数:{lenALL} 人,用时:{stopwatch.ElapsedMilliseconds} 毫秒.");
     Console.WriteLine($"\r\n是否超售:{(lenALL > stockNumber ? "是" : "否")}");
     Console.WriteLine("\r\n秒杀成功人员名单:");
     for (int i = 0; i < stockNumber; i++)
     {
         Console.WriteLine(csredis.RPop(key));
     }
 }

3.结论

至此,结束。

Logo

更多推荐