.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.问题

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

相关推荐
用户7227868123441 天前
.NET 实现雪花算法:高效生成分布式唯一 ID
.net
玩泥巴的1 天前
.NET 8+ 飞书API实战:自动化群组管理与消息推送
c#·.net·二次开发·飞书
唐青枫1 天前
C#.NET 范围与索引(Range、Index)完全解析:语法、用法与最佳实践
c#·.net
许泽宇的技术分享2 天前
当AI Agent遇上.NET:微软Agent Framework的架构奥秘与实战启示
人工智能·microsoft·.net
SEO-狼术2 天前
DevExpress DXperience Crack
.net
我是唐青枫2 天前
一文理解 C#.NET Tuples:从基础到高级应用
c#·.net
缺点内向2 天前
C# 中 Word 文档目录的插入与删除指南
开发语言·c#·word·.net
唐青枫2 天前
告别 if-else:C#.NET 模式匹配让代码更优雅的正确方式
c#·.net
绿荫阿广2 天前
使用.NET开发并上线一个小智AI对话机器人的MCP服务转接平台
.net·asp.net core·mcp
棉晗榜3 天前
无法解析位于...\global.json 的 global.json 中指定的 .NET SDK 版本
.net