admin

  • 标题: OceanBase创始人阳老师荣誉退休,致敬并祝福;Databricks10亿美金收购创新数据库Neon;MySQL 30周年中国区活动下周举行

    重点更新

    OceanBase创始人阳老师荣誉退休,致敬并祝福[11]

    RedisSSPL协议改成更被社区接受的AGPL协议, 并发布 8.0 版本 [10]

    Databricks 持续并购,将以 10 亿美元收购极具创新架构的开源数据库 Neon[1][2] 。Neon 提供了Serverless、扩展能力极强、具备非常好的AI特性的数据库服务,Neon 在架构上最大限度的利用了云基础设施能力,如对象存储的性能/持久性/扩展性/可复制性等、EC2的快速扩展/随时可用等,再叠加了PostgreSQL极强的生态能力。

    MySQL 30周年中国区活动将于下周再成都举行,感兴趣的可以去现场围观[3]。时间:5月23日-24日(13:00-17:50);地址:成都市武侯区吉庆四路188号IMC国际广场成都书声科技(叫叫)四楼演播厅[3]

    其他值得关注的产品更新包括:

    • Google MCP 数据库工具箱支持 Bigtable 连接器 [63]
    • OCI上Oracle Database 23ai 支持使用数据库内转换模型与 Select AI RAG 结合使用,简化向量模型处理[84]

    更新详情

    阿里云
    • RDS MySQL标准版云盘只读实例新增128核512 GB规格(mysqlro.x4.16xlarge.1c)[x]。
    • RDS MySQL 高可用系列升级为集群系列—高可用系列升级为集群系列时支持保留数据库代理。[4]
    Azure(微软云)
    • Cosmos DB for MongoDB 中的数据 API 发布 [5]
    GCP(谷歌云)
    • BigQuery 新增多项 SQL 功能中[9]
    • Bigtable Studio 支持导出查询结果 [13]
    • AlloyDB 支持在 AlloyDB Studio 中使用 IAM 身份验证 [27]
    • 支持使用 Cloud SQL for PostgreSQL 备份迁移到 AlloyDB for PostgreSQL[39]
    • Dataflow 支持构建向量嵌入的提取、转换和加载 (ETL) 数据到AlloyDB [41]
    • Cloud SQL Enterprise Plus 版支持AI 辅助故障排除 [44]
    • Google MCP 数据库工具箱支持 Bigtable 连接器 [63]
    Oracle云
    • Oracle Database 23ai 支持使用数据库内转换模型与 Select AI RAG 结合使用,简化向量模型处理[84]
    火山云(字节)
    • 云数据库 MySQL 支持恢复数据至原实例和批量回档场景下,支持极速恢复,加速恢复任务,提升整体恢复效率[8]
    • MongoDB 新增支持设置默认白名单,设置默认白名单后,新建实例时会自动选中默认白名单。[89]
    • MongoDB 事件中心新增支持展示事件描述信息,当执行了按备份文件恢复新实例、按时间点恢复到新实例或恢复已删除实例操作后,您可以在事件中心找到对应的事件操作,并在事件描述中查看源实例和新实例的实例 ID 信息。[90]
    百度云
    • ((2025-5-9)) PegaDB支持本地盘[94]
    AWS(亚马逊云)
    • RDS 支持 MySQL 8.0.42[95] 8.4.5 [96]
    • Aurora MySQL 3.09(兼容 MySQL 8.0.40)现已正式发布[99]
    • Aurora 和 RDS for PostgreSQL、MySQL 和 MariaDB 现提供 R8g 和 M8g 实例的预留实例[101]
    • Aurora 和 RDS for PostgreSQL、MySQL 和 MariaDB 现提供适用于 R7i 和 M7i 实例的预留实例[102]
    • RDS for PostgreSQL 支持次要版本 17.5、16.9、15.13、14.18 和 13.21[109]
    • Aurora PostgreSQL Limitless 数据库现在支持 PostgreSQL 16.8[111]
    腾讯云
    • TDSQL-C MySQL 版、云数据库 MySQL发布数据库代理版本1.3.17。[124][130]
    • TDSQL-C MySQL、云数据库 MySQL 只读分析引擎发布了全新内核版本1.2404.24.0与2.2410.6.0。[125]
    • 云数据 MySQL 支持备份保留设置功能,可避免实例误删除导致备份被立即销毁,确保数据可追溯和可恢复性。[126]
    • TDSQL-C MySQL 版、云数据库 MySQL 只读分析引擎实例支持对实例配置进行变更。[127][132]
    • TDSQL-C MySQL 版、云数据库 MySQL 只读分析引擎实例支持了多节点高可用能力。[128][133]
    • TDSQL-C MySQL 版、云数据库 MySQL 只读分析引擎实例支持了配置告警能力。[129][134]
    • ((2025-05-09)) 云数据库 SQL Server 支持表级别账号授权,可针对某一个账号对表的权限进行授予[135]

    参考链接

  • Oracle 云(即Oracle Cloud Infrastructure,简称OCI )的免费策略大概是所有云厂商中最为彻底与直接的。对于注册的账号,可以持续免费使用主要的云资源,包括虚拟主机(VM.Standard.E2.1.Micro)、数据库(1OCPU 16GB内存)等。

    相比于 AWS 的 一个月免费、GCP的 $300 代金券,OCI在免费策略上是最具诚意的云厂商。在使用方式上,也更具有诚意,不太因为使用超时、忘记关闭、规格选择错误等因素,而造成误收费,这些在AWS、GCP上都是很容易发生的。

    创建免费的数据库

    创建免费的 HeatWave MySQL 实例

    在创建数据库时,就可以选择“Always Free”选项,这时,在后续规格选择的时候,就不会误选成计费实例了。

    开启免费的 HeatWave 实例

    HeatWave是最近几年Oracle/MySQL最重要的发展方向,可以非常好的支持各种复杂与分析类的查询,很好的弥补了MySQL在分析能力上的短板。

    “Always Free” 也非常友好的支持了 HeatWave 相关的特性,可以让开发者非常好的体验HeatWave相关功能。

    创建实例账号

    创建数据库的管理员账号:

    查看免费实例

    在完成实例创建后,实例详情,可以看到“Always Free”标签:

    创建虚拟主机

    选择免费的虚拟主机规格

    这里需要注意,目前支持的免费规格,需要在分类“Virtual Machine->Specialty and previous generation”中选择:

    虚拟主机的基础选项

    查看实例状态

    在虚拟主机的实例列表页,可以查看该实例,并且看到免费标签“Always Free”:

  • Protected: 古城荆州

    ·

    This content is password protected. To view it please enter your password below:

  • 理解 NumPy 中的高维数组

    ·

    在机器学习中大量的使用NumPy作为其基础的数据结构,ndarrayNumPy的核心数据对象。对于ndarray高维数组的一个非常容易产生的误解是,使用数学中的矩阵(或者叫“行列式”)概念去尝试理解更高维的场景,或者使用更高维空间去理解,这样都会导致难以较好的理解更高维(5或6维)的数组。本文使用较为直观的示例和可视化的展示,更为“标准”(文档推荐的)的方式去理解ndarray的更高维数组。更多详细内容,可以参考阅读:

    问题

    在机器学习中,经常要对多维的数组做各种操作。对高维数组建立更好的直觉理解,则有利于去理解这些操作。例如,我们考虑右侧的代码,想一想该代码的输出是什么?

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape
    (考虑输出是什么)

    要回答这个问题,则需要建立对于多维数组结构的理解。

    文档中对于高维数组理解的建议

    NumPy: the absolute basics for beginners中有如下一段话:

    It is familiar practice in mathematics to refer to elements of a matrix by the row index first and the column index second. This happens to be true for two-dimensional arrays, but a better mental model is to think of the column index as coming last and the row index as second to last. This generalizes to arrays with any number of dimensions.

    “矩阵”是“线性代数”的主要研究对象,一个\( m \times n \)的矩阵即是一个平面上的\( m \)行\( m \)列的行列式。一种常见的向高维扩展的思考方式是,会将三维数组扩展为三维空间中的数组。但,这样的扩展,非常不利于去理解更高维的数组。这里提到的方案是这样:“a better mental model is to think of the column index as coming last and the row index as second to last.”。

    这种理解,也是本文的核心,概况如下:

    • 总是将最后一个维度理解为列维度
    • 总是将倒数第二个维度理解为行维度
    • 剩余的维度,则是通过层的方式去构建

    从 4×5 的数组开始

    先通过直观的书写表达,看看这样的数组应该是怎样的。

    一般的矩阵(行列式表示):

    \begin{bmatrix}
    0 & 1 & 2 & 3 & 4 \\
    5 & 6 & 7 & 8 & 9 \\
    10 & 11 & 12 & 13 & 14 \\
    15 & 16 & 17 & 18 & 19 \\
    \end{bmatrix}

    本文推荐的理解方式:

    \[
    \begin{bmatrix}
    [0 & 1 & 2 & 3 & 4] \\
    [5 & 6 & 7 & 8 & 9] \\
    [10 & 11 & 12 & 13 & 14] \\
    [15 & 16 & 17 & 18 & 19]
    \end{bmatrix}
    \]

    numpy的输出:

    >>> np.arange(20).reshape(4,5)
    array([[ 0,  1,  2,  3,  4],
           [ 5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14],
           [15, 16, 17, 18, 19]])

    如果按照“矩阵”思想去理解这个矩阵很简单。但这里,我们重新按照上述的原则去理解这个数组。即:

    • 最后一个维度(即第二个维度),该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即第一个维度,该维度的长度是4,将其理解为维度

    形式上,这与一般的矩阵,是完全一致的。只是,思维方式,反过来了。

    再考虑 3x4x5 的数组

    这个数组已经不能用简单的平面表示了,这里使用了符合上述描述的形式描述,“剩余的维度,则是通过层的方式去构建”,则有:

    本文推荐的理解方式:

    \[
    \begin{array}{r c}
    \text{Layer 1:} &
    \left[
    \begin{array}{c}
    [0 & 1 & 2 & 3 & 4] \\
    [5 & 6 & 7 & 8 & 9] \\
    [10 & 11 & 12 & 13 & 14] \\
    [15 & 16 & 17 & 18 & 19] \\
    \end{array}
    \right]
    \\
    \text{Layer 2:} &
    \left[
    \begin{array}{c}
    [20 & 21 & 22 & 23 & 24] \\
    [25 & 26 & 27 & 28 & 29] \\
    [30 & 31 & 32 & 33 & 34] \\
    [35 & 36 & 37 & 38 & 39] \\
    \end{array}
    \right]
    \\
    \text{Layer 3:} &
    \left[
    \begin{array}{c}
    [40 & 41 & 42 & 43 & 44] \\
    [45 & 46 & 47 & 48 & 49] \\
    [50 & 51 & 52 & 53 & 54] \\
    [55 & 56 & 57 & 58 & 59] \\
    \end{array}
    \right]
    \end{array}
    \]

    >>> np.arange(60).reshape(3,4,5)
    array([[[ 0,  1,  2,  3,  4],
            [ 5,  6,  7,  8,  9],
            [10, 11, 12, 13, 14],
            [15, 16, 17, 18, 19]],
    
           [[20, 21, 22, 23, 24],
            [25, 26, 27, 28, 29],
            [30, 31, 32, 33, 34],
            [35, 36, 37, 38, 39]],
    
           [[40, 41, 42, 43, 44],
            [45, 46, 47, 48, 49],
            [50, 51, 52, 53, 54],
            [55, 56, 57, 58, 59]]])

    这时,矩阵的想法就不太好用了。这里继续按照上面的原则,考虑:

    • 最后一个维度,即这里的第三个维度,该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即这里第二个维度,该维度的长度是4,将其理解为维度
    • 倒数第三个维度(第一个维度)该维度长度是3,将其理解为行列式前面的

    循着这样的思考模式,不断地叠加更多的“层”,就可以理解更高维度的数组了。

    考虑 2x3x4x5 的数组

    这里先试用“层”的思维,可视化的表示该数组如下:

    \[
    \left[
    \begin{array}{c}
    \text{Layer}^{(0)}_1
    \left[
    \begin{array}{c}
    \text{Layer}^{(1)}_1
    \left[
    \begin{array}{c}
    [000 & 001 & 002 & 003 & 004] \\
    [005 & 006 & 007 & 008 & 009] \\
    [010 & 011 & 012 & 013 & 014] \\
    [015 & 016 & 017 & 018 & 019] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_2
    \left[
    \begin{array}{c}
    [020 & 021 & 022 & 023 & 024] \\
    [025 & 026 & 027 & 028 & 029] \\
    [030 & 031 & 032 & 033 & 034] \\
    [035 & 036 & 037 & 038 & 039] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_3
    \left[
    \begin{array}{c}
    [040 & 041 & 042 & 043 & 044] \\
    [045 & 046 & 047 & 048 & 049] \\
    [050 & 051 & 052 & 053 & 054] \\
    [055 & 056 & 057 & 058 & 059] \\
    \end{array}
    \right]
    \end{array}
    \right] \\
    \text{Layer}^{(0)}_2
    \left[
    \begin{array}{c}
    \text{Layer}^{(1)}_1
    \left[
    \begin{array}{c}
    [060 & 061 & 062 & 063 & 064] \\
    [065 & 066 & 067 & 068 & 069] \\
    [070 & 071 & 072 & 073 & 074] \\
    [075 & 076 & 077 & 078 & 079] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_2
    \left[
    \begin{array}{c}
    [080 & 081 & 082 & 083 & 084] \\
    [085 & 086 & 087 & 088 & 089] \\
    [090 & 091 & 092 & 093 & 094] \\
    [095 & 096 & 097 & 098 & 099] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_3
    \left[
    \begin{array}{c}
    [100 & 101 & 102 & 103 & 104] \\
    [105 & 106 & 107 & 108 & 109] \\
    [110 & 111 & 112 & 113 & 114] \\
    [115 & 116 & 117 & 118 & 119] \\
    \end{array}
    \right]
    \end{array}
    \right]
    \end{array}
    \right]
    \]

    >>> np.arange(120).reshape(2,3,4,5)
    array([[[[  0,   1,   2,   3,   4],
             [  5,   6,   7,   8,   9],
             [ 10,  11,  12,  13,  14],
             [ 15,  16,  17,  18,  19]],
    
            [[ 20,  21,  22,  23,  24],
             [ 25,  26,  27,  28,  29],
             [ 30,  31,  32,  33,  34],
             [ 35,  36,  37,  38,  39]],
    
            [[ 40,  41,  42,  43,  44],
             [ 45,  46,  47,  48,  49],
             [ 50,  51,  52,  53,  54],
             [ 55,  56,  57,  58,  59]]],
    
    
           [[[ 60,  61,  62,  63,  64],
             [ 65,  66,  67,  68,  69],
             [ 70,  71,  72,  73,  74],
             [ 75,  76,  77,  78,  79]],
    
            [[ 80,  81,  82,  83,  84],
             [ 85,  86,  87,  88,  89],
             [ 90,  91,  92,  93,  94],
             [ 95,  96,  97,  98,  99]],
    
            [[100, 101, 102, 103, 104],
             [105, 106, 107, 108, 109],
             [110, 111, 112, 113, 114],
             [115, 116, 117, 118, 119]]]])

    继续按照上面的原则,考虑:

    • 最后一个维度,即这里的第四个维度,该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即这里第三个维度,该维度的长度是4,将其理解为维度
    • 倒数第三个维度,即第二个维度,该维度的长度是3,将其理解为行列式前面的
    • 倒数第四个维度,即第一个维度,该维度的长度是2,将其理解为行列式前面的层的层,也就是上述的“Layer”

    使用这样的模式,就可以将一个多维数组的表示平面化。并且注意到,这与ndarray输出的形式是几乎完全一致的。

    回到前面的问题

    有了上面的可视化展示以及上面逐步的介绍,应该可以更容易理解前面NumPy: the absolute basics for beginners所提到的直觉“a better mental model is to think of the column index as coming last and the row index as second to last”。

    有了这个直觉,我们再来考虑最前面提到的问题:

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape

    最内层的列,就是最后的维度长度,这里是 1,所以就是 ? x 1;该列所对应的行数,就是倒数第二个维度的长度,这里做如下的格式化,可以看到有两行,所以这是一个? x 2 x 1的数组;再向上看一层,共有两个该2x1的数组,故,该数组的shape时:2x2x1

    [
      [
        [1],
        [2]
      ],
      [
        [3],
        [4]
      ]
    ]

    再确认最后的输出:

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape
    (2, 2, 1)

    最后

    这种理解的核心即是“a better mental model is to think of the column index as coming last and the row index as second to last”,简单概括如下:

    • 总是将最后一个维度理解为列维度
    • 总是将倒数第二个维度理解为行维度
    • 剩余的维度,则是通过层的方式去构建

  • Oracle LogMiner 使用

    ·

    Oracle官方文档Using LogMiner to Analyze Redo Log Files[2]中,对该功能有详细的介绍,包括了LogMiner的配置与使用、数据过滤、补充日志(Supplemental Logging)、使用示例等。

    Oracle 何时引入的LogMiner?

    自 1999 年发布 Oracle 8i 的时候,正式引入 LogMiner 功能(参考:Redo Log Analysis Using LogMiner[1])。该功能支持以SQL的形式分析redo中的数据,最初考虑的应用场景,主要还是偏于故障恢复、异常诊断、审计等,但是该功能的潜力很大,现在已经逐步成为Oracle CDC的主流方案之一。

    目前已经有很多的集成/同步工具都使用LogMiner进行变化数据获取,虽然,目前官方依旧不推荐这么做,文档中的原文如下:“Note:LogMiner is intended for use as a debugging tool, to extract information from the redo logs to solve problems. It is not intended to be used for any third party replication of data in a production environment.”

    根据经验来看,LogMiner用于数据集成并没有什么太大的问题。

    打开补充日志

    ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;

    查看归档日志

    SQL> SELECT name FROM v$archived_log ORDER BY FIRST_TIME DESC FETCH FIRST 3 ROWS ONLY;
    
    NAME
    ----------------------------------------------------------------------------------------------------
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_761_n0mbccbd_.arc
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_760_n0mbcc62_.arc
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_759_n0m6tdfp_.arc

    添加需要解析的日志文件

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/PATH_TO_YOUR_ARCHIVE/o1_mf_1_761_n0mbccbd_.arc',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /

    例如,实际的SQL可能是如下的样子:

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_955_n0mwc01v_.arc',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /
    
    o1_mf_1_955_n0mwc01v_.arc
    o1_mf_1_909_n0mv4sc3_.arc
    o1_mf_1_908_n0mv4ohs_.arc

    启动LogMiner

    启动时,可以带不同的参数以指定LogMiner不同的行为。

    使用在线数据字典启动
    EXECUTE DBMS_LOGMNR.START_LOGMNR( -
       OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);
    使用日志中的数据字典启动
    EXECUTE DBMS_LOGMNR_D.BUILD( -
        OPTIONS => DBMS_LOGMNR_D.STORE_IN_REDO_LOGS);

    获取LogMiner中的变更数据

    获取变更数据
    SELECT 
      SCN,
      TIMESTAMP,
      OPERATION,
      SQL_REDO,
      SQL_UNDO,
      SEG_OWNER,
      TABLE_NAME,
      USERNAME
    FROM 
      V$LOGMNR_CONTENTS
    WHERE 
        OPERATION IN ('INSERT', 'UPDATE', 'DELETE')
    --  AND SEG_OWNER = 'TEST_USER'
        AND (TABLE_NAME = 'T2' OR TABLE_NAME = 't2')
    ORDER BY TIMESTAMP DESC
    FETCH FIRST 3 ROWS ONLY;

    退出 LogMiner

    EXECUTE DBMS_LOGMNR.END_LOGMNR();

    解析未归档的 Redo 日志

    除了解析归档之外,LogMiner 可以直接解析当前正在使用的 redo 文件。先根据小节“获取当前正在使用的redo文件”中的SQL获取当前正在使用的 redo 文件,然后在添加日志文件时,像上述添加归档一样添加即可。

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/u04/app/oracle/redo/redo003.log',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /

    使用上述(小结“获取变更数据”)的SQL可以获得如下的输出:

           SCN TIMESTAMP OPERATION	  SQL_REDO
    ---------- --------- ------------ ---------------------------------------------
       3175619 24-APR-25 INSERT       insert into "SYS"."T2"("ID") values ('31');
       3175846 24-APR-25 INSERT       insert into "SYS"."T2"("ID") values ('32');

    一些常见的 SQL

    获取归档日志

    获取最新的ARCHIVE LOG的列表,以及对应的SCN号范围:

    SELECT 
      FIRST_CHANGE#,
      TO_CHAR(FIRST_TIME, 'YYYY-MM-DD HH24:MI:SS'),
      NEXT_CHANGE# ,
      TO_CHAR(NEXT_TIME, 'YYYY-MM-DD HH24:MI:SS'),
      NAME 
    FROM  
      V$ARCHIVED_LOG 
    ORDER BY NEXT_CHANGE# DESC 
    FETCH FIRST 3 ROWS ONLY;
    强制切换归档日志文件
    ALTER SYSTEM SWITCH LOGFILE;
    获取当前正在使用的redo文件
    SELECT A.GROUP#,A.MEMBER, B.STATUS
    FROM   V$LOGFILE A
    JOIN   V$LOG B ON A.GROUP# = B.GROUP#
    WHERE  B.STATUS = 'CURRENT'; 

    参考

  • 为什么需要间隙锁

    关于为什么需要 “Gap Locks” 或者 “Next-key Locks” ,在MySQL的文档“17.7.1 InnoDB Locking”的小节中有较为详细的介绍,这里不再赘述。这里使用一个具体的示例,以便开发者方便构造与观察间隙锁。

    Repeatable-Read隔离级别下,在整个事务的过程中,数据需要保持一致,经常需要使用间隙锁对数据或资源进行保护。例如,在如下的事务中:

    DROP TABLE IF EXISTS t1;
    CREATE TABLE t1 (
      id int,
      nick varchar(32),
      age int,
      KEY ind_n (nick),
      PRIMARY KEY (id)
    );
    
    INSERT INTO t1 VALUES ( 1, "a", 27 );
    INSERT INTO t1 VALUES ( 11, "k" ,23 );
    INSERT INTO t1 VALUES ( 24, "x" ,22 );
    
    START TRANSACTION;
    SELECT * FROM t1 WHERE nick >= "k" for update;
    +----+------+------+
    | id | nick | age  |
    +----+------+------+
    | 11 | k    |   23 |
    | 24 | x    |   22 |
    +----+------+------+
    2 rows in set (0.00 sec)  Warnings: 0

    在上述事务中,为了实现将 SELECT 涉及的“相关记录”全部加上“排它锁”,从而阻止其他事务对该部分数据进行修改。即,如果有任何其他的事务,尝试修改该事务中的“相关记录”,都需要被阻塞。这里的“相关记录”,具体是指:WHERE nick >= "k" 查询扫描到的索引入口(二级索引),以及对应的数据(即主键入口)。

    试想,如果有其他事务尝试写入一条 nick = 'm'的记录,那么上述的...for update语句则也会返回该记录。为了阻止上述的不一致,上述事务不仅要对单个记录或索引入口进行加锁,还需要对索引入口之间的间隙进行加锁。

    具体的,该案例中详细的锁信息如下:

    mysql> SELECT
        ENGINE_TRANSACTION_ID AS TRX_ID,     OBJECT_NAME,     
        INDEX_NAME, LOCK_MODE,     LOCK_STATUS,     
        LOCK_DATA    FROM performance_schema.data_locks 
      WHERE LOCK_TYPE="RECORD";
    +--------+-------------+------------+---------------+-------------+------------------------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_MODE     | LOCK_STATUS | LOCK_DATA              |
    +--------+-------------+------------+---------------+-------------+------------------------+
    |  10703 | t1          | ind_n      | X             | GRANTED     | supremum pseudo-record |
    |  10703 | t1          | ind_n      | X             | GRANTED     | 'k', 11                |
    |  10703 | t1          | ind_n      | X             | GRANTED     | 'x', 24                |
    |  10703 | t1          | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 11                     |
    |  10703 | t1          | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 24                     |
    +--------+-------------+------------+---------------+-------------+------------------------+

    上述的锁信息显示,该事务获得了索引 (a,supremum)间隙的排它锁,以及对应的数据记录锁。

    如果说上述案例中的 SELECT ... FOR UPDATE在实际中不那么常见的话,类似的直接使用UPDATE语句也需要类似的加锁信息,这里使用SELECT ... FOR UPDATE展示则更为直接。

    InnoDB 中“间隙锁”的表示

    理解 InnoDB 锁的困难有很多,其中一个非常大的困难就是理解在 InnoDB 间隙锁的表示,因为 InnoDB 使用了一个比较反直觉的模式去实现间隙锁。具体的:InnoDB 在表示间隙锁的时候,并没有把 GAPNext-Key当做一个“资源”。而是依旧把Record当做资源,然后,以“锁类型的”(Lock Mode)不同的取值表示记录锁或间隙锁。例如,

    • Lock Mode取值为S,GAP时,则表示间隙锁,锁类型为S
    • Lock Mode取值为S,REC_NOT_GAP时,则表示记录锁,锁类型为S

    锁的对象,均为索引入口或主键入口。

    关于该实现,在InnoDB Data Locking – Part 2 “Locks”中有着较为详细的描述:

    “Even without knowing too much about how databases like InnoDB operate, we can guess, that sometimes the operation involves just the record, sometimes the gap before a record, and at yet another times we need to access both, the record and a gap. One way to model that, would be to consider records and gaps to be two different kinds of resources which you can lock independently. Current InnoDB implementation takes a different approach: there is just one resource for each point, but there are multiple kinds of access right you can request for it, and the access right specifies if you need the row, the gap or both parts. One benefit of this is that it is optimized for the most common case where you need both.”

    InnoDB Data Locking – Part 2 “Locks”

    锁类型与加锁模式

    这里使用如下表格表示了InnoDB在进行加锁时常见的加锁对象与类型。这里Dm表示具体的加锁数据,例如索引入口数据或主键数据。详细的表格如下:

    LOCK_DATA
    加锁目标/资源
    LOCK_MODE
    加锁模式
    加锁对象/
    资源
    加锁类型
    DmS,REC_NOT_GAPDmS
    DmX,REC_NOT_GAPDmX
    DmS,GAP(D(m-1),Dm)S
    DmX,GAP(D(m-1),Dm)X
    DmS(D(m-1),Dm]S
    DmX(D(m-1),Dm]X

    通常的,

    • performance_schema中看到的Dm取值可能是这样:0x000000000207
    • show engine innodb status\G中看到的Dm会是:0: len 4; hex 00000001; asc ;;
    • D(m-1)则表示在索引中,Dm前面的一个索引入口或主键取值
    • (...)表示开区间;(...]表示半开半闭区间

    show engine innodb status\G 输出示例:

    RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `sysbenchdb`.`t1` trx id 10094 lock mode S locks rec but not gap waiting
    Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
     0: len 4; hex 00000001; asc     ;;
     1: len 6; hex 00000000276d; asc     'm;;
     2: len 7; hex 810000008d0110; asc        ;;
     3: len 1; hex 61; asc a;;
     4: len 4; hex 8000000c; asc     ;;

    UPDATE 语句的间隙锁

    环境说明

    mysql> show variables like '%iso%';
    +-----------------------+-----------------+
    | Variable_name         | Value           |
    +-----------------------+-----------------+
    | transaction_isolation | REPEATABLE-READ |
    +-----------------------+-----------------+
    1 row in set (0.01 sec)

    场景构造

    Session ASession B
    DROP TABLE IF EXISTS t1;
    CREATE TABLE t1 (
    id int,
    nick varchar(32),
    age int,
    KEY ind_n (nick),
    PRIMARY KEY (id)
    );

    INSERT INTO t1 VALUES ( 1, "a", 27 );
    INSERT INTO t1 VALUES ( 11, "k" ,23 );
    INSERT INTO t1 VALUES ( 24, "x" ,22 );
    START TRANSACTION;
    UPDATE t1 SET age = 127 WHERE nick = "k";

    观测间隙锁

    mysql> SELECT      
      ENGINE_TRANSACTION_ID AS TRX_ID,     
      OBJECT_NAME,  INDEX_NAME, LOCK_MODE,     
      LOCK_STATUS,  LOCK_DATA
    FROM performance_schema.data_locks WHERE LOCK_TYPE="RECORD";
    +--------+-------------+------------+---------------+-------------+-----------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
    +--------+-------------+------------+---------------+-------------+-----------+
    |  10644 | t1          | ind_n      | X,GAP         | GRANTED     | 'x', 24   |
    |  10644 | t1          | ind_n      | X             | GRANTED     | 'k', 11   |
    |  10644 | t1          | PRIMARY    | X,REC_NOT_GAP | GRANTED     | 11        |
    +--------+-------------+------------+---------------+-------------+-----------+

    可以看到,在索引上(KEY ind_n (nick))的锁范围为:(a,x)。这个范围由两个锁构成,分别是:

    • X,GAP : 'x', 24 对应的范围为 (k,x)
    • X : 'k', 11 对应的范围为 (a,k]

    故,上述两个范围,共同构成的范围为:(a,x),锁类型都是排它锁(X)。

    测试验证

    可以尝试写入一个在 (a,k)(k,x)范围为的记录,观察阻塞情况。继续上述的两个Session,并执行如下的SQL 语句进行观察:

    Session ASession B
    START TRANSACTION;
    INSERT INTO t1 VALUES (2,"c",32);
    -- blocking / waiting
    ROLLBACK
    START TRANSACTION;
    INSERT INTO t1 VALUES (2,"m",32);
    -- blocking / waiting
    ROLLBACK

    参考链接