“秒杀”主题结对编程准备之测试只用MySQL的性能

“秒杀”主题结对编程准备之测试只用MySQL的性能
0

FCC广州将在8月18日(周日)下午举办一场 pair coding 活动。Pair coding 要实现的需求是,提供一个 web API,让众多用户尝试“秒杀”少量商品,而且要求能处理超过关系式数据库处理能力一倍的并发请求。

为了测试关系式数据库在实现本次需求时的并发处理能力,我决定用自己之前买的阿里云的RDS服务器做个测试。


实现方式设计

需求要求,多个用户抢库存较少的一个商品,每个用户只限抢一个商品。因此,只需要设计一个商品抢购结果表,表中只需要存一列用户 ID 即可。对于每个抢购的请求,先查询这个表中用户 ID 是否已存在,若存在则返回“商品已被抢到”。若不存在,再查询这个表中的用户数是否已达到商品库存数,若已达到则返回“抢不到商品”。若没达到库存数,则插入一行来存储当前用户的用户 ID,然后返回“抢到商品”。

为了防止一个线程查询到用户 ID 之后、插入记录之前,有另一个线程也查询同一个用户 ID,也发现可以插入,导致同一用户抢到两个商品;还有防止一个线程查询到表中用户数等于库存数减一,在其插入记录之前,另一个线程页查询到表中用户数仍少于库存数,从而导致抢到商品的用户多于库存,在查表之前要先锁表,在插入新记录或者发现不能插入新记录之后再释放表锁。


对于数据库表设计的其他考虑

因为本次测试使用阿里云的 RDS 服务器,即使用 MySQL,而 MySQL 把行数据存储在主键索引中,所以为了达到最好的插入性能,减小乱序插入时维护主键索引的开销,我不把用户 ID 作为主键,而是额外再增加一个自增列作为主键。

在实际应用中,商品抢购结果表可能有很多数据,为了加速按用户 ID 的查询,我也给用户 ID 一列加上索引。

我最终实际使用的建表语句:

CREATE TABLE `t_test` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int unsigned NOT NULL,
  `create_timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

压测方法设计

为了测试上述实现方式能处理的并发数,我假设用户有 2000 人,每人都试图抢商品 10 次,商品库存数为 20。我创建 200 个线程,每个线程持有一个数据库连接,每个线程中循环执行上述流程 100 次,从而模拟用 200 个数据库连接的连接池来处理 20000 个请求的情况。


测试代码


测试结果

我的阿里云 RDS 服务器的配置:
image

我是在一台阿里云 ECS 服务器的一个 Docker 容器中运行 Java 测试代码的。该 ECS 服务器的配置:

参数
实例规格 ecs.n1.medium
CPU 2.5 GHz主频的Intel Xeon E5-2680 v3(Haswell)2核心
内存 4GB
操作系统(uname -a 的结果) Linux iZ94isewrugZ 4.4.0-105-generic #128-Ubuntu SMP Thu Dec 14 12:42:11 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
Docker 版本 version 17.05.0-ce, build 89658be
Docker 镜像 maven:3.6.1-jdk-8 (ID: 2fa604c5c53b)

以下是 2000 个线程循环 10 次(执行上文图中的流程 20000 次)的耗时:

测试次数 1 2 3 4 5 6 7 8 9 10
耗时(毫秒) 37942 38209 37215 36666 37370 37657 36528 37578 36015 37162
平均耗时(毫秒) 标准差
37234.2 638.766
2赞