樂觀並行控制 (OCC) 是一種策略,用於管理共用資源,並防止多位使用者或程序嘗試同時修改相同資源時,發生「更新遺失」或競爭狀況。
以 IAM 這類系統為例,共用資源是套用至資源 (例如專案、Bucket 或服務) 的 IAM 政策。 Google Cloud 如要實作 OCC,系統通常會在資源物件上使用版本號碼或 etag (實體標記) 欄位。
OCC 簡介
假設有兩個程序 A 和 B 同時嘗試更新共用資源:
程序 A 會讀取資源的目前狀態。
程序 B 會讀取相同的目前狀態。
程序 A 會修改副本,然後寫回伺服器。
程序 B 會修改副本,並寫回伺服器。
由於程序 B 會覆寫資源,但不知道程序 A 已變更資源,因此程序 A 的更新會遺失。
OCC 導入每次實體修改時都會變更的專屬指紋,解決了這個問題。在許多系統 (例如 IAM) 中,這項作業是使用 etag 完成。伺服器會在每次寫入時檢查這個標記:
讀取資源時,伺服器會傳回
etag(專屬指紋)。傳回修改後的資源時,請務必附上原始
etag。如果伺服器發現儲存的
etag與您傳送的etag不符 (表示您讀取資源後,有人修改了該資源),寫入作業就會失敗,並傳回ABORTED或FAILED_PRECONDITION錯誤。
這項失敗會強制用戶端重試整個程序,包括重新讀取新狀態、重新套用變更,以及使用新的 etag 再次嘗試寫入。
實作 OCC 迴圈
OCC 實作的核心是 while 迴圈,可處理重試邏輯。設定合理的重試次數上限,以防高爭用情況下出現無限迴圈。
迴圈步驟
| Step | 動作 | 導入範例 |
|---|---|---|
| 已讀 | 擷取目前的資源狀態,包括 etag。 |
Policy policy = client.getIamPolicy(resourceName); |
| 修改 | 將變更套用至本機物件。 | policy = policy.toBuilder().addBinding(newBinding).build(); |
| 撰寫/檢查 | 嘗試使用舊版 etag 儲存修改後的資源。這項動作必須位於 try 區塊中。 |
try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop } |
| 成功/重試 | 如果寫入成功,請結束迴圈。如果因並行錯誤而失敗,請遞增重試計數器並繼續迴圈 (返回「讀取」步驟)。 |
下列檔案提供可執行的範例,說明如何以專案資源的 IAM 政策為目標,實作 OCC 迴圈。
安裝
如要使用這個範例,請在 pom.xml 中加入下列依附元件:
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-resourcemanager</artifactId>
<version>1.45.0</version>
</dependency>
範例
import com.google.api.gax.rpc.AbortedException;
import com.google.api.gax.rpc.FailedPreconditionException;
import com.google.cloud.resourcemanager.v3.ProjectName;
import com.google.cloud.resourcemanager.v3.ProjectsClient;
import com.google.iam.v1.Binding;
import com.google.iam.v1.GetIamPolicyRequest;
import com.google.iam.v1.Policy;
import com.google.iam.v1.SetIamPolicyRequest;
import java.util.ArrayList;
import java.util.List;
public class IamOccExample {
/**
* Executes an Optimistic Concurrency Control (OCC) loop to safely update a resource.
*
* This method demonstrates the core Read-Modify-Write-Retry pattern.
*
* @param projectId The Google Cloud Project ID (e.g., "my-project-123").
* @param role The IAM role to grant (e.g., "roles/storage.objectAdmin").
* @param member The member to add (e.g., "user:user@example.com").
* @param maxRetries The maximum number of times to retry the update.
* @return The successfully updated IAM policy (or null on failure).
*/
public static Policy updateIamPolicyWithOcc(
String projectId,
String role,
String member,
int maxRetries
) throws Exception {
// Setup Client
try (ProjectsClient projectsClient = ProjectsClient.create()) {
String projectName = ProjectName.of(projectId).toString();
int retries = 0;
// START OCC LOOP (Read-Modify-Write-Retry)
while (retries < maxRetries) {
try {
// READ: Get the current policy. This includes the current etag.
System.out.printf("Attempt %d: Reading current IAM policy for %s...%n", retries, projectName);
GetIamPolicyRequest getIamPolicyRequest = GetIamPolicyRequest.newBuilder()
.setResource(projectName)
.build();
Policy policy = projectsClient.getIamPolicy(getIamPolicyRequest);
// MODIFY: Apply the desired changes to the local Policy object.
List<Binding> bindings = new ArrayList<>(policy.getBindingsList());
Binding targetBinding = null;
int bindingIndex = -1;
for (int i = 0; i < bindings.size(); i++) {
if (bindings.get(i).getRole().equals(role)) {
targetBinding = bindings.get(i);
bindingIndex = i;
break;
}
}
if (targetBinding != null) {
if (targetBinding.getMembersList().contains(member)) {
System.out.printf("Policy for role %s and member %s exists already!%n", role, member);
return policy;
}
// Create a new binding based on existing one to add the member
Binding updatedBinding = targetBinding.toBuilder()
.addMembers(member)
.build();
bindings.set(bindingIndex, updatedBinding);
} else {
// Role not found, create a new binding
Binding newBinding = Binding.newBuilder()
.setRole(role)
.addMembers(member)
.build();
bindings.add(newBinding);
}
// The policy builder now contains the modified bindings AND the original etag.
Policy updatedPolicy = policy.toBuilder()
.clearBindings()
.addAllBindings(bindings)
.build();
// WRITE/CHECK: Attempt to write the modified policy.
System.out.printf("Attempt %d: Setting modified IAM policy...%n", retries);
SetIamPolicyRequest setIamPolicyRequest = SetIamPolicyRequest.newBuilder()
.setResource(projectName)
.setPolicy(updatedPolicy)
.build();
Policy resultPolicy = projectsClient.setIamPolicy(setIamPolicyRequest);
// SUCCESS: If the call succeeds, return the new policy and exit the loop.
System.out.printf("Successfully updated IAM policy in attempt %d.%n", retries);
return resultPolicy;
} catch (AbortedException | FailedPreconditionException e) {
// If the etag is stale (concurrency conflict), this will throw a retryable exception.
retries++;
System.out.printf("Concurrency conflict detected (etag mismatch). Retrying... (%d/%d)%n",
retries, maxRetries);
// Exponential backoff (100ms * retry count)
Thread.sleep(100L * retries);
}
}
// END OCC LOOP
}
System.out.printf("Failed to update IAM policy after %d attempts due to persistent concurrency conflicts.%n", maxRetries);
return null;
}
}