锁统计信息

Cloud Spanner 提供锁统计信息,这让您可以确定在特定时间段内作为数据库中事务锁冲突的主要来源的行键和表列。您可以使用 SQL 语句从 SPANNER_SYS.LOCK_STATS* 系统表中检索这些统计信息。

可用性

SPANNER_SYS 数据只能通过 SQL 接口获得(例如通过 executeQuerygcloud spanner databases execute-sql接口);Cloud Spanner 提供的其他单一读取方法不支持 SPANNER_SYS

按行键划分的锁统计信息

以下表会跟踪具有最长等待时间的行键:

  • SPANNER_SYS.LOCK_STATS_TOP_MINUTE:在 1 分钟时间段内具有最长锁等待时间的行键。

  • SPANNER_SYS.LOCK_STATS_TOP_10MINUTE:在 10 分钟时间段内具有最长锁等待时间的行键。

  • SPANNER_SYS.LOCK_STATS_TOP_HOUR:在 1 小时时间段内具有最长锁等待时间的行键

这些表具有以下属性:

  • 每个表包含表名指定的非重叠时间段长度的数据。

  • 间隔基于时钟时间。1 分钟间隔结束于分钟,10 分钟间隔从小时开始每 10 分钟结束一次,而 1 小时间隔结束于小时。在每个间隔后,Cloud Spanner 都会从所有服务器收集数据,然后稍后在 SPANNER_SYS 表中提供这些数据。

    例如,在上午 11:59:30,SQL 查询可用的最近时间段为:

    • 1 分钟:上午 11:58:00–11:58:59
    • 10 分钟:上午 11:40:00–11:49:59
    • 1 小时:上午 10:00:00–10:59:59
  • Cloud Spanner 按起始行键范围对统计信息进行分组。

  • 每行包含 Cloud Spanner 在指定时间段内捕获的特定起始行键范围的总锁等待时间的统计信息。

  • 如果 Cloud Spanner 无法存储有关在指定时间段内等待锁的每个行键范围的信息,则系统会优先存储该时间段内具有最长锁等待时间的行键范围。

表架构

列名 类型 说明
INTERVAL_END TIMESTAMP 所含锁冲突发生的时间段结束。
ROW_RANGE_START_KEY BYTES(MAX) 发生锁冲突的行键。如果冲突涉及一个行范围,则此值表示该范围的起始键。加号 + 表示范围。如需了解详情,请参阅什么是行范围起始键
LOCK_WAIT_SECONDS FLOAT64 针对行键范围内的所有列记录到的锁冲突的累计锁等待时间,以秒为单位。
SAMPLE_LOCK_REQUESTS ARRAY<STRUCT<
  lock_mode STRING,
  column STRING>>
此数组中的每个条目均对应一个示例锁请求,这些示例请求均对给定行键或行键范围上发生的锁冲突有影响。此数组中的示例数量上限为 20。每个示例均包含以下两个字段:

  • lock_mode:所请求的锁定模式。如需了解详情,请参阅锁定模式
  • column:遇到锁冲突的列。此值的格式为 tablename.columnname

锁定模式

如果 Cloud Spanner 操作是读写事务的一部分,则会获取锁。只读事务不会获取锁。Cloud Spanner 使用不同的锁定模式来最大限度地增加在给定时间可访问特定数据单元的事务数量。不同的锁具有不同的特性。例如,某些锁可在多个事务之间共享,而其他一些锁则不能在多个事务之间共享。

您在事务中尝试获取以下某种锁定模式时,可能会发生锁冲突。

  • ReaderShared 锁 - 这种锁仍然允许其他读取操作访问您的数据,直到事务准备好提交。在读写事务读取数据时,会获取此共享锁。

  • WriterShared 锁 - 在读写事务尝试提交写入操作时,会获取此锁。

  • Exclusive 锁 - 在已获得 ReaderShared 锁的读写事务在读取完毕后尝试写入数据时,会获取此独占锁。独占锁是在 ReaderShared 锁的基础上升级而来的。独占锁是一个事务同时持有 ReaderShared 锁和 WriterShared 锁的一种特殊情况。其他事务无法获取在相同单元上获取任何锁。

  • WriterSharedTimestamp 锁 - WriterShared 锁的一种特殊类型,在向主键中包含提交时间戳的表中插入新行时会获取这种锁。这种类型的锁会阻止事务参与者创建完全相同的行,避免由此而产生的相互冲突。Cloud Spanner 会更新所插入的行的键,以便与执行插入的事务的提交时间戳相匹配。

如需详细了解事务类型和可用的锁类型,请参阅事务

锁定模式冲突

下表显示了不同锁定模式之间可能存在的冲突。

锁定模式 ReaderShared WriterShared Exclusive WriterSharedTimestamp
ReaderShared
WriterShared 不适用
Exclusive 不适用
WriterSharedTimestamp 不适用 不适用

只有在插入主键中包含时间戳的新行时,才会使用 WriterSharedTimestamp 锁。在写入现有单元或插入不带时间戳的新行时,将会使用 WriterSharedExclusive 锁。因此,WriterSharedTimestamp 不能与其他类型的锁产生冲突,相应的冲突场景在上表中显示为不适用

唯一的例外情况是 ReaderShared,它可以应用于不存在的行,因此可能与 WriterSharedTimestamp 发生冲突。例如,全表扫描甚至会针对尚未创建的行锁定整个表,因此 ReaderShared 有可能会与 WriterSharedTimestamp 发生冲突。

什么是行范围起始键?

ROW_RANGE_START_KEY 列标识具有锁冲突的复合主键或行键范围的主键。以下架构用于演示示例。

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  TrackId      INT64 NOT NULL,
  SongName     STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
  ...
) PRIMARY KEY (UserId, LastAccess);

如以下行键和行键范围表所示,范围由加号“+”所表示。在这些情况下,键表示发生锁冲突的键范围的起始键。

ROW_RANGE_START_KEY 说明
singers(2) Singers 表,键为 SingerId=2
albums(2,1) Albums 表,键为 SingerId=2,AlbumId=1
songs(2,1,5) Songs 表,键为 SingerId=2,AlbumId=1,TrackId=5
songs(2,1,5+) Songs 表键范围,起始于 SongerId=2,AlbumId=1,TrackId=5
albums(2,1+) Albums 表键范围,起始于 SingerId=2,AlbumId=1
users(3, 2020-11-01 12:34:56.426426+00:00) Users 表,键为 UserId=3, LastAccess=commit_timestamp

聚合统计信息

SPANNER_SYS 还包含用于存储 Cloud Spanner 在特定时间段内捕获的锁统计信息的聚合数据的表:

  • SPANNER_SYS.LOCK_STATS_TOTAL_MINUTE:1 分钟时间段内所有锁等待的聚合统计信息。

  • SPANNER_SYS.LOCK_STATS_TOTAL_10MINUTE:10 分钟时间段内所有锁等待的聚合统计信息。

  • SPANNER_SYS.LOCK_STATS_TOTAL_HOUR:1 小时时间段内所有锁等待的聚合统计信息。

聚合统计信息表具有以下属性:

  • 每个表包含表名指定的非重叠时间段长度的数据。

  • 时间段基于时钟时间。1 分钟时间段结束于整点分钟,10 分钟时间段结束于整点 10 分钟(自当前小时数开始),1 小时时间段结束于整点小时数。

    例如,在上午 11:59:30,SQL 查询可用的聚合锁统计信息的最近时间段为:

    • 1 分钟:上午 11:58:00–11:58:59
    • 10 分钟:上午 11:40:00–11:49:59
    • 1 小时:上午 10:00:00–10:59:59
  • 每行包含指定时间段内在数据库上发生的所有锁等待的聚合统计信息。每个时间段只有一行。

  • SPANNER_SYS.LOCK_STATS_TOTAL_* 表中捕获的统计信息包含 Cloud Spanner 在 SPANNER_SYS.LOCK_STATS_TOP_* 表中未捕获的锁等待。

表架构

列名 类型 说明
INTERVAL_END TIMESTAMP 发生锁冲突的时间间隔的结束时间。
TOTAL_LOCK_WAIT_SECONDS FLOAT64 记录整个数据库的锁冲突总时间,以秒为单位。

示例查询

以下是一个可用于检索锁统计信息的 SQL 语句的示例。您可以使用客户端库gcloud 命令行工具或 Cloud Console 运行这些 SQL 语句。

列出前 1 分钟时间段内的锁统计信息

以下查询返回在前 1 分钟时间段内,具有一次锁冲突的各行键的锁等待信息,包括在总锁冲突中所占比例。

CAST() 函数将 row_range_start_key BYTES 字段转换为 STRING 字段。

SELECT CAST(s.row_range_start_key AS STRING) AS row_range_start_key,
       t.total_lock_wait_seconds,
       s.lock_wait_seconds,
       s.lock_wait_seconds/t.total_lock_wait_seconds frac_of_total,
       s.sample_lock_requests
FROM spanner_sys.lock_stats_total_minute t, spanner_sys.lock_stats_top_minute s
WHERE t.interval_end =
  (SELECT MAX(interval_end)
   FROM spanner_sys.lock_stats_total_minute)
AND s.interval_end = t.interval_end
ORDER BY s.lock_wait_seconds DESC;
查询输出
row_range_start_key total_lock_wait_seconds lock_wait_seconds frac_of_total sample_lock_requests
Songs(2,1,1) 0.184476 0.184476 1 LOCK_MODE: ReaderShared

COLUMN: Singers.SingerInfo

LOCK_MODE: WriterShared

COLUMN: Singers.SingerInfo
Users(3, 2020-11-01 12:34:56.426426+00:00) 5.21 2.37 0.454 LOCK_MODE: ReaderShared

COLUMN: users._exists1

LOCK_MODE: WriterShared

COLUMN: users._exists1

1 _exists 是一个内部字段,用于检查特定行是否存在。

数据保留

Cloud Spanner 至少为每个表保留以下时间段内的数据:

  • SPANNER_SYS.LOCK_STATS_TOP_MINUTESPANNER_SYS.LOCK_STATS_TOTAL_MINUTE:前 6 个小时中的时间段。

  • SPANNER_SYS.LOCK_STATS_TOP_10MINUTESPANNER_SYS.LOCK_STATS_TOTAL_10MINUTE:前 4 天中的时间段。

  • SPANNER_SYS.LOCK_STATS_TOP_HOURSPANNER_SYS.LOCK_STATS_TOTAL_HOUR:前 30 天中的时间段。

使用锁统计信息排查数据库中的锁冲突问题

在以下过程中,我们将为您展示如何使用锁统计信息调查数据库中的锁冲突。

假设您听说贵公司的应用运行速度缓慢。您可以按照如下步骤调查问题。

选择要调查的时间段

您检查了 Cloud Spanner 数据库的延迟时间指标,并确定了您的应用发生高延迟、高 CPU 占用率的时间段。例如,假设问题于 2020 年 11 月 12 日晚上 10:50 左右开始出现。

确定在选定时间段内,事务提交延迟时间是否随着锁等待时间的延长而延长

锁是通过事务获取的,因此,如果锁冲突导致等待时间过长,我们应该能够看到事务提交延迟时间与锁等待时间同时延长。

选择开始调查的时间段后,我们将此事务期间的事务统计信息 TXN_STATS_TOTAL_10MINUTE 与大约在此时间范围内的锁统计信息 LOCK_STATS_TOTAL_10MINUTE 相联接,以便我们了解锁等待时间的延长是否会造成平均提交延迟时间延长。

SELECT t.interval_end, t.avg_commit_latency_seconds, l.total_lock_wait_seconds
FROM spanner_sys.txn_stats_total_10minute t
LEFT JOIN spanner_sys.lock_stats_total_10minute l
ON t.interval_end = l.interval_end
WHERE
  t.interval_end >= "2020-11-12T21:50:00Z"
  AND t.interval_end <= "2020-11-12T23:50:00Z"
ORDER BY interval_end;

我们以下面的数据为例,作为我们从查询中得到的结果。

interval_end avg_commit_latency_seconds total_lock_wait_seconds
2020-11-12 21:40:00-07:00 0.002 0.090
2020-11-12 21:50:00-07:00 0.003 0.110
2020-11-12 22:00:00-07:00 0.002 0.100
2020-11-12 22:10:00-07:00 0.002 0.080
2020-11-12 22:20:00-07:00 0.030 0.240
2020-11-12 22:30:00-07:00 0.034 0.220
2020-11-12 22:40:00-07:00 0.034 0.218
2020-11-12 22:50:00-07:00 3.741 780.193
2020-11-12 23:00:00-07:00 0.042 0.240
2020-11-12 23:10:00-07:00 0.038 0.129
2020-11-12 23:20:00-07:00 0.021 0.128
2020-11-12 23:30:00-07:00 0.038 0.231

以上结果表明,avg_commit_latency_secondstotal_lock_wait_seconds2020 年 11 月 12 日 22:40:002020 年 11 月 12 日 22:50:00 这同一时间段内显著增加,此后又下降了。需要注意的是,avg_commit_latency_seconds 只是提交步骤所花费的平均时间。另一方面,total_lock_wait_seconds 是该时间段的聚合锁定时间,因此显著长于事务提交时间。

至此,我们已经确认了锁等待时间与写入延迟时间的延长有密切关系,下一步我们来研究是哪些行和列造成了较长的等待时间。

确定哪些行键和列在选定时间段内具有较长的锁等待时间

为确定在我们调查的时间段内,哪些行键和列遭遇了较长的锁等待时间,我们查询 LOCK_STAT_TOP_10MINUTE 表,这会列出对锁等待时间有最大影响的行键和列。

以下查询中的 CAST() 函数会将 row_range_start_key BYTES 字段转换为 STRING 字段。

SELECT CAST(s.row_range_start_key AS STRING) AS row_range_start_key,
       t.total_lock_wait_seconds,
       s.lock_wait_seconds,
       s.lock_wait_seconds/t.total_lock_wait_seconds frac_of_total,
       s.sample_lock_requests
FROM spanner_sys.lock_stats_total_10minute t, spanner_sys.lock_stats_top_10minute s
WHERE
  t.interval_end = "2020-11-12T22:50:00Z" and s.interval_end = t.interval_end;
row_range_start_key total_lock_wait_seconds lock_wait_seconds frac_of_total sample_lock_requests
Singers(32) 780.193 780.193 1 LOCK_MODE: WriterShared

COLUMN: Singers.SingerInfo

LOCK_MODE: ReaderShared

COLUMN: Singers.SingerInfo

从这个结果表中,我们可以看到键为 SingerId=32Singers 表中发生了冲突。Singers.SingerInfo 是 ReaderShared 与 WriterShared 之间发生锁冲突的列。

这是一种常见的冲突类型,如果有一个事务尝试读取特定单元,而另一个事务尝试写入同一单元,就会发生这种冲突。现在,我们已经了解了事务具体在哪个数据单元上争用锁,所以在下一步中,我们要确定争用锁的事务。

查明哪些事务正在访问锁冲突中涉及的列

为了确定哪些事务尝试过读取或写入锁冲突所涉及的列,我们将在以下查询中将事务统计信息与锁统计信息相联接。

我们可以从 TXN_STATS_TOTAL_10MINUTE 中检索 fprintread_columnswrite_constructive_columnsavg_commit_latency_seconds 来识别这些事务。

SELECT
  fprint,
  read_columns,
  write_constructive_columns,
  avg_commit_latency_seconds
FROM spanner_sys.txn_stats_top_10minute t2
WHERE (
  EXISTS (
    SELECT * FROM t2.read_columns columns WHERE columns IN (
      SELECT DISTINCT(req.COLUMN)
      FROM spanner_sys.lock_stats_top_10minute t, t.SAMPLE_LOCK_REQUESTS req
      WHERE req.LOCK_MODE = "ReaderShared" AND t.interval_end ="2020-11-12T23:50:00Z"))
OR
  EXISTS (
    SELECT * FROM t2.write_constructive_columns columns WHERE columns IN (
      SELECT DISTINCT(req.COLUMN)
      FROM spanner_sys.lock_stats_top_10minute t, t.SAMPLE_LOCK_REQUESTS req
      WHERE req.LOCK_MODE = "WriterShared" AND t.interval_end ="2020-11-12T23:50:00Z"))
)
AND t2.interval_end ="2020-11-12T23:50:00Z"
ORDER BY avg_commit_latency_seconds DESC;

该查询识别了我们在所调查的时间段内的以下事务。

  • 尝试获取 ReaderShared 锁时从造成锁冲突的任何列中进行读取的事务。

  • 尝试获取 WriterShared 锁时向造成锁冲突的任何列写入的事务。

查询结果按 avg_commit_latency_seconds 列排序,因此可能受锁等待影响最大的的事务会优先返回。

fprint read_columns write_constructive_columns avg_commit_latency_seconds
1866043996151916800


['Singers.SingerInfo',
'Singers.FirstName',
'Singers.LastName',
'Singers._exists']
['Singers.SingerInfo'] 4.89
4168578515815911936 [] ['Singers.SingerInfo'] 3.65

如查询结果所示,有两个事务尝试访问 Singers.SingerInfo,这也是在该时间段内存在锁冲突的列。在查明导致锁冲突的事务之后,您就可以继续利用其指纹 fprint 分析事务,以识别导致此问题的潜在问题。

使用 fprint866604399611916800 检查事务后,我们发现事务中的 DML 未根据主键 SingerId 进行过滤。因此,这造成了全表扫描,并且在事务提交之前锁定了表。如果我们可以知道 SingerId,就可以将语句中的 WHERE 子句更新为 SingerId 上的条件。如果我们不知道待更新行的 ID,则可以使用单独的只读事务来提取 ID,然后发送另一个读写事务来根据 ID 更新行。

运用最佳做法以减少锁争用

在示例场景中,我们可以使用锁统计信息和事务统计信息,将问题缩小到那些在更新时不使用表的主键的事务。我们提出了根据事先想要更新的行的键,对事务进行改进的思路。

在分析解决方案中的潜在问题时,甚至在设计解决方案时,请考虑采用这些最佳做法来减少数据库中的锁冲突数量。

  • 请尽可能使用只读事务,因为它们不会获取任何锁。

  • 在读写事务中避免全表扫描。这包括在主键上写入 DML 条件或在使用 Read API 时分配特定键范围。

  • 在读写事务中读取数据后,尽快提交更改,以缩短锁定期限。读写事务可确保数据在您读取数据后仍保持不变,直到您成功提交更改为止。为实现此目的,事务需要在读取期间和提交期间锁定数据单元格。因此,如果您可以将锁定期保持在较短的程度上,那么事务就不太可能发生锁冲突。

  • 尽量使用小事务而非大事务,或者考虑为长时间运行的 DML 事务使用分区 DML。长时间运行的事务会获取长时间的锁,因此请尽可能考虑将涉及数千行的行拆分为多个较小的事务,并仅更新数百行。

  • 如果您不需要读写事务提供的保证,请避免在提交更改之前在读写事务中读取任何数据,例如在单独的只读事务中读取数据。确保数据在读取与提交之间保持不变的强大保障是大多数锁冲突的发生原因。因此,如果读写事务不会读取任何数据,则无需长时间锁定单元。

  • 遵循架构设计最佳做法

后续步骤