Skip to content

Commit 1c38bc5

Browse files
committed
feat: add support for Concurrency Control
1 parent bf69b1a commit 1c38bc5

File tree

6 files changed

+398
-57
lines changed

6 files changed

+398
-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: 56 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,36 @@ 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+
* Specifying this instructs the transaction to request {@link ReadLockMode} from the backend.
169+
*
170+
* <p>This option controls the locking behavior for read operations and queries within a
171+
* read-write transaction. It works in conjunction with the transaction's {@link IsolationLevel}.
172+
*
173+
* <ul>
174+
* <li>{@link ReadLockMode#OPTIMISTIC}: Reads and queries do not acquire locks during the
175+
* transaction's execution phase. Instead, Spanner validates at commit time that the data
176+
* read has not been modified by other transactions since the read occurred. This mode can
177+
* reduce lock contention and improve read performance but may result in more transaction
178+
* aborts under high write contention on the same data.
179+
* <li>{@link ReadLockMode#PESSIMISTIC}: The transaction acquires locks on data as it's
180+
* accessed. For example, Data Manipulation Language (DML) statements and {@code SELECT ...
181+
* FOR UPDATE} queries will take locks. This approach blocks other transactions from
182+
* modifying the locked data until the current transaction completes, which can prevent
183+
* conflicts and reduce commit-time aborts, but may increase lock wait times and overall
184+
* contention.
185+
* </ul>
186+
*/
187+
public static TransactionOption readLockMode(ReadLockMode readLockMode) {
188+
return new ReadLockModeOption(readLockMode);
161189
}
162190

163191
/**
@@ -367,16 +395,6 @@ void appendToOptions(Options options) {
367395
}
368396
}
369397

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-
380398
/** Option to request the transaction to be excluded from change streams. */
381399
static final class ExcludeTxnFromChangeStreamsOption extends InternalOption
382400
implements UpdateTransactionOption {
@@ -516,6 +534,20 @@ void appendToOptions(Options options) {
516534
}
517535
}
518536

537+
/** Option to set read lock mode for read/write transactions. */
538+
static final class ReadLockModeOption extends InternalOption implements TransactionOption {
539+
private final ReadLockMode readLockMode;
540+
541+
public ReadLockModeOption(ReadLockMode readLockMode) {
542+
this.readLockMode = readLockMode;
543+
}
544+
545+
@Override
546+
void appendToOptions(Options options) {
547+
options.readLockMode = readLockMode;
548+
}
549+
}
550+
519551
private boolean withCommitStats;
520552

521553
private Duration maxCommitDelay;
@@ -530,7 +562,6 @@ void appendToOptions(Options options) {
530562
private String tag;
531563
private String etag;
532564
private Boolean validateOnly;
533-
private Boolean withOptimisticLock;
534565
private Boolean withExcludeTxnFromChangeStreams;
535566
private Boolean dataBoostEnabled;
536567
private DirectedReadOptions directedReadOptions;
@@ -540,6 +571,7 @@ void appendToOptions(Options options) {
540571
private Boolean lastStatement;
541572
private IsolationLevel isolationLevel;
542573
private XGoogSpannerRequestId reqId;
574+
private ReadLockMode readLockMode;
543575

544576
// Construction is via factory methods below.
545577
private Options() {}
@@ -644,10 +676,6 @@ Boolean validateOnly() {
644676
return validateOnly;
645677
}
646678

647-
Boolean withOptimisticLock() {
648-
return withOptimisticLock;
649-
}
650-
651679
Boolean withExcludeTxnFromChangeStreams() {
652680
return withExcludeTxnFromChangeStreams;
653681
}
@@ -704,6 +732,10 @@ IsolationLevel isolationLevel() {
704732
return isolationLevel;
705733
}
706734

735+
ReadLockMode readLockMode() {
736+
return readLockMode;
737+
}
738+
707739
@Override
708740
public String toString() {
709741
StringBuilder b = new StringBuilder();
@@ -740,9 +772,6 @@ public String toString() {
740772
if (validateOnly != null) {
741773
b.append("validateOnly: ").append(validateOnly).append(' ');
742774
}
743-
if (withOptimisticLock != null) {
744-
b.append("withOptimisticLock: ").append(withOptimisticLock).append(' ');
745-
}
746775
if (withExcludeTxnFromChangeStreams != null) {
747776
b.append("withExcludeTxnFromChangeStreams: ")
748777
.append(withExcludeTxnFromChangeStreams)
@@ -772,6 +801,9 @@ public String toString() {
772801
if (reqId != null) {
773802
b.append("requestId: ").append(reqId.toString());
774803
}
804+
if (readLockMode != null) {
805+
b.append("readLockMode: ").append(readLockMode).append(' ');
806+
}
775807
return b.toString();
776808
}
777809

@@ -807,15 +839,15 @@ public boolean equals(Object o) {
807839
&& Objects.equals(tag(), that.tag())
808840
&& Objects.equals(etag(), that.etag())
809841
&& Objects.equals(validateOnly(), that.validateOnly())
810-
&& Objects.equals(withOptimisticLock(), that.withOptimisticLock())
811842
&& Objects.equals(withExcludeTxnFromChangeStreams(), that.withExcludeTxnFromChangeStreams())
812843
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
813844
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
814845
&& Objects.equals(orderBy(), that.orderBy())
815846
&& Objects.equals(isLastStatement(), that.isLastStatement())
816847
&& Objects.equals(lockHint(), that.lockHint())
817848
&& Objects.equals(isolationLevel(), that.isolationLevel())
818-
&& Objects.equals(reqId(), that.reqId());
849+
&& Objects.equals(reqId(), that.reqId())
850+
&& Objects.equals(readLockMode(), that.readLockMode());
819851
}
820852

821853
@Override
@@ -857,9 +889,6 @@ public int hashCode() {
857889
if (validateOnly != null) {
858890
result = 31 * result + validateOnly.hashCode();
859891
}
860-
if (withOptimisticLock != null) {
861-
result = 31 * result + withOptimisticLock.hashCode();
862-
}
863892
if (withExcludeTxnFromChangeStreams != null) {
864893
result = 31 * result + withExcludeTxnFromChangeStreams.hashCode();
865894
}
@@ -887,6 +916,9 @@ public int hashCode() {
887916
if (reqId != null) {
888917
result = 31 * result + reqId.hashCode();
889918
}
919+
if (readLockMode != null) {
920+
result = 31 * result + readLockMode.hashCode();
921+
}
890922
return result;
891923
}
892924

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.spanner.v1.RequestOptions;
4646
import com.google.spanner.v1.Transaction;
4747
import com.google.spanner.v1.TransactionOptions;
48+
import com.google.spanner.v1.TransactionOptions.ReadWrite;
4849
import java.time.Instant;
4950
import java.util.ArrayList;
5051
import java.util.Collection;
@@ -76,16 +77,16 @@ static TransactionOptions createReadWriteTransactionOptions(
7677
transactionOptions.setExcludeTxnFromChangeStreams(true);
7778
}
7879
TransactionOptions.ReadWrite.Builder readWrite = TransactionOptions.ReadWrite.newBuilder();
79-
if (options.withOptimisticLock() == Boolean.TRUE) {
80-
readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC);
81-
}
8280
if (previousTransactionId != null
8381
&& previousTransactionId != com.google.protobuf.ByteString.EMPTY) {
8482
readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId);
8583
}
8684
if (options.isolationLevel() != null) {
8785
transactionOptions.setIsolationLevel(options.isolationLevel());
8886
}
87+
if (options.readLockMode() != null) {
88+
readWrite.setReadLockMode(options.readLockMode());
89+
}
8990
transactionOptions.setReadWrite(readWrite);
9091
return transactionOptions.build();
9192
}
@@ -283,6 +284,10 @@ public CommitResponse writeAtLeastOnceWithOptions(
283284
if (options.isolationLevel() != null) {
284285
transactionOptionsBuilder.setIsolationLevel(options.isolationLevel());
285286
}
287+
if (options.readLockMode() != null) {
288+
transactionOptionsBuilder.setReadWrite(
289+
ReadWrite.newBuilder().setReadLockMode(options.readLockMode()));
290+
}
286291
requestBuilder.setSingleUseTransaction(
287292
defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build()));
288293

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
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;
73+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
7274
import io.grpc.CallCredentials;
7375
import io.grpc.CompressorRegistry;
7476
import io.grpc.Context;
@@ -1700,6 +1702,7 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) {
17001702
* <pre>{@code
17011703
* DefaultReadWriteTransactionOptions options = DefaultReadWriteTransactionOptions.newBuilder()
17021704
* .setIsolationLevel(IsolationLevel.SERIALIZABLE)
1705+
* .setReadLockMode(ReadLockMode.OPTIMISTIC)
17031706
* .build();
17041707
* }</pre>
17051708
*/
@@ -1724,6 +1727,13 @@ public DefaultReadWriteTransactionOptionsBuilder setIsolationLevel(
17241727
return this;
17251728
}
17261729

1730+
public DefaultReadWriteTransactionOptionsBuilder setReadLockMode(
1731+
ReadLockMode readLockMode) {
1732+
transactionOptionsBuilder.setReadWrite(
1733+
ReadWrite.newBuilder().setReadLockMode(readLockMode));
1734+
return this;
1735+
}
1736+
17271737
public DefaultReadWriteTransactionOptions build() {
17281738
return new DefaultReadWriteTransactionOptions(transactionOptionsBuilder.build());
17291739
}

0 commit comments

Comments
 (0)