结合上一期 .NET CORE 分布式事务(三) DTM实现Saga及高并发下的解决方案(.NET CORE 分布式事务(三) DTM实现Saga及高并发下的解决方案-CSDN博客)。有的小伙伴私信说如果锁内锁定的程序或者资源未在上锁时间内执行完,造成的使用资源冲突,需要如何解决。本来打算之后在发博文说明这个问题。那就先简短的说明一下。
这是一个Redis分布式锁续命或者称之为续期的问题。废话不多说,直接上代码。
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Redlock.CSharp;
using StackExchange.Redis;
using System.Diagnostics;
using System.Globalization;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Threading;
public class RedisService
{
private readonly ConnectionMultiplexer _redis;
private readonly IDatabase _database;
/// <summary>
/// 初始化 <see cref="RedisService"/> 类的新实例。
/// </summary>
/// <param name="connectionMultiplexer">连接多路复用器。</param>
public RedisService(string connectionString)
{
_redis = ConnectionMultiplexer.Connect(connectionString);
_database = _redis.GetDatabase();
}
#region 分布式锁
#region 阻塞锁
/// <summary>
/// 阻塞锁--加锁
/// </summary>
/// <param name="key">阻塞锁的键</param>
/// <param name="expireSeconds">阻塞锁的缓存时间</param>
/// <param name="timeout">加锁超时时间</param>
/// <returns></returns>
public bool AcquireLock(string key, int expireSeconds, int timeout)
{
var script = @"local isNX = redis.call('SETNX', KEYS[1], ARGV[1])
if isNX == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0";
RedisKey[] scriptkey = { key };
RedisValue[] scriptvalues = { key, expireSeconds * 1000 };
var stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed.TotalSeconds < timeout)
{
if (_database.ScriptEvaluate(script, scriptkey, scriptvalues).ToString() == "1")
{
stopwatch.Stop();
return true;
}
}
Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁超时");
stopwatch.Stop();
return false;
}
Action<string, int, int, IDatabase> postponeAction = (string key, int expireSeconds, int postponetime, IDatabase database) =>
{
var stopwatchpostpone = Stopwatch.StartNew();
while (true)
{
//记录时钟大于锁的设置时间说明这个锁已经自动释放了,没必要再用lua脚本去判断了,直接提前退出
if (stopwatchpostpone.Elapsed.TotalSeconds > expireSeconds) return;
//提前三分之一时间续命,必须提前。要不真释放了
if (stopwatchpostpone.Elapsed.TotalSeconds > expireSeconds * 0.66)
{
var scriptpostpone = @"local isNX = redis.call('EXISTS', KEYS[1])
if isNX == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0";
RedisKey[] scriptkey = { key };
RedisValue[] scriptvalues = { key, postponetime * 1000 };
if (database.ScriptEvaluate(scriptpostpone, scriptkey, scriptvalues).ToString() == "1")
Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁续命成功");
else
Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁续命失败");
return;
}
}
};
/// <summary>
/// 阻塞续命锁
/// </summary>
/// <param name="key">阻塞锁的键</param>
/// <param name="expireSeconds">阻塞锁的缓存时间</param>
/// <param name="timeout">加锁超时时间</param>
/// <param name="postponetime">续命时间</param>
/// <returns></returns>
public bool AcquireLock(string key, int expireSeconds, int timeout, int postponetime)
{
var script = @"local isNX = redis.call('SETNX', KEYS[1], ARGV[1])
if isNX == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0";
RedisKey[] scriptkey = { key };
RedisValue[] scriptvalues = { key, expireSeconds * 1000 };
var stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed.TotalSeconds < timeout)
{
if (_database.ScriptEvaluate(script, scriptkey, scriptvalues).ToString() == "1")
{
stopwatch.Stop();
//锁续命
Thread postponeThread = new Thread(() =>
{
postponeAction.Invoke(key, expireSeconds, postponetime, _database);
});
postponeThread.Start();
return true;
}
}
Console.WriteLine($"[{DateTime.Now}]{key}--阻塞锁超时");
stopwatch.Stop();
return false;
}
/// <summary>
/// 阻塞锁--释放锁
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool UnAcquireLock(string key)
{
var script = @"local getLock = redis.call('GET', KEYS[1])
if getLock == ARGV[1] then
redis.call('DEL', KEYS[1])
return 1
end
return 0"
;
RedisKey[] scriptkey = { key };
RedisValue[] scriptvalues = { key };
return _database.ScriptEvaluate(script, scriptkey, scriptvalues).ToString() == "1";
}
#endregion
#endregion
}
.NET CORE中是没有现成的Redis锁续命的api,只能自己造轮子。续命同样使用了Redis的Lua脚本来实现,确保了原子性。获取了Redis锁之后,直接开启了一个新的线程,在设置时间还剩三分之一的时候进行了续命,这在程序中是有必要使用的,比如说因为网络原因造成的延时,本来我的这个接口执行完毕只需要3秒钟,但是有于网络延时造成了我的这个接口执行超过了3秒,这时候就需要Redis锁续命。以上代码就可以完美结局这个问题。
[HttpGet("AcquireLockPostpone")]
public void AcquireLockPostpone()
{
string key = Guid.NewGuid().ToString();
if (_redisService.AcquireLock("AcquireLockPostpone", 3, 100, 3))
{
Thread.Sleep(5000);
_redisService.UnAcquireLock("AcquireLockPostpone");
Console.WriteLine($"AcquireLockPostpone--释放锁");
}
}
控制器API,调用可以续命的阻塞锁,缓存时间设置为3秒 续命时间也是延长3秒。我们走个100阻塞锁的并发试一下。
这100个阻塞锁均续命完成。也都正常执行完毕。