Distributed locking plays an important role in the cluster architecture. The following are the main usage scenarios
1. In high concurrency scenarios such as flash sale and rush purchase, multiple users order the same product at the same time may lead to oversold inventory.
2. Financial operations such as payment and transfer must ensure that the changes in funds in the same account are performed serially.
3. In a distributed environment, multiple nodes may trigger the same task at the same time (such as timed report generation).
4. Users repeatedly submit forms due to network delays, which may lead to repeated insertion of data.
-
Custom distributed locks
- Get the lock
- Release the lock
- Automatic renewal
-
Distributed lock
- Get the lock
- Release the lock
- Automatic renewal
Custom distributed locks
Get the lock
For example, in a scenario, the order number needs to beorder-88888944010The orders are deducted because the backend is multi-node, which prevents users from repeatedly clicking, causing deduction requests to unused cluster nodes, so only one node needs to process the order at the same time.
public static async Task<(bool Success, string LockValue)> LockAsync(string cacheKey, int timeoutSeconds = 5)
{
var lockKey = GetLockKey(cacheKey);
var lockValue = ().ToString();
var timeoutMilliseconds = timeoutSeconds * 1000;
var expiration = (timeoutMilliseconds);
bool flag = await _redisDb.StringSetAsync(lockKey, lockValue, expiration, );
return (flag, flag ? lockValue : );
}
public static string GetLockKey(string cacheKey)
{
return $"MyApplication:locker:{cacheKey}";
}
The above code stores the order number as part of the redis key in redis at request and generates a random lockValue as a value. It can only be successfully set when the key does not exist in redis, that is, it is to obtain the distributed lock for the order.
await LockAsync("order-88888944010",30); //Acquire the lock and set the timeout to 30 seconds
Release the lock
public static async Task<bool> UnLockAsync(string cacheKey, string lockValue)
{
var lockKey = GetLockKey(cacheKey);
var script = @"local invalue = @value
local currvalue = ('get',@key)
if(invalue==currvalue) then ('del',@key)
return 1
else
return 0
end";
var parameters = new { key = lockKey, value = lockValue };
var prepared = (script);
var result = (int)await _redisDb.ScriptEvaluateAsync(prepared, parameters);
return result == 1;
}
The release lock uses a lua script to determine whether the lockValue is a delete request sent by the same processing node, that is, to determine whether the lock is added and released lock are the same source.
Reasons for using lua scripts instead of directly using the API to perform deletion:
After acquiring the lock, the lock expires due to GC pause or network delay. At this time, client B acquires the lock. If A directly calls DEL after recovery, the lock held by B will be deleted incorrectly.
2. The script is executed in Redis single thread to ensure that the GET and DEL are not interrupted by other commands.
Automatic renewal
Some time-consuming tasks may not be able to complete business processing within the specified timeout time, and an automatic renewal mechanism is required.
/// <summary>
/// Automatic renewal
/// </summary>
/// <param name="redisDb"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="milliseconds">Renewal time</param>
/// <returns></returns>
public async static Task Delay(IDatabase redisDb, string key, string value, int million seconds)
{
if (!(key))
return;
var script = @"local val = ('GET', @key)
if val==@value then
('PEXPIRE', @key, @milliseconds)
return 1
end
return 0";
object parameters = new { key, value, millionseconds };
var prepared = (script);
var result = await (prepared, parameters, );
if ((int)result == 0)
{
(key);
}
return;
}
Processors that save automatic renewal tasks
public class AutoDelayHandler
{
private static readonly Lazy<AutoDelayHandler> lazy = new Lazy<AutoDelayHandler>(() => new AutoDelayHandler());
private static ConcurrentDictionary<string, (Task, CancellationTokenSource)> _tasks = new ConcurrentDictionary<string, (Task, CancellationTokenSource)>();
public static AutoDelayHandler Instance => ;
/// <summary>
/// Add task token to the collection
/// </summary>
/// <param name="key"></param>
/// <param name="task"></param>
/// <returns></returns>
public bool TryAdd(string key, Task task, CancellationTokenSource token)
{
if (_tasks.TryAdd(key, (task, token)))
{
();
return true;
}
else
{
return false;
}
}
public void CloseTask(string key)
{
if (_tasks.ContainsKey(key))
{
if (_tasks.TryRemove(key, out (Task, CancellationTokenSource) item))
{
item.Item2?.Cancel();
item.Item1?.Dispose();
}
}
}
public bool ContainsKey(string key)
{
return _tasks.ContainsKey(key);
}
}
Apply for full code for distributed locks with automatic renewal
/// <summary>
/// Acquire the lock
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="timeoutSeconds">Timeout</param>
/// <param name="autoDelay">Is it automatic renewal?</param>
/// <returns></returns>
public static async Task<(bool Success, string LockValue)> LockAsync(string cacheKey, int timeoutSeconds = 5, bool autoDelay = false)
{
var lockKey = GetLockKey(cacheKey);
var lockValue = ().ToString();
var timeoutMilliseconds = timeoutSeconds * 1000;
var expiration = (timeoutMilliseconds);
bool flag = await _redisDb.StringSetAsync(lockKey, lockValue, expiration, );
if (flag && autoDelay)
{
//It is necessary to renew automatically and create background tasks
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var autoDelaytask = new Task(async() =>
{
While (!)
{
await (timeoutMilliseconds / 2);
await Delay(lockKey, lockValue, timeoutMilliseconds);
}
}, );
var result = (lockKey, autoDelaytask, cancellationTokenSource);
if (!result)
{
();
await UnLockAsync(cacheKey, lockValue);
return (false, );
}
}
return (flag, flag ? lockValue : );
}
The expiration time accuracy of Redis is about 1 second, and the expiration check is performed periodically (default 10 times per second). Select the interval of TTL/2:
Make sure to complete the renewal before Redis's next expiration check.
Redis-compatible master-slave synchronization delay (usually <1 second)
Distributed lock
Get the lock
string lockKey = "order:88888944010:lock";
string lockValue = ().ToString(); // Uniquely identify the lock holder
TimeSpan expiry = (10); // Lock automatically expires
// Try to acquire the lock (atomic operation)
bool lockAcquired = (lockKey, lockValue, expiry);
Release the lock
bool released = await ReleaseLockAsync(db, lockKey, lockValue);
Automatic renewal
It also needs to be implemented by yourself