顺便聊一下MySQL 8.0 新特性:NOWAIT and SKIP LOCKED

前言

在过去,出现秒杀,抢购等业务场景时,很多产品、程序、架构师都会优先考虑 redis,memcache 这类 NoSQL 数据库,或者是 zookeeper 这类消息队列。RDBMS(MySQL,Oracle 等) 一般只作为最后结果的持久化存储,而不会直接去支持这类型的业务。

核心的问题在于这类业务在固定的某些数据上存在「排他性」,即对某一行,或者某一部分数据的操作在业务层面的要求是互斥的,不能让多个 client 同时获取到这些数据(超卖另论)。而 RDBMS 在处理类似需求的时候,一般只能依靠数据库中的排它锁,但是会导致 Client 的操作从并行变为串行,对整体性能的负面影响通常是不可接受的。

功能简介

MySQL 8.0.1 中发布了一个 Feature,为 select ... for update 添加了两个新的关键字:NOWAIT 和 SKIP。通过这两个新增的关键字来应对如前文所述的一些业务场景。

在旧版本的 MySQL 中,如果一定要在数据库层面实现类似的业务场景,一般会使用 select ... for update 来为目标数据加上排它锁,阻止其他的 Client 访问这些数据,其他的 Client 则会因为无法获取到锁而进入 lock wait 的状态,达到 innodb_lock_wait_timeout 的限制之后就会抛出异常。通常情况下,这种 wait 会导致大量的 client hang 在数据库中,不仅造成资源浪费,还可能会占满数据库的连接数(max_connection)。

NOWAIT 关键字的效果与字面意思基本一致,当遇到需要进入 lock wait 的场景时,不再进行 lock wait,而是直接抛出异常,避免因为 lock wait 导致大量 client 阻塞在 MySQL 中。这种方式既减轻了连接方面的压力,也避免了在内部 lock 结构中堆积大量的信息,影响到 MySQL。

SKIP 关键字也很直观的描述了实际的效果:当遇到需要进入 lock wait 的场景时,不再进入 lock wait,而是跳过对这一行数据加锁的操作,继续寻找下一行符合查询条件的数据。这种方式不仅避免了 lock wait 的异常,而且也不会因为进入 lock wait 状态而导致大量连接阻塞在 MySQL 中。

原理简介

官方实现这个功能的思路还是比较简单的,这里结合一部分源代码进行说明。

select 的加锁由新增的 select_mode 进行管理。

enum select_mode {
    SELECT_ORDINARY,    /* default behaviour */
    SELECT_SKIP_LOCKED, /* skip the row if row is locked */
    SELECT_NOWAIT       /* return immediately if row is locked */
};
...
...
    /* Set select mode for SKIP LOCKED / NOWAIT */
    if (lock_type != TL_IGNORE) {
        switch (table->pos_in_table_list->lock_descriptor().action) {
        case THR_SKIP:
            m_prebuilt->select_mode = SELECT_SKIP_LOCKED;
            break;
        case THR_NOWAIT:
            m_prebuilt->select_mode = SELECT_NOWAIT;
            break;
        default:
            m_prebuilt->select_mode = SELECT_ORDINARY;
            break;
        }
    }

当遇到 lock wait 的场景时,参考 row_search_mvcc() 中的部分代码,

...
...
            switch (err) {
            case DB_SUCCESS_LOCKED_REC:
                err = DB_SUCCESS;
            case DB_SUCCESS:
                break;
            case DB_SKIP_LOCKED:
            case DB_LOCK_NOWAIT:
                ut_ad(0);
                goto next_rec;
            default:
                goto lock_wait_or_error;
            }
...
... 

当遇到 SKIP_LOCKED 和 LOCK_NOWAIT 时,代码逻辑并没有进入 lock_wait_or_error 的代码段,而是进入了 next_rec 的代码段,继续搜索数据。

使用限制

  • 主动开启事务,需要设置 autocommit = OFF
  • SKIP 和 NOWAIT 关键字在 binlog_format 设置为 statement 时存在安全隐患,需要使用 row。
    • 都2020了,还是别用 statement 了吧,MyISAM 也该丢掉了。
  • SKIP 关键字提供的结果是违反数据一致性的,在使用的时候仅限于避免锁等待的需求,不要滥用。

体验示例

先创建一个测试表,再写点测试数据。

create table goods(
    id int not null auto_increment primary key,
    name varchar(16) not null,
    cnt int not null);

insert into goods values(null,"猪肉脯",100);
insert into goods values(null,"华夫饼",100);
insert into goods values(null,"辣条",100);
insert into goods values(null,"葡萄干",100);

效果如图1:

图1

如果有四个 client 同时想抢 100 份猪肉脯,那么看看 SKIP 关键字的效果,如图2。

图2

多跑几次会会发现每次只有一个 client 能查到信息,其他的 client 端都是 empty set。

NOWAIT 的效果是直接抛出异常,效果上和 SKIP 比较像,只是 client 端收到的信息不一样,如图3。

图3

结语

随着 MySQL 8.0 版本不断的迭代,越来越多的新功能加入到了官方版本,对开发、运维都带来了非常多的便利,是时候升级 MySQL 的版本,开始接受和拥抱全新的 MySQL 了。

正文完