架构师_程序员

查看: 228|回复: 2

[.NET Core] 【实战】ASP.NET Core 基于 Redis 分布式锁秒杀

[复制链接]
发表于 2020-9-26 15:34:26 | 显示全部楼层
以前写的基于 zk 实现的分布式锁,如下:

.net/c# Zookeeper分布式锁的实现[源码]
https://www.itsvse.com/thread-4651-1-1.html
Redis 实现分布式锁原理:

.NET Core 基于 Redis 实现分布式锁原理解析
https://www.itsvse.com/thread-9391-1-1.html
Redis 实现分布式锁的原理是调用 redis 的 SETNX 命令,若键 key 已经存在, 则 SETNX 命令不做任何动作。命令在设置成功时返回 1 , 设置失败时返回 0 。


首先,我们网站模拟有 10000 件商品,然后写了一个控制台模拟HTTP请求用时40秒抢购完成,效果图如下:

QQ截图20200926150716.jpg

新建一个 ASP.NET Core 3.1 的网站,redis锁封装如下:

  1. /// <summary>
  2.     /// 基于redis锁
  3.     /// </summary>
  4.     public static class RedisLockHelper
  5.     {
  6.         private readonly static ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("127.0.0.1:8899,password=123");
  7.         private readonly static IDatabase database = redis.GetDatabase(0);

  8.         /// <summary>
  9.         /// 获取锁
  10.         /// </summary>
  11.         /// <param name="key"></param>
  12.         /// <param name="seconds"></param>
  13.         /// <returns></returns>
  14.         public static (bool, string) GetLock(string key, int seconds)
  15.         {
  16.             string gid = Guid.NewGuid().ToString("N");
  17.             var ret = database.StringSet(key, gid, TimeSpan.FromSeconds(seconds), When.NotExists, CommandFlags.None);
  18.             if (ret) Task.Run(() => ExtendKey(key, gid, seconds));
  19.             return (ret, gid);
  20.         }

  21.         /// <summary>
  22.         /// 释放锁
  23.         /// </summary>
  24.         /// <param name="key"></param>
  25.         /// <param name="gid"></param>
  26.         public static void UnLock(string key, string gid)
  27.         {
  28.             var val = database.StringGet(key);
  29.             if (val.HasValue && val.ToString().Equals(gid))
  30.             {
  31.                 database.KeyDelete(key);
  32.             }
  33.         }

  34.         /// <summary>
  35.         /// 延长锁的失效时间
  36.         /// </summary>
  37.         /// <param name="key"></param>
  38.         /// <param name="gid"></param>
  39.         /// <param name="seconds"></param>
  40.         private static void ExtendKey(string key, string gid, int seconds)
  41.         {
  42.             var errorCount = 0;
  43.             //出错超过缓存秒数,退出线程
  44.             while (errorCount < seconds)
  45.             {
  46.                 try
  47.                 {
  48.                     var val = database.StringGet(key);
  49.                     if (val.HasValue && val.ToString().Equals(gid))
  50.                     {
  51.                         database.KeyExpire(key, TimeSpan.FromSeconds(seconds));
  52.                     }
  53.                     else break;
  54.                     errorCount = 0;
  55.                 }
  56.                 catch
  57.                 {
  58.                     errorCount++;
  59.                 }
  60.                 Thread.Sleep(1000);
  61.             }
  62.         }
  63.     }
复制代码
看到网上很多释放锁的写法如下:

  1. //先判断随机数,是同一个则删除锁
  2.     if ($redis->get($key) == $random) {
  3.         $redis->del($key);
  4.     }
复制代码
但是,感觉可能会出现问题,在并发的情况下,有可能在判断的时候返回 true,确实是自己加的锁,但是在删除该锁的时候,有可能在极端的情况下,该锁已经被别的线程获取到了,万一把别人的锁给删除了。

所以,在获取到锁的时候,需要开启一个线程去延长锁的失效时间。

WeatherForecastController 接口如下:

  1. [ApiController]
  2.     [Route("[controller]")]
  3.     public class WeatherForecastController : ControllerBase
  4.     {
  5.         private static readonly List<int> vs = new List<int>();
  6.         private const string item = "itsvse";

  7.         static WeatherForecastController()
  8.         {
  9.             for (int i = 0; i < 10000; i++)
  10.             {
  11.                 vs.Add(i);
  12.             }
  13.         }

  14.         private readonly ILogger<WeatherForecastController> _logger;

  15.         public WeatherForecastController(ILogger<WeatherForecastController> logger)
  16.         {
  17.             _logger = logger;
  18.         }

  19.         /// <summary>
  20.         /// 0:异常
  21.         /// 1:成功
  22.         /// 2:失败
  23.         /// 3:售完
  24.         /// </summary>
  25.         /// <returns></returns>
  26.         [HttpGet]
  27.         public int Get()
  28.         {
  29.             string gid = null;
  30.             try
  31.             {
  32.                 var ret = RedisLockHelper.GetLock(item, 5);
  33.                 if (ret.Item1)
  34.                 {
  35.                     gid = ret.Item2;
  36.                     //获取到锁,开始更新库存
  37.                     if (vs.Count == 0)
  38.                     {
  39.                         _logger.LogInformation("售完!");
  40.                         return 3;//售完
  41.                     }
  42.                     else {
  43.                         vs.RemoveAt(0);
  44.                     }
  45.                     //释放锁
  46.                     RedisLockHelper.UnLock(item, gid);
  47.                     _logger.LogInformation("抢购成功!");
  48.                     return 1;
  49.                 }
  50.                 _logger.LogInformation("抢购失败!");
  51.                 return 2;//抢购失败
  52.             }
  53.             catch (Exception ex)
  54.             {
  55.                 _logger.LogInformation("抢购异常!" + ex);
  56.                 return 0;
  57.             }
  58.             finally {
  59.                 if (gid != null) RedisLockHelper.UnLock(item, gid);
  60.             }
  61.         }
  62.     }
复制代码

可以通过命令行启动api网站,可以方便重启,如下:

  1. dotnet WebApplication4.dll --urls="http://*:3268/"
复制代码


新建一个 .NET Core 模拟 HTTP 请求,进行抢购,代码如下:

  1. using StackExchange.Redis;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Net.Http;
  6. using System.Threading;
  7. using System.Threading.Tasks;

  8. namespace ConsoleApp1
  9. {
  10.     class Program
  11.     {
  12.         private static int safeInstanceSuccessedCount = 0;
  13.         private static int safeInstanceFailedCount = 0;
  14.         private static int safeInstanceDoneCount = 0;
  15.         static CancellationTokenSource cts = new CancellationTokenSource();

  16.         static void Main(string[] args)
  17.         {
  18.             Task.Run(() =>
  19.             {
  20.                 HttpClient client = new HttpClient();
  21.                 client.BaseAddress = new Uri("http://localhost:3268/");
  22.                 Stopwatch watch = new Stopwatch();
  23.                 watch.Start();
  24.                 while (!cts.IsCancellationRequested)
  25.                 {
  26.                     Task.Run(() =>
  27.                     {
  28.                         using var response = client.GetAsync(
  29.                     "/weatherforecast").Result;
  30.                         var content = response.Content.ReadAsStringAsync().Result;
  31.                         /// <summary>
  32.                         /// 0:异常
  33.                         /// 1:成功
  34.                         /// 2:失败
  35.                         /// 3:售完
  36.                         /// </summary>
  37.                         switch (content)
  38.                         {
  39.                             case "0":
  40.                                 Console.WriteLine("异常");
  41.                                 break;
  42.                             case "1":
  43.                                 Interlocked.Increment(ref safeInstanceSuccessedCount);
  44.                                 break;
  45.                             case "2":
  46.                                 Interlocked.Increment(ref safeInstanceFailedCount);
  47.                                 break;
  48.                             case "3":
  49.                                 Interlocked.Increment(ref safeInstanceDoneCount);
  50.                                 cts.Cancel();
  51.                                 break;
  52.                             default:
  53.                                 Console.WriteLine(content);
  54.                                 break;
  55.                         }
  56.                     }, cts.Token);
  57.                 }
  58.                 watch.Stop();
  59.                 Console.WriteLine($"抢购成功:{safeInstanceSuccessedCount},失败:{safeInstanceFailedCount},售完:{safeInstanceDoneCount}");
  60.                 Console.WriteLine("用时{0}毫秒", watch.ElapsedMilliseconds);
  61.                 Task.Run(() =>
  62.                 {
  63.                     //延迟5秒,等待一些线程执行完成
  64.                     Thread.Sleep(5000);
  65.                     Console.WriteLine($"抢购成功:{safeInstanceSuccessedCount},失败:{safeInstanceFailedCount},售完:{safeInstanceDoneCount}");
  66.                 });
  67.             });

  68.             Console.WriteLine("ok");
  69.             Console.WriteLine("by:itsvse.com");
  70.             Console.ReadKey();
  71.         }
  72.     }
  73. }
复制代码

如有不对的地方,感谢提出指正。

最后,附上源码:

游客,如果您要查看本帖隐藏内容请回复





上一篇:JS 根据屏幕大小轮播图自适应
下一篇:Javascript 的 this 详解
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
发表于 2020-9-27 14:13:33 | 显示全部楼层
mark,mark,mark
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
发表于 2020-10-5 11:12:20 | 显示全部楼层
mark mark mark
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

免责声明:
码农网所发布的一切软件、编程资料或者文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:help@itsvse.com

QQ|手机版|小黑屋|架构师 ( 鲁ICP备14021824号-2 )|网站地图

GMT+8, 2020-10-28 12:12

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表