MySQL

  • 存储引擎在存储整数时,一般会使用最高位作为标志位,标记存储的整数是正数还是负数(参考),最高位也被称为“most significant bit (MSb)”。通常,最高位为1则表示正数,最高位为0,则表示负数。更进一步的,负数则会通过补码(参考:two’s complement)的方式表示。但是,InnoDB没有使用这种方法。

    InnoDB 的整数存储

    在死锁诊断时,偶然注意到,InnoDB 在存储整数时,与一般的系统是不同的。例如,int 类型存储 1 的时候,使用的表示是:0x80000001。更多的示例可以参考右图:

    整数值InnoDB 表示
    10x80000001
    -10x7fffffff
    70x80000007
    -70x7ffffff9

    可以看到,这与一般的有符号型的整数存储是相反的。即:

    • 正数表示时,最高位(MSb)为1
    • 负数表示时,最高位(MSb)为0

    关于这个问题,在 Stackoverflow上也有看到有部分用户有类似的疑问:

    本文将讨论为什么会这样。

    考虑 8-bit 场景下的

    这里来回顾一下“体系结构”中的最为基础的一些知识吧。

    整数值绝对值绝对值的二进制原码2-补码Offset binary(“移码”)
    110000-00010000-00010000-00011000-0001
    -110000-00011000-00011111-11110111-1111
    770000-01110000-01110000-01111000-0111
    -770000-01111000-01111111-100101111001

    说明:

    移码有两种计算方式,结果是等价的,即:

    • 直接将原始数据加上2(n-1),然后转化为二进制即可以
    • 将其补码,最高位进行一次翻转,即 “补码 XOR 2(n-1)

    验证存储方式

    为了确认 InnoDB 的整数处理,再MySQL 8.4.4的源码中找到如下 InnoDB 处理整型数据的代码:

      if (type == DATA_INT) {
        /* Store integer data in Innobase in a big-endian format,
        sign bit negated if the data is a signed integer. In MySQL,
        integers are stored in a little-endian format. */
    
        byte *p = buf + col_len;
    
        for (;;) {
          p--;
          *p = *mysql_data;
          if (p == buf) {
            break;
          }
          mysql_data++;
        }
    
        if (!(dtype->prtype & DATA_UNSIGNED)) {
          *buf ^= 128;
        }
    
        ptr = buf;
        buf += col_len;

    这段代码中,先将字节序做了颠倒(从最高字节位开始,逐个字节进行拷贝存储),即将 MySQL 层面的小端(little-endian)转化为了InnoDB层面的(big-endian)存储。而后,再对最高位进行了一次翻转,即这里的:*buf ^= 128操作。

    即:先将数据在MySQL层面的表示做了大小端的转化并拷贝过来,然后,将最高位进行翻转。即,先将2补码的表示模式拷贝过来,再将最高位进行翻转。

    什么要这么存储

    在 MySQL/InnoDB 官方文档或者代码中,并没有关于该实现的说明。不过这么做,有一个非常明显的好处,即所有的整数表示的大小关系,与实际存储的数据(当中无符号型对待)的大小关系是一致的。

    即,在上述的例子中:7 > 1 > -1 > -7,而对应的编码表示,也有对应的大小关系:

    0x80000007 > 0x80000001 >0x7fffffff > 0x7ffffff9

    这里对这个问题做一个简单探讨。先说结论吧,这是一种较为典型的整数编码方式:Offset binary(“移码”)。即,将需要表示的整数,加上一个数值,让所有的整数映射到自然数空间。例如,在MySQL中使用32位的int类型,需要表示的整数范围为[-231,231]。那么,实际表示时,则加上231。更为一般的,对于[-2(n-1), 2(n-1)]之间的所有整数在表示时,都加上了2(n-1)。即,建立的映射关系是:

    f(i) = i + 2(n-1)

    即对于任何要存储的整数i,实际存储时都存储上述的f(i)。而在实际运算时,则是,将补码的最高位进行一次翻转即可。

    关于补码

    例如,在 8 位二进制中,00000001 表示 +1,而 11111111 代表 -1。具体的,在表示-3 时,先取 3 的二进制 00000011,再逐位取反 11111100,最后加 1 得到 11111101,即 -3 的补码表示。这种方式让计算机能够高效地进行整数运算,是典型的正负数的方法,该方法的更多优势可以参考:two’s complement

    补充说明

    MySQL 层面的整数表示和 InnoDB 的整数存储是不同的。在“验证存储方式”小结中的代码中可以看到:

    • MySQL使用了小端(little-endian),InnoDB层面使用了大端(big-endian)存储
    • 在 MySQL 层面使用2-补码做有符号整数类型存储;而InnoDB层面使用了“移码”存储

    参考文档

  • InnoDB 的锁诊断是一个比较困难的事情。首先,锁机制是一种较为复杂的资源竞争管理机制,如果涉及的资源类型比较多,锁类型又比较多,那么锁机制就会看起来比较复杂。具体到,MySQL/InnoDB 上,确实也就非常复杂了。这里主要关注 InnoDB 层面的锁,涉及的内容则包括了记录锁(record)、间隙锁(gap)、索引缝隙锁(next-key),而为什么要加锁,则又涉及到隔离级别、MVCC的实现,而锁的实现,则又与 InnoDB 底层的数据存储结构有有一定的关系,总得来说涉及的面比较多,如果对于这些概念没有了解,则比较难理解 InnoDB 的锁机制,也就比较难去排查 InnoDB 锁出现的问题。

    另一个层面是排查手段。MySQL/InnoDB在早期的版本中,对于锁问题的排查手段是比较有限的,而且与很多的配置参数有关,所以了解这些参数,熟悉MySQL/InnoDB锁信息查看的一些方法,则是另一个需要了解的。

    所以,关于 InnoDB 锁问题的也并不是一两个话题能够说清楚的。本文可能是一个系列(给自己挖坑),一个自己学习以及锁问题排查经验的分享。

    构造主键锁竞争

    记录锁,应该是 InnoDB 锁类型中较为常见,也是在READ-COMMITTED事务级别下,比较容易遇到的死锁类型(如果有死锁的话)。这里通过观察主键死锁、唯一键死锁,初步了解 InnoDB 锁信息内容,以及结构。

    查看当前的隔离级别

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

    准备表结构

    DROP TABLE IF EXISTS t1;
    
    CREATE TABLE t1 ( 
      id int unsigned, 
      nick varchar(32),
      age int,
      primary key (id)
    )
    mysql> desc t1;
    +-------+--------------+------+-----+---------+-------+
    | Field | Type         | Null | Key | Default | Extra |
    +-------+--------------+------+-----+---------+-------+
    | id    | int unsigned | NO   | PRI | NULL    |       |
    | nick  | varchar(32)  | YES  |     | NULL    |       |
    | age   | int          | YES  |     | NULL    |       |
    +-------+--------------+------+-----+---------+-------+

    构建锁等待

    在两个会话中,按从上到下,执行下述的SQL:

    时间顺序Session ASession B
    1START TRANSACTION;
    2INSERT INTO t1 VALUES ( 1, "a",12 );
    3START TRANSACTION;
    4INSERT INTO t1 VALUES ( 1, "x",23 );

    这时候,Session B会陷入等待。

    观察锁信息

    通过 SHOW INNODB STATUS 观察

    此时查看 InnoDB 锁信息,则有如下数据:

    ---TRANSACTION 10094, ACTIVE 14 sec inserting
    mysql tables in use 1, locked 1
    LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s)
    MySQL thread id 84, OS thread handle 140453961758272, query id 9262 10.88.0.1 sysb update
    INSERT INTO t1 VALUES ( 1, "x",23 )
    ------- TRX HAS BEEN WAITING 14 SEC FOR THIS LOCK TO BE GRANTED:
    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     ;;
    
    ------------------
    ---TRANSACTION 10093, ACTIVE 23 sec
    2 lock struct(s), heap size 1128, 1 row lock(s), undo log entries 1
    MySQL thread id 83, OS thread handle 140454159017536, query id 9260 10.88.0.1 sysb

    SHOW ENGINE INNODB STATUS\G中仅打印了处于等待授予状态的锁信息,即这里仅打印了事务10094的等待的锁详情。

    详解锁信息

    这里详细看看其中的内容:

    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 LOCKS”这是一个记录锁
    space id 27 page no 4该记录处于物理位置,包括页面编号,以及页面所处的物理文件编号
    n bits 72该页面有72个记录对应标记位
    index PRIMARY of table sysbenchdb.t1 对应表
    trx id 10094所在的事务 ID
    lock mode S锁类型,为 S ,即共享锁
    locks rec but not gap这是一个简单记录锁,无需对记录前的“间隙”加锁
    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     ;;
    Record lock(等待的)记录锁
    heap no 2在页面中,记录以堆的方式存放,该记录的堆编号
    PHYSICAL RECORD: n_fields 5;记录共五个字段
    0: len 4; hex 00000001; asc ;;id 字段值,为1
    1: len 6; hex 00000000276d; asc 'm;;该记录的DB_TRX_ID,即为0x276d10093
    2: len 7; hex 810000008d0110; asc ;;DB_ROLL_PTR
    3: len 1; hex 61; asc a;;字段nick取值 a
    4: len 4; hex 8000000c; asc ;;字段age取值 0x8000000c,即12

    这里需要注意的是,这里一共有五个字段(n_fields 5; )。那实际这个表,只有三个字段,为什么这里会有五个字段?原因在于,InnoDB 在存储数据信息的时候,会额外的存储两个信息:DB_TRX_IDDB_ROLL_PTR。这是两个InnoDB的较为底层的概念,具体的可以参考该文档:17.3 InnoDB Multi-Versioning

    注意到,这里的 DB_TRX_ID 取值为 0x276d,转化为10进制则为:10093。即,该条记录最后一次被修改是被事务10093所修改,即上述表格中的Session A所执行的SQL所修改。

    在内置视图中查看锁信息

    mysql> SELECT
        ->     ENGINE_TRANSACTION_ID AS TRX_ID,
        ->     OBJECT_NAME,
        ->     INDEX_NAME,
        ->     LOCK_TYPE,
        ->     LOCK_MODE,
        ->     LOCK_STATUS,
        ->     LOCK_DATA
        ->   FROM performance_schema.data_locks;
    +--------+-------------+------------+-----------+---------------+-------------+-----------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
    +--------+-------------+------------+-----------+---------------+-------------+-----------+
    |  10224 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
    |  10225 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
    |  10224 | t1          | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 1         |
    |  10225 | t1          | PRIMARY    | RECORD    | S,REC_NOT_GAP | WAITING     | 1         |
    +--------+-------------+------------+-----------+---------------+-------------+-----------+

    (注:这里的TRX_ID与上述不同,因为这是重新执行了上述的冲突事务)

    补充说明

    在本示例中,写入的数据仅为主键(也是一种唯一键),故无论是READ-COMMITTED、还是REPEATABLE-READ,其所需要的锁都相同的。

    相关参考资源

    1. InnoDB Locking and Transaction Model@MySQL 8.4 Reference Manual
    2. InnoDB Data Locking – Part 1 “Introduction”@MySQL Blog Archive
    3. InnoDB Data Locking – Part 2 “Locks”@MySQL Blog Archive
    4. InnoDB Data Locking – Part 2.5 “Locks” (Deeper dive)@MySQL Blog Archive
    5. InnoDB Data Locking – Part 3 “Deadlocks”@MySQL Blog Archive
    6. Understanding InnoDB Locks and Deadlocks@2015 Percona Live
    7. Introduction to Transaction Locks in InnoDB Storage Engine@2013
  • Terraform 可以自动化的创建云端的资源,但是要想实现更高的灵活度,则需要更为灵活的使用Terraform的“Data Sources”能力。例如,在自动化的创建数据库时,云厂商允许创建的版本号是在动态变化的,例如,当前最新的允许的创建的MySQL版本通常是 8.0.40,但通常过了一个季度之后,就变成了 8.0.41。这时,对应的 Terraform 的脚本就需要调整或者传递参数就需要发生变化。而 Terraform 提供的 “Data Sources” 能力则可以很好的解决这个问题。

    在 Oracle 的 Terraform 中可以使用 “Data Source: oci_mysql_mysql_versions” 实现该能力。

    示例

    首先使用 data 命令定义该对象:

    data "oci_mysql_mysql_versions" "gmv" {
        compartment_id = oci_identity_compartment.oic.id
    }

    这里会获取该租户环境下支持的所有MySQL版本。

    然后,再使用 output 命令就可以获取并输出这些版本信息。详细的output命令如下:

    output "mysql_version" {
      value       = data.oci_mysql_mysql_versions.gmv.versions
    }

    详细的输出示例如下:

    mysql_version = tolist([
      {
        "version_family" = "8.0"
        "versions" = tolist([
          {
            "description" = "8.0.36"
            "version" = "8.0.36"
          },
          {
            "description" = "8.0.37"
            "version" = "8.0.37"
          },
          {
            "description" = "8.0.38"
            "version" = "8.0.38"
          },
          {
            "description" = "8.0.39"
            "version" = "8.0.39"
          },
          {
            "description" = "8.0.40"
            "version" = "8.0.40"
          },
          {
            "description" = "8.0.41"
            "version" = "8.0.41"
          },
        ])
      },
      {
        "version_family" = "8.4 - LTS"
        "versions" = tolist([
          {
            "description" = "8.4.0"
            "version" = "8.4.0"
          },
          {
            "description" = "8.4.1"
            "version" = "8.4.1"
          },
          {
            "description" = "8.4.2"
            "version" = "8.4.2"
          },
          {
            "description" = "8.4.3"
            "version" = "8.4.3"
          },
          {
            "description" = "8.4.4"
            "version" = "8.4.4"
          },
        ])
      },
      {
        "version_family" = "9 - Innovation"
        "versions" = tolist([
          {
            "description" = "9.1.0"
            "version" = "9.1.0"
          },
          {
            "description" = "9.1.1"
            "version" = "9.1.1"
          },
          {
            "description" = "9.1.2"
            "version" = "9.1.2"
          },
          {
            "description" = "9.2.0"
            "version" = "9.2.0"
          },
        ])
      },
    ])

    获取特定大版本的各小版本

    可以通过 data资源中新增filter模块以过滤出需要的对象。

    在 Terraform 中,关于 data 资源是否可以使用 filter,以及filter支持的完整度视乎并没有明确的说明。这需要更具不同的供应商的实现。常见的,在data resourcefilter可以支持“列表匹配”、“通配符匹配”或者“正则匹配”。具体的匹配方式,则需要通过文档、或者测试区验证。

    添加带正则匹配的 filter
    data "oci_mysql_mysql_versions" "gmv" {
        #Required
        compartment_id = oci_identity_compartment.oic.id
        filter {
            name = "version_family"
            values = ["8.0.*"]
            regex  = true
        }
    }

    通过 HCL 语言获取最新的版本
    output "latest_versions" {
      value = {
        for db_version in data.oci_mysql_mysql_versions.gmv.versions : db_version.version_family => sort([
          for v in db_version.versions : v.version
        ])[length(db_version.versions) - 1] // 取排序后的最后一个版本
      }
    }

    最后的输出如下:

    latest_versions = {
      "8.0" = "8.0.41"
      "8.4 - LTS" = "8.4.4"
      "9 - Innovation" = "9.2.0"
    }

    参考链接

  • MySQL 8.0 中的 CTE 功能

    ·

    CTE 的全称是 Common Table Expressions,有时候也缩写为 CTEs,在 SQL 标准中使用 WITH clause表达[1]。在 ANSI standard 标准的 SQL:1999 中定义,参考:ISO/IEC 9075-2:1999[7][8]。该特性支持在 MySQL 8.0 中正式被引入[9]

    通常,如果不需要编写特别复杂的 SQL ,更为具体的,如果无需编写带有递归功能的 SQL 时,CTE 通常都不是必须的。所以,该语法功能,对于分析型的场景或者数据开发等场景更为常用。

    这里概述 CTE 的两个常用功能:简化子查询结构、实现递归执行。其中,实现递归执行,是 CTE 独特的能力,是对 SQL 能力的一种补充。

    简化子查询结构

    CTE 功能提供了一种新的子查询或临时表的写法。可以把后续 SQL 中需要使用的临时表或子查询独立的放在WITH子句中,使得结构看起来更为清晰一些。

    例如,如下两个写法在语义上是等价的:

    WITH 
      t_mid as (
        SELECT * FROM t_1 JOIN t_2
      )
    SELECT * FROM t_mid;
    SELECT * 
    FROM 
      (
        SELECT * FROM t_1 JOIN t_2
      ) t_mid;

    递归执行

    该功能是 CTE 的核心能力。例如,在表中查询好友的好友、查询主管的主管、递归数据生成等功能。

    递归执行方式概述

    在各个数据库的关于 CTE 介绍都会较为详细的描述,这里对该执行过程做一个简单的描述。

    对于递归 CTE 对应的 SQL 总是有类似结构:

    WITH RECURSIVE table_cte (col_01 ... ) AS (
      SELECT ...                 -- return initial row set
      UNION ALL
      SELECT ... FROM  table_cte -- return additional row sets
    )

    在该结构中:

    • 第一个SELECT为数据初始化
    • 第二个SELECT则为递归执行的部分,该部分在引用表table_cte时,总是使用该表的最后一条生成记录

    斐波那契数列的生成

    这是一个MySQL文档中的示例[10]

    WITH RECURSIVE fibonacci (n, fib_n, next_fib_n) AS
    (
      SELECT 1, 0, 1
      UNION ALL
      SELECT n + 1, next_fib_n, fib_n + next_fib_n
        FROM fibonacci WHERE n < 10
    )
    SELECT * FROM fibonacci;
    
    +------+-------+------------+
    | n    | fib_n | next_fib_n |
    +------+-------+------------+
    |    1 |     0 |          1 |
    |    2 |     1 |          1 |
    |    3 |     1 |          2 |
    |    4 |     2 |          3 |
    |    5 |     3 |          5 |
    ...
    |   10 |    34 |         55 |
    +------+-------+------------+

    根据前面小结“递归执行方式概述”中所描述,在每次执行上述的第二个 SELECT,即SELECT n + 1, next_fib_n, fib_n + next_fib_n FROM fibonacci时,总是使用 CTE 表fibonacci最后生成的记录。所以,该SQL中引用的列nfib_nnext_fib_n均为表fibonacci最后生成的记录,即上一条记录。

    所以,该 SQL 第一列返回的是一个自增的序列;第二列,即为斐波那契数列;第三列是用于计算的中间列。

    广度优先还是深度优先

    在 MySQL 的递归遍历中,并没有像 PostgreSQL 那样可以通过 SQL 关键字去控制。在文档中,也没有描述是广度优先还是深度优先。这里通过一些 SQL 变量来看看,到底是广度优先还是深度优先。

    先说结论吧:MySQL 使用的是广度优先遍历。关注验证过程的,可以继续阅读这一段的示例和解读。

    这里使用 MySQL 官方文档中另一个示例。该示例包含一个企业人员组织架构的关系:

    CREATE TABLE employees (
      id         INT PRIMARY KEY NOT NULL,
      name       VARCHAR(100) NOT NULL,
      manager_id INT NULL,
      INDEX (manager_id),
    FOREIGN KEY (manager_id) REFERENCES employees (id)
    );
    INSERT INTO employees VALUES
    (333, "Yasmina", NULL),  # Yasmina is the CEO (manager_id is NULL)
    (198, "John", 333),      # John has ID 198 and reports to 333 (Yasmina)
    (692, "Tarek", 333),
    (29, "Pedro", 198),
    (4610, "Sarah", 29),
    (72, "Pierre", 29),
    (123, "Adil", 692);

    我们可以使用如下的递归 CTE 语句进行组织架构遍历:

    WITH RECURSIVE employee_paths (id, name, path) AS
    (
      SELECT id, name, CAST(id AS CHAR(200))
        FROM employees
        WHERE manager_id IS NULL
      UNION ALL
      SELECT e.id, e.name, CONCAT(ep.path, ',', e.id)
        FROM employee_paths AS ep JOIN employees AS e
          ON ep.id = e.manager_id
    )
    SELECT * FROM employee_paths ORDER BY path;

    我们对这条 SQL 语句进行如下修改,以观察是广度优先还是深度优先:

    WITH RECURSIVE employee_paths (id, name, path,seq) AS
    (
      SELECT id, name, CAST(id AS CHAR(200)),@seq:=1
        FROM employees
        WHERE manager_id IS NULL
      UNION ALL
      SELECT e.id, e.name, CONCAT(ep.path, ',', e.id),@seq:=@seq+1
        FROM employee_paths AS ep JOIN employees AS e
          ON ep.id = e.manager_id
    )
    SELECT * FROM employee_paths ORDER BY seq;
    
    +------+---------+-----------------+------+
    | id   | name    | path            | seq  |
    +------+---------+-----------------+------+
    |  333 | Yasmina | 333             |    1 |
    |  198 | John    | 333,198         |    2 |
    |  692 | Tarek   | 333,692         |    3 |
    |   29 | Pedro   | 333,198,29      |    4 |
    |  123 | Adil    | 333,692,123     |    5 |
    |   72 | Pierre  | 333,198,29,72   |    6 |
    | 4610 | Sarah   | 333,198,29,4610 |    7 |
    +------+---------+-----------------+------+

    该结果的最后一列 seq,反映了输出的先后顺序。可以看到,这里使用的广度优先遍历。

    参考链接

  • 在昨天进行的SQL编程大赛中,所有 MySQL 选手的成绩都没有进入八强的。个人也对这个问题比较感兴趣,经过初步分析,重要的原因在于 MySQL 实现中没有比较好的并行加速的能力。而在 MySQL 的衍生版本中,倒是有几个版本提供了并行执行的能力。包括了 PolarDB 的 Elastic Parallel Query[2]Amazon Aurora 的 Parallel query[3] 。所以,也打算验证一下,如果加上这些并行能力,是否能够更快。

    结果综述

    PolarDB MySQL 运行了与 MySQL “几乎”(仅添加Hint开启并行)相同的SQL(参考)运行最快为:3.821 s。相比在同一个集群,不开启并行的时间是 6.647s,速度提升了 42.5% 。此外也测试了Aurora的相同的规格,几经调试依旧无法使用其并行能力。

    在相同的SQL实现下,PolarDB MySQL 可能是所有 MySQL 版本中性能最好的。如果,感觉还有什么版本可能有更好的性能,欢迎留言。

    数据与SQL

    这次是尝试使用 MySQL 高性能的完成“第二次SQL编程大赛”的进阶挑战。完整的题目描述可以参考:赛题说明[4]。这里实现的 PolarDB MySQL 版本 SQL 参考:gt.polardb.sql@GitHub[1](或参考本文结尾部分)。

    PolarDB的规格选择

    这里选择与试题类似的4c8g规格,详细参数如下:

    主要参数:

    • CPU架构:x86
    • 产品版本:企业版
    • 小版本号:8.0.2(与 MySQL 8.0.18 完全兼容)
    • IMCI只读节点个数:0
    • 初始只读节点个数:0
    • 初始读写节点个数:1
    • 节点规格:4 核 8GB(通用)
    • 存储类型:PSL5

    在 PolarDB 上并行执行

    PolarDB 的并行执行可以使用 Hint 较为方便的开启:

    SELECT
      /*+PARALLEL(8)*/
    ...

    可以通过执行计划观察,实际是否使用了并行:

    | -> Gather (merge sort; slice: 1; workers: 8)  (cost=2024861688.70 rows=1995124000) (actual time=1774.761..2159.495 rows=1000000 loops=1)
        -> Sort: <temporary>.p_id  (cost=1705198767.38 rows=249390500) (actual time=480.641,781.971,239.446..503.108,818.048,250.387 rows=125000,220305,60043 loops=1,1,1)
            -> Stream results (actual time=1.631,2.232,1.396..380.381,630.231,168.213 rows=125000,220305,60043 loops=1,1,1)
                -> Left hash join (t_01.a_s = p_01.a_s), (t_01.d_s = p_01.d_s), extra conditions: ((p_01.seq >= ((t_01.p_seat_to - t_01.seat_count) + 1)) and (p_01.seq <= t_01.p_seat_to))  (cost=25015432.30 rows=249390500) (actual time=1.621,2.220,1.387..200.048,332.885,86.841 rows=125000,220305,60043 loops=1,1,1)
                    -> Parallel table scan on p_01, with parallel partitions: 8 (actual time=0.002,0.003,0.002..48.149,74.839,20.489 rows=125000,220305,60043 loops=1,1,1)
                        -> Materialize (shared access, partitions: 8, partition_keys: a_s,) (actual time=0.001,0.002,0.001..24.240,34.098,9.331 rows=125000,220305,60043 loops=1,1,1)
                            -> Gather (slice: 1; workers: 8)  (cost=1146187.18 rows=997560) (actual time=158.204..362.897 rows=1000000 loops=1)
                                -> Window aggregate  (cost=1090064.43 rows=124695) (actual time=160.066,167.128,157.433..296.715,314.531,278.945 rows=125000,149658,102922 loops=1,1,1)
                                    -> Repartition (hash keys: passenger.departure_station, passenger.arrival_station; merge sort; slice: 2; workers: 8)  (cost=514229.38 rows=124695) (actual time=160.059,167.121,157.424..223.566,236.036,217.548 rows=125000,149658,102922 loops=1,1,1)
                                        -> Sort: passenger.departure_station, passenger.arrival_station  (cost=12554.66 rows=124695) (actual time=152.998,157.330,149.335..172.035,180.041,168.003 rows=125000,132932,111478 loops=1,1,1)
                                            -> Parallel table scan on passenger, with parallel partitions: 745 (actual time=0.052,0.058,0.046..57.559,65.907,54.511 rows=125000,132932,111478 loops=1,1,1)
                    -> Hash
                        -> Table scan on t_01
                            -> Materialize (shared access) (actual time=0.002,0.003,0.001..0.744,0.935,0.666 rows=2000,2000,2000 loops=1,1,1)
                                -> Gather (slice: 1; workers: 8)  (cost=3416.33 rows=2000) (actual time=6.884..7.646 rows=2000 loops=1)
                                    -> Window aggregate with buffering  (cost=3293.83 rows=250) (actual time=1.955,2.233,1.724..3.692,3.760,3.633 rows=250,292,196 loops=1,1,1)
                                        -> Repartition (hash keys: t_include_no_seat.d_s, t_include_no_seat.a_s; slice: 2; workers: 1)  (cost=3161.31 rows=250) (actual time=1.930,2.215,1.691..2.759,2.922,2.589 rows=250,292,196 loops=1,1,1)
                                            -> Sort: t_include_no_seat.d_s, t_include_no_seat.a_s, t_include_no_seat.if_no_seat, t_include_no_seat.t_id (actual time=1.383,1.383,1.383..2.070,2.070,2.070 rows=2000,2000,2000 loops=1,1,1)
                                                -> Table scan on t_include_no_seat (actual time=1.381,1.381,1.381..1.685,1.685,1.685 rows=2000,2000,2000 loops=1,1,1)
                                                    -> Materialize with deduplication (shared access) (actual time=0.001,0.001,0.001..0.295,0.295,0.295 rows=2000,2000,2000 loops=1,1,1)
                                                        -> Table scan on <union temporary>  (cost=2.50 rows=0) (actual time=0.001..0.290 rows=2000 loops=1)
                                                            -> Union materialize with deduplication  (actual time=2.330..3.003 rows=2000 loops=1)
                                                                -> Table scan on train  (cost=101.25 rows=1000) (actual time=0.050..0.380 rows=1000 loops=1)
                                                                -> Table scan on train  (cost=101.25 rows=1000) (actual time=0.023..0.348 rows=1000 loops=1)
     |

    这里的诸如Gather (merge sort; slice: 1; workers: 8)等内容,显示对应的部分会通过多线程并行执行。

    执行时间统计

    这里运行了该 SQL 三次的结果统计如下:

    real	0m3.863s
    user	0m0.379s
    sys	0m0.100s
    
    real	0m3.917s
    user	0m0.414s
    sys	0m0.123s
    
    real	0m3.821s
    user	0m0.422s
    sys	0m0.128s

    说明:这里,因为PolarDB是运行在云端,故仅需关注这里的 real 部分的时间。

    不开启并行时 PolarDB 的性能

    该组数据可用对比:

    real	0m6.743s
    user	0m0.394s
    sys	0m0.120s
    
    real	0m6.647s
    user	0m0.393s
    sys	0m0.120s
    
    real	0m6.665s
    user	0m0.407s
    sys	0m0.125s

    并行执行的一些状态参数

    mysql> show global status like '%pq_%';
    +-------------------------------------+-------+
    | Variable_name                       | Value |
    +-------------------------------------+-------+
    | PQ_fallback_one_worker              | 0     |
    | PQ_local_workers_created            | 297   |
    | PQ_migrant_workers_created          | 0     |
    | PQ_net_exchange_fail_connect        | 0     |
    | PQ_refused_over_computing_resource  | 0     |
    | PQ_refused_over_max_queuing_time    | 0     |
    | PQ_refused_over_total_workers       | 0     |
    | PQ_remote_workers_created           | 0     |
    | PQ_running_local_workers            | 0     |
    | PQ_running_migrant_workers          | 0     |
    | PQ_running_remote_workers           | 0     |
    | PQ_sched_adative_resource_dec_count | 0     |
    | PQ_sched_adative_resource_inc_count | 0     |
    +-------------------------------------+-------+

    使用 Aurora 的并行执行

    这里也尝试使用 Aurora 的并行查询进行优化,但是并没有成功。Aurora 的并行执行并没有 Hint 可以控制,而是优化器根据需要选择使用。在本次测试中,在一个 4c32gb 的Aurora实例上,几经尝试,都未能实现并行。故未成功测试。在非并行时,Aurora的执行时间为 6.921s。

    开启Aurora并行执行

    要使用 Aurora 的并行执行能力,需要先创建最新版本的Aurora,在选择的参数组(parameter group)时,该参数组需要打开 aurora_parallel_query 参数。在实例创建完成后,可以通过如下命令查看并行查询是否打开:

    mysql> show global variables like '%aurora_parallel_query%';
    +-----------------------+-------+
    | Variable_name         | Value |
    +-----------------------+-------+
    | aurora_parallel_query | ON    |
    +-----------------------+-------+
    Aurora上的执行时间

    这里记录相关SQL的执行时间如下:

    real	0m6.921s
    user	0m1.022s
    sys	0m0.076s
    [ec2-user@xterm-256color- delete_me]$ time mysql --local-infile=true -hpq-testing.cluster-cjzowaj9vqpd.ap-northeast-1.rds.amazonaws.com -ub_admin -p-f7HNhmp_frX game_ticket < aurora.sql > aurora.ret
    mysql: [Warning] Using a password on the command line interface can be insecure.
    
    real	0m6.955s
    user	0m1.012s
    sys	0m0.076s
    [ec2-user@xterm-256color- delete_me]$ time mysql --local-infile=true -hpq-testing.cluster-cjzowaj9vqpd.ap-northeast-1.rds.amazonaws.com -ub_admin -p-f7HNhmp_frX game_ticket < aurora.sql > aurora.ret
    mysql: [Warning] Using a password on the command line interface can be insecure.
    
    real	0m7.154s
    user	0m0.970s
    sys	0m0.138s

    最后

    PolarDB MySQL 在并行执行开启的情况下性能提升了42.5%,最终执行时间为 3.821 s。有可能是所有 MySQL 兼容的发型版本中性能最快的。

    gt.polardb.sql

    -- explain analyze
    WITH
      t_no_seat_virtual AS (
        select
          train_id as t_id,
          departure_station as d_s,
          arrival_station as a_s,
          seat_count,
          seat_count*0.1 as seat_count_no_seat
        from train
      ),
      t_include_no_seat AS (
        select t_id,d_s ,a_s ,seat_count, 0 as if_no_seat
        from t_no_seat_virtual
        union
        select t_id,d_s ,a_s ,seat_count_no_seat, 1 as if_no_seat
        from t_no_seat_virtual
      )
    SELECT
      /*+PARALLEL(8)*/
      p_01.p_id,         -- output 01
      p_01.d_s,          -- output 02
      p_01.a_s,          -- output 03
      t_01.t_id as t_id, -- output 04
      IF(
          if_no_seat,
          "" ,
          ceil((p_01.seq-t_01.p_seat_to + t_01.seat_count)/100)
      ) as t_carr_id, -- output 05
    
      CASE IF( !isnull(t_01.t_id) and if_no_seat,-1,ceil((( p_01.seq-t_01.p_seat_to + t_01.seat_count )%100)%5))
        WHEN 1  THEN CONCAT( ceil((( p_01.seq-t_01.p_seat_to + t_01.seat_count )%100)/5) ,"A")
        WHEN 2  THEN CONCAT( ceil((( p_01.seq-t_01.p_seat_to + t_01.seat_count )%100)/5) ,"B")
        WHEN 3  THEN CONCAT( ceil((( p_01.seq-t_01.p_seat_to + t_01.seat_count )%100)/5) ,"C")
        WHEN 4  THEN CONCAT( ceil((( p_01.seq-t_01.p_seat_to + t_01.seat_count )%100)/5) ,"E")
        WHEN 0  THEN CONCAT( IF( (p_01.seq-t_01.p_seat_to + t_01.seat_count)%100 = 0, "20" ,ceil((( p_01.seq-t_01.p_seat_to + t_01.seat_count )%100)/5)) ,"F")
        WHEN -1 THEN "无座"
        ELSE NULL
      END as seat_index   -- output 06
    FROM
      (
        select
          /*+PARALLEL(8)*/
          ROW_NUMBER() over(PARTITION BY departure_station,arrival_station) as seq ,
          passenger_id as p_id,
          departure_station as d_s,
          arrival_station as a_s
        from
        passenger
      ) as p_01
    
      LEFT JOIN
    
      (
        select
          /*+PARALLEL(8)*/
          seat_count,
          sum(seat_count)
            over (
                   PARTITION BY d_s,a_s
                   ORDER BY     if_no_seat,t_id
                 ) as p_seat_to ,
          t_id,
          d_s ,
          a_s ,
          if_no_seat
        from
        t_include_no_seat
      ) t_01
    
      ON
            p_01.seq >= p_seat_to-seat_count + 1
        and p_01.seq <= p_seat_to
        and p_01.d_s =  t_01.d_s
        and p_01.a_s =  t_01.a_s
    ORDER BY p_01.p_id

    参考链接

  • 随着企业规模增长,或者,企业原本就在监管非常严格的行业或环境,所面临合规要求也会逐步增多,密码的定期轮换通常也是众多要求中的一个基础项。MySQL 8.0 提供的“双密码”管理机制,可以让大规模数据库密码轮换变得更平滑,而无需任何的停机时间。

    100台MySQL、1000台应用服务

    考虑这样的场景,你是一个 DBA ,生产环境有 100 台 MySQL 实例,运行在这些数据库上约有 1000 台应用服务器。如果需要对,这些 MySQL 的应用账号密码统一做一次更新,如何实现不停机的迁移。

    在 MySQL 之前(8.0.14版本之前)的单密码机制下,更换密码通常会遇到以下问题:无论是先更新数据库服务器上的密码,还是新调整应用服务器上的密码,这个先后顺序都会带来一定的服务不可用时间。通常,为了降低这个服务不可用时间,应用变更人员和 DBA 则需要非常密切的配合。

    双密码机制(dual password)

    从 MySQL 8.0.14 起,开始支持了双密码策略。在密码更换时,可以保留旧密码依旧可用:

    ALTER USER 'appuser1'@'host1.example.com'
      IDENTIFIED BY 'password_b'
      RETAIN CURRENT PASSWORD;

    在一段时间后,可以删除旧密码:

    ALTER USER 'appuser1'@'host1.example.com'
      DISCARD OLD PASSWORD;

    构建批量密码轮换流程

    有了该功能,就可以使用上述的命令,更加平滑的实现大规模数据库场景下的密码更换。

    1. 给变更数据库服务器上账号的密码,同时将原密码作为备用密码保留(使用“RETAIN CURRENT PASSWORD”)
    2. 变更应用服务器上的访问数据库的密码
    3. 检查并观察一段时间,删除数据库中的原密码(使用“DISCARD OLD PASSWORD ”)

    这是一个简化的过程,实际的操作还需要考虑:

    • 严格遵循企业内部的变更规范
    • 灰度的进行发布,而不是一次性操作所有的实例/服务器
    • 做好检查,需要确保诸如密码已经变更完成、应用服务器密码均已经更新
    • 做好回滚方案,避免过程中操作失误导致的问题
    • 在业务低峰时间进行操作

    参考阅读