Locking in CockroachDB, Part 2 - Finding Locked Rows
This is part 2 of a series on locking in CockroachDB. The first post showed how you can end up with locked rows or tables, and how to keep your locks to the fewest number of rows by using indexes.
But even with the best use of indexes, you can still end up with rows that get locked. This can be worsened if you have some other issue, like a statement or transaction that is slow or doesn’t complete, these locks will be held for too long.
Here is an example that will result in some rows being locked.
My table
I have a products table with indexes on product_id
, product_category
and price
.
The value are created by a library called Bogus
, so they are random, but realistic enough for my purposes.
product_id | name | product_category | price | code
-------------+----------------------------+------------------+-------+-----------
1 | Ergonomic Granite Sausages | 1 | 13.91 | 65662748
2 | Gorgeous Cotton Computer | 1 | 88.60 | 36621408
3 | Gorgeous Frozen Hat | 4 | 51.57 | 51949419
4 | Awesome Rubber Chips | 3 | 60.88 | 23445857
5 | Incredible Plastic Pants | 3 | 9 | 02021362
6 | Handmade Fresh Keyboard | 2 | 68.05 | 59125532
7 | Licensed Concrete Soap | 4 | 63.24 | 10179406
8 | Generic Rubber Bacon | 1 | 72.31 | 07966279
9 | Fantastic Wooden Fish | 4 | 73.05 | 68922436
10 | Unbranded Metal Shirt | 4 | 78.59 | 29417476
Locking some rows
Just like in the previous post, locking rows is done inside a transaction that is kept open -
BEGIN; SELECT * FROM products WHERE product_id <= 5 FOR UPDATE; -- don't commit or rollback yet
To find what is locked, run -
select * from crdb_internal.cluster_locks;
This will return 5 rows. There are quite a few columns, so I have removed some for clarity -
table_id | schema_name | table_name | index_name | lock_key_pretty | txn_id | lock_strength | duration | isolation_level
-----------+-------------+------------+------------+------------------+--------------------------------------+---------------+-----------------+------------------
123 | salesdb | products | | /Table/123/1/1/0 | cea02774-e2b1-44aa-8e14-78a9d31c8a6e | Exclusive | 00:05:09.895328 | SERIALIZABLE
123 | salesdb | products | | /Table/123/1/2/0 | cea02774-e2b1-44aa-8e14-78a9d31c8a6e | Exclusive | 00:05:09.895319 | SERIALIZABLE
123 | salesdb | products | | /Table/123/1/3/0 | cea02774-e2b1-44aa-8e14-78a9d31c8a6e | Exclusive | 00:05:09.895316 | SERIALIZABLE
123 | salesdb | products | | /Table/123/1/4/0 | cea02774-e2b1-44aa-8e14-78a9d31c8a6e | Exclusive | 00:05:09.895308 | SERIALIZABLE
123 | salesdb | products | | /Table/123/1/5/0 | cea02774-e2b1-44aa-8e14-78a9d31c8a6e | Exclusive | 00:05:09.895306 | SERIALIZABLE
There is a lot of useful information here -
- table_id, this will help later when looking up indexes.
- schema_name and table_name identify the table that has locks.
- index_name, when the query uses the primary key alone, this is blank.
- lock_key_pretty, identifies the rows that are locked. For this one, its table 123, index 1, and rows with the primary key 1,2,3,4,5. I don’t know what the last 0 refers to, and Cockroach Labs has not made it easy to figure out.
- txn_id, the id of the transaction that is locking the rows, will be useful when cancelling the lock in the next post.
- duration, how long the lock has been in place.
More on lock_key_pretty
Some of its values are easy enough to figure out. But not everything.
When locking with a query that uses a non-primary key index, the format changes a little.
But first find the indexes on a table, there are at least two ways.
The first is -
show index from products;
But for my purposes here, I prefer -
select index_id, index_name, index_type from crdb_internal.table_indexes where descriptor_name = 'products';
And this is the result -
index_id | index_name | index_type
-----------+------------------------------+-------------
1 | pk_products | primary
2 | ix_products_price | secondary
4 | ix_products_product_category | secondary
The primary key index always seems to be number 1, the price
index is number 2, and the product_category
index is number 4.
Now run a query like this -
BEGIN; SELECT * FROM products@ix_products_price WHERE price > 60 FOR UPDATE;
My query returns 7 rows -
product_id | name | product_category | price | code
-------------+--------------------------+------------------+-------+-----------
2 | Gorgeous Cotton Computer | 1 | 88.60 | 36621408
4 | Awesome Rubber Chips | 3 | 60.88 | 23445857
6 | Handmade Fresh Keyboard | 2 | 68.05 | 59125532
7 | Licensed Concrete Soap | 4 | 63.24 | 10179406
8 | Generic Rubber Bacon | 1 | 72.31 | 07966279
9 | Fantastic Wooden Fish | 4 | 73.05 | 68922436
10 | Unbranded Metal Shirt | 4 | 78.59 | 29417476
Then run -
select * from crdb_internal.cluster_locks;
Again, here is a trimmed-down set of the results -
table_id | schema_name | table_name | index_name | lock_key_pretty | txn_id | duration
-----------+-------------+------------+-------------------+-------------------------+--------------------------------------+------------------
123 | salesdb | products | | /Table/123/1/2/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448656
123 | salesdb | products | | /Table/123/1/4/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448651
123 | salesdb | products | | /Table/123/1/6/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448649
123 | salesdb | products | | /Table/123/1/7/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448649
123 | salesdb | products | | /Table/123/1/8/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448648
123 | salesdb | products | | /Table/123/1/9/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448647
123 | salesdb | products | | /Table/123/1/10/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.448646
123 | salesdb | products | ix_products_price | /Table/123/2/60.88/4/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450732
123 | salesdb | products | ix_products_price | /Table/123/2/63.24/7/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450728
123 | salesdb | products | ix_products_price | /Table/123/2/68.05/6/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450727
123 | salesdb | products | ix_products_price | /Table/123/2/72.31/8/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450726
123 | salesdb | products | ix_products_price | /Table/123/2/73.05/9/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450725
123 | salesdb | products | ix_products_price | /Table/123/2/78.59/10/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450724
123 | salesdb | products | ix_products_price | /Table/123/2/88.6/2/0 | 54737f41-5e14-4fb3-adc5-2bf3e8a99c36 | 00:04:34.450723
Some things to note -
- there are 14 rows returned, twice the number of rows returned by the actual query.
- the first seven, seem to use the primary key index. In
lock_key_pretty
, the index 1 is referenced. - the second set of seven rows use the
ix_products_price
index. Thelock_key_pretty
here follows a slightly different format than the shown above.- 123 refers to the table as before
- 2 refers to the index; this matches the price index on the list of indexes found earlier
- the next section is the price as stored in the row
- the following one is the primary key value
- the last, I don’t know
- txn_id is the transaction holding the lock
- duration, how long the lock has been held.
If your primary key, or the index column is a GUID, a truncated and harder to read representation of the will be shown in the lock_key_pretty
, but with some effort you can figure out which row it refers to.
Locking the whole table
If the query doesn’t use an index, a table scan will be performed, and the whole table will be locked.
The code
column is not indexed.
BEGIN; SELECT * FROM products where code = '36621408' FOR UPDATE;
Now check for locked rows -
select * from crdb_internal.cluster_locks;
table_id | schema_name | table_name | index_name | lock_key_pretty | txn_id | duration
-----------+-------------+------------+------------+-------------------+--------------------------------------+------------------
123 | salesdb | products | | /Table/123/1/1/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075405
123 | salesdb | products | | /Table/123/1/2/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075393
123 | salesdb | products | | /Table/123/1/3/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075388
123 | salesdb | products | | /Table/123/1/4/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075383
123 | salesdb | products | | /Table/123/1/5/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075379
123 | salesdb | products | | /Table/123/1/6/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075374
123 | salesdb | products | | /Table/123/1/7/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075369
123 | salesdb | products | | /Table/123/1/8/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075364
123 | salesdb | products | | /Table/123/1/9/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.07536
123 | salesdb | products | | /Table/123/1/10/0 | 05843b20-6149-4276-bb06-ebb2e16e51b3 | 00:03:16.075357
The whole table is locked, but like when the primary key is used, there is no index_name
, and the index id in the lock_key_pretty
shows 1.
Conclusion
You should now know how to find locked data and identify what index was being used in the query that caused the lock.
If no index_name
is listed, it is likely the primary key index, or no index was used in the query.
If no index was used in the query, the whole table will be locked. If the primary key was used, there should be a subset of rows locked.
This should help you identify the code that caused the lock.
In the next post, I’ll show how to use this information along with the the session, role, and application that caused the lock, to kill the transaction that is holding the rows.