技术背景与核心逻辑
在电商运营活动中,秒杀是典型的“收割”流量与转化的场景。高并发下的库存超卖是系统最大的技术痛点。本指南将直接构建一套基于Python Flask + Redis + Lua的高并发秒杀系统。核心思路是将库存扣减操作前置到Redis中执行,利用Redis的单线程特性和Lua脚本的原子性,彻底杜绝数据库层面的超卖问题,同时极大减轻数据库压力。
一、环境准备与依赖安装
在开始编码前,需要确保服务器或本地环境已安装基础组件。请严格按照以下步骤执行,不要跳过任何环节。
1. 安装Redis服务
Redis是核心组件,用于存储热点库存数据。以Ubuntu系统为例,执行以下命令直接安装并启动:
- 安装命令:
sudo apt-get update && sudo apt-get install redis-server -y
- 启动服务:
sudo service redis-server start
- 验证连接:
redis-cli ping,若返回PONG则表示正常。
2. 安装Python依赖库
我们需要Flask作为Web框架,redis-py作为客户端驱动。执行以下命令安装所需依赖:
pip install flask redis pymysql
注意:建议使用Python 3.8及以上版本以确保库的兼容性。
3. 安装并配置MySQL
虽然主要逻辑在Redis,但MySQL仍用于最终订单落库。确保MySQL服务已启动,并创建专用数据库。
- 登录MySQL:
mysql -u root -p
- 创建数据库:
CREATE DATABASE seckill_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- 使用数据库:
USE seckill_demo;
二、数据库表结构设计
我们需要两张核心表:一张是商品库存表,一张是订单表。请在MySQL命令行中直接执行以下SQL语句,确保表结构完整无误。
1. 创建商品库存表
```sql
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL COMMENT '商品名称',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存数量',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
2. 创建订单记录表
```sql
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户ID',
`goods_id` int(11) NOT NULL COMMENT '商品ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
3. 初始化测试数据
插入一个测试商品,库存设置为100,用于后续压测验证。
```sql
INSERT INTO `goods` (`name`, `stock`, `version`) VALUES ('iPhone 15 Pro', 100, 0);
```
三、Redis库存预热脚本
系统启动前,必须将MySQL中的库存数据同步加载到Redis中。这一步称为“库存预热”。创建一个名为init_stock.py的文件,并写入以下代码。
```python
import redis
import pymysql
数据库连接配置
DB_CONFIG = {
'host': 'localhost',
'user': 'root',
'password': 'your_password', 请修改为你的MySQL密码
'db': 'seckill_demo',
'charset': 'utf8mb4'
}
Redis连接配置
REDIS_POOL = redis.ConnectionPool(host='localhost', port=6379, db=0)
def sync_stock_to_redis(goods_id=1):
"""
将MySQL中的库存同步到Redis
"""
conn = pymysql.connect(DB_CONFIG)
cursor = conn.cursor()
try:
查询当前库存
sql = "SELECT stock FROM goods WHERE id = %s"
cursor.execute(sql, (goods_id,))
result = cursor.fetchone()
if not result:
print("商品不存在")
return
stock = result[0]
r = redis.Redis(connection_pool=REDIS_POOL)
将库存写入Redis,Key命名为 goods_stock_{id}
r.set(f"goods_stock_{goods_id}", stock)
print(f"商品ID {goods_id} 库存预热成功,当前库存: {stock}")
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
sync_stock_to_redis(goods_id=1)
```
执行命令python init_stock.py。如果输出“库存预热成功”,则说明数据已成功加载至Redis。
四、核心秒杀逻辑实现(Lua原子操作)
这是本系统的核心。我们将库存判断、扣减和订单资格检查封装在一个Lua脚本中。Redis保证Lua脚本的执行是原子性的,即在同一时刻不会有其他命令插入执行,从而避免了并发冲突。

创建主程序文件app.py,完整代码如下。请务必修改数据库密码部分。
```python
import time
import redis
import pymysql
from flask import Flask, request, jsonify
app = Flask(__name__)
配置信息
DB_CONFIG = {
'host': 'localhost',
'user': 'root',
'password': 'your_password', 修改为真实密码
'db': 'seckill_demo',
'charset': 'utf8mb4'
}
创建Redis连接池,设置超时时间防止阻塞
REDIS_POOL = redis.ConnectionPool(
host='localhost',
port=6379,
db=0,
socket_timeout=5,
socket_connect_timeout=5,
retry_on_timeout=True
)
Lua脚本:获取库存 -> 判断是否大于0 -> 扣减库存 -> 返回结果
这段脚本逻辑不可分割,是防超卖的关键
SECKILL_SCRIPT = """
local goods_id = KEYS[1]
local user_id = ARGV[1]
local stock_key = "goods_stock_" .. goods_id
local bought_key = "user_bought_" .. goods_id .. "_" .. user_id
-- 1. 检查是否重复购买
if redis.call('exists', bought_key) == 1 then
return -1 -- 已购买
end
-- 2. 检查并扣减库存
local stock = tonumber(redis.call('get', stock_key))
if stock == nil then
return -2 -- 商品不存在或未预热
end
if stock > 0 then
redis.call('decr', stock_key)
redis.call('set', bought_key, 1, 'EX', 3600) -- 标记已购买,防止一人多单
return 1 -- 抢购成功
else
return 0 -- 库存不足
end
"""
def get_db_connection():
"""获取数据库连接"""
return pymysql.connect(DB_CONFIG)
@app.route('/seckill', methods=['POST'])
def seckill():
"""
秒杀接口
参数: goods_id, user_id
"""
goods_id = request.form.get('goods_id', '1')
user_id = request.form.get('user_id')
if not user_id:
return jsonify({'code': 400, 'msg': '缺少用户ID'})
r = redis.Redis(connection_pool=REDIS_POOL)
try:
执行Lua脚本
KEYS[1] = goods_id
ARGV[1] = user_id
result = r.eval(SECKILL_SCRIPT, 1, goods_id, user_id)
if result == 1:
Redis扣减成功,开始写入MySQL
在生产环境中,这里建议使用消息队列异步解耦
为保证本指南的零门槛可运行性,此处采用同步写入
conn = get_db_connection()
try:
with conn.cursor() as cursor:
insert_sql = "INSERT INTO orders (user_id, goods_id) VALUES (%s, %s)"
cursor.execute(insert_sql, (user_id, goods_id))
更新MySQL库存(仅作数据一致性最终校准,不参与并发锁)
update_sql = "UPDATE goods SET stock = stock - 1 WHERE id = %s"
cursor.execute(update_sql, (goods_id,))
conn.commit()
return jsonify({'code': 200, 'msg': '抢购成功'})
except Exception as e:
conn.rollback()
记录日志,此处需配合补偿机制将库存加回Redis
print(f"数据库写入失败: {e}")
return jsonify({'code': 500, 'msg': '系统繁忙,请稍后重试'})
finally:
conn.close()
elif result == 0:
return jsonify({'code': 201, 'msg': '库存不足'})
elif result == -1:
return jsonify({'code': 202, 'msg': '重复购买'})
else:
return jsonify({'code': 500, 'msg': '系统异常'})
except Exception as e:
print(f"Redis执行异常: {e}")
return jsonify({'code': 500, 'msg': '服务繁忙'})
if __name__ == '__main__':
启动Flask服务
app.run(host='0.0.0.0', port=5000, threaded=True)
```
五、系统启动与压力测试
代码编写完毕后,我们需要模拟高并发场景来验证系统是否真正防超卖。
1. 启动Web服务
在终端执行:
python app.py
看到输出Running on http://0.0.0.0:5000即表示服务启动成功。
2. 使用Apache Bench进行压测
我们使用Linux自带的ab工具模拟1000个并发请求,总共发送1000个请求。如果系统逻辑正确,数据库中的最终订单数和剩余库存应完美对应。
- 安装ab工具(如果没有):
sudo apt-get install apache2-utils
执行压测命令:
ab -n 1000 -c 100 -p user.txt -T application/x-www-form-urlencoded http://127.0.0.1:5000/seckill
注意:ab工具需要post数据文件。创建一个名为user.txt的文件,内容如下。为了模拟不同用户,我们使用简单的固定参数(本例主要验证库存原子性,用户ID参数主要用于去重逻辑演示)。
```text
goods_id=1&user_id=10001
```
六、结果验证与排错
压测完成后,执行以下SQL验证数据一致性。
1. 查看最终订单数
```sql
SELECT COUNT() as total_orders FROM orders WHERE goods_id = 1;
``>
2. 查看商品剩余库存
```sql
SELECT stock FROM goods WHERE id = 1;
``>
3. 结果分析
如果初始库存为100,压测发送了1000个请求:
- 正常情况:订单数应为100,库存应为0。即便请求量远超库存,也不会出现-1或-2的情况。
- 异常情况:如果发现库存为负数,说明Redis脚本未生效或数据库逻辑有误;如果订单数大于100,说明Redis的原子锁失效。
通过上述步骤,我们成功构建了一套零门槛、可落地的防超卖秒杀系统。该方案利用Redis的原子操作拦截了99%的无效流量,保护了脆弱的数据库层,是运营收割流量时必须掌握的核心技术。