锁统计信息

Spanner 提供锁统计信息,以便您识别在特定时间段内是数据库中发生事务锁定冲突的主要原因的行键和表列。您可以使用 SQL 语句从 SPANNER_SYS.LOCK_STATS* 系统表中检索这些统计信息。

可用情况

SPANNER_SYS 数据只能通过 SQL 接口获得;例如:

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 小时的间隔以小时结束。在每个时间间隔后,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
  • Spanner 按起始行键范围对统计信息进行分组。

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

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

表架构

列名 类型 说明
INTERVAL_END TIMESTAMP 所含锁冲突发生的时间段结束。
ROW_RANGE_START_KEY BYTES(MAX) 发生锁冲突的行键。如果冲突涉及一个行范围,则此值表示该范围的起始键。加号 + 表示范围。如需了解详情,请参阅什么是行范围起始键
LOCK_WAIT_SECONDS FLOAT64 针对行键范围内的所有列记录到的锁冲突的累计锁等待时间,以秒为单位。
SAMPLE_LOCK_REQUESTS ARRAY<STRUCT<
  column STRING,
  lock_mode STRING,
   transaction_tag STRING>>
此数组中的每个条目都对应于一个示例锁定请求,该请求在给定行键(范围)上等待了锁,或阻止其他事务持有锁,因而造成了锁定冲突。此数组中的样本数上限为 20。
每个示例均包含以下三个字段:
  • lock_mode:所请求的锁定模式。如需了解详情,请参阅锁定模式
  • column:遇到锁冲突的列。此值的格式为 tablename.columnname
  • transaction_tag:发出请求的事务的标记。如需详细了解如何使用标记,请参阅使用事务标记进行问题排查
所有导致锁定冲突的锁请求都是随机采样的,因此可能只有冲突的一半(持有者或 waiter)记录在此数组中。

锁定模式

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

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

  • ReaderShared 锁 - 一种锁,可让其他读取在您的事务准备好提交之前仍可访问数据。当读写事务读取数据时,系统会获取此共享锁。

  • WriterShared Lock - 当读写事务尝试提交写入时,系统会获取此锁。

  • Exclusive 锁 - 当已获取 ReaderShared 锁的读写事务尝试在完成读取后写入数据时,系统会获取独占锁。独占锁定是从 ReaderShared 锁定的升级。独占锁是同时持有 ReaderShared 锁和 WriterShared 锁的事务的一种特殊情况。其他事务均无法获得同一单元的任何锁。

  • WriterSharedTimestamp 锁 - 一种特殊类型的 WriterShared 锁,在向具有提交时间戳作为主键一部分的表中插入新行时,会获取此锁。这种锁定可防止事务参与者创建完全相同的行,从而避免彼此冲突。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 还包含一些表,用于存储 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_* 表中捕获的统计信息包括 Spanner 在 SPANNER_SYS.LOCK_STATS_TOP_* 表中未捕获的锁定等待时间。

  • 这些表中的某些列在 Cloud Monitoring 中显示为指标。公开的指标包括:

    • 锁定等待时间

    如需了解详情,请参阅 Spanner 指标

表架构

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

示例查询

以下是一个可用于检索锁统计信息的 SQL 语句的示例。您可以使用客户端库gcloud spannerGoogle Cloud 控制台运行这些 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) 2.37 1.76 0.7426 LOCK_MODE: ReaderShared

COLUMN: Singers.SingerInfo

LOCK_MODE: WriterShared

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

COLUMN: users._exists1

LOCK_MODE: WriterShared

COLUMN: users._exists1

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

数据保留

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 天中的时间段。

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

您可以使用 SQL 或锁定数据分析信息中心来查看数据库中的锁定冲突。

以下主题介绍了如何使用 SQL 代码调查此类锁冲突。

选择要调查的时间段

查看 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

上述结果显示,在 2020-11-12 22:40:002020-11-12 22:50:00 的同一时间段内,avg_commit_latency_secondstotal_lock_wait_seconds 的数值大幅增加,此后下降。需要注意的一点是,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.SingerInfoReaderSharedWriterShared 之间发生锁定冲突的列。

当有一个事务尝试读取某个单元,而另一个事务尝试向同一单元写入时,这是一种常见的冲突类型。现在,我们已经知道事务争用锁的确切数据单元,因此在下一步中,我们将确定正在争用锁的事务。

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

如需确定在特定时间间隔内因锁冲突而出现明显提交延迟的事务,您需要从 SPANNER_SYS.TXN_STATS_TOTAL_10MINUTE 表中查询以下列:

  • fprint
  • read_columns
  • write_constructive_columns
  • avg_commit_latency_seconds

您需要过滤从 SPANNER_SYS.LOCK_STATS_TOP_10MINUTE 表标识的锁定列:

  • 事务在尝试获取 ReaderShared 锁时读取发生了锁冲突的任何列。

  • 写入数据到任何列,从而在尝试获取 WriterShared 锁时产生锁冲突的事务。

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;

查询结果按 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 来分析事务,以确定导致锁冲突的潜在问题。

在使用 fprint=1866043996151916800 查看事务后,您可以使用 read_columnswrite_constructive_columns 列来识别应用代码的哪一部分触发了事务。然后,您可以查看未按主键 SingerId 进行过滤的底层 DML。这会导致执行全表扫描并锁定表,直到事务提交为止。

如需解决锁定冲突,您可以执行以下操作:

  1. 使用只读事务来识别所需的 SingerId 值。
  2. 使用单独的读写事务来更新所需 SingerId 值的行。

运用最佳实践减少锁争用

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

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

  • 在读写事务中避免大量读取

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

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

  • 在读写事务中读取数据后,尽快提交更改,以缩短锁定期。读写事务可保证在您读取数据后,数据保持不变,直到您成功提交更改为止。为此,事务需要在读取和提交期间锁定数据单元。因此,如果您可以保持较短的锁定期,事务不太可能发生锁定冲突。

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

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

  • 仅指定读写事务中所需的最小列集。由于 Spanner 锁是针对每个数据单元,因此当读写事务读取的列过多时,它会获取这些单元的 ReaderShared 锁。当其他事务在写入过多列时获取 WriterShared 锁时,这可能会导致锁冲突。例如,考虑在读取时指定一组列,而不是 *

  • 尽可能减少读写事务中的 API 调用。API 调用的延迟可能会导致 Spanner 中出现锁争用,因为 API 调用会受到网络延迟以及服务端延迟的影响。建议尽可能在读写事务之外进行 API 调用。如果您必须在读写事务内执行 API 调用,请务必监控 API 调用的延迟时间,以最大限度地降低对锁定获取期的影响。

  • 遵循架构设计最佳做法

后续步骤