Skip to content

Commit 7d752d6

Browse files
authored
feat: support read lock mode for R/W transactions (#4010)
Samples and integration tests will be added in separate PRs.
1 parent 6886eb5 commit 7d752d6

File tree

6 files changed

+408
-57
lines changed

6 files changed

+408
-57
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.cloud.spanner.Statement.StatementFactory;
2525
import com.google.spanner.v1.BatchWriteResponse;
2626
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
27+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
2728

2829
/**
2930
* Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An
@@ -417,6 +418,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
417418
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
418419
* {@link CommitResponse}.
419420
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
421+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
420422
* </ul>
421423
*/
422424
TransactionRunner readWriteTransaction(TransactionOption... options);
@@ -458,6 +460,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
458460
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
459461
* {@link CommitResponse}.
460462
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
463+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
461464
* </ul>
462465
*/
463466
TransactionManager transactionManager(TransactionOption... options);
@@ -499,6 +502,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
499502
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
500503
* {@link CommitResponse}.
501504
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
505+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
502506
* </ul>
503507
*/
504508
AsyncRunner runAsync(TransactionOption... options);
@@ -554,6 +558,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
554558
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
555559
* {@link CommitResponse}.
556560
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
561+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
557562
* </ul>
558563
*/
559564
AsyncTransactionManager transactionManagerAsync(TransactionOption... options);

google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.spanner.v1.ReadRequest.OrderBy;
2323
import com.google.spanner.v1.RequestOptions.Priority;
2424
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
25+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
2526
import java.io.Serializable;
2627
import java.time.Duration;
2728
import java.util.Objects;
@@ -155,9 +156,50 @@ public static TransactionOption commitStats() {
155156
* process in the commit phase (when any needed locks are acquired). The validation process
156157
* succeeds only if there are no conflicting committed transactions (that committed mutations to
157158
* the read data at a commit timestamp after the read timestamp).
159+
*
160+
* @deprecated Use {@link Options#readLockMode(ReadLockMode)} instead.
158161
*/
162+
@Deprecated
159163
public static TransactionOption optimisticLock() {
160-
return OPTIMISTIC_LOCK_OPTION;
164+
return Options.readLockMode(ReadLockMode.OPTIMISTIC);
165+
}
166+
167+
/**
168+
* Returns a {@link TransactionOption} to set the desired {@link ReadLockMode} for a read-write
169+
* transaction.
170+
*
171+
* <p>This option controls the locking behavior for read operations and queries within a
172+
* read-write transaction. It works in conjunction with the transaction's {@link IsolationLevel}.
173+
*
174+
* <ul>
175+
* <li>{@link ReadLockMode#PESSIMISTIC}: Read locks are acquired immediately on read. This mode
176+
* only applies to {@code SERIALIZABLE} isolation. This mode prevents concurrent
177+
* modifications by locking data throughout the transaction. This reduces commit-time aborts
178+
* due to conflicts but can increase how long transactions wait for locks and the overall
179+
* contention.
180+
* <li>{@link ReadLockMode#OPTIMISTIC}: Locks for reads within the transaction are not acquired
181+
* on read. Instead the locks are acquired on commit to validate that read/queried data has
182+
* not changed since the transaction started. If a conflict is detected, the transaction
183+
* will fail. This mode only applies to {@code SERIALIZABLE} isolation. This mode defers
184+
* locking until commit, which can reduce contention and improve throughput. However, be
185+
* aware that this increases the risk of transaction aborts if there's significant write
186+
* competition on the same data.
187+
* <li>{@link ReadLockMode#READ_LOCK_MODE_UNSPECIFIED}: This is the default if no mode is set.
188+
* The locking behavior depends on the isolation level:
189+
* <ul>
190+
* <li>For {@code REPEATABLE_READ} isolation: Locking semantics default to {@code
191+
* OPTIMISTIC}. However, validation checks at commit are only performed for queries
192+
* using {@code SELECT FOR UPDATE}, statements with {@code LOCK_SCANNED_RANGES} hints,
193+
* and DML statements. <br>
194+
* Note: It is an error to explicitly set {@code ReadLockMode} when the isolation
195+
* level is {@code REPEATABLE_READ}.
196+
* <li>For all other isolation levels: If the read lock mode is not set, it defaults to
197+
* {@code PESSIMISTIC} locking.
198+
* </ul>
199+
* </ul>
200+
*/
201+
public static TransactionOption readLockMode(ReadLockMode readLockMode) {
202+
return new ReadLockModeOption(readLockMode);
161203
}
162204

163205
/**
@@ -367,16 +409,6 @@ void appendToOptions(Options options) {
367409
}
368410
}
369411

370-
/** Option to request Optimistic Concurrency Control for read/write transactions. */
371-
static final class OptimisticLockOption extends InternalOption implements TransactionOption {
372-
@Override
373-
void appendToOptions(Options options) {
374-
options.withOptimisticLock = true;
375-
}
376-
}
377-
378-
static final OptimisticLockOption OPTIMISTIC_LOCK_OPTION = new OptimisticLockOption();
379-
380412
/** Option to request the transaction to be excluded from change streams. */
381413
static final class ExcludeTxnFromChangeStreamsOption extends InternalOption
382414
implements UpdateTransactionOption {
@@ -516,6 +548,20 @@ void appendToOptions(Options options) {
516548
}
517549
}
518550

551+
/** Option to set read lock mode for read/write transactions. */
552+
static final class ReadLockModeOption extends InternalOption implements TransactionOption {
553+
private final ReadLockMode readLockMode;
554+
555+
public ReadLockModeOption(ReadLockMode readLockMode) {
556+
this.readLockMode = readLockMode;
557+
}
558+
559+
@Override
560+
void appendToOptions(Options options) {
561+
options.readLockMode = readLockMode;
562+
}
563+
}
564+
519565
private boolean withCommitStats;
520566

521567
private Duration maxCommitDelay;
@@ -530,7 +576,6 @@ void appendToOptions(Options options) {
530576
private String tag;
531577
private String etag;
532578
private Boolean validateOnly;
533-
private Boolean withOptimisticLock;
534579
private Boolean withExcludeTxnFromChangeStreams;
535580
private Boolean dataBoostEnabled;
536581
private DirectedReadOptions directedReadOptions;
@@ -540,6 +585,7 @@ void appendToOptions(Options options) {
540585
private Boolean lastStatement;
541586
private IsolationLevel isolationLevel;
542587
private XGoogSpannerRequestId reqId;
588+
private ReadLockMode readLockMode;
543589

544590
// Construction is via factory methods below.
545591
private Options() {}
@@ -644,10 +690,6 @@ Boolean validateOnly() {
644690
return validateOnly;
645691
}
646692

647-
Boolean withOptimisticLock() {
648-
return withOptimisticLock;
649-
}
650-
651693
Boolean withExcludeTxnFromChangeStreams() {
652694
return withExcludeTxnFromChangeStreams;
653695
}
@@ -704,6 +746,10 @@ IsolationLevel isolationLevel() {
704746
return isolationLevel;
705747
}
706748

749+
ReadLockMode readLockMode() {
750+
return readLockMode;
751+
}
752+
707753
@Override
708754
public String toString() {
709755
StringBuilder b = new StringBuilder();
@@ -740,9 +786,6 @@ public String toString() {
740786
if (validateOnly != null) {
741787
b.append("validateOnly: ").append(validateOnly).append(' ');
742788
}
743-
if (withOptimisticLock != null) {
744-
b.append("withOptimisticLock: ").append(withOptimisticLock).append(' ');
745-
}
746789
if (withExcludeTxnFromChangeStreams != null) {
747790
b.append("withExcludeTxnFromChangeStreams: ")
748791
.append(withExcludeTxnFromChangeStreams)
@@ -772,6 +815,9 @@ public String toString() {
772815
if (reqId != null) {
773816
b.append("requestId: ").append(reqId.toString());
774817
}
818+
if (readLockMode != null) {
819+
b.append("readLockMode: ").append(readLockMode).append(' ');
820+
}
775821
return b.toString();
776822
}
777823

@@ -807,15 +853,15 @@ public boolean equals(Object o) {
807853
&& Objects.equals(tag(), that.tag())
808854
&& Objects.equals(etag(), that.etag())
809855
&& Objects.equals(validateOnly(), that.validateOnly())
810-
&& Objects.equals(withOptimisticLock(), that.withOptimisticLock())
811856
&& Objects.equals(withExcludeTxnFromChangeStreams(), that.withExcludeTxnFromChangeStreams())
812857
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
813858
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
814859
&& Objects.equals(orderBy(), that.orderBy())
815860
&& Objects.equals(isLastStatement(), that.isLastStatement())
816861
&& Objects.equals(lockHint(), that.lockHint())
817862
&& Objects.equals(isolationLevel(), that.isolationLevel())
818-
&& Objects.equals(reqId(), that.reqId());
863+
&& Objects.equals(reqId(), that.reqId())
864+
&& Objects.equals(readLockMode(), that.readLockMode());
819865
}
820866

821867
@Override
@@ -857,9 +903,6 @@ public int hashCode() {
857903
if (validateOnly != null) {
858904
result = 31 * result + validateOnly.hashCode();
859905
}
860-
if (withOptimisticLock != null) {
861-
result = 31 * result + withOptimisticLock.hashCode();
862-
}
863906
if (withExcludeTxnFromChangeStreams != null) {
864907
result = 31 * result + withExcludeTxnFromChangeStreams.hashCode();
865908
}
@@ -887,6 +930,9 @@ public int hashCode() {
887930
if (reqId != null) {
888931
result = 31 * result + reqId.hashCode();
889932
}
933+
if (readLockMode != null) {
934+
result = 31 * result + readLockMode.hashCode();
935+
}
890936
return result;
891937
}
892938

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,16 @@ static TransactionOptions createReadWriteTransactionOptions(
7676
transactionOptions.setExcludeTxnFromChangeStreams(true);
7777
}
7878
TransactionOptions.ReadWrite.Builder readWrite = TransactionOptions.ReadWrite.newBuilder();
79-
if (options.withOptimisticLock() == Boolean.TRUE) {
80-
readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC);
81-
}
8279
if (previousTransactionId != null
8380
&& previousTransactionId != com.google.protobuf.ByteString.EMPTY) {
8481
readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId);
8582
}
8683
if (options.isolationLevel() != null) {
8784
transactionOptions.setIsolationLevel(options.isolationLevel());
8885
}
86+
if (options.readLockMode() != null) {
87+
readWrite.setReadLockMode(options.readLockMode());
88+
}
8989
transactionOptions.setReadWrite(readWrite);
9090
return transactionOptions.build();
9191
}
@@ -283,6 +283,9 @@ public CommitResponse writeAtLeastOnceWithOptions(
283283
if (options.isolationLevel() != null) {
284284
transactionOptionsBuilder.setIsolationLevel(options.isolationLevel());
285285
}
286+
if (options.readLockMode() != null) {
287+
transactionOptionsBuilder.getReadWriteBuilder().setReadLockMode(options.readLockMode());
288+
}
286289
requestBuilder.setSingleUseTransaction(
287290
defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build()));
288291

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import com.google.spanner.v1.SpannerGrpc;
7070
import com.google.spanner.v1.TransactionOptions;
7171
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
72+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
7273
import io.grpc.CallCredentials;
7374
import io.grpc.CompressorRegistry;
7475
import io.grpc.Context;
@@ -1700,6 +1701,7 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) {
17001701
* <pre>{@code
17011702
* DefaultReadWriteTransactionOptions options = DefaultReadWriteTransactionOptions.newBuilder()
17021703
* .setIsolationLevel(IsolationLevel.SERIALIZABLE)
1704+
* .setReadLockMode(ReadLockMode.OPTIMISTIC)
17031705
* .build();
17041706
* }</pre>
17051707
*/
@@ -1724,6 +1726,12 @@ public DefaultReadWriteTransactionOptionsBuilder setIsolationLevel(
17241726
return this;
17251727
}
17261728

1729+
public DefaultReadWriteTransactionOptionsBuilder setReadLockMode(
1730+
ReadLockMode readLockMode) {
1731+
transactionOptionsBuilder.getReadWriteBuilder().setReadLockMode(readLockMode);
1732+
return this;
1733+
}
1734+
17271735
public DefaultReadWriteTransactionOptions build() {
17281736
return new DefaultReadWriteTransactionOptions(transactionOptionsBuilder.build());
17291737
}

0 commit comments

Comments
 (0)