.NET 8 WebApi 库存出入库并发锁实现教程
本文通过一个完整的示例,演示如何在 .NET 8 WebApi 项目中实现库存的出入库操作,并逐步引入不同的并发控制机制:
- 使用
lock关键字实现线程同步(单进程内) - 使用
SemaphoreSlim支持异步锁(单进程内异步场景) - 使用 Redis 实现分布式锁(跨进程/跨服务器场景)
所有代码均基于内存模拟库存数据,不依赖真实数据库,便于理解和测试。
环境准备
- .NET 8 SDK
- Redis
- IDE:Visual Studio 2022
第一步:创建 WebApi 项目
打开终端,执行以下命令创建项目:
dotnet new webapi -n InventoryLockDemo
cd InventoryLockDemo
删除默认生成的 WeatherForecast 相关文件。
第二步:定义库存服务接口
创建 Services/IInventoryService.cs 文件,定义库存操作的基本接口:
namespace InventoryLockDemo.Services;
public interface IInventoryService
{
int GetCurrentStock(); // 同步获取库存
bool Outbound(int quantity); // 同步出库
void Inbound(int quantity); // 同步入库
// 异步版本(后续使用)
Task<int> GetCurrentStockAsync();
Task<bool> OutboundAsync(int quantity);
Task InboundAsync(int quantity);
}
注意:为了后续演示异步锁,我们在接口中同时包含了同步和异步方法。实际项目中可根据需要拆分为不同接口。
第三步:实现同步锁(lock)
创建 Services/InventoryServiceSync.cs,使用 lock 关键字保证线程安全。
using InventoryLockDemo.Services;
namespace InventoryLockDemo.Services;
public class InventoryServiceSync : IInventoryService
{
private int _stock = 100; // 初始库存
private readonly object _lock = new object();
public int GetCurrentStock()
{
lock (_lock)
{
return _stock;
}
}
public bool Outbound(int quantity)
{
lock (_lock)
{
if (_stock >= quantity)
{
// 模拟耗时操作(便于观察并发问题)
Thread.Sleep(100);
_stock -= quantity;
return true;
}
return false;
}
}
public void Inbound(int quantity)
{
lock (_lock)
{
Thread.Sleep(100);
_stock += quantity;
}
}
// 异步方法暂不实现,或直接调用同步方法(不推荐)
public Task<int> GetCurrentStockAsync() => Task.FromResult(GetCurrentStock());
public Task<bool> OutboundAsync(int quantity) => Task.FromResult(Outbound(quantity));
public Task InboundAsync(int quantity)
{
Inbound(quantity);
return Task.CompletedTask;
}
}
原理:lock 语句在编译时会被翻译为 Monitor.Enter 和 Monitor.Exit,并在 finally 块中释放锁,确保同一时刻只有一个线程能进入临界区。但 lock 不支持 await,因此仅适用于同步方法。
第四步:实现异步锁(SemaphoreSlim)
当需要在异步方法中保护共享资源时,可以使用 SemaphoreSlim。创建 Services/InventoryServiceAsync.cs:
using InventoryLockDemo.Services;
namespace InventoryLockDemo.Services;
public class InventoryServiceAsync : IInventoryService
{
private int _stock = 100;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // 初始1,最大1
public int GetCurrentStock() => _stock; // 简单读取,不加锁也可(视业务需求)
public bool Outbound(int quantity) => OutboundAsync(quantity).GetAwaiter().GetResult(); // 不推荐,仅演示
public void Inbound(int quantity) => InboundAsync(quantity).GetAwaiter().GetResult();
public async Task<int> GetCurrentStockAsync()
{
await _semaphore.WaitAsync();
try
{
return _stock;
}
finally
{
_semaphore.Release();
}
}
public async Task<bool> OutboundAsync(int quantity)
{
await _semaphore.WaitAsync();
try
{
if (_stock >= quantity)
{
await Task.Delay(100); // 模拟异步IO
_stock -= quantity;
return true;
}
return false;
}
finally
{
_semaphore.Release();
}
}
public async Task InboundAsync(int quantity)
{
await _semaphore.WaitAsync();
try
{
await Task.Delay(100);
_stock += quantity;
}
finally
{
_semaphore.Release();
}
}
}
说明:
SemaphoreSlim(1, 1)创建一个只允许一个线程进入的信号量,相当于异步版本的lock。WaitAsync()异步等待获取信号量,不会阻塞线程。- 必须在
finally块中调用Release(),确保锁总能被释放。
第五步:基于 Redis 实现分布式锁
当应用部署为多个实例时,单进程内的锁无法跨进程同步,此时需要分布式锁。这里使用 Redis 实现。
5.1 安装 Redis 客户端
在项目中添加 StackExchange.Redis 包:
dotnet add package StackExchange.Redis
5.2 配置 Redis 连接
在 appsettings.json 中添加连接字符串:
{
"Redis": {
"ConnectionString": "localhost:6379"
}
}
5.3 实现分布式锁辅助服务
创建 Services/IRedisDistributedLock.cs 和 Services/RedisDistributedLock.cs:
// IRedisDistributedLock.cs
namespace InventoryLockDemo.Services;
public interface IRedisDistributedLock
{
Task<bool> AcquireAsync(string lockKey, string lockValue, TimeSpan expiry);
Task<bool> ReleaseAsync(string lockKey, string lockValue);
}
// RedisDistributedLock.cs
using StackExchange.Redis;
namespace InventoryLockDemo.Services;
public class RedisDistributedLock : IRedisDistributedLock
{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _db;
public RedisDistributedLock(IConnectionMultiplexer redis)
{
_redis = redis;
_db = redis.GetDatabase();
}
public async Task<bool> AcquireAsync(string lockKey, string lockValue, TimeSpan expiry)
{
// SET key value NX EX seconds
return await _db.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);
}
public async Task<bool> ReleaseAsync(string lockKey, string lockValue)
{
// Lua脚本:只有值匹配才删除
string script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
var result = await _db.ScriptEvaluateAsync(script, new RedisKey[] { lockKey }, new RedisValue[] { lockValue });
return (long)result == 1;
}
}
关键点:
AcquireAsync使用 Redis 的SET NX EX原子命令,保证锁的唯一性和自动过期。ReleaseAsync使用 Lua 脚本确保“检查值+删除”的原子性,防止误删他人持有的锁。
5.4 改造库存服务使用分布式锁
创建 Services/InventoryServiceDistributed.cs,注入 IRedisDistributedLock:
using InventoryLockDemo.Services;
namespace InventoryLockDemo.Services;
public class InventoryServiceDistributed : IInventoryService
{
private int _stock = 100;
private readonly IRedisDistributedLock _distributedLock;
private readonly string _lockKey = "inventory:stock:lock"; // Redis 锁的键
public InventoryServiceDistributed(IRedisDistributedLock distributedLock)
{
_distributedLock = distributedLock;
}
public int GetCurrentStock() => _stock;
public bool Outbound(int quantity) => OutboundAsync(quantity).GetAwaiter().GetResult();
public void Inbound(int quantity) => InboundAsync(quantity).GetAwaiter().GetResult();
public async Task<int> GetCurrentStockAsync()
{
// 读操作可以不加锁,但若要求强一致性,也可加锁
return _stock;
}
public async Task<bool> OutboundAsync(int quantity)
{
var lockValue = Guid.NewGuid().ToString();
var expiry = TimeSpan.FromSeconds(10); // 锁超时时间
if (!await _distributedLock.AcquireAsync(_lockKey, lockValue, expiry))
return false; // 获取锁失败
try
{
await Task.Delay(100); // 模拟业务操作
if (_stock >= quantity)
{
_stock -= quantity;
return true;
}
return false;
}
finally
{
await _distributedLock.ReleaseAsync(_lockKey, lockValue);
}
}
public async Task InboundAsync(int quantity)
{
var lockValue = Guid.NewGuid().ToString();
var expiry = TimeSpan.FromSeconds(10);
if (!await _distributedLock.AcquireAsync(_lockKey, lockValue, expiry))
throw new Exception("无法获取分布式锁,入库失败");
try
{
await Task.Delay(100);
_stock += quantity;
}
finally
{
await _distributedLock.ReleaseAsync(_lockKey, lockValue);
}
}
}
第六步:注册服务到依赖注入容器
在 Program.cs 中根据需求选择要使用的实现,并注册相关服务。
6.1 基础注册(同步锁或异步锁)
using InventoryLockDemo.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注册库存服务(选择其中一个)
// builder.Services.AddSingleton<IInventoryService, InventoryServiceSync>(); // 同步锁
builder.Services.AddSingleton<IInventoryService, InventoryServiceAsync>(); // 异步锁
var app = builder.Build();
// 配置管道...
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthorization();
app.MapControllers();
app.Run();
6.2 分布式锁注册
需要同时注册 Redis 连接和分布式锁服务:
using InventoryLockDemo.Services;
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
// 添加控制器、Swagger等...
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注册 Redis 连接(单例)
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = builder.Configuration.GetValue<string>("Redis:ConnectionString");
return ConnectionMultiplexer.Connect(config);
});
// 注册分布式锁辅助服务
builder.Services.AddSingleton<IRedisDistributedLock, RedisDistributedLock>();
// 注册使用分布式锁的库存服务
builder.Services.AddSingleton<IInventoryService, InventoryServiceDistributed>();
var app = builder.Build();
// 配置管道...
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthorization();
app.MapControllers();
app.Run();
第七步:创建控制器
创建 Controllers/InventoryController.cs,统一调用 IInventoryService 接口:
using Microsoft.AspNetCore.Mvc;
using InventoryLockDemo.Services;
namespace InventoryLockDemo.Controllers;
[ApiController]
[Route("api/[controller]")]
public class InventoryController : ControllerBase
{
private readonly IInventoryService _inventoryService;
public InventoryController(IInventoryService inventoryService)
{
_inventoryService = inventoryService;
}
[HttpGet("stock")]
public async Task<ActionResult<int>> GetStock()
{
var stock = await _inventoryService.GetCurrentStockAsync();
return Ok(stock);
}
[HttpPost("outbound")]
public async Task<IActionResult> Outbound(int quantity)
{
if (quantity <= 0)
return BadRequest("数量必须大于0");
var success = await _inventoryService.OutboundAsync(quantity);
if (success)
{
var currentStock = await _inventoryService.GetCurrentStockAsync();
return Ok($"出库成功,当前库存:{currentStock}");
}
return BadRequest("库存不足或系统繁忙,请稍后重试");
}
[HttpPost("inbound")]
public async Task<IActionResult> Inbound(int quantity)
{
if (quantity <= 0)
return BadRequest("数量必须大于0");
try
{
await _inventoryService.InboundAsync(quantity);
var currentStock = await _inventoryService.GetCurrentStockAsync();
return Ok($"入库成功,当前库存:{currentStock}");
}
catch (Exception ex)
{
return StatusCode(500, $"入库失败:{ex.Message}");
}
}
}
第八步:测试并发安全
8.1 手动测试
启动项目,通过 Swagger 或 Postman 调用接口:
GET /api/inventory/stock查看当前库存(初始 100)。- 同时发送多个
POST /api/inventory/outbound?quantity=10请求,观察最终库存是否为 100 - 请求次数×10。
8.2 并发测试脚本(可选)
使用简单的控制台程序或工具(如 Apache JMeter)模拟并发请求。下面是一个 C# 控制台测试示例(需先启动 WebApi):
using System.Diagnostics;
var client = new HttpClient { BaseAddress = new Uri("https://localhost:5001") };
var tasks = new List<Task>();
// 同时发送 10 个出库请求,每个出库 10 件
for (int i = 0; i < 10; i++)
{
tasks.Add(client.PostAsync("/api/inventory/outbound?quantity=10", null));
}
await Task.WhenAll(tasks);
var response = await client.GetAsync("/api/inventory/stock");
var stock = await response.Content.ReadAsStringAsync();
Console.WriteLine($"最终库存: {stock}");
预期输出:库存应为 0(若未加锁则可能出现负数)。
总结
本文通过一个简单的库存出入库示例,展示了 .NET 8 中三种常见的并发控制手段:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
lock | 单进程内同步方法 | 简单、编译器保证释放 | 不支持 await |
SemaphoreSlim | 单进程内异步方法 | 支持异步等待,轻量 | 需要手动释放 |
| Redis 分布式锁 | 多实例/分布式系统 | 跨进程同步,自动过期 | 引入外部组件,需考虑网络 |
你可以根据实际部署架构选择合适的锁机制。完整的项目代码已包含上述所有实现,可根据需要在 Program.cs 中切换。