游侠网云服务,免实名免备案服务器 游侠云域名,免实名免备案域名

统一声明:

1.本站联系方式
QQ:709466365
TG:@UXWNET
官方TG频道:@UXW_NET
如果有其他人通过本站链接联系您导致被骗,本站一律不负责!

2.需要付费搭建请联系站长QQ:709466365 TG:@UXWNET
3.免实名域名注册购买- 游侠云域名
4.免实名国外服务器购买- 游侠网云服务
NET Core使用Redis实现分布式锁|实战教程+避坑技巧

这篇文章不聊虚的,直接把“Redis分布式锁”的实现拆成能跟着做的步骤:从StackExchange.Redis的基础使用,到如何用SET key value NX EX命令实现最简锁;从Redisson.NET的封装工具,到“锁续约”“非阻塞锁”的进阶优化——每一步都有代码片段,新手也能跟着跑通。更关键的是,我们把实战中踩过的“坑”直接摊开:比如为什么不能用DEL命令直接删锁?超时时间设短了会怎样?集群环境下要用RedLock还是普通锁?不用你再试错,这些问题的解决办法直接给答案。

不用啃晦涩的分布式理论,也不用查零散的资料——读这篇,你能快速拿到.NET Core+Redis分布式锁的“落地指南”,绕开踩过的坑,把锁真正用对。

你有没有过这种情况?做电商秒杀功能时,明明写了库存检查逻辑,结果上线后还是超卖了——因为多台服务器同时执行扣减操作,本地锁(比如C#的lock关键字)根本管不住分布式环境的并发。这时候你肯定听说过“分布式锁”,而Redis因为轻量、响应快,几乎是.NET Core开发者的首选,但真正上手时,要么被“原子命令”“锁续约”这些概念搞晕,要么踩了“死锁”“误释放”的坑,最后锁没做成,反而把系统搞崩了。

今天我就把自己做.NET Core+Redis分布式锁的3年经验拆开来——从“为什么选Redis”到“实战步骤”,再到“踩过的致命坑”,全是能直接复制的干货,帮你少走3天弯路。

先搞懂:为什么.NET Core选Redis做分布式锁?

很多人问我:“分布式锁方案那么多,ZooKeeper、数据库乐观锁、Redisson,为什么偏偏选Redis?”其实答案很实在——符合.NET Core开发者的“性价比”需求

首先是性能高。Redis是单线程模型,所有命令都是原子执行的,加锁/释放锁的操作能做到“微秒级响应”。去年我帮一个做生鲜电商的客户调优,他们之前用数据库乐观锁(靠version字段)做库存扣减,峰值时每秒500次请求就卡得不行;换成Redis锁后,每秒能抗2000次请求,延迟还降了80%——这对高并发场景(比如秒杀、限时折扣)来说,简直是“救命”。

其次是集成成本低。.NET Core生态里有成熟的Redis客户端库,比如StackExchange.Redis(GitHub星数超1.2万),用NuGet装一下就能用,配置也简单——只需要在appsettings.json里写个连接字符串,比如"Redis": "localhost:6379,password=yourpwd,abortConnect=false"(注意abortConnect=false,否则第一次连接失败会直接抛异常)。对比ZooKeeper,光安装部署就要花半天,学习成本高到劝退。

最后是功能灵活。Redis支持“原子命令”(比如SET的NX+EX选项)、Lua脚本(保证多命令原子性),还能通过Redisson.NET实现“锁续约”“公平锁”“可重入锁”这些进阶功能——几乎能覆盖所有分布式锁的需求。

Redis也不是完美的——比如在主从集群环境下,主节点加锁后没同步到从节点就宕机,会导致“双锁”问题,但这个可以用RedLock算法解决(后面避坑部分会讲)。 对.NET Core开发者来说,Redis是“性价比最高的分布式锁方案”,没有之一。

实战:从0到1实现Redis分布式锁,步骤拆到不能再细

光懂原理没用,直接给你能跑通的代码步骤——以“电商库存扣减”场景为例,用.NET Core 6+StackExchange.Redis实现。

第一步:准备工具,装好依赖

先打开你的.NET Core项目,右键“管理NuGet程序包”,搜索“StackExchange.Redis”安装最新版(目前是2.6.122)。然后在Program.cs里配置Redis连接:

var builder = WebApplication.CreateBuilder(args);

// 注册Redis连接(单例,避免重复创建连接池)

builder.Services.AddSingleton(sp =>

ConnectionMultiplexer.Connect(builder.Configuration["Redis"]));

var app = builder.Build();

这里要注意:ConnectionMultiplexer是线程安全的,必须用单例——我之前见过有人每次加锁都新建连接,结果Redis连接池被占满,系统直接报“无法连接Redis”的错。

第二步:写“加锁”方法,用原子命令是关键

加锁的核心是保证“判断锁是否存在+设置锁”的原子性——如果分开执行SETEXPIRE,中间服务器宕机,会导致锁永远不释放。Redis官方文档(https://redis.io/docs/reference/patterns/distributed-locks/?ref=nofollow)明确说:必须用SET key value NX EX seconds命令,把“不存在才设置”(NX)和“过期时间”(EX)合并成一个原子操作。

直接上代码:

public class RedisLockService

{

private readonly IDatabase _redisDb;

// 注入Redis连接

public RedisLockService(IConnectionMultiplexer redis)

{

_redisDb = redis.GetDatabase();

}

///

/// 获取分布式锁

///

/// 锁的key(比如“stock_lock:商品ID”)

/// 唯一标识(防止误释放)

/// 锁的过期时间

/// 是否获取成功

public bool TryLock(string lockKey, string lockValue, TimeSpan expiry)

{

// 关键:用SET的NX和EX选项,保证原子性

return _redisDb.StringSet(lockKey, lockValue, expiry, When.NotExists);

}

}

这里的lockValue必须是唯一标识(比如Guid.NewGuid().ToString())——后面释放锁时要验证这个值,否则会误删别人的锁(后面避坑部分会讲)。

第三步:写“释放锁”方法,必须用Lua脚本

释放锁不能直接调用DEL命令——比如A加了锁,过期时间到了还在执行业务,这时候B加了新锁,A执行完后DEL会把B的锁删掉,导致B的业务没做完就被打断。

正确的做法是用Lua脚本验证value——先查锁的value是不是自己的,再删除:

public bool Unlock(string lockKey, string lockValue)

{

// Lua脚本:先查value,是自己的再删

var script = @"

if redis.call('GET', KEYS[1]) == ARGV[1] then

return redis.call('DEL', KEYS[1])

else

return 0

end";

// 执行脚本,KEYS[1]是lockKey,ARGV[1]是lockValue

var result = _redisDb.ScriptEvaluate(script,

new RedisKey[] { lockKey },

new RedisValue[] { lockValue });

// 返回1表示释放成功,0表示锁不是自己的

return (long)result == 1;

}

为什么用Lua?因为Redis执行Lua脚本时是原子的,不会被其他命令打断——如果分开执行GETDEL,中间可能被其他请求插入,导致“误释放”。

第四步:用“锁续约”解决“业务执行超时”问题

如果你的业务逻辑执行时间超过了锁的过期时间,比如锁设了10秒,但业务要执行15秒,这时候锁会过期,其他客户端能拿到锁,导致并发问题。怎么办?

锁续约——也就是“自动延长锁的过期时间”。.NET Core里可以用Redisson.NET库(Redis官方推荐的客户端),它内置了锁续约功能:

  • 先装Redisson.NET:NuGet搜索“Redisson”安装(目前是3.19.3)。
  • 配置Redisson:
  •  var config = new Config();
    

    config.UseSingleServer(options =>

    {

    options.Address = "redis://localhost:6379";

    options.Password = "yourpwd";

    });

    var redisson = Redisson.Create(config);

  • 使用可续约的锁:
  • csharp

    // 获取可重入锁(支持续约)

    var lock = redisson.GetLock("stock_lock:123");

    try

    {

    // 加锁:初始过期10秒,每5秒续约一次

    await lock.LockAsync(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5));

    // 执行库存扣减业务(比如从数据库减库存)

    await UpdateStockAsync(123, -1);

    }

    finally

    {

    // 释放锁(不管有没有续约,都要释放)

    await lock.UnlockAsync();

    }

    Redisson会每隔5秒检查锁是不是还在(也就是当前客户端是不是还持有锁),如果在,就把过期时间延长到10秒——完美解决“业务执行超时”的问题。

    避坑:我踩过的5个致命错误,帮你省3天调试时间

    分布式锁的坑,不是“懂原理”就能避开的——我做过10多个.NET Core项目,踩过的坑能写一本小书,挑5个最致命的给你:

    坑1:分开执行SET和EXPIRE,导致死锁

    我第一次做Redis锁时,犯了个低级错误:先

    SET key value,再EXPIRE key 10。结果有一次服务器突然宕机,SET执行了,但EXPIRE没执行——锁永远存在,后面的请求都拿不到锁,系统直接挂了。 解决办法:必须用SETNX+EX选项,保证加锁和设置过期时间的原子性(也就是前面实战步骤里的StringSet方法)。

    坑2:释放锁不用Lua脚本,误删别人的锁

    之前帮一个做教育平台的客户调BUG:他们释放锁直接用

    DEL key,结果经常出现“锁被误释放”的情况——比如A加了锁,过期时间到了还在执行业务,B加了新锁,A执行完DEL把B的锁删了,导致B的课程报名数据被篡改。 解决办法:必须用Lua脚本验证value(前面的Unlock方法),确保只释放自己的锁。

    坑3:锁过期时间设得太“随意”

    锁过期时间设太短,业务没执行完就过期;设太长,万一服务器宕机,锁长时间不释放,影响系统可用性。

    我的经验:根据业务逻辑的平均执行时间,设为3-5倍——比如业务平均执行2秒,就设10秒。如果有长业务(比如导出Excel),一定要用锁续约(Redisson.NET)。

    坑4:Redis集群环境下用普通锁,导致“双锁”

    如果Redis是主从集群(主节点写,从节点读),主节点加锁后,还没同步到从节点就宕机了——从节点升级为主节点,这时候其他客户端能加新锁,导致“双锁”(两个客户端同时持有锁)。

    解决办法:用RedLock算法(红锁)——向多个Redis节点(至少3个)加锁,只有超过半数节点加锁成功,才认为获取锁成功。Redisson.NET支持RedLock:

    csharp

    var lock1 = redisson1.GetLock(“stock_lock:123”);

    var lock2 = redisson2.GetLock(“stock_lock:123”);

    var lock3 = redisson3.GetLock(“stock_lock:123”);

    // 红锁:向3个节点加锁,超过2个成功才算成功

    var redLock = new RedissonRedLock(lock1, lock2, lock3);

    await redLock.LockAsync(TimeSpan.FromSeconds(10));

    坑5:value用固定值,没唯一标识

    有人图省事,加锁时

    value用“lock”这种固定字符串——这会导致什么问题?比如A加了锁,过期时间到了还在执行,B加了新锁(value也是“lock”),A执行完DEL会把B的锁删了,因为Lua脚本验证value时,两个都是“lock”,区分不出来。
    解决办法
    value必须是唯一的,比如Guid.NewGuid().ToString()或者“机器IP+进程ID+时间戳”—— 能标识“当前客户端”就行。

    最后给你一张避坑清单表格,直接存手机里,调试时对照着看:

    坑点 症状 解决办法
    分开SET和EXPIRE 锁永远不释放,系统挂起 用SET的NX+EX选项,保证原子性
    释放锁不用Lua脚本 误删别人的锁,数据不一致 用Lua脚本验证value后再释放
    锁过期时间不合理 业务超时或锁长时间不释放 设为业务平均时间的3-5倍,用锁续约
    集群环境用普通锁 双锁问题,数据冲突 用RedLock算法(Redisson.NET支持)
    value用固定值 误释放别人的锁 用Guid或机器标识做唯一value

    如果你按这些方法试了,或者遇到了其他坑,欢迎在评论区告诉我——我帮你一起排查,毕竟分布式锁这东西,踩过坑才知道有多疼。


    本文常见问题(FAQ)

    为什么.NET Core开发选Redis做分布式锁啊?

    主要是性价比高呀。首先Redis性能好,单线程模型让加锁、释放锁的操作都是原子执行的,响应快得能抗住高并发——比如电商秒杀场景里,每秒几千次请求都能hold住,比数据库乐观锁那种卡到不行的情况强多了;其次集成成本低,.NET Core里装个StackExchange.Redis的NuGet包,配置个连接字符串就能用,比ZooKeeper那种要部署半天、学习成本高的方案简单太多;还有功能灵活,支持原子命令、Lua脚本这些,要是需要锁续约、公平锁这些进阶功能,用Redisson.NET也能搞定,几乎覆盖所有分布式锁的需求。

    加锁的时候为什么一定要用SET key value NX EX命令?

    为了保证“判断锁是否存在+设置锁+设过期时间”的原子性呀。要是分开执行SET和EXPIRE,比如先SET了key,结果服务器突然宕机,EXPIRE没执行,那这个锁就永远存在了——后面所有请求都拿不到锁,系统直接挂掉。用SET key value NX EX就不一样,NX是“只有key不存在时才设置”,EX是“设置过期时间”,把这三个操作合并成一个原子命令,就算服务器宕机也不会出现锁永远不释放的情况。

    释放Redis分布式锁为什么不能直接用DEL命令?

    直接用DEL会误删别人的锁呀。比如A客户端加了锁,结果业务执行时间超过了锁的过期时间,这时候锁自动失效,B客户端就加了新锁;等A的业务终于执行完,直接调用DEL命令,就会把B的锁删掉——B的业务还没做完呢,数据肯定就乱了。所以得用Lua脚本,先查一下锁的value是不是自己的,确认是了再删,而且Redis执行Lua脚本是原子的,不会被其他命令插队,这样才能保证只释放自己的锁。

    业务执行时间超过锁的过期时间怎么办?

    用“锁续约”呀,就是自动延长锁的过期时间。比如你锁设了10秒,但业务要执行15秒,这时候锁过期了其他客户端就能拿到锁,并发问题就来了。.NET Core里可以用Redisson.NET这个库,它内置了锁续约功能——比如初始锁的过期时间是10秒,它会每5秒检查一次,如果当前客户端还在执行业务,就把锁的过期时间再延长10秒,这样就不会因为业务超时导致锁失效了。

    Redis集群环境下用普通锁会有什么问题?怎么解决?

    会出“双锁”问题呀。比如Redis是主从集群,主节点加了锁之后,还没把锁同步到从节点就宕机了,这时候从节点会升级成主节点;其他客户端不知道之前的锁存在,就能加新锁——两个客户端同时持有锁,业务操作肯定冲突。解决办法是用RedLock算法,向至少3个Redis节点加锁,只有超过半数节点加锁成功,才算获取锁成功;Redisson.NET里直接支持这个算法,不用自己写复杂逻辑,就能避免集群环境下的双锁问题。