Die optimistische Parallelitätskontrolle (Optimistic Concurrency Control, OCC) ist eine Strategie zur Verwaltung freigegebener Ressourcen und zur Vermeidung von „verlorenen Updates“ oder Race Conditions, wenn mehrere Nutzer oder Prozesse versuchen, dieselbe Ressource gleichzeitig zu ändern.
Ein Beispiel sind Systeme wie Google Cloud IAM, bei denen
die freigegebene Ressource eine IAM-Richtlinie ist, die auf eine Ressource
(z. B. ein Projekt, einen Bucket oder einen Dienst) angewendet wird. Zur Implementierung von OCC verwenden Systeme in der Regel eine Versionsnummer oder ein etag-Feld (Entity-Tag) für das Ressourcenobjekt.
Einführung in OCC
Stellen Sie sich vor, zwei Prozesse, A und B, versuchen, gleichzeitig eine freigegebene Ressource zu aktualisieren:
Prozess A liest den aktuellen Status der Ressource.
Prozess B liest denselben aktuellen Status.
Prozess A ändert seine Kopie und schreibt sie zurück auf den Server.
Prozess B ändert seine Kopie und schreibt sie zurück auf den Server.
Da Prozess B die Ressource übersriegt, ohne zu wissen, dass Prozess A sie bereits geändert hat, gehen die Updates von Prozess A verloren.
OCC löst dieses Problem, indem ein eindeutiger Fingerabdruck eingeführt wird, der sich jedes Mal ändert, wenn eine Entität geändert wird. In vielen Systemen (z. B. IAM) wird dies mit einem etag erreicht. Der Server prüft dieses Tag bei jedem Schreibvorgang:
Wenn Sie die Ressource lesen, gibt der Server ein
etag(einen eindeutigen Fingerabdruck) zurück.Wenn Sie die geänderte Ressource zurücksenden, müssen Sie das ursprüngliche
etagangeben.Wenn der Server feststellt, dass das gespeicherte
etagnicht mit dem von Ihnen gesendetenetagübereinstimmt (d. h., dass jemand anderes die Ressource geändert hat, seit Sie sie gelesen haben), schlägt der Schreibvorgang mit einemABORTED- oderFAILED_PRECONDITION-Fehler fehl.
Dieser Fehler zwingt den Client, den gesamten Prozess zu wiederholen – den neuen Status noch einmal zu lesen, die Änderungen noch einmal anzuwenden und den Schreibvorgang mit dem neuen etag noch einmal zu versuchen.
OCC-Schleife implementieren
Das Herzstück der OCC-Implementierung ist eine while-Schleife, die die Logik für die Wiederholungen verarbeitet. Legen Sie eine angemessene maximale Anzahl von Wiederholungen fest, um unendliche Schleifen bei hoher Auslastung zu vermeiden.
Schritte der Schleife
| Step | Aktion | Implementierungsbeispiel |
|---|---|---|
| Lesen | Rufen Sie den aktuellen Ressourcenstatus einschließlich des etag ab. |
Policy policy = client.getIamPolicy(resourceName); |
| Ändern | Wenden Sie die Änderungen auf das lokale Objekt an. | policy = policy.toBuilder().addBinding(newBinding).build(); |
| Schreiben/Prüfen | Versuchen Sie, die geänderte Ressource mit dem alten etag zu speichern. Diese Aktion muss sich in einem try-Block befinden. |
try { client.setIamPolicy(resourceName, policy); return policy; } catch (AbortedException e) { // retry loop } |
| Erfolg/Wiederholung | Wenn der Schreibvorgang erfolgreich ist, beenden Sie die Schleife. Wenn ein Parallelitätsfehler auftritt, erhöhen Sie den Zähler für die Wiederholungen und setzen Sie die Schleife fort (gehen Sie zurück zum Schritt „Lesen“). |
Die folgende Datei enthält ein ausführbares Beispiel für die Implementierung der OCC-Schleife mit einer IAM-Richtlinie für eine Projektressource als Ziel.
Installation
Fügen Sie die folgende Abhängigkeit zu Ihrer pom.xml-Datei hinzu, um dieses Beispiel zu verwenden:
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-resourcemanager</artifactId>
<version>1.45.0</version>
</dependency>
Beispiel
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;
}
}