Jump to Content
Databases

Understanding transactional locking in Cloud Spanner

November 15, 2022
Manit Gupta

Software Engineer

Try Google Cloud

Start building on Google Cloud with $300 in free credits and 20+ always free products.

Free trial

Cloud Spanner is a fully managed relational database with unlimited scale, strong consistency, and up to 99.999% availability.  It is designed for highly concurrent applications that read and update data, for example, to process payments or for online game play. To ensure the consistency across multiple concurrent transactions, Cloud Spanner uses a combination of shared locks and exclusive locks to control access to the data. In this blog, we will explore the different types of locks present in Cloud Spanner. We will also discuss some common cases of transactional locking in Cloud Spanner, and what to look out for to detect when these cases might be occurring.

What is a lock?

Before we get into the details, let us first quickly recap and define a lock in the context of database systems.

“Locks” in databases are a mechanism for concurrency control. Locks are typically held on a resource, which may mean rows, columns, tables or even entire databases. When a resource is locked by a transaction, it cannot be accessed by another transaction until the lock is released.

Timeline view of transactions

Before we proceed to discuss transaction locking in the context of read and read-write transactions, it is important to recap the timeline view of the transactions in Spanner.

https://storage.googleapis.com/gweb-cloudblog-publish/images/1_naFJT2z.max-1600x1600.jpg

Please also keep in mind the concept of write buffering in transactions, since we refer to it in the Common Transaction Patterns section below. Write buffering refers to the Cloud Spanner server(s) accepting the writes. Note that these writes are not durable until a commit has been performed. 

Types of locks in Cloud Spanner

Cloud Spanner operations acquire locks when the operations are part of a read-write transaction. Read-only transactions do not acquire locks. Unlike other approaches that lock entire tables or rows, the granularity of transactional locks in Spanner is a cell, or the intersection of a row and a column. This means that two transactions can read and modify different columns of the same row at the same time. To maximize the number of transactions that have access to a particular data cell at a given time, Cloud Spanner uses different lock modes. 

Here is a brief description of the different lock types. Learn more about each lock type in the Cloud Spanner documentation.

  1. ReaderShared Lock - Acquired when a read-write transaction reads data.

  2. WriterShared Lock - Acquired when a read-write transaction writes data without reading it.

  3. Exclusive Lock - Acquired when a read-write transaction which has already acquired a ReaderShared lock tries to write data after the completion of read. It is a special case for a transaction to hold both the ReaderShared lock and WriterShared lock at the same time. 

  4. WriterSharedTimestamp Lock - Special type of lock acquired when inserting new rows with the transaction’s commit timestamp as part of the primary key. 

Handling Lock Conflicts

Since read-write transactions use locks to execute atomically, they run the risk of deadlocking. For example, consider the following scenario: transaction Txn1 holds a lock on record A and is waiting for a lock on record B, and Txn2 holds a lock on record B and is waiting for a lock on record A. The only way to make progress in this situation is to abort one of the transactions so it releases its lock, allowing the other transaction to proceed.

Cloud Spanner uses the standard "wound-wait" algorithm to handle deadlock detection. Under the hood, Spanner keeps track of the age of each transaction that requests conflicting locks. It also allows older transactions to abort younger transactions, where "older" means that the transaction's earliest read, query, or commit happened sooner.

Common Transaction Patterns 

Armed with the basics of transactions and locks in Cloud Spanner, we will now walk through a few practical use-cases. We take the example of an application which queries and updates users’ balance in a table named accounts. The accounts table has the following columns -

https://storage.googleapis.com/gweb-cloudblog-publish/images/2A.max-900x900.jpg

Case 1: Transaction waiting to get exclusive lock because of higher priority shared-lock

https://storage.googleapis.com/gweb-cloudblog-publish/images/2_rmx0ARD.max-1000x1000.jpg

Sequence

  1. Txn1 begins.

  2. Txn2 begins.

  3. Txn2 reads the table and acquires a ReadShared Lock.

  4. Txn1 buffers its write.

  5. Txn1 tries to commit, but since Txn2 has higher priority (because it has executed its first operation first), Txn1 will have to wait until Txn2 releases the ReadShared Lock.

  6. After committing, Txn2 releases its ReadShared Lock. Txn1 is now able to upgrade to an Exclusive Lock. It acquires the Exclusive Lock and commits.

Note 1: UPDATE WHERE is always transformed into SELECT WHERE, so an UPDATE statement is not a blind write, but a read-write operation.

Note 2: While the above example considers the use-case of querying and updating a single row, the same transaction pattern is also applicable across a key-range.

What to watch out for

When does this typically happen

  • Concurrent updates to a single key/key-range in a table.

Case 2: Transaction aborted because of concurrent execution succeeding

https://storage.googleapis.com/gweb-cloudblog-publish/images/3_N9kf29e.max-1000x1000.jpg

Sequence

  1. Txn1 begins.

  2. Txn2 begins.

  3. Txn1 reads a row from the table and acquires a ReadShared Lock.

  4. Txn2 reads the same row as Txn1 and acquires a ReadShared Lock.

  5. Txn1 buffers its write.

  6. Txn2 buffers its write.

  7. Txn1 tries to commit, has higher priority (because it has executed its first operation first), it will get priority in upgrading to an Exclusive Lock. It acquires the Exclusive Lock and commits.

  8. Since Txn1 has committed and was the higher priority transaction, Txn1 will abort Txn2.. The abort will likely happen before Txn2 tries to commit.

What to watch out for

When does this typically happen

  • Concurrent updates to a single key/key-range. Typically this happens in the case of hot keys being present in the table.

Case 3: Transaction waiting to get exclusive lock because of higher priority shared-lock & getting aborted because of prior succeeding concurrent execution

https://storage.googleapis.com/gweb-cloudblog-publish/images/4_xW46m4b.max-1000x1000.jpg

Sequence

  1. Txn1 begins.

  2. Txn2 begins.

  3. Txn2 reads the table and acquires a ReadShared Lock.

  4. Txn1 reads the table and acquires a ReadShared Lock.

  5. Txn1 buffers its write.

  6. Txn2 buffers its write.

  7. Txn1 tries to commit, but since Txn2 holds a ReadShared Lock, it has to wait until it gets cleared.

  8. Since Txn2 has higher priority (because it has executed its first operation first), it will acquire the Exclusive Lock first. Txn2 upgrades its ReaderShared Lock to an Exclusive Lock and commits.

Finally, after Step 8, when Txn1 tries to commit (since ReaderShared Lock from Txn2 is now cleared) it gets aborted. This is done to prevent deadlock with the higher priority transaction (Txn2).

Note:  In Case 1,  even though Txn2 had acquired a ReaderShared Lock earlier as in Case 3, it never upgraded to an Exclusive Lock (since there were no writes). Hence there was no need to abort Txn1, it could simply wait for Txn2 to release its ReaderShared lock and then commit. 

What to watch out for

When does this typically happen

  • Concurrent updates to a single key/key-range. Typically this happens in the case of hot keys being present in the table.

Recommendations

In order to mitigate these issues and reduce their occurrence. We recommend adopting the following best practices:

  • If you need to perform more than one read at the same timestamp, and know in advance that you only need to read, consider using a read-only transaction. Because read-only transactions don't write, they don't hold locks and they don't block other transactions. Further, read-only transactions never abort, so you don’t need to wrap them in retry loops.

  • Always acquire ReadShared Locks on the smallest subset of keys or key ranges. This reduces the chances of lock contention.

  • Analyze your code to only include critical path code within a transaction. Avoiding unneeded remote calls or complex, long-running business logic is a good way to ensure a transaction process quickly.

  • Analyze your needs for multi-split transactions. Since transactions that update more than one split use a 2-phase commit protocol, they hold locks for a longer duration, thereby increasing chances of lock contention.

Get started today

Spanner’s unique architecture allows it to scale horizontally without compromising on the consistency guarantees that developers rely on in modern relational databases. Try out Spanner today for free for 90 days or for as low as $65 USD per month.

Posted in