如何管理 Azure 认知搜索中的并发How to manage concurrency in Azure Cognitive Search

管理索引和数据源等 Azure 认知搜索资源时,务必安全地更新资源,尤其是应用程序的不同组件并发访问资源的情况下。When managing Azure Cognitive Search resources such as indexes and data sources, it's important to update resources safely, especially if resources are accessed concurrently by different components of your application. 当两个客户端在没有协调的情况下并发更新资源时,可能出现争用条件。When two clients concurrently update a resource without coordination, race conditions are possible. 为防止此情况,Azure 认知搜索提供“乐观并发模型”。 To prevent this, Azure Cognitive Search offers an optimistic concurrency model. 对资源没有任何锁定。There are no locks on a resource. 相反,每个资源均带有一个 ETag 用于标识资源版本,便于创建避免意外覆盖的请求。Instead, there is an ETag for every resource that identifies the resource version so that you can craft requests that avoid accidental overwrites.

提示

示例 C# 解决方案中的概念代码阐释了并发控制如何在 Azure 认知搜索中工作。Conceptual code in a sample C# solution explains how concurrency control works in Azure Cognitive Search. 该代码会创建调用并发控制的条件。The code creates conditions that invoke concurrency control. 对大多数开发人员而言,读取以下代码片段可能就已足够,但若想运行它,请编辑 appsettings.json,以添加服务名称和管理员 API 密钥。Reading the code fragment below is probably sufficient for most developers, but if you want to run it, edit appsettings.json to add the service name and an admin api-key. 假设服务 URL 为 http://myservice.search.azure.cn,服务名称是 myserviceGiven a service URL of http://myservice.search.azure.cn, the service name is myservice.

工作原理How it works

乐观并发通过写入索引、索引器、数据源和 synonymMap 资源的 API 调用中的访问条件检查实现。Optimistic concurrency is implemented through access condition checks in API calls writing to indexes, indexers, datasources, and synonymMap resources.

所有资源均有一个实体标记 (ETag),它提供对象版本信息。 All resources have an entity tag (ETag) that provides object version information. 通过先检查 ETag,确保资源的 ETag 与本地副本匹配,可避免典型工作流(获取、本地修改、更新)中的并发更新。By checking the ETag first, you can avoid concurrent updates in a typical workflow (get, modify locally, update) by ensuring the resource's ETag matches your local copy.

每次更新资源时,其 ETag 将自动更改。Every time you update a resource, its ETag changes automatically. 实现并发管理时,只需对更新请求设置一个前提条件,要求远程资源的 ETag 与在客户端上修改的资源副本的 ETag 相同。When you implement concurrency management, all you're doing is putting a precondition on the update request that requires the remote resource to have the same ETag as the copy of the resource that you modified on the client. 如果并发进程已更改远程资源,ETag 将不满足前提条件,请求将失败并出现 HTTP 412。If a concurrent process has changed the remote resource already, the ETag will not match the precondition and the request will fail with HTTP 412. 如果使用 .NET SDK,这表示为 CloudException,此时 IsAccessConditionFailed() 扩展方法返回 true。If you're using the .NET SDK, this manifests as a CloudException where the IsAccessConditionFailed() extension method returns true.

备注

只存在一个并发机制。There is only one mechanism for concurrency. 无论对资源更新使用哪个 API,都始终使用该机制。It's always used regardless of which API is used for resource updates.

用例和示例代码Use cases and sample code

以下代码演示对密钥更新操作的 accessCondition 检查:The following code demonstrates accessCondition checks for key update operations:

  • 如果资源不复存在,则更新将失败Fail an update if the resource no longer exists
  • 如果资源版本更改,更新将失败Fail an update if the resource version changes

DotNetETagsExplainer 程序中的示例代码Sample code from DotNetETagsExplainer program

    class Program
    {
        // This sample shows how ETags work by performing conditional updates and deletes
        // on an Azure Cognitive Search index.
        static void Main(string[] args)
        {
            IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
            IConfigurationRoot configuration = builder.Build();

            SearchServiceClient serviceClient = CreateSearchServiceClient(configuration);

            Console.WriteLine("Deleting index...\n");
            DeleteTestIndexIfExists(serviceClient);

            // Every top-level resource in Azure Cognitive Search has an associated ETag that keeps track of which version
            // of the resource you're working on. When you first create a resource such as an index, its ETag is
            // empty.
            Index index = DefineTestIndex();
            Console.WriteLine(
                $"Test index hasn't been created yet, so its ETag should be blank. ETag: '{index.ETag}'");

            // Once the resource exists in Azure Cognitive Search, its ETag will be populated. Make sure to use the object
            // returned by the SearchServiceClient! Otherwise, you will still have the old object with the
            // blank ETag.
            Console.WriteLine("Creating index...\n");
            index = serviceClient.Indexes.Create(index);
            
            Console.WriteLine($"Test index created; Its ETag should be populated. ETag: '{index.ETag}'");

            // ETags let you do some useful things you couldn't do otherwise. For example, by using an If-Match
            // condition, we can update an index using CreateOrUpdate and be guaranteed that the update will only
            // succeed if the index already exists.
            index.Fields.Add(new Field("name", AnalyzerName.EnMicrosoft));
            index =
                serviceClient.Indexes.CreateOrUpdate(
                    index,
                    accessCondition: AccessCondition.GenerateIfExistsCondition());

            Console.WriteLine(
                $"Test index updated; Its ETag should have changed since it was created. ETag: '{index.ETag}'");

            // More importantly, ETags protect you from concurrent updates to the same resource. If another
            // client tries to update the resource, it will fail as long as all clients are using the right
            // access conditions.
            Index indexForClient1 = index;
            Index indexForClient2 = serviceClient.Indexes.Get("test");

            Console.WriteLine("Simulating concurrent update. To start, both clients see the same ETag.");
            Console.WriteLine($"Client 1 ETag: '{indexForClient1.ETag}' Client 2 ETag: '{indexForClient2.ETag}'");

            // Client 1 successfully updates the index.
            indexForClient1.Fields.Add(new Field("a", DataType.Int32));
            indexForClient1 =
                serviceClient.Indexes.CreateOrUpdate(
                    indexForClient1,
                    accessCondition: AccessCondition.IfNotChanged(indexForClient1));

            Console.WriteLine($"Test index updated by client 1; ETag: '{indexForClient1.ETag}'");

            // Client 2 tries to update the index, but fails, thanks to the ETag check.
            try
            {
                indexForClient2.Fields.Add(new Field("b", DataType.Boolean));
                serviceClient.Indexes.CreateOrUpdate(
                    indexForClient2,
                    accessCondition: AccessCondition.IfNotChanged(indexForClient2));

                Console.WriteLine("Whoops; This shouldn't happen");
                Environment.Exit(1);
            }
            catch (CloudException e) when (e.IsAccessConditionFailed())
            {
                Console.WriteLine("Client 2 failed to update the index, as expected.");
            }

            // You can also use access conditions with Delete operations. For example, you can implement an
            // atomic version of the DeleteTestIndexIfExists method from this sample like this:
            Console.WriteLine("Deleting index...\n");
            serviceClient.Indexes.Delete("test", accessCondition: AccessCondition.GenerateIfExistsCondition());

            // This is slightly better than using the Exists method since it makes only one round trip to
            // Azure Cognitive Search instead of potentially two. It also avoids an extra Delete request in cases where
            // the resource is deleted concurrently, but this doesn't matter much since resource deletion in
            // Azure Cognitive Search is idempotent.

            // And we're done! Bye!
            Console.WriteLine("Complete.  Press any key to end application...\n");
            Console.ReadKey();
        }

        private static SearchServiceClient CreateSearchServiceClient(IConfigurationRoot configuration)
        {
            string searchServiceName = configuration["SearchServiceName"];
            string adminApiKey = configuration["SearchServiceAdminApiKey"];

            SearchServiceClient serviceClient =
                new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));
            serviceClient.SearchDnsSuffix = "search.azure.cn";
            return serviceClient;
        }

        private static void DeleteTestIndexIfExists(SearchServiceClient serviceClient)
        {
            if (serviceClient.Indexes.Exists("test"))
            {
                serviceClient.Indexes.Delete("test");
            }
        }

        private static Index DefineTestIndex() =>
            new Index()
            {
                Name = "test",
                Fields = new[] { new Field("id", DataType.String) { IsKey = true } }
            };
    }
}

设计模式Design pattern

用于实现乐观并发的设计模式应包含一个循环用于重试访问条件检查,包含一个访问条件测试,并在尝试重新应用更改前选择性地检索更新后的资源。A design pattern for implementing optimistic concurrency should include a loop that retries the access condition check, a test for the access condition, and optionally retrieves an updated resource before attempting to re-apply the changes.

此代码片段演示如何向已有的索引添加 synonymMap。This code snippet illustrates the addition of a synonymMap to an index that already exists. 此代码来自 Azure 认知搜索的同义词 C# 示例This code is from the Synonym C# example for Azure Cognitive Search.

代码片段获取“hotels”索引,检查更新操作上的对象版本,在条件失败时引发异常,然后重试该操作(最多三次),从服务器开始索引检索以获取最新版本。The snippet gets the "hotels" index, checks the object version on an update operation, throws an exception if the condition fails, and then retries the operation (up to three times), starting with index retrieval from the server to get the latest version.

        private static void EnableSynonymsInHotelsIndexSafely(SearchServiceClient serviceClient)
        {
            int MaxNumTries = 3;

            for (int i = 0; i < MaxNumTries; ++i)
            {
                try
                {
                    Index index = serviceClient.Indexes.Get("hotels");
                    index = AddSynonymMapsToFields(index);

                    // The IfNotChanged condition ensures that the index is updated only if the ETags match.
                    serviceClient.Indexes.CreateOrUpdate(index, accessCondition: AccessCondition.IfNotChanged(index));

                    Console.WriteLine("Updated the index successfully.\n");
                    break;
                }
                catch (CloudException e) when (e.IsAccessConditionFailed())
                {
                    Console.WriteLine($"Index update failed : {e.Message}. Attempt({i}/{MaxNumTries}).\n");
                }
            }
        }
        
        private static Index AddSynonymMapsToFields(Index index)
        {
            index.Fields.First(f => f.Name == "category").SynonymMaps = new[] { "desc-synonymmap" };
            index.Fields.First(f => f.Name == "tags").SynonymMaps = new[] { "desc-synonymmap" };
            return index;
        }

后续步骤Next steps

有关如何安全更新现有索引的更多上下文,请查看同义词 C# 示例Review the synonyms C# sample for more context on how to safely update an existing index.

尝试修改以下任一示例,将 ETag 或 AccessCondition 对象包含在内。Try modifying either of the following samples to include ETags or AccessCondition objects.

另请参阅See also

常见的 HTTP 请求和响应标头 HTTP 状态代码 索引操作 (REST API)Common HTTP request and response headers HTTP status codes Index operations (REST API)