1、分布式锁的应用场景
目的:在分布式部署的环境下,协调多个进程/服务对共享资源的互斥访问,保证数据一致性。
场景:
1)防止重复提交: 用户快速多次点击下单、付款、表单提交等,避免生成重复订单或数据。
2)分布式全局ID生成:在分库分表场景下需要生成全局唯一且递增的ID(如订单号,流水号),通过分布式锁获取一段
3)避免缓存击穿,缓存重建:热点Key过期后,大量请求同步打到数据库,导致DB压力骤增,通过使用互斥锁,第一个请求获取锁后去数据库查询并重建缓存,其他请求等待或返回旧数据。
4)服务注册中心的健康检查防重:多实例同时检测到服务下线,只需要一个实例去通知注册中心更新状态。使用分布式锁确保只有一个执行者。
......
2、分布式锁的实现技术
2.1、CAP的三个概念
C(一致性):所有节点同一时刻看到的数据是相同的。对于客户端,写操作完成后,读操作必须返还最新的数据。
A(可用性):服务始终能返回响应,每个请求都能收到非错误的响应
P(分区容错性):当网络出现分区(节点之间无法通信)时,系统仍能继续工作。
| 实现方案 | 优点 | 缺点 | CAP 倾向 | 适用场景 |
|---|---|---|---|---|
| Redis | 性能极高,实现简单,易上手 | 强一致性难保证,锁可能在主从切换时丢失 | AP | 高性能场景,可容忍极低概率的锁失效 |
| ZooKeeper | 强一致性,有可靠的通知机制,无单点问题 | 性能相对较差,依赖复杂,有"惊群"效应- | CP | 对数据一致性要求高的金融、交易场景 |
| etcd | 强一致性,内置租约机制,设计现代,API友好 | 生态成熟度较前两者稍弱 | CP | 云原生环境,追求高可靠性的系统 |
| 数据库 | 简单直观,无额外组件成本 | 性能差,有死锁风险,可能拖垮数据库 | CA | 并发极低的简单场景,或作为极端兜底方案 |
3、使用Redis实现分布式锁

3.1、创建一个WebApi项目 Demos.MicrosoftRedLockNet 1)引入nuget包
csproj
<ItemGroup>
<!-- 官方推荐的 .NET 分布式锁实现 -->
<PackageReference Include="RedLock.net" Version="2.3.2" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="MySqlConnector" Version="2.3.7" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<!--数据迁移-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native</PrivateAssets>
</PackageReference>
<!--AOP方式实现-->
<PackageReference Include="Autofac" Version="7.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
<PackageReference Include="Castle.Core" Version="5.1.1" />
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />
</ItemGroup>
2)创建2个实体,这里只是模拟锁的使用,不限定业务环境
csharp
1、商品库存表
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Demos.MicrosoftRedLockNet.Entities
{
/// <summary>
/// 商品库存表
/// </summary>
[Table("shop_inventory")]
[Comment("商品库存表")]
public class ShopInventory
{
/// <summary>
/// 库存表Id
/// </summary>
[Key]
public int Id { get; set; }
/// <summary>
/// 商品Id
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// 库存数
/// </summary>
public int Total { get; set; }
}
}
2、订单表
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Demos.MicrosoftRedLockNet.Entities
{
/// <summary>
/// 订单信息表
/// </summary>
[Table("order_info")]
[Comment("订单信息表")]
public class ShopOrder
{
/// <summary>
/// 订单表Id
/// </summary>
[Key]
public Guid Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 商品ID
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// 订单号
/// </summary>
[Display(Name = "序号")]
[Required(AllowEmptyStrings = false)]
[Column(TypeName = "varchar(200)")]
public string OrderNum { get; set; }
/// <summary>
/// 商品数量
/// </summary>
public int OrderQuantity { get; set; }
/// <summary>
/// 订单金额
/// </summary>
[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }
/// <summary>
/// 支付时间
/// </summary>
public DateTime? PayTime { get; set; }
/// <summary>
/// 支付订单号
/// </summary>
public string PayOrderNum { get; set; }
/// <summary>
/// CreateTime
/// </summary>
public DateTime? CreateTime { get; set; }
/// <summary>
/// 订单备注
/// </summary>
[Column(TypeName = "varchar(200)")]
public string Remark { get; set; }
}
}
3)创建DbContext
csharp
using Demos.MicrosoftRedLockNet.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Collections;
using System.Data;
namespace Demos.MicrosoftRedLockNet.Contexts
{
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
}
public DbSet<ShopOrder> ShopOrder { get; set; }
public DbSet<ShopInventory> ShopInventory { set; get; }
}
}
4)配置appsettings.json
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"ConnectionStrings": {
"Redis": "localhost:6379",
"Default": "Server=localhost;Port=3306;Database=demo_orderservice;Uid=root;Pwd=123456;"
},
"AllowedHosts": "*"
}
3.2、创建数据库
1)先创建demo_orderservice数据库,采用utf8mb4字符集,utf8mb4_general_ci排序规则
2)执行数据迁移
shell
//生成项目
PS D:\Workspace\DotNet\netCoreDemos\dev\Demos.MicrosoftRedLockNet> dotnet build
//生成迁移文件
PS D:\Workspace\DotNet\netCoreDemos\dev\Demos.MicrosoftRedLockNet> dotnet ef migrations add MicrosoftRedLockNetMigrations
//执行迁移生成文件,执行前检查项目中是否生成了MIgrations文件夹以及相关迁移文件
PS D:\Workspace\DotNet\netCoreDemos\dev\Demos.MicrosoftRedLockNet> dotnet ef database update
3.3、创建service层
1)创建IOrderService.cs接口
csharp
using Demos.MicrosoftRedLockNet.AopLocks;
using Demos.MicrosoftRedLockNet.Common;
using Demos.MicrosoftRedLockNet.Services.Dtos;
namespace Demos.MicrosoftRedLockNet.Services
{
/// <summary>
/// 订单服务
/// </summary>
public interface IOrderService
{
/// <summary>
/// 创建订单
/// </summary>
/// <param name="orderDto"></param>
/// <returns></returns>
[MyRedisLock(Key = "seckill:product:{orderDto.ProductId}", WaitTime = 3, LeaseTime = 10, FailMessage = "系统繁忙,请稍后重试")]
Task<ResultObject> CreateOrderAsync(ShopOrderDto orderDto);
}
}
2)创建实现类OrderServiceAopInstance.cs
csharp
using Demos.MicrosoftRedLockNet.Common;
using Demos.MicrosoftRedLockNet.Contexts;
using Demos.MicrosoftRedLockNet.Entities;
using Demos.MicrosoftRedLockNet.Services.Dtos;
using Mapster;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
namespace Demos.MicrosoftRedLockNet.Services
{
public class OrderServiceAopInstance : IOrderService
{
private readonly MyDbContext _dbContext;
private readonly IDatabase _redisDb;
public OrderServiceAopInstance(MyDbContext dbContext, IConnectionMultiplexer redisConnection)
{
_dbContext = dbContext;
_redisDb = redisConnection.GetDatabase(1);
}
/// <summary>
/// 商品抢购
/// </summary>
/// <param name="orderDto"></param>
/// <returns></returns>
public async Task<ResultObject> CreateOrderAsync(ShopOrderDto orderDto)
{
var result = new ResultObject();
if (orderDto == null || orderDto.ProductId <= 0 || orderDto.ProductCount <= 0)
{
result.code = 1004;
result.message = "抢购失败,参数错误";
return result;
}
// 1. 预热/获取库存
string stockKey = $"inventory:product:{orderDto.ProductId}";
long redisStock = await GetOrWarmupStockAsync(orderDto.ProductId, stockKey);
if (redisStock < orderDto.ProductCount)
{
result.code = 1001;
result.message = "库存不足";
return result;
}
// 2. 原子扣减 Redis 库存
long newStock = await _redisDb.StringDecrementAsync(stockKey, orderDto.ProductCount);
if (newStock < 0)
{
await _redisDb.StringIncrementAsync(stockKey, orderDto.ProductCount);
result.code = 1001;
result.message = "库存不足";
return result;
}
// 3. 数据库事务(扣库存 + 创建订单)
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
bool deductOk = await DeductInventoryFromDbAsync(orderDto.ProductId, orderDto.ProductCount);
if (!deductOk)
{
result.code = 1002;
result.message = "库存扣减失败";
return result;
}
bool orderOk = await InsertOrderRecordAsync(orderDto);
if (!orderOk)
{
result.code = 1003;
result.message = "创建订单失败";
return result;
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
await _redisDb.StringIncrementAsync(stockKey, orderDto.ProductCount);
result.code = 1004;
result.message = "抢购失败";
return result;
}
result.message = $"抢购成功,当前库存={newStock}";
result.code = 1000;
return result;
}
/// <summary>
/// 添加订单记录
/// </summary>
/// <returns></returns>
private async Task<bool> InsertOrderRecordAsync(ShopOrderDto orderInfo)
{
if (orderInfo == null) return false;
var order = orderInfo.Adapt<ShopOrder>();
order.Id = Guid.NewGuid();
order.OrderNum = Guid.NewGuid().ToString("N");
order.CreateTime = DateTime.Now;
order.PayOrderNum = Guid.NewGuid().ToString("N");
order.OrderQuantity = orderInfo.ProductCount;
order.Remark = "秒杀活动下单";
order.Price = 19.99M;
await _dbContext.ShopOrder.AddAsync(order);
var result = await _dbContext.SaveChangesAsync();
return result > 0;
}
/// <summary>
/// 扣减库存
/// </summary>
/// <param name="productId"></param>
/// <param name="quantity"></param>
/// <returns></returns>
private async Task<bool> DeductInventoryFromDbAsync(long productId, int quantity)
{
//使用原子更新,防止库存跳动
int affectedRows = await _dbContext.ShopInventory
.Where(i => i.ProductId == productId && i.Total >= quantity)
.ExecuteUpdateAsync(s => s.SetProperty(i => i.Total, i => i.Total - quantity));
return affectedRows > 0;
}
/// <summary>
/// 查询数据库库存
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
private async Task<int> GetOrWarmupStockAsync(long productId, string stockKey)
{
// 1. 先检查 Redis 中是否已有库存
if (await _redisDb.KeyExistsAsync(stockKey))
{
var stock = await _redisDb.StringGetAsync(stockKey);
return (int)stock;
}
// 2. 只有 Redis 没有时,才去查数据库并预热
try
{
var inventory = await _dbContext.ShopInventory
.Where(i => i.ProductId == productId)
.FirstOrDefaultAsync();
if (inventory?.Total > 0)
{
// 加上过期时间,防止死数据
await _redisDb.StringSetAsync(stockKey, inventory.Total, TimeSpan.FromHours(1));
}
return inventory?.Total ?? 0;
}
catch (Exception)
{
return 0;
}
}
}
}
返回类型
namespace Demos.MicrosoftRedLockNet.Common
{
public class ResultObject
{
public bool status { get; set; } = true;
public int code { get; set; }
public string message { get; set; }
public static ResultObject Fail(string msg)
{
return new ResultObject
{
status = false,
code = 0,
message = msg
};
}
public static ResultObject Success(string msg)
{
return new ResultObject
{
code = 200,
message = msg
};
}
}
}
3.4、创建AOP方法
csharp
//1、参数读取类LockKeyParser
using System.Reflection;
using System.Text.RegularExpressions;
namespace Demos.MicrosoftRedLockNet.AopLocks
{
/// <summary>
/// 请求参数读取
/// </summary>
public class LockKeyParser
{
/// <summary>
///
/// </summary>
/// <param name="expression"></param>
/// <param name="arguments"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public static string Parse(string expression, object[] arguments, ParameterInfo[] parameters)
{
if (string.IsNullOrEmpty(expression)) return expression;
// 匹配 {参数名} 或 {参数名.属性名} 格式
var regex = new Regex(@"\{([^{}]+)\}");
return regex.Replace(expression, match =>
{
string path = match.Groups[1].Value;
var value = GetValueFromArguments(path, arguments, parameters);
return value?.ToString() ?? "";
});
}
/// <summary>
/// 从action读取参数
/// </summary>
/// <param name="path"></param>
/// <param name="arguments"></param>
/// <param name="parameters"></param>
/// <returns></returns>
private static object GetValueFromArguments(string path, object[] arguments, ParameterInfo[] parameters)
{
var parts = path.Split('.');
var firstPart = parts[0];
// 查找参数
var paramIndex = Array.FindIndex(parameters, p => p.Name == firstPart);
if (paramIndex == -1) return null;
var current = arguments[paramIndex];
for (int i = 1; i < parts.Length && current != null; i++)
{
current = current.GetType().GetProperty(parts[i])?.GetValue(current);
}
return current;
}
}
}
//2、特性类MyRedisLockAttribute.cs
namespace Demos.MicrosoftRedLockNet.AopLocks
{
/// <summary>
/// 分布式事务AOP
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class MyRedisLockAttribute : Attribute
{
/// <summary>
/// 锁的Key,支持SpEL表达式,例如 "seckill:product:{ProductId}:user:{UserId}"
/// </summary>
public string Key { get; set; }
/// <summary>
/// 等待锁的超时时间(秒)
/// </summary>
public int WaitTime { get; set; } = 3;
/// <summary>
/// 锁自动释放时间(秒)
/// </summary>
public int LeaseTime { get; set; } = 10;
/// <summary>
/// 获取锁失败时的提示信息
/// </summary>
public string FailMessage { get; set; } = "系统繁忙,请稍后重试";
}
}
//3、AOP实现类RedisLockInterceptor.cs
using Castle.DynamicProxy;
using RedLockNet;
using StackExchange.Redis;
using System.Reflection;
namespace Demos.MicrosoftRedLockNet.AopLocks
{
public class RedisLockInterceptor : IInterceptor
{
private readonly IDistributedLockFactory _lockFactory;
private readonly IDatabase _redisDb;
public RedisLockInterceptor(IConnectionMultiplexer redisConnection, IDistributedLockFactory lockFactory)
{
_lockFactory = lockFactory;
_redisDb = redisConnection.GetDatabase();
}
/// <summary>
/// 异步方法拦截器
/// </summary>
/// <param name="invocation"></param>
/// <exception cref="NotSupportedException"></exception>
public void Intercept(IInvocation invocation)
{
var method = invocation.Method;
var returnType = method.ReturnType;
if (returnType == typeof(Task))
{
invocation.ReturnValue = InterceptAsyncVoid(invocation);
}
else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
var resultType = returnType.GetGenericArguments()[0];
var genericMethod = GetType()
.GetMethod(nameof(InterceptAsyncWithResult), BindingFlags.NonPublic | BindingFlags.Instance)
.MakeGenericMethod(resultType);
invocation.ReturnValue = genericMethod.Invoke(this, new object[] { invocation });
}
else
{
throw new NotSupportedException("仅支持异步方法 (Task 或 Task<T>)");
}
}
/// <summary>
/// 处理无返回值的异步方法 (Task)
/// </summary>
private async Task InterceptAsyncVoid(IInvocation invocation)
{
var attribute = GetMyRedisLockAttribute(invocation);
if (attribute == null)
{
invocation.Proceed();
await (Task)invocation.ReturnValue;
return;
}
var redLock = await AcquireLockAsync(attribute, invocation);
if (redLock == null) return;
try
{
invocation.Proceed();
await (Task)invocation.ReturnValue;
}
finally
{
redLock.Dispose();
}
}
/// <summary>
/// 处理有返回值的异步方法 (Task<TResult>)
/// </summary>
private async Task<TResult> InterceptAsyncWithResult<TResult>(IInvocation invocation)
{
var attribute = GetMyRedisLockAttribute(invocation);
if (attribute == null)
{
invocation.Proceed();
return await (Task<TResult>)invocation.ReturnValue;
}
var redLock = await AcquireLockAsync(attribute, invocation);
if (redLock == null) return default!; // 正常情况下不会走到这里
try
{
invocation.Proceed();
return await (Task<TResult>)invocation.ReturnValue;
}
finally
{
redLock.Dispose();
}
}
/// <summary>
///
/// </summary>
/// <param name="attribute"></param>
/// <param name="invocation"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="Exception"></exception>
private async Task<IRedLock> AcquireLockAsync(MyRedisLockAttribute attribute, IInvocation invocation)
{
var lockKey = LockKeyParser.Parse(
attribute.Key,
invocation.Arguments,
invocation.Method.GetParameters()
);
if (string.IsNullOrEmpty(lockKey))
throw new ArgumentException("RedisLock Key 不能为空");
var redLock = await _lockFactory.CreateLockAsync(
lockKey,
TimeSpan.FromSeconds(attribute.LeaseTime),
TimeSpan.FromSeconds(attribute.WaitTime),
TimeSpan.FromSeconds(1)
);
if (!redLock.IsAcquired)
throw new Exception(attribute.FailMessage);
return redLock;
}
/// <summary>
///
/// </summary>
/// <param name="invocation"></param>
/// <returns></returns>
private MyRedisLockAttribute GetMyRedisLockAttribute(IInvocation invocation)
{
var targetMethod = invocation.MethodInvocationTarget ?? invocation.Method;
return targetMethod.GetCustomAttribute<MyRedisLockAttribute>();
}
}
}
3.5、创建控制器OrderController.cs
csharp
using Demos.MicrosoftRedLockNet.Services;
using Demos.MicrosoftRedLockNet.Services.Dtos;
using Microsoft.AspNetCore.Mvc;
namespace Demos.MicrosoftRedLockNet.Controllers
{
/// <summary>
///
/// </summary>
[Route("/api/app/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
/// <summary>
/// 秒杀商品
/// </summary>
/// <returns></returns>
[HttpPost("seckill")]
public async Task<IActionResult> Seckill(ShopOrderDto dto)
{
try
{
var result = await _orderService.CreateOrderAsync(dto);
Console.WriteLine($"{result.message}");
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpGet("seckill2")]
public async Task<IActionResult> Seckill2([FromQuery] ShopOrderDto dto)
{
try
{
var result = await _orderService.CreateOrderAsync(dto);
Console.WriteLine($"{result.message}");
return Ok(result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}
//请求参数类
namespace Demos.MicrosoftRedLockNet.Services.Dtos
{
public class ShopOrderDto
{
/// <summary>
///
/// </summary>
public long UserId { get; set; }
/// <summary>
///
/// </summary>
public long ProductId { get; set; }
/// <summary>
///
/// </summary>
public int ProductCount { get; set; }
}
}
4、使用ab模拟并发环境测试
4.1、下载Apache测试工具
通过网盘分享的文件:Apache24(ab压测工具).zip
链接: pan.baidu.com/s/1nVV18oP8... 提取码: 8090, 解压后如下:

4.2、部署nginx,通过nginx负载均衡模拟服务分发
下载地址:通过网盘分享的文件:nginx-1.20.2.zip
链接: pan.baidu.com/s/1lTxJZfWe... 提取码: 8090

nginx配置已经配好,代理本地5001和5002端口,NGINX使用的9021端口
shell
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# --- 新增:定义后端负载均衡服务器组 ---
upstream backend_pool {
# 默认使用轮询 (round-robin) 算法
server localhost:5001;
server localhost:5002;
# 如果需要加权轮询,可以使用 weight 参数,例如:
# server 127.0.0.1:5001 weight=3;
# server 127.0.0.1:5002 weight=1;
}
server {
# ---监听端口改为 9021 ---
listen 9021;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# root html;
# index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
5、测试
5.1、启动两个webapi实例
shell
在项目目录下运行
dotnet run --urls http://localhost:5001
另外再打开一个powershell窗口运行
dotnet run --urls http://localhost:5002
5.3、使用ab命令发送并发请求
shell
D:\Itools\Apache24\bin> .\ab.exe -n 100 -c 50 "http://localhost:9021/api/app/order/seckill2?pid=111&uid=889900&count=1"
ab指令说明:
-n:表示总共100个请求
-c:表示一次50个并发
返回的库存数量顺序不一致是由于执行库存扣减和写入订单记录后,每次请求的响应时间差异导致,确保最终扣减数量是请求数量,并且没有出现重复的库存说明该分布式锁是正常运行的. 源码来源:gitee.com/inc-zz/netC...,个人开源dotnet基础技术,持续更新中,欢迎Star