preamble
existWeb
application development process.Anti-Shake (Debounce)
It is an effective means to ensure that the same operation will not be triggered repeatedly within a short period of time. Common scenarios include preventing users from repeatedly submitting forms within a short period of time, or avoiding multiple button clicks that cause a background service to perform the same action multiple times. There are scenarios that require its use, both in a standalone environment and in a distributed system. In this article, we will describe how the Core
The dithering is achieved by using locks to ensure that duplicate operations are effectively avoided regardless of whether they are deployed in single or multi-instance deployments.
Distributed Lock Interface Definition
The first step to implementing distributed locks is to define a common lock interface. This is accomplished through theIDistributedLock
interface, applications can choose to use different types of locks for different scenarios.
public interface IDistributedLock
{
/// <summary>
/// Attempts to acquire a distributed lock.
/// </summary>
/// <param name="resourceKey"> Identifier of the resource to lock. </param>
/// <param name="lockDuration"> The duration of the lock. </param>
/// <returns> Whether the lock was successfully acquired. </returns>
Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null);
/// <summary>
/// Release a distributed lock.
/// </summary>
/// <param name="resourceKey"> /// Identifier of the resource to release. </param>
Task ReleaseLockAsync(string resourceKey);;
}
This interface defines two core methods:
-
TryAcquireLockAsync
: Attempts to acquire a distributed lock. If the lock is acquired successfully, returntrue
Otherwise, returnfalse
。 -
ReleaseLockAsync
: Release an acquired lock to allow other operations to enter the critical zone.
Redis version of distributed locking implementation
In the programs developed on a daily basis, theRedis
is a common distributed locking implementation. It is implemented by means of theRedis
atomic operation in conjunction withSETNX
directive, which ensures that only one instance in a multiple-instance environment can acquire the lock. The followingRedis
version of the distributed lock implementation code.
public class RedisDistributedLock : IDistributedLock
{
private readonly ConnectionMultiplexer _redisConnection;
private IDatabase _database;
public RedisDistributedLock(ConnectionMultiplexer redisConnection)
{
_redisConnection = redisConnection;
_database = _redisConnection.GetDatabase();
}
public Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null)
{
var isLockAcquired = _database.StringSetAsync(resourceKey, 1, lockDuration, );
return isLockAcquired;
}
public Task ReleaseLockAsync(string resourceKey)
{
return _database.KeyDeleteAsync(resourceKey);
}
}
In this implementation the use of(used form a nominal expression)
SDK
, of course, you can choose your own suitable library to implement, mainly for the convenience of the demonstration, because the other libraries need to use the script to implement their own expiration of the expiration of theSETNX
:
- We use the
ConnectionMultiplexer
to manage connections to Redis. -
TryAcquireLockAsync
method uses theStringSetAsync
method, where theparameter ensures that the value can only be set successfully if the key does not exist, thus enabling the lock function.
-
ReleaseLockAsync
method simply removes the key corresponding to the lock, thereby releasing the lock.
If you go with another Redis SDK, you will generally need to write scripts to enable expiration of theSETNX
, you can refer to the followingLUA
scripts
-- Parameters: KEYS[1] for key, ARGV[1] for value, ARGV[2] for expiration time (sec)
if ("SETNX", KEYS[1], ARGV[1]) == 1 then
("EXPIRE", KEYS[1], ARGV[2])
return 1
return 1
return 0
return 0 else
- utilization
SETNX
Try the setup keyKEYS[1]
The value of theARGV[1]
If the key does not exist, it returns 1 and sets the key successfully. Returns 1 if the key does not exist and successfully sets the key, or 0 if the key already exists. - in the event that
SETNX
Returns 1, the expiration time is set for the key, and the expiration time isARGV[2]
Seconds. - The final script returns 1 to indicate that the key-value pair was successfully set and the expiration time was set, and 0 to indicate that the key already exists and the operation was unsuccessful.
Local Lock Implementation
In some cases, such as in standalone or monolithic applications, it may be more appropriate to use local locks. It may be better to use a memory-based local locking implementation at this time. Some students may be concerned about the volume of requests, resulting in high memory usage. In fact, consider it from another perspective, if there is a large number of requests or concurrency, most of us may not directly use a single machine. Well let's continue to look at, here we are for convenience, directly using theConcurrentDictionary
to realize.
public class LocalLock : IDistributedLock
{
private readonly ConcurrentDictionary<string, byte> lockCounts = new ConcurrentDictionary<string, byte>();
public Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null)
{
byte lockCount = 0;
if ((resourceKey, lockCount))
{
lockCounts[resourceKey] = 1;
return (true);
}
return (false);
}
public Task ReleaseLockAsync(string resourceKey)
{
(resourceKey, out _);
return ;
}
}
In this realization:
- We use
ConcurrentDictionary
to manage the state of the lock and ensure thread safety. -
TryAcquireLockAsync
method tries to add a key to the dictionary, and if it succeeds it indicates a successful lock acquisition. -
ReleaseLockAsync
method removes the corresponding key from the dictionary, thereby releasing the lock.
In fact, if
C#
furnishConcurrentHashSet
words, withConcurrentHashSet
to realize it would be a little better. After all.ConcurrentDictionary
is the way KV is to be realized, and eachValue
Both will waste some memory space. Of course you can also choose to implement your own set ofConcurrentHashSet
, it is important to note that implementations try to use thekeg lock
Avoid usingglobal lock
。
Implementation of anti-shake filters
Next we use the above definedIDistributedLock
cap (a poem)Filter
To implement an anti-shake filter, we create a filter based on theIAsyncActionFilter
interface implements filters that make it easier to acquire and release lock operations before and after request execution.
public class DistributedLockFilterAttribute : Attribute, IAsyncActionFilter
{
private readonly string _lockPrefix;
private readonly LockType _lockType;
public DistributedLockFilterAttribute(string keyPrefix, LockType lockType = )
{
_lockPrefix = keyPrefix;
_lockType = lockType;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
IDistributedLock distributedLock = <IDistributedLock>(_lockType.GetDescription());
string controllerName = ["controller"]?.ToString() ?? "";
string actionName = ["action"]?.ToString() ?? "";
//User information or other unique identifiers can
var userKey = !.Identity!.Name;
string lockKey = $"{_lockPrefix}:{userKey}:{controllerName}_{actionName}";
bool isLockAcquired = await (lockKey);
if (!isLockAcquired)
{
= new ObjectResult(new { code = 400, message = "Please do not repeat the operation" });
return;
}
try
{
await next();
}
finally
{
await (lockKey);
}
}
}
in the operation of this filter:
- We pass the container and the
LockType
Get a specific distributed lock implementation. - utilization
controllerName
cap (a poem)actionName
and the user identifier construct (or other unique identifier) builds the key of the lock, ensuring that the lock is unique. - If acquiring a lock fails, an error response is returned directly, avoiding the execution of subsequent operations.
- Releases the lock after the operation has been performed, regardless of whether it was successful or not.
For more flexibility in switching between different locking implementations, we define an enumerationLockType
The method is extended by extending theGetDescription
Get its description to make it easy for us to use its value.
public enum LockType
{
[Description("redis")]
Redis,
[Description("local")]
Local
}
public static class EnumExtensions
{
public static string GetDescription(this Enum @enum)
{
Type type = @();
string name = (type, @enum);
if (name == null)
{
return null;
}
FieldInfo field = (name);
DescriptionAttribute attribute = (field, typeof(DescriptionAttribute)) as DescriptionAttribute;
if (attribute == null)
{
return name;
}
return attribute?.Description;
}
}
This extension method makes it easier to get the corresponding enumeration description based on the type of the enumeration, so that you can flexibly choose different lock implementations in dependency injection, and if there is a better way to implement it that's fine too, we'll try to use the way that's easier to understand.
Registering and using filters
exist Core
The following is sample code for registering and using distributed locks.
<ConnectionMultiplexer>(_ => (["Redis:ConnectionString"]!));
//do sth (for sb)IDistributedLockAdding different implementations
<IDistributedLock, RedisDistributedLock>(());
<IDistributedLock, LocalLock>(());
Here, we register two distributed lock implementations, Redis and local, and use the key (key
) to distinguish between them so that specific lock types can be selected as needed at runtime.
Next, apply to the controller's action method our definedDistributedLockFilter
Filter to implement theAction
The anti-shake function of the
[HttpGet("GetCurrentTime")]
[DistributedLockFilter("GetCurrentTime", )]
public async Task<string> GetCurrentTime()
{
await (10000); // Simulation of prolonged operation
return ("yyyy-MM-dd HH:mm:ss");
}
In this simple example:
-
DistributedLockFilter
The filter ensures that when a user requestsGetCurrentTime
operation, the same operation will not be triggered repeatedly within a short period of time. - The lock type is set to
, so this lock can also be shared between multiple instances in a distributed environment, but of course this type is optional.
If the request is made more than once in a row within 10s the following error is returned
{
"code": 400,
"message": "Please do not repeat the operation."
}
summarize
This article details how to use distributed locks to implement anti-shaking in Core. This is accomplished by defining a genericIDistributedLock
interface, we can implement different types of locking mechanisms, including Redis and local memory locks.Redis locks utilize their atomic operations to ensure uniqueness in a distributed environment, while local locks are suitable for a standalone environment. Local locks are suitable for standalone environments.DistributedLockFilter
filter, we integrate the locking mechanism into the Core
In the controller, preventing the need forAction
Perform a repeat operation.
This method not only improves the stability of the application, but also enhances the user experience and avoids the problem of repeated operations in a short period of time. I hope this article is helpful. If there are any questions or needs for further discussion, please feel free to leave a message in the comment section.