管理索引和数据源等 Azure AI 搜索资源时,务必安全地更新资源,尤其是应用程序的不同组件并发访问资源的情况下。 当两个客户端在没有协调的情况下并发更新资源时,可能出现争用条件。 为防止此情况,Azure AI 搜索使用乐观并发模型。 对资源没有任何锁定。 而每个资源均带有一个用于标识资源版本的 ETag,方便你表述那些避免意外覆盖的请求。
乐观并发通过写入索引、索引器、数据源、技能组和 synonymMap 资源的 API 调用中的访问条件检查来实现。
所有资源都有一个实体标记 (ETag),
此标记提供对象版本信息。 通过先检查 ETag,确保资源的 ETag 与本地副本匹配,可避免典型工作流(获取、本地修改、更新)中的并发更新。
REST API 在请求头使用 ETag。
用于 .Net 的 Azure SDK 通过 accessCondition 对象,对资源设置 If-Match | If-Match-None 标头 来设置 ETag。 使用 ETag(如 SynonymMap.ETag 和 SearchIndex.ETag)的对象具有一个 accessCondition 对象。
每次更新资源时,其 ETag 将自动更改。 实现并发管理时,只需对更新请求设置一个前提条件,要求远程资源的 ETag 与在客户端上修改的资源副本的 ETag 相同。 如果另一个进程会更改远程资源,则 ETag 与前置条件不匹配,并且请求失败并出现 HTTP 412。 如果使用 .NET SDK,则此失败表现为异常,此时 IsAccessConditionFailed()
扩展方法返回 true。
备注
只存在一个并发机制。 无论对资源更新使用哪个 API 或 SDK,都始终使用该机制。
以下代码演示更新操作的乐观并发。 第二次更新失败的原因是对象的 ETag 已被前一次更新更改。 更具体地说,当请求标头中的 ETag 不再与对象的 ETag 匹配时,搜索服务将返回状态代码 400(错误请求),并且更新失败。
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using System;
using System.Net;
using System.Threading.Tasks;
namespace AzureSearch.SDKHowTo
{
class Program
{
// This sample shows how ETags work by performing conditional updates and deletes
// on an Azure Search index.
static void Main(string[] args)
{
string serviceName = "PLACEHOLDER FOR YOUR SEARCH SERVICE NAME";
string apiKey = "PLACEHOLDER FOR YOUR SEARCH SERVICE ADMIN API KEY";
// Create a SearchIndexClient to send create/delete index commands
Uri serviceEndpoint = new Uri($"https://{serviceName}.search.azure.cn/");
AzureKeyCredential credential = new AzureKeyCredential(apiKey);
SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);
// Delete index if it exists
Console.WriteLine("Check for index and delete if it already exists...\n");
DeleteTestIndexIfExists(adminClient);
// Every top-level resource in Azure 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.
SearchIndex index = DefineTestIndex();
Console.WriteLine(
$"Test searchIndex hasn't been created yet, so its ETag should be blank. ETag: '{index.ETag}'");
// Once the resource exists in Azure Search, its ETag is populated. Make sure to use the object
// returned by the SearchIndexClient. Otherwise, you will still have the old object with the
// blank ETag.
Console.WriteLine("Creating index...\n");
index = adminClient.CreateIndex(index);
Console.WriteLine($"Test index created; Its ETag should be populated. ETag: '{index.ETag}'");
// ETags prevent 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.
SearchIndex indexForClientA = index;
SearchIndex indexForClientB = adminClient.GetIndex("test-idx");
Console.WriteLine("Simulating concurrent update. To start, clients A and B see the same ETag.");
Console.WriteLine($"ClientA ETag: '{indexForClientA.ETag}' ClientB ETag: '{indexForClientB.ETag}'");
// indexForClientA successfully updates the index.
indexForClientA.Fields.Add(new SearchField("a", SearchFieldDataType.Int32));
indexForClientA = adminClient.CreateOrUpdateIndex(indexForClientA);
Console.WriteLine($"Client A updates test-idx by adding a new field. The new ETag for test-idx is: '{indexForClientA.ETag}'");
// indexForClientB tries to update the index, but fails due to the ETag check.
try
{
indexForClientB.Fields.Add(new SearchField("b", SearchFieldDataType.Boolean));
adminClient.CreateOrUpdateIndex(indexForClientB);
Console.WriteLine("Whoops; This shouldn't happen");
Environment.Exit(1);
}
catch (RequestFailedException e) when (e.Status == 400)
{
Console.WriteLine("Client B failed to update the index, as expected.");
}
// Uncomment the next line to remove test-idx
//adminClient.DeleteIndex("test-idx");
Console.WriteLine("Complete. Press any key to end application...\n");
Console.ReadKey();
}
private static void DeleteTestIndexIfExists(SearchIndexClient adminClient)
{
try
{
if (adminClient.GetIndex("test-idx") != null)
{
adminClient.DeleteIndex("test-idx");
}
}
catch (RequestFailedException e) when (e.Status == 404)
{
//if an exception occurred and status is "Not Found", this is working as expected
Console.WriteLine("Failed to find index and this is because it's not there.");
}
}
private static SearchIndex DefineTestIndex() =>
new SearchIndex("test-idx", new[] { new SearchField("id", SearchFieldDataType.String) { IsKey = true } });
}
}
用于实现乐观并发的设计模式应包含一个循环用于重试访问条件检查,包含一个访问条件测试,并在尝试重新应用更改前选择性地检索更新后的资源。
此代码片段演示如何向已有的索引添加 synonymMap。
代码片段获取“hotels”索引,检查更新操作上的对象版本,在条件失败时引发异常,然后重试该操作(最多三次),从服务器开始索引检索以获取最新版本。
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 (Exception 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;
}
- ETag 结构
- Common HTTP request and response headers(常见 HTTP 请求和响应标头)
- HTTP 状态代码