C# 用Redis实现的分布式锁
Redis实现分布式锁(悲观锁/乐观锁)
对锁的概念和应用场景在此就不阐述了,网上搜索有很多解释,只是我搜索到的使用C#利用Redis的SetNX命令实现的锁虽然能用,但是都不太适合我需要的场景。
基于ServiceStack.Redis写了一个帮助类
Redis连接池
public static PooledRedisClientManager RedisClientPool = CreateManager();
private static PooledRedisClientManager CreateManager()
{
var redisHosts = System.Configuration.ConfigurationManager.AppSettings["redisHosts"];
if (string.IsNullOrEmpty(redisHosts))
{
throw new Exception("AppSetting redisHosts no found");
}
string[] redisHostarr = redisHosts.Split(new string[] { ",", "," }, StringSplitOptions.RemoveEmptyEntries);
return new PooledRedisClientManager(redisHostarr, redisHostarr, new RedisClientManagerConfig
{
MaxWritePoolSize = 1000,
MaxReadPoolSize = 1000,
AutoStart = true,
DefaultDb = 0
});
}
使用Redis的SetNX命令实现加锁,
/// <summary>
/// 加锁
/// </summary>
/// <param name="key">锁key</param>
/// <param name="selfMark">自己标记</param>
/// <param name="lockExpirySeconds">锁自动过期时间[默认10](s)</param>
/// <param name="waitLockMilliseconds">等待锁时间(ms)</param>
/// <returns></returns>
public static bool Lock(string key, out string selfMark, int lockExpirySeconds = 10, long waitLockMilliseconds = long.MaxValue)
{
DateTime begin = DateTime.Now;
selfMark = Guid.NewGuid().ToString("N");//自己标记,释放锁时会用到,自己加的锁除非过期否则只能自己打开
using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
{
string lockKey = "Lock:" + key;
while (true)
{
string script = string.Format("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE',KEYS[1],{0}) return 1 else return 0 end", lockExpirySeconds * 1000);
//循环获取取锁
if (redisClient.ExecLuaAsInt(script, new[] { lockKey }, new[] { selfMark }) == 1)
{
return true;
}
//不等待锁则返回
if (waitLockMilliseconds == 0)
{
break;
}
//超过等待时间,则不再等待
if ((DateTime.Now - begin).TotalMilliseconds >= waitLockMilliseconds)
{
break;
}
Thread.Sleep(100);
}
return false;
}
}
- 参数key:锁的key
- 参数selfMark:在设置锁的时候会产生一个自己的标识,在释放锁的时候会用到,所谓解铃还须系铃人。防止锁被误释放,导致锁无效.
- 参数lockExpirySeconds:锁的默认过期时间,防止被永久死锁.
- 参数waitLockMilliseconds:循环获取锁的等待时间.
如果设置为0,为乐观锁机制,获取不到锁,直接返回未获取到锁.
默认值为long最大值,为悲观锁机制,约等于很多很多天,可以理解为一直等待.
释放锁
/// <summary>
/// 释放锁
/// </summary>
/// <param name="key">锁key</param>
/// <param name="selfMark">自己标记</param>
public void UnLock(string key, string selfMark)
{
using (RedisClient redisClient = (RedisClient)RedisClientPool.GetClient())
{
string lockKey = "Lock:" + key;
var script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisClient.ExecLuaAsString(script, new[] { lockKey }, new[] { selfMark });
}
}
- 参数key:锁的key
- 参数selfMark:在设置锁的时候返回的自己标识,用来解锁自己加的锁(此值不能随意传,必须是加锁时返回的值)
调用方式
- 悲观锁方式
int num = 10;
string lockkey = "xianseng";
//悲观锁开启20个人同时拿宝贝
for (int i = 0; i < 20; i++)
{
Task.Run(() =>
{
string selfmark = "";
try
{
if (PublicLockHelper.Lock(lockkey, out selfmark))
{
if (num > 0)
{
num--;
Console.WriteLine($"我拿到了宝贝:宝贝剩余{num}个\t\t{selfmark}");
}
else
{
Console.WriteLine("宝贝已经没有了");
}
Thread.Sleep(100);
}
}
finally
{
PublicLockHelper.UnLock(lockkey, selfmark);
}
});
}
- 乐观锁方式
int num = 10;
string lockkey = "xianseng";
//乐观锁开启10个线程,每个线程拿5次
for (int i = 0; i < 10; i++)
{
Task.Run(() =>
{
for (int j = 0; j < 5; j++)
{
string selfmark = "";
try
{
if (PublicLockHelper.Lock(lockkey, out selfmark, 10, 0))
{
if (num > 0)
{
num--;
Console.WriteLine($"我拿到了宝贝:宝贝剩余{num}个\t\t{selfmark}");
}
else
{
Console.WriteLine("宝贝已经没有了");
}
Thread.Sleep(1000);
}
else
{
Console.WriteLine("没有拿到,不想等了");
}
}
finally
{
PublicLockHelper.UnLock(lockkey, selfmark);
}
}
});
}
单机只能用多线模拟使用分布式锁了
此锁已经可以满足大多数场景了,若有不妥,还请多多指出,以免误别人!
()