比较 Exchange 同步多个预留(唯一约束)

Compare Exchange sync multiple reservations (UNIQUE-constraints)

当我阅读 Compare Exhange for RavenDB 时,我在文档中发现了以下用于保留电子邮件的用户案例。基本上是一种强制执行唯一约束的方法。如果您只想对一个 属性 强制执行此约束,但如果您引入多个属性(电子邮件和用户名),它将不再按预期工作。

参见文档:Link

class Program
{
    public class User
    {
        public string Id { get; set; }

        public string Name { get; set; }

        public string Email { get; set; }
    }

    static void Main(string[] args)
    {
        var store = new DocumentStore()
        {
            Urls = new[] {
                "http://127.0.0.1:8080/"
            }, 
            Database = "example",
        }.Initialize();

        string name = "admin";
        string email = "admin@example.com";

        var user = new User
        {
            Name = name,
            Email = email
        };

        using (IDocumentSession session = store.OpenSession())
        {
            session.Store(user);

            // Try to reserve a new user email 
            // Note: This operation takes place outside of the session transaction, 
            //       It is a cluster-wide reservation
            CompareExchangeResult<string> namePutResult
                = store.Operations.Send(
                    new PutCompareExchangeValueOperation<string>("names/" + name, user.Id, 0));


            if (namePutResult.Successful == false)
            {
                throw new Exception("Name is already in use");
            }
            else
            {
                // Try to reserve a new user email 
                // Note: This operation takes place outside of the session transaction, 
                //       It is a cluster-wide reservation
                CompareExchangeResult<string> emailPutResult
                    = store.Operations.Send(
                        new PutCompareExchangeValueOperation<string>("emails/" + email, user.Id, 0));

                // Unlock name again (Because if we dont the name wil be locked)
                if (emailPutResult.Successful == false)
                {
                    // First, get existing value
                    CompareExchangeValue<string> readResult =
                        store.Operations.Send(
                            new GetCompareExchangeValueOperation<string>("names/" + name));

                    // Delete the key - use the index received from the 'Get' operation
                    CompareExchangeResult<string> deleteResult
                        = store.Operations.Send(
                            new DeleteCompareExchangeValueOperation<string>("names/" + name, readResult.Index));

                    // The delete result is successful only if the index has not changed between the read and delete operations
                    if (deleteResult.Successful == false)
                    {
                        throw new Exception("The name is forever lost");
                    }
                    else
                    {
                        throw new Exception("Email is already in use");
                    }
                }
            }

            // At this point we managed to reserve/save both the user name and email
            // The document can be saved in SaveChanges
            session.SaveChanges();
        }
    }
}

在上面的示例中,您可以看到为什么这不再按预期工作。因为现在如果电子邮件 Compare Exchange 失败或已被采用,则名称 Compare Exchange 不能为 reversed/removed,因为删除 Compare Exchange 理论上可能会失败。现在因为这个有一个变化,用户名将被永久锁定并且不能再次使用。当您尝试更新用户名时也会发生同样的问题,因为一旦新用户名被保留,您将不得不 unlock/remove 比较旧用户名的交换。

对于这样的事情,最好的方法是什么?这种情况发生了什么变化?

如果您处于 namePutResult.Successful 上下文中,那么您肯定知道 namePutResult.Index 是用于创建 CompareExchange 的唯一索引,因此如果电子邮件被盗用,您可以直接使用namePutResult.Index 删除 CompareExchange,如果失败可以处理异常(重新发送 DeleteCompareExchangeValueOperation`)。

        using (IDocumentSession session = store.OpenSession())
        {
            session.Store(user);

            // Try to reserve a new user email 
            // Note: This operation takes place outside of the session transaction, 
            //       It is a cluster-wide reservation
            CompareExchangeResult<string> namePutResult
                = store.Operations.Send(
                    new PutCompareExchangeValueOperation<string>("names/" + name, user.Id, 0));


            if (namePutResult.Successful == false)
            {
                throw new Exception("Name is already in use");
            }
            else
            {
                // Try to reserve a new user email 
                // Note: This operation takes place outside of the session transaction, 
                //       It is a cluster-wide reservation
                CompareExchangeResult<string> emailPutResult
                    = store.Operations.Send(
                        new PutCompareExchangeValueOperation<string>("emails/" + email, user.Id, 0));

                // Unlock name again (Because if we dont the name wil be locked)
                if (emailPutResult.Successful == false)
                {
                    // Delete the key - use the index of PUT operation
                    
                    // TODO: handle failure of this command
                    CompareExchangeResult<string> deleteResult
                        = store.Operations.Send(
                            new DeleteCompareExchangeValueOperation<string>("names/" + name, namePutResult.Index));

                    if (deleteResult.Successful == false)
                    {
                        throw new Exception("The name is forever lost");
                    }
                    else
                    {
                        throw new Exception("Email is already in use");
                    }
                }
            }

            // At this point we managed to reserve/save both the user name and email
            // The document can be saved in SaveChanges
            session.SaveChanges();
        }

您是否尝试过使用集群范围的事务会话在单个事务中存储两个比较交换值?

https://ravendb.net/docs/article-page/5.1/Csharp/client-api/session/cluster-transaction#create-compare-exchange