本文详细介绍如何利用Docker容器化技术,结合Node.js、Express、Kafka、Redis和MySQL搭建一个简易的秒杀系统。通过JMeter模拟高并发请求,利用Redis原子操作处理库存扣减,Kafka异步解耦消息处理,最终将结果持久化至MySQL。文章提供了完整的Docker Compose配置及核心代码逻辑,旨在帮助开发者理解高并发场景下的架构设计思路与实战细节。
秒杀系统架构概述
秒杀活动在互联网领域的应用极为广泛,从12306抢票到各类电商抢购活动,随处可见其身影。这种高并发场景对架构师的设计能力提出了严峻挑战。本文并非提供一套可直接投入生产环境的完整方案,而是旨在通过一个简化的示例,揭示秒杀系统背后的核心设计思路,帮助读者建立感性认知并动手实践。所有相关配置及源代码均可在文末提供的GitHub仓库中获取。
为了清晰展示系统架构,首先对文中涉及的关键组件进行简要介绍,具体架构如下图所示:

JMeter:用于模拟秒杀场景中大量用户并发发起的请求。
Seckill Service:基于Node.js和Express框架开发的秒杀服务模块。图中所示的步骤2、3、4均在该服务内部处理。
Redis:作为一个独立的Docker容器运行,用于存储名为counter的数据,代表当前剩余的库存数量。
Kafka:同样以Docker容器形式运行。虽然Kafka依赖Zookeeper存储元数据,但Zookeeper容器未在图中单独列出,且程序代码中未直接涉及。当Seckill Service完成Redis更新后,会向Kafka发送一条消息,标识一次成功的秒杀操作。
Seckill Kafka Consumer:基于Node.js开发的Kafka消费者服务。它从Kafka中获取秒杀成功的消息,进行相应处理后将数据持久化存储到MySQL数据库中。
MySQL:作为一个独立的Docker容器运行。所有最终确认成功的秒杀请求,最终都会转化为数据库表中的一条具体记录。
环境搭建与配置
环境搭建
1. 安装JMeter 从官网下载JMeter的二进制包,执行bin目录下的jmeter脚本即可启动。启动界面如下所示,需新建一个名为Seckill的Thread Group,并配置为在5秒内发起2000次并发请求。

在该Thread Group下新建一个名为Seckill的Http Request Sampler,按照下图配置主机名、端口号、HTTP请求方法以及请求路径。

2. 安装Redis、Kafka、Zookeeper和MySQL 为简化环境搭建过程,上述组件均以Docker容器形式启动。在此之前,需从Docker官网下载并安装Docker Engine、Docker Machine和Docker Compose。若使用Windows或Mac系统,官网提供的Docker For Windows/Mac安装包可便捷地完成这三个组件的安装。
3. 编写Docker Compose文件 创建一个名为Seckill的项目文件夹,并在其中新建docker-compose.yml文件,内容如下:

配置文件中定义了4个服务,分别对应zookeeper、kafka、redis和mysql四个Docker容器。配置中有两处需根据实际环境修改:一是kafka配置下的KAFKA_ADVERTISED_HOST_NAME字段,需设置为本地机器的IP地址;二是MYSQL配置下的MYSQL_ROOT_PASSWORD字段,可设置为任意密码。
文件创建完成后,在项目根目录的命令行中执行docker-compose up命令,Docker Engine便会启动上述配置的四个组件。
注意:启动完成后,需手动执行以下初始化操作:在Kafka容器中创建名为CAR_NUMBER的topic;在Redis容器中创建名为counter的计数器,并设置初始值为100(代表库存初始量);在MySQL容器中创建名为seckill的数据表,包含自增id字段和timestamp格式的date字段。
核心代码实现
代码片段
1. Seckill Service


第1-8行:引入程序所需的对象,包括Node.js的MVC框架Express、Redis客户端、Kafka客户端等。
第10行:利用Express框架暴露一个路径为/seckill的POST接口。
第12行:定义了一个核心处理方法,该方法在第54行被调用。
第13-22行:新建Redis客户端实例,并监听error事件。
第23行:关键代码,作用是让Redis客户端监视counter值。随后启动事务,若在提交事务时发现其他客户端修改了counter值,则放弃当前事务。
第24行:通过Redis客户端的异步方法获取counter的值。由于Redis的get操作是原子的,因此此处无需担心并发读写问题。
第25-28行:判断返回的库存值是否大于0。若大于0,通过client.multi()启动事务,使用decr()方法将counter值减1,最后通过exec()提交事务;若小于0,则执行第47行,打印“卖完了”并关闭Redis客户端。
第29-46行:查看multi.exec()中的回调方法。此前已使用watch对counter进行监视。若在事务提交过程中有其他客户端修改了counter值,回调方法中的replies参数将为null。代码第29-31行会打印“可能有冲突”并再次调用fn方法进行重试。 若replies值不为null,则使用Kafka producer向CAR_NUMBER topic发送一条消息。
2. seckill_kafka_consumer

此部分代码较为简单,初始化一个Kafka consumer监听CAR_NUMBER topic。对于新获取的消息,程序将向MySQL的seckill表中插入一条记录。
测试与结果分析
操作步骤
启动Docker容器
启动Seckill_Service
启动seckill_kafka_consumer
启动JMeter发送2000个并发请求
结果 JMeter request results 
Redis counter field

MySQL seckill table

结果显示,最终Redis中的counter值变为0,seckill数据表中插入了100条记录,未发生超卖或少卖现象。当然,实际生产环境中还需考虑更多因素,希望本文能起到抛砖引玉的作用,帮助读者更好地理解秒杀场景。
项目GitHub地址: MockSecKill