Location>code7788 >text

Implementing Anti-Shake in Core with Distributed Locks

Popularity:995 ℃/2024-09-04 08:33:15

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 CoreThe 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, returntrueOtherwise, 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 withSETNXdirective, 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 theConnectionMultiplexer to manage connections to Redis.
  • TryAcquireLockAsync method uses theStringSetAsync method, where the parameter 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 followingLUAscripts

-- 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
  • utilizationSETNX 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 thatSETNX 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 theConcurrentDictionaryto 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 useConcurrentDictionary 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, ifC#furnishConcurrentHashSetwords, withConcurrentHashSetto realize it would be a little better. After all.ConcurrentDictionaryis the way KV is to be realized, and eachValueBoth 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 lockAvoid usingglobal lock

Implementation of anti-shake filters

Next we use the above definedIDistributedLockcap (a poem)FilterTo 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 theLockTypeGet a specific distributed lock implementation.
  • utilizationcontrollerName 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 enumerationLockTypeThe 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 CoreThe 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 theActionThe 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 forActionPerform 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.

👇 Feel free to scan the code and follow my public number 👇.