前言
在过去,出现秒杀,抢购等业务场景时,很多产品、程序、架构师都会优先考虑 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:
如果有四个 client 同时想抢 100 份猪肉脯,那么看看 SKIP 关键字的效果,如图2。
多跑几次会会发现每次只有一个 client 能查到信息,其他的 client 端都是 empty set。
NOWAIT 的效果是直接抛出异常,效果上和 SKIP 比较像,只是 client 端收到的信息不一样,如图3。
结语
随着 MySQL 8.0 版本不断的迭代,越来越多的新功能加入到了官方版本,对开发、运维都带来了非常多的便利,是时候升级 MySQL 的版本,开始接受和拥抱全新的 MySQL 了。