知也无涯

吾生也有涯,而知也无涯,能学一点算一点…

  • 为什么需要间隙锁

    关于为什么需要 “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

    参考链接

  • Docker 大大简化了数据库的安装,特别是在产品测试阶段的时候,可以让开发者以最快速的方式体验技术产品,尤其是当这个技术产品已经非常复杂的时候。

    Oracle 官方提供了哪些镜像

    Oracle 镜像官方页面

    Oracle 在官方站点中列出了所有支持的产品,以及对应的仓库列表。 Oracle 镜像仓库的官方页面:

    仓库官方页: https://container-registry.oracle.com/

    找到软件对应的子仓库

    这里关注 Oracle Database 相关的仓库,故选择第一个仓库列表页。在这里可以看到有很多的子仓库,可以用于安装不同的 Oracle 数据库版本或组件:

    选择版本与镜像站点

    进入单个子仓库,在页面的最底下可以看到,该仓库有哪些版本的镜像可以使用,例如,这里选择了 express 子仓库,在页面最底端找到支持的版本列表:

    另外,这里还提供了一些可供选择的镜像列表,开发者可以根据自己的地理位置选择合适的镜像站点。

    安装 Oracle 数据库

    拉取镜像

    docker pull container-registry.oracle.com/database/express:latest
      or:  
    docker pull container-registry.oracle.com/database/express:21.3.0-xe
      or:
    docker pull container-registry.oracle.com/database/express:18.4.0-xe

    这里的测试选择了express:18.4.0-xe版本进行安装。查看本地的镜像:

    docker image ls
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    REPOSITORY                                            TAG         IMAGE ID      CREATED       SIZE
    container-registry.oracle.com/mysql/community-server  9.1         f1f889678a73  6 months ago  606 MB
    container-registry.oracle.com/database/express        18.4.0-xe   364598d20118  4 years ago   6.03 GB

    创建 Oracle 数据库的容器

    docker container create \
       -it \
       --name oracle-18ex \
       -p 1521:1521 \
       -e ORACLE_PWD=oracledocker \
       container-registry.oracle.com/database/express:18.4.0-xe

    启动容器

    docker start oracle-18ex

    观察启动状态

    docker logs -f oracle-18ex
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    ORACLE PASSWORD FOR SYS AND SYSTEM: oracledocker
    Specify a password to be used for database accounts. Oracle recommends that the password entered should be at least 8 characters in length, contain at least 1 uppercase character, 1 lower case character and 1 digit [0-9]. Note that the same password will be used for SYS, SYSTEM and PDBADMIN accounts:
    Confirm the password:
    Configuring Oracle Listener.
    Listener configuration succeeded.
    Configuring Oracle Database XE.
    Enter SYS user password:
    ****************
    Enter SYSTEM user password:
    *************
    Enter PDBADMIN User Password:
    *************
    Prepare for db operation
    7% complete
    Copying database files
    29% complete
    Creating and starting Oracle instance
    30% complete
    31% complete
    34% complete
    38% complete
    41% complete
    43% complete
    Completing Database Creation
    47% complete
    50% complete
    Creating Pluggable Databases
    54% complete
    71% complete
    Executing Post Configuration Actions
    93% complete
    Running Custom Scripts
    100% complete
    Database creation complete. For details check the logfiles at:
     /opt/oracle/cfgtoollogs/dbca/XE.
    Database Information:
    Global Database Name:XE
    System Identifier(SID):XE
    Look at the log file "/opt/oracle/cfgtoollogs/dbca/XE/XE.log" for further details.
    
    Connect to Oracle Database using one of the connect strings:
         Pluggable database: bd127ae4faab/XEPDB1
         Multitenant container database: bd127ae4faab
    Use https://localhost:5500/em to access Oracle Enterprise Manager for Oracle Database XE
    The Oracle base remains unchanged with value /opt/oracle
    #########################
    DATABASE IS READY TO USE!
    #########################
    The following output is now a tail of the alert.log:
    2025-04-17T03:40:43.663079+00:00
    XEPDB1(3):Resize operation completed for file# 10, old size 358400K, new size 368640K
    2025-04-17T03:40:44.483587+00:00
    XEPDB1(3):CREATE SMALLFILE TABLESPACE "USERS" LOGGING  DATAFILE  '/opt/oracle/oradata/XE/XEPDB1/users01.dbf' SIZE 5M REUSE AUTOEXTEND ON NEXT  1280K MAXSIZE UNLIMITED  EXTENT MANAGEMENT LOCAL  SEGMENT SPACE MANAGEMENT  AUTO
    XEPDB1(3):Completed: CREATE SMALLFILE TABLESPACE "USERS" LOGGING  DATAFILE  '/opt/oracle/oradata/XE/XEPDB1/users01.dbf' SIZE 5M REUSE AUTOEXTEND ON NEXT  1280K MAXSIZE UNLIMITED  EXTENT MANAGEMENT LOCAL  SEGMENT SPACE MANAGEMENT  AUTO
    XEPDB1(3):ALTER DATABASE DEFAULT TABLESPACE "USERS"
    XEPDB1(3):Completed: ALTER DATABASE DEFAULT TABLESPACE "USERS"
    2025-04-17T03:40:44.930766+00:00
    ALTER PLUGGABLE DATABASE XEPDB1 SAVE STATE
    Completed: ALTER PLUGGABLE DATABASE XEPDB1 SAVE STATE

    登录容器中的 Oracle 数据库

    在上面的输出中可以看到,安装时会默认创建如下数据库:

    Database Information:
    Global Database Name:XE
    System Identifier(SID):XE

    根据在上述 docker 命令中指定的密码,则可以使用如下的命令登录数据数据库:

    docker exec -it oracle-18ex sqlplus sys/oracledocker@XE as sysdba
      or:
    docker exec -it oracle-18ex sqlplus system/oracledocker@XE

    登录后,会有如下提示输入:

    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    
    SQL*Plus: Release 18.0.0.0.0 - Production on Thu Apr 17 03:56:27 2025
    Version 18.4.0.0.0
    
    Copyright (c) 1982, 2018, Oracle.  All rights reserved.
    
    Last Successful login time: Thu Apr 17 2025 03:49:37 +00:00
    
    Connected to:
    Oracle Database 18c Express Edition Release 18.0.0.0.0 - Production
    Version 18.4.0.0.0
    
    SQL>

    授权协议与认证

    如果你要使用 Oracle 数据库的企业版的话,在拉取镜像前则需要先登录官网“同意”相关的协议,并使用docker login的方式进行认证,然后才可以拉取镜像。

    requested access to the resource is denied

    如果没有认证或提前在官网统一协议,则可能遇到如下报错:requested access to the resource is denied

    docker pull container-registry.oracle.com/database/enterprise:12.2.0.1
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    Trying to pull container-registry.oracle.com/database/enterprise:12.2.0.1...
    Error: initializing source docker://container-registry.oracle.com/database/enterprise:12.2.0.1: reading manifest 12.2.0.1 in container-registry.oracle.com/database/enterprise: requested access to the resource is denied

    或者:invalid username/password: authentication required

    docker pull container-registry-tokyo.oracle.com/database/enterprise:12.2.0.1
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    Trying to pull container-registry-tokyo.oracle.com/database/enterprise:12.2.0.1...
    Error: initializing source docker://container-registry-tokyo.oracle.com/database/enterprise:12.2.0.1: unable to retrieve auth token: invalid username/password: authentication required

    解决上述问题,首先需要登录Oracle镜像的官方站点,并同意相关协议,然后使用docker login完成认证,后即可下载。

    登录Oracle仓库站点并同意协议

    例如,如果需要下载“Oracle Database Enterprise Edition”,则需要先进入对应仓库站点:链接,并在页面的右侧栏点击协议并同意协议:

    docker login

    同意协议后,就可以使用docker login登录账号并进行镜像的下载了。

    Enterprise Edition的Docker安装

    在参考 Oracle 企业版官方文档(参考)进行安装部署的时候,在 AlmaLinux 部署时会遇到如下的问题:

    docker exec -it oracle-1202ee sqlplus / as sysdba
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    Error: crun: executable file `sqlplus` not found in $PATH: No such file or directory: OCI runtime attempted to invoke a command that was not found

    当前的绕过方案是,进入容器的bash,然后再执行即可:

    [root@oracle-docker-test ~]# docker exec -it oracle-1202ee bash
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    [oracle@8bb1ec09ec5e /]$ sqlplus / as sysdba
    
    SQL*Plus: Release 12.2.0.1.0 Production on Fri Apr 18 02:37:30 2025
    
    Copyright (c) 1982, 2016, Oracle.  All rights reserved.
    
    
    Connected to:
    Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
    
    SQL>

    创建容器时的参数

    在创建容器的时候,可以使用命令行进行部分启动参数的配置。例如,默认启动时,是没有开启归档日志的(Archive Logs)的,则可以通过添加如下容器构建参数:

    -e ENABLE_ARCHIVELOG=true

    完整的命令:

    docker container create \
       -it \
       --name oracle-1202ee \
       -p 1521:1521 \
       -e ENABLE_ARCHIVELOG=true \
       -e ORACLE_PWD=oracledocker \
       container-registry.oracle.com/database/enterprise:12.2.0.1

    相关资源

  • MySQL/InnoDB 的 隐式锁

    ·

    InnoDB 的锁容易被忽略的细节是关于“隐式锁”(即:implicit locks)的存在。表现上,有的锁是存在的,但在使用SHOW ENGINE INNODB STATUS或者performance_schema.data_locks中却查看不到。最为常见的隐式锁是在写入(INSERT)时,当前事务会持有该记录对应的锁,但是在系统中,通常是查看不到的。但,如果发生了该锁冲突(或竞争)时,系统中则可以看到此类锁信息。

    本文重现了较为常见的隐式锁场景,包括:数据写入(INSERT)时的隐式锁、根据主键操作是可能产生的二级索引隐式锁等。帮助开发者能够更系统的理解,InnoDB 的锁机制。

    写入数据产生的隐式锁

    准备数据

    DROP TABLE IF exists t1;
    
     CREATE TABLE `t1` (
      `id` int unsigned,
      `nick` varchar(32),
      `age` int,
      UNIQUE KEY `uk_n` (`nick`)
    );
    mysql> desc t1;
    +-------+--------------+------+-----+---------+
    | Field | Type         | Null | Key | Default |
    +-------+--------------+------+-----+---------+
    | id    | int unsigned | YES  |     | NULL    |
    | nick  | varchar(32)  | YES  | UNI | NULL    |
    | age   | int          | YES  |     | NULL    |
    +-------+--------------+------+-----+---------+

    mysql> INSERT INTO t1 VALUES (1, 'a' , 12),(20,'z',29);
    Query OK, 2 rows affected (0.01 sec)
    Records: 2  Duplicates: 0  Warnings: 0
    
    mysql> select * from t1;
    +------+------+------+
    | id   | nick | age  |
    +------+------+------+
    |    1 | a    |   12 |
    |   20 | z    |   29 |
    +------+------+------+
    2 rows in set (0.00 sec)

    构建隐式锁

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO t1 VALUES (8, 'h' , 32);
    Query OK, 1 row affected (0.00 sec)

    查看锁信息:

    > 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 |
    +--------+-------------+------------+-----------+-----------+-------------+-----------+
    |  10165 | t1          | NULL       | TABLE     | IX        | GRANTED     | NULL      |
    +--------+-------------+------------+-----------+-----------+-------------+-----------+

    可以看到,在 data_locks 表中没有任何关于事务中写入数据相关的锁。这是,因为这是一个隐式的锁,在没有任何锁竞争的情况下,系统并不会将该类型的锁展示出来(注:这可能与底层的存储和实现有关,隐式锁在实现上可能就没有“显式”的存储在锁相关的数据结构中)。

    构建锁竞争/隐式转显式

    这里通过在另一个事务中尝试并发写入一条冲突的记录,来构建锁竞争:

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO  t1 VALUES (9,'h',17);
    ...

    该事务执行时,则会陷入锁等待。这时,再次查看锁信息如下:

    +--------+-------------+------------+-----------+---------------+-------------+---------------------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA           |
    +--------+-------------+------------+-----------+---------------+-------------+---------------------+
    |  10165 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL                |
    |  10168 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL                |
    |  10165 | t1          | uk_n       | RECORD    | X,REC_NOT_GAP | GRANTED     | 'h', 0x000000000214 |
    |  10168 | t1          | uk_n       | RECORD    | S             | WAITING     | 'h', 0x000000000214 |
    +--------+-------------+------------+-----------+---------------+-------------+---------------------

    这时候,可以看到事务10165,持有一个记录锁,该锁是一个排它记录锁(X,REC_NOT_GAP ),加锁对象是'h', 0x000000000214(注,这是一个唯一索引的入口,前面'h'是唯一索引值,后面的0x000000000214部分是该表的InnoDB内置rowid)。

    主键/二级索引操作相关的隐式锁

    InnoDB 的锁管理和实现确实一个超级复杂的部分(”mega-complicated“)。隐式锁的使用场景也非常多,如果对此不了解的话,那么在观察 InnoDB 的锁信息时,是会有很多的困惑的。这里再列举一类也算,较为常用的隐式锁:“主键索引/二级索引”相关的隐式说。即:

    • 当对记录进行操作时,即便是通过主键扫描,也可能对二级索引进行加锁
    • 当对记录进行操作时,即便是通过二级索引扫描,也可能对主键进行加锁

    这类场景的加锁,通常都是会存在隐式锁。

    主键操作时二级索引上的隐式锁

    在下面的测试中,我们先主键 id = 8的记录进行删除操作,然后通过系统表data_locks观察该事务是否持有二级索引相关的锁;而后,在另一个事务中,通过二级索引(nick = 'Henry')对该记录进行操作(共享读),而后再重新观察前面事务的锁状态。

    在下面的测试可以观察到,在Session B没有开始前;在 Session ADELETE语句是观测不到二级索引上的锁的;但当Session B尝试去锁定二级索引上的入口时,再次观察Session A上的锁信息,就可以看到,在Session A没有任何操作的情况下,多出了一个额外的、持有的二级索引上的锁,该锁原本是一个“隐式锁”,在发生锁竞争后,转化为一个“显式锁”。即便是在Session B因为等待操作或结束了,Session A持有的已经转化的“显式锁”也不会再回退了。详细测试如下。

    准备数据

    DROP TABLE IF exists t1;
     CREATE TABLE `t1` (
      `id` int unsigned,
      `nick` varchar(32),
      `age` int,
      PRIMARY KEY (`id`),
      UNIQUE KEY `uk_n` (`nick`)
    );
    mysql> INSERT INTO t1 VALUES (1,"Alice",12);
    mysql> INSERT INTO t1 VALUES (8,"Henry",27);
    mysql> INSERT INTO t1 VALUES (16,"Peter",15);
    mysql> show variables like '%iso%';
    +-----------------------+----------------+
    | Variable_name         | Value          |
    +-----------------------+----------------+
    | transaction_isolation | READ-COMMITTED |
    +-----------------------+----------------+

    构建隐式锁

    Session A

    mysql> START TRANSACTION;
    mysql> DELETE FROM t1 WHERE id = 8;
    
    mysql> SELECT
        ->     ENGINE_TRANSACTION_ID AS TRX_ID,
        ->     INDEX_NAME,LOCK_TYPE,
        ->     LOCK_MODE, LOCK_STATUS,
        ->     LOCK_DATA
        ->   FROM performance_schema.data_locks;
    +--------+------------+-----------+---------------+-------------+-----------+
    | TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
    +--------+------------+-----------+---------------+-------------+-----------+
    |  10317 | NULL       | TABLE     | IX            | GRANTED     | NULL      |
    |  10317 | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 8         |
    +--------+------------+-----------+---------------+-------------+-----------+

    Session B

    隐式锁转换为显式锁

    继续上述两个Sessions的操作:

    Session A

    Session B

    mysql> START TRANSACTION;
    mysql> SELECT * FROM t1 WHERE nick = 'Henry' FOR SHARE;
    ( Waiting )
    ...(Query Locks From performance_schema.data_locks like above)...
    +-----------------+------------+-----------+---------------+-------------+------------+
    | TRX_ID          | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA  |
    +-----------------+------------+-----------+---------------+-------------+------------+
    |           10317 | NULL       | TABLE     | IX            | GRANTED     | NULL       |
    | 421929568337920 | NULL       | TABLE     | IS            | GRANTED     | NULL       |
    |           10317 | uk_n       | RECORD    | X,REC_NOT_GAP | GRANTED     | 'Henry', 8 |
    | 421929568337920 | uk_n       | RECORD    | S,REC_NOT_GAP | WAITING     | 'Henry', 8 |
    |           10317 | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 8          |
    +-----------------+------------+-----------+---------------+-------------+------------+
    (... Abort last statement...)
    ERROR 1205 (HY000): Lock wait timeout exceeded; 
    try restarting transaction
    ...(Query Locks From performance_schema.data_locks like above)...
    +--------+------------+-----------+---------------+-------------+------------+
    | TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA  |
    +--------+------------+-----------+---------------+-------------+------------+
    |  10317 | NULL       | TABLE     | IX            | GRANTED     | NULL       |
    |  10321 | NULL       | TABLE     | IS            | GRANTED     | NULL       |
    |  10317 | uk_n       | RECORD    | X,REC_NOT_GAP | GRANTED     | 'Henry', 8 |
    |  10317 | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 8          |
    +--------+------------+-----------+---------------+-------------+------------+

    最后

    一些理解

    “隐式锁”可以理解为,在某些条件下,这里一定是存在“锁”的,所以,既然一定是存在的,并且这类场景可能还比较广泛,那么为了节省存储空间与操作,就省略了此类“锁”的表示。例如,通常,如果事务写入了一条数据,那么该事务一定是持有该数据的排它锁的。

    但,当真的有其他事务也尝试去获取该“隐式锁”的时候,那么为了便于进行锁检测与管理,则会重新将该锁表示出来。并且,也不再有必要重新转化为隐式锁。

    所以,如果有人问,你是否可以把当前数据库的所有的锁情况,都打印或记录下来,这是做不到的,也是没有必要的。事实上,“隐式锁”是广泛存在的,但因为通常并没有那么锁竞争,这些“隐式锁”也就一直不会被表示出来。

    一块拼图

    通常,隐式锁是可以被忽略的,如上述示例,这可能是一个没有任何竞争的锁。但,当出现对应的锁竞争时,则会变得可见。一般地,是可以不用关注隐式锁的,但,如果希望能够对 InnoDB 锁有非常系统的了解,这也是一块重要的“拼图”。

  • 存储引擎在存储整数时,一般会使用最高位作为标志位,标记存储的整数是正数还是负数(参考),最高位也被称为“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
  • 我的 Docker 常见命令

    ·

    查看当前容器列表

    docker ps -a

    可能看到输出如下:

    docker ps -a
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    CONTAINER ID  IMAGE                                                     COMMAND     CREATED       STATUS                   PORTS       NAMES
    c84f74eedd65  container-registry.oracle.com/mysql/community-server:9.1  mysqld      5 months ago  Exited (0) 4 months ago              mysql91
    13985429990f  container-registry.oracle.com/mysql/community-server:9.1  mysqld      2 months ago  Created                              mysql91_n

    启动/关闭/重启某个容器

    docker start/stop/restart <CONTAINER NAMES>
    docker start/stop/restart <CONTAINER ID>

    例如:

    docker start mysql91

    查看容器的运行日志

    docker logs -f <CONTAINER ID>

    这里的参数 -f则类似于 tail-f的参数。

    在容器中执行命令

    docker exec -it <CONTAINER NAMES> COMMAND

    例如

    docker exec -it mysql91 mysql -uroot -p

    这里参数,可以通过 man docker exec去查看。-it是两个参数,表示分配一个可交互式操作的终端。

    进入容器并启动一个Bash

    类似的,使用上面的命令,可以在容器环境下启动一个Bash

    docker exec -it mysql91 /bin/bash

    将容器中的文件拷贝到宿主机

    有时候,在容器中,工具和命令比较有限,需要将对应的容器中的文件拷贝出来并进行分析或存档。则可以使用如下命令:

    docker cp <CONTAINER NAMES>:<PATH_OF_FILE> <PATH_OF_HOST>

    例如:

    docker cp mysql91:/var/lib/mysql/binlog.000005 ./

    查看当前的本地的镜像列表

    docker image ls
    Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
    REPOSITORY                                            TAG         IMAGE ID      CREATED       SIZE
    container-registry.oracle.com/mysql/community-server  9.1         f1f889678a73  6 months ago  606 MB
    container-registry.oracle.com/database/express        18.4.0-xe   364598d20118  4 years ago   6.03 GB

    其他资源