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.问题
感觉还是不完美,没有使用异步,没有最大化并发处理能力,也没有事务回滚,在后续我将继续完善