Skip to content

命令锁

顾名思义,命令锁是为了解决命令并发执行的问题。在一些场景下,我们需要保证某个命令在同一时间只能被一个实例执行,这时候就可以使用命令锁。 本质上命令锁是一种分布式锁,它的实现方式有很多种,我们默认提供了基于Redis来实现的命令锁。

注册命令锁

在Program.cs注册CommandLocks

builder.Services.AddMediatR(cfg =>
        cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly())
            .AddCommandLockBehavior()  //注册命令锁行为
            .AddKnownExceptionValidationBehavior()
            .AddUnitOfWorkBehaviors());

builder.Services.AddCommandLocks(typeof(Program).Assembly); //注册所有的命令锁类型

注意: 命令锁应该在事务开启前执行,所以需要在AddKnownExceptionValidationBehavior之前添加AddCommandLockBehavior

使用命令锁

定义一个命令锁,实现ICommandLock<TCommand>接口,其中TCommand是命令类型,例如:

public record PayOrderCommand(OrderId Id) : ICommand<OrderId>;

public class PayOrderCommandLock : ICommandLock<PayOrderCommand>
{
    public Task<CommandLockSettings> GetLockKeysAsync(PayOrderCommand command,
        CancellationToken cancellationToken = default)
    {
        return Task.FromResult(command.Id.ToCommandLockSettings());
    }
}

其中command.Id.ToCommandLockSettings()是将OrderId转换为CommandLockSettingsCommandLockSettings是命令锁的配置,包含了锁的Key、获取锁之前可以等待的过期时间。

设计上,命令锁与命令是一对一关系,建议将命令锁与命令、命令处理器放在同一个类文件中,便于维护。

多key命令锁

命令锁支持多Key机制,即一个命令可以对应多个Key,例如:

public class PayOrderCommandLock : ICommandLock<PayOrderCommand>
{
    public Task<CommandLockSettings> GetLockKeysAsync(PayOrderCommand command,
        CancellationToken cancellationToken = default)
    {
        var ids = new List<OrderId> { new OrderId(1), new OrderId(2) };
        return Task.FromResult(ids.ToCommandLockSettings());
    }
}

在这个例子中,PayOrderCommand对应两个Key,分别是OrderId(1)OrderId(2)

当需要锁定多个Key时,CommandLockSettings会对多个Key进行排序,然后逐个锁定,如果其中一个Key锁定失败,则会释放已经锁定的Key。

可重入机制

命令锁实现了可重入机制,即在同一个请求上下文中,相同的Key可以重复获取锁,不会造成死锁。 例如上面示例的命令执行后序的事件处理过程中再次执行携带相同Key的命令锁,不会死锁。