.net实现秒杀商品(Redis高并发)

1.思想

模拟不同用户在同一时刻对一件商品进行秒杀,每次调用随机一个userId,代表不同的用户。

设置三个键

userRecordKey(记录用户是否已经秒杀过这个商品),

stockKey(商品的库存),

lockKey(互斥锁,是分布式锁机制的核心标识。过这个键的 "存在 / 不存在" 来标识 "是否有线程在操作华为手机的秒杀资源",从而实现 多线程或者多服务器之间的互斥访问。当某个线程获取锁时,会在 Redis 中创建这个 lockKey。其他线程尝试获取锁时,会检查这个lockKey是否存在,存在则表示 "资源被占用",需等待或放弃;线程释放锁时,会删除这个 lockKey,释放资源占用。)

如果有一个进程在进行操作,其他线程就会显示秒杀拥挤,等待当前进程释放锁。具体业务逻辑,判断是否有锁后,如果没有则判断是否重复下单 -》检查库存 -》 扣减库存 -》 记录用户 -》释放锁。

2.业务代码

复制代码
 #region stackRedis封装
 ///<summary>
 /// stackredis
 /// </summary>
 [HttpGet("stackRedis")]
 [AllowAnonymous]
 public IActionResult StackRedis()
 {
     int userId = _threadLocalRandom.Value.Next(1000, 10_000);
     var userRecordKey = $"usersession:seckill:user:record:{userId}";
     const string stockKey = "usersession:seckill:stock:huawei_phone";
     const string lockKey = "usersession:seckill:lock:huawei_phone";
     var lockVal = Guid.NewGuid().ToString();
     // 1.原子拿锁
     bool getKey = StackExchangeRedisHelper.Db.StringSet(lockKey, lockVal,TimeSpan.FromSeconds(10),When.NotExists);
     Thread.Sleep(10000);
     if (!getKey)
         return ToResponse(ResultCode.CUSTOM_ERROR, "秒杀拥挤!");
     try
     {
         // 2.判断重复下单
         bool reBuy = StackExchangeRedisHelper.Db.KeyExists(userRecordKey);
         if (reBuy)
             return ToResponse(ResultCode.CUSTOM_ERROR,"重复下单!");
         // 3.检查库存
         int stock = (int)StackExchangeRedisHelper.Db.StringGet(stockKey);
         if (stock <= 0)
             return ToResponse(ResultCode.CUSTOM_ERROR, "商品已抢完");
         // 4.减库存
         int a = (int)StackExchangeRedisHelper.Db.StringDecrement(stockKey);
         Console.WriteLine($"当前有{a}");
         // 5.记录用户
         StackExchangeRedisHelper.Db.StringSet(userRecordKey,1,TimeSpan.FromMinutes(10));
         return SUCCESS("秒杀成功!");
     }
     finally
     {
         //释放锁
         try
         {
             var lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
             StackExchangeRedisHelper.Db.ScriptEvaluate(lua, new RedisKey[] { lockKey }, new RedisValue[] { lockVal });
         }
         catch (Exception ex)
         {
             
             Console.WriteLine(ex+"释放 Redis 锁失败,lockKey: {lockKey}"+lockKey);
         }
     }
 }
 #endregion

3. StackExchangeRedisHelper.cs

基于 StackExchange.Redis 的单例 Redis 连接助手,考虑了连接的自动重建和旧连接的延迟释放,,也可以在controller里自己连接,我觉得这样方便,美观点。

复制代码
using Infrastructure;
using StackExchange.Redis;
using System;
using System.Threading.Tasks;
namespace ZR.Infrastructure.Helper
{
    /// <summary>
    /// Redis,连接单例
    /// </summary>
    public static class StackExchangeRedisHelper
    {
        private static readonly object _lock = new object();
        private static ConnectionMultiplexer _mux;
        ///<summary>
        /// 连接字符串,可在 Program.cs 或 appsettings.json 里先赋值
        /// </summary>
        
        private static string config = AppSettings.GetConfig("StackRedis:Redis") + ",connectTimeout=5000,syncTimeout=3000";
        // <summary>
        /// 单例 ConnectionMultiplexer
        /// </summary>
        public static ConnectionMultiplexer Mux
        {
            get
            {
                if (_mux == null || !_mux.IsConnected)
                {
                    lock (_lock)
                    {
                        if (_mux == null || !_mux.IsConnected)
                        {
                            // 关键修改:不直接 Dispose 旧连接,而是创建新连接后让旧连接被 GC 回收
                            // (避免正在使用旧连接的请求报错)
                            var oldMux = _mux;
                            _mux = ConnectionMultiplexer.Connect(config); // 创建新连接

                            // 延迟释放旧连接(给正在使用的请求留时间完成)
                            if (oldMux != null)
                            {
                                Task.Run(() =>
                                {
                                    try
                                    {
                                        // 等待 5 秒后释放旧连接(根据业务调整)
                                        Task.Delay(5000).Wait();
                                        oldMux.Dispose();
                                    }
                                    catch { /* 忽略释放失败的异常 */ }
                                });
                            }
                        }
                    }
                }
                return _mux;
            }
        }
        /// <summary>
        /// 默认数据库(0 号)
        /// </summary>
        public static IDatabase Db => Mux.GetDatabase();
        /// <summary>
        /// 按需获取指定数据库
        /// </summary>
        public static IDatabase GetDatabase(int db = 0) => Mux.GetDatabase(db);
    }
}

4.Appsettings.json

复制代码
"StackRedis": {
  "Redis": "localhost:6379"
}

5.jemeter测试(200个线程)

6.结果

7.问题

感觉还是不完美,没有使用异步,没有最大化并发处理能力,也没有事务回滚,在后续我将继续完善

相关推荐
mudtools6 小时前
解放双手!使用Roslyn生成代码让你的 HTTP 客户端开发变得如此简单
低代码·c#·.net
主宰者9 小时前
WPF CalcBinding简化判断逻辑
c#·.net·wpf
【D'accumulation】9 小时前
.NET Framework 4.8 + Microsoft.Data.Sqlite 报 Library e_sqlite3 not found
microsoft·sqlite·.net
数据的世界0121 小时前
技术变革:为何C#与.NET是未来的开发方向
java·c#·.net
大龄Python青年21 小时前
C#快入教程:Linux安装.NET
linux·c#·.net
向上的车轮21 小时前
Actix Web适合什么类型的Web应用?可以部署 Java 或 .NET 的应用程序?
java·前端·rust·.net
我是唐青枫1 天前
C#.NET Random 深入解析:随机数生成原理与最佳实践
c#·.net
咕白m6251 天前
如何通过 C# 提取 PDF 图片?单页与全文档提取
c#·.net