.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);}}}#endregion3. 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.结果

