.NET 8 WebApi 库存出入库并发锁实现教程

4 阅读6分钟

.NET 8 WebApi 库存出入库并发锁实现教程

本文通过一个完整的示例,演示如何在 .NET 8 WebApi 项目中实现库存的出入库操作,并逐步引入不同的并发控制机制:

  1. 使用 lock 关键字实现线程同步(单进程内)
  2. 使用 SemaphoreSlim 支持异步锁(单进程内异步场景)
  3. 使用 Redis 实现分布式锁(跨进程/跨服务器场景)

所有代码均基于内存模拟库存数据,不依赖真实数据库,便于理解和测试。


环境准备


第一步:创建 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.EnterMonitor.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.csServices/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 中切换。


参考资料