锁定统计信息

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 无法存储有关在指定时间段内等待锁的每个行键范围的信息,则系统会优先存储该时间段内具有最长锁等待时间的行键范围。

  • 表中的所有列均可为 null。

表架构

列名 类型 说明
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:发出请求的事务的标记。如需详细了解如何使用标记,请参阅使用事务标记进行问题排查
所有导致锁冲突的锁请求均采用均匀随机抽样方式,因此此数组中可能只记录了冲突的一半(持有者或等待者)。

锁定模式

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

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

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

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

  • 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

以上结果表明,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.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 调用的延迟时间,以最大限度地减少对锁定获取时段的影响。

  • 遵循架构设计最佳做法

后续步骤