锁定统计信息

Spanner 提供锁统计信息,让您能够识别行键和 表列是在您的 特定时间段内的数据库。您可以使用 SQL 语句从 SPANNER_SYS.LOCK_STATS* 系统表中检索这些统计信息。

可用性

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

不支持 Spanner 提供的其他单次读取方法 SPANNER_SYS.

按行键划分的锁统计信息

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

  • SPANNER_SYS.LOCK_STATS_TOP_MINUTE:锁定等待时间最长的行键 次。

  • 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。
每个样本均包含以下三个字段: <ph type="x-smartling-placeholder">
    </ph>
  • lock_mode:所请求的锁定模式。对于 有关详情,请参阅锁定模式
  • column:遇到锁冲突的列。此值的格式为 tablename.columnname
  • transaction_tag:发出请求的事务的标记。如需详细了解如何使用标记,请参阅使用事务标记进行问题排查
导致锁定冲突的所有锁定请求 随机均匀抽样,因此可能只有一半的 冲突(容器持有者或等候程序)记录在此数组中。

锁定模式

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

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

  • ReaderShared 锁定 - 一种锁定,可让其他读取操作仍可访问 直到事务准备好提交为止。已获取此共享锁定 当读写事务读取数据时触发

  • WriterShared 锁定 - 执行读写事务时,会获取此锁定 它尝试提交写入

  • Exclusive 锁定 - 在执行读写操作时,获取独占锁定 已获取 ReaderShared 锁的事务,它会尝试写入 数据。独占锁定是 ReaderShared 锁。独占锁定是事务的特例 同时持有 ReaderShared 锁和 WriterShared 锁。 其他任何事务都无法获得同一单元上的任何锁。

  • WriterSharedTimestamp 锁 - 一种特殊的 WriterShared 锁, 在向具有 提交的表中插入新行时,会获取 timestamp。这种类型的锁 防止事务参与者创建完全相同的行,以及 因此彼此冲突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 控制台

列出前 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 使用率。例如,这个问题是在晚上 10:50 左右开始出现的 2020 年 11 月 12 日

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

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

选择开始调查的时间段后,我们将此事务期间的事务统计信息 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_seconds大幅提升 和total_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 是列 ReaderSharedWriterShared之间发生了锁定冲突。

当有一个事务试图 读取某个单元,而另一个事务则尝试写入同一个 单元格。我们现在知道交易竞争的确切数据单元格 所以在下一步中,我们将确定 锁争用

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

识别存在明显提交延迟的事务 特定时间间隔内发生锁冲突的问题,则需要查询 从 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_columns 列和 write_constructive_columns 列,用于确定 您的应用代码中触发了该事务。然后,您可以查看 不对主键 SingerId 进行过滤的底层 DML。这会导致 全表扫描并锁定表,直到事务被提交。

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

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

运用最佳实践减少锁争用

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

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

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

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

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

  • 在完成更改之后立即提交更改,以缩短锁定期 在读写事务中尽可能读取数据。读写 事务来保证在您读取完 数据,直至您成功提交更改。为了实现这一点, 事务需要在读取期间和 commit。因此,如果您可以保持较短的锁定期,则事务处理 从而降低出现锁定冲突的可能性。

  • 尽量使用小事务而非大事务,或者考虑为长时间运行的 DML 事务使用分区 DML。长期坚持 事务锁定很长时间,因此请考虑破坏 将数千行数据转换为 尽可能更新数百行的事务。

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

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

  • 尽量减少读写事务中的 API 调用。API 调用的延迟时间 可能导致 Spanner 中发生锁争用,因为 API 调用 可能会受到网络延迟和服务端延迟的影响。我们建议 在读写事务之外进行 API 调用。如果 因此必须在读写事务中执行 API 调用 监控 API 调用的延迟时间,以最大限度地降低对锁定的影响 。

  • 遵循架构设计最佳做法

后续步骤