• NetCore/Net8下使用Redis的分布式锁实现秒杀功能


    目的

    本文主要是使用NetCore/Net8加上Redis来实现一个简单的秒杀功能,学习Redis的分布式锁功能。

    准备工作

    1.Visual Studio 2022开发工具

    2.Redis集群(6个Redis实例,3主3从)或者单个Redis实例也可以。

    实现思路

    1.秒杀开始前,将商品的数量缓存到Redis中

    2.使用Redis的分布式缓存锁,保证只有一个人能获取到锁,进而保证减库存的操作的原子性。

    3.获取到Redis分布式锁后,开始后续的业务操作,减少库存。

    实现代码

    1. // See https://aka.ms/new-console-template for more information
    2. using StackExchange.Redis;
    3. WriteLine("开始秒杀活动......");
    4. WriteLine("请输入秒杀商品的ID,按回车键确认:", ConsoleColor.Blue);
    5. //ThreadPool.SetMinThreads(200, 200);
    6. var db = GetDataBase();
    7. string? productId = Console.ReadLine();
    8. if (!string.IsNullOrWhiteSpace(productId))
    9. {
    10. int maxProductNumber = 100;
    11. //设置商品的最大库存数量
    12. await db.StringSetAsync($"ProductNumber:{productId}", maxProductNumber);
    13. //开始模拟购买
    14. List allTaskList = new List();
    15. for (int i = 0; i < 1000; i++)
    16. {
    17. var task = BuyProduct(db, buyerId: i);
    18. allTaskList.Add(task);
    19. }
    20. await Task.WhenAll(allTaskList);
    21. int buySuccessNumber = Directory.GetFiles($"{AppContext.BaseDirectory}/buyer/").Length;
    22. WriteLine($"秒杀产品数量={maxProductNumber},购买成功用户数量={buySuccessNumber}", ConsoleColor.Green);
    23. Console.ReadLine();
    24. }
    25. else
    26. {
    27. Console.WriteLine("输入商品ID为空,自动退出");
    28. }
    29. IDatabase GetDataBase()
    30. {
    31. ConnectionMultiplexer cm = ConnectionMultiplexer.Connect("127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384");
    32. return cm.GetDatabase();
    33. }
    34. async Task BuyProduct(IDatabase db, int buyerId)
    35. {
    36. int threadId = Environment.CurrentManagedThreadId;
    37. try
    38. {
    39. //首先获取当前库存,判断是否还可以购买
    40. var leftProductNumber = await GetProductCurrentNumberAsync(db, productId);
    41. if (leftProductNumber < 1)
    42. {
    43. WriteLine($"线程Id={threadId},购买失败,用户Id:{buyerId},库存不足1,当前库存:{leftProductNumber}", ConsoleColor.Red);
    44. return;
    45. }
    46. string key = $"ProductId:{productId}";
    47. string lockValue = Guid.NewGuid().ToString();
    48. //锁的过期时间一定要比成功获取锁后操作业务所需的时间长,
    49. //否则会导致业务还没有操作完成(减库存)锁就释放了,导致后面的用户获取到锁,最终导致超卖的情况
    50. bool lockSuccess = await GetLockAsync(db, key, lockValue, TimeSpan.FromSeconds(5));
    51. if (!lockSuccess)
    52. {
    53. WriteLine($"线程Id={threadId},用户Id={buyerId},购买锁获取失败", ConsoleColor.Red);
    54. return;
    55. }
    56. try
    57. {
    58. //再次获取当前库存,判断是否还可以购买
    59. leftProductNumber = await GetProductCurrentNumberAsync(db, productId);
    60. if (leftProductNumber < 1)
    61. {
    62. WriteLine($"线程Id={threadId},购买失败:{lockValue},用户Id:{buyerId},库存不足2,当前库存:{leftProductNumber}", ConsoleColor.Red);
    63. return;
    64. }
    65. //扣减库存
    66. await db.StringDecrementAsync($"ProductNumber:{productId}");
    67. WriteLine($"线程Id={threadId},购买成功:{lockValue},用户Id:{buyerId}", ConsoleColor.Green);
    68. var dirPath = $"{AppContext.BaseDirectory}/buyer";
    69. if (!Directory.Exists(dirPath))
    70. {
    71. Directory.CreateDirectory(dirPath);
    72. }
    73. await File.WriteAllTextAsync($"{dirPath}/buy-success-{buyerId}.txt", $"锁Id={lockValue},用户Id={buyerId},产品Id={productId},剩余产品数量={leftProductNumber}");
    74. }
    75. finally
    76. {
    77. bool lockReleased = await db.LockReleaseAsync(key, lockValue);
    78. if (!lockReleased)
    79. {
    80. WriteLine($"线程Id={threadId},用户Id={buyerId},锁释放失败:{lockValue}", ConsoleColor.Yellow);
    81. }
    82. }
    83. }
    84. catch(Exception ex)
    85. {
    86. WriteLine($"线程Id={threadId},用户Id={buyerId},购买失败:{ex}", ConsoleColor.Red);
    87. }
    88. }
    89. async Task<bool> GetLockAsync(IDatabase db, string key, string lockValue, TimeSpan timeout)
    90. {
    91. //每个用户有五次获取Redis分布式产品锁的机会,如果5次重试后,都没有获取到锁,则默认秒杀失败
    92. int i = 5;
    93. while (i > 0)
    94. {
    95. bool lockSuccess = await db.LockTakeAsync(key, lockValue, timeout);
    96. if (lockSuccess)
    97. {
    98. return true;
    99. }
    100. await Task.Delay(TimeSpan.FromMilliseconds(new Random(Guid.NewGuid().GetHashCode()).Next(100, 500)));
    101. i--;
    102. }
    103. return false;
    104. }
    105. async Task<long> GetProductCurrentNumberAsync(IDatabase db, string productId)
    106. {
    107. string? leftProductNumberString = await db.StringGetAsync($"ProductNumber:{productId}");
    108. _ = long.TryParse(leftProductNumberString, out long leftProductNumber);
    109. return leftProductNumber;
    110. }
    111. static void WriteLine(string text, ConsoleColor colour = ConsoleColor.White)
    112. {
    113. Console.ForegroundColor = colour;
    114. Console.WriteLine(text);
    115. }

    运行效果

  • 相关阅读:
    函数的参数
    分享一下微信报名系统怎么做
    Flink K8s Operator 测试验证
    SDRAM的数据存储实现并对其数据进行读写操作
    docker安装(持续更新中)
    Java之final和abstract关键字(9)
    Perl6中的垃圾收集
    【2022新版】Java 终极学习路线(文末高清大图)-共计9大模块/6大框架/13个中间件
    编程语言界再添新锐,Google 前工程师开源 Toit 语言
    【前端三栏布局总结】常见的前端三栏布局有哪些
  • 原文地址:https://blog.csdn.net/allenwdj/article/details/133910619