高并发图书秒杀系统实战:Spring Boot + Redis + React
Meta Description: 从零构建高并发图书秒杀系统,Spring Boot 3 + Redis Lua脚本实现原子扣库存,React前端,完整技术方案分享。
前言
秒杀系统是电商领域最经典的面试题之一,也是检验后端工程师能力的重要标准。本文将分享如何从零构建一个完整的图书秒杀系统,涵盖高并发场景下的核心技术方案。
这篇文章适合:
- 想要学习秒杀系统设计的开发者
- 需要了解Redis高并发方案的同学
- 对Spring Boot + React全栈开发感兴趣的朋友
阅读本文,你将学到:
- 秒杀系统的核心设计思路
- Redis Lua脚本实现原子扣库存
- 高并发下的库存一致性保障
- 前后端分离架构实践
一、秒杀系统核心挑战
1.1 典型场景
秒杀场景的特点:
- 瞬时高并发:上万用户同时抢购
- 库存有限:商品数量有限,不能超卖
- 限时限量:活动时间固定,每人限购一件
1.2 核心问题
| 超卖 | 库存只有100件,卖出101件 |
| 重复购买 | 同一用户购买多次 |
| 系统崩溃 | 瞬时流量导致服务不可用 |
| 数据一致性 | Redis缓存与数据库不一致 |
1.3 解决方案
| 超卖 | Redis Lua脚本原子扣库存 |
| 重复购买 | Redis Set记录已购用户 |
| 系统崩溃 | 缓存预热 + 限流 |
| 数据一致性 | 缓存失效策略 |
二、系统架构设计
2.1 技术选型
后端:
- Spring Boot 3.2.0
- MyBatis-Plus 3.5.5
- Redis(缓存 + 库存管理)
- JWT(用户认证)
- Java 17
前端:
- React 18.2.0
- TypeScript 5.3.3
- Vite 5.0.10
- Tailwind CSS 3.4.0
- Zustand(状态管理)
- ECharts(数据可视化)
部署:
- 阿里云服务器
- Nginx反向代理
- systemd服务管理
2.2 系统架构图
┌─────────────────────────────────────────────────────────┐
│ 用户请求 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Nginx (反向代理) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Spring Boot 后端 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Controller │ │ Service │ │ Mapper │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Redis │ │ MySQL │ │ JWT认证 │
│ (库存缓存) │ │ (持久化) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
三、数据库设计
3.1 核心表结构
秒杀活动表(seckill_activities):
| id | BIGINT | 主键 |
| book_id | BIGINT | 图书ID |
| seckill_price | DECIMAL | 秒杀价格 |
| total_stock | INT | 总库存 |
| remaining_stock | INT | 剩余库存 |
| start_time | TIMESTAMP | 开始时间 |
| end_time | TIMESTAMP | 结束时间 |
| status | VARCHAR | 状态 |
秒杀订单表(seckill_orders):
| id | BIGINT | 主键 |
| order_no | VARCHAR(50) | 订单号 |
| user_id | BIGINT | 用户ID |
| book_id | BIGINT | 图书ID |
| activity_id | BIGINT | 活动ID |
| total_price | DECIMAL | 总价 |
| status | VARCHAR | 订单状态 |
3.2 完整建表SQL
— 秒杀活动表
CREATE TABLE IF NOT EXISTS seckill_activities (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
book_id BIGINT NOT NULL,
seckill_price DECIMAL(10,2) NOT NULL,
total_stock INT NOT NULL,
remaining_stock INT NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (book_id) REFERENCES books(id),
INDEX idx_status (status),
INDEX idx_time (start_time, end_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
— 秒杀订单表
CREATE TABLE IF NOT EXISTS seckill_orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
book_id BIGINT NOT NULL,
activity_id BIGINT NOT NULL,
quantity INT DEFAULT 1,
total_price DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING_PAYMENT',
pay_time TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (book_id) REFERENCES books(id),
FOREIGN KEY (activity_id) REFERENCES seckill_activities(id),
INDEX idx_user_id (user_id),
INDEX idx_order_no (order_no),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
四、核心实现:Redis原子扣库存
4.1 为什么用Lua脚本?
问题: 如果用Java代码分步操作Redis:
// 错误示范:非原子操作
Long stock = redisTemplate.opsForValue().get(stockKey);
if (stock > 0) {
redisTemplate.opsForValue().decrement(stockKey);
// 这里可能被其他线程抢占!
}
在高并发下,多个线程可能同时读到stock=1,都认为有库存,导致超卖。
解决: 使用Lua脚本,Redis保证原子执行:
local stockKey = KEYS[1]
local userKey = KEYS[2]
local stock = tonumber(redis.call('GET', stockKey))
if stock == nil then
return –2 — 库存未初始化
end
if stock <= 0 then
return –1 — 库存不足
end
— 检查是否已购买
if redis.call('SISMEMBER', userKey, ARGV[1]) == 1 then
return –3 — 已购买
end
— 原子扣库存 + 记录用户
redis.call('DECR', stockKey)
redis.call('SADD', userKey, ARGV[1])
return 1 — 成功
4.2 Java实现
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// Lua脚本
private static final String STOCK_DECREMENT_LUA =
"local stockKey = KEYS[1]\\n" +
"local userKey = KEYS[2]\\n" +
"local stock = tonumber(redis.call('GET', stockKey))\\n" +
"if stock == nil then return -2 end\\n" +
"if stock <= 0 then return -1 end\\n" +
"if redis.call('SISMEMBER', userKey, ARGV[1]) == 1 then return -3 end\\n" +
"redis.call('DECR', stockKey)\\n" +
"redis.call('SADD', userKey, ARGV[1])\\n" +
"return 1";
@Transactional
public Result<SeckillOrder> seckill(SeckillRequest request) {
Long userId = request.getUserId();
Long activityId = request.getActivityId();
String stockKey = "seckill:stock:" + activityId;
String userKey = "seckill:users:" + activityId;
// 执行Lua脚本
DefaultRedisScript<Long> script = new DefaultRedisScript<>(STOCK_DECREMENT_LUA, Long.class);
Long result = redisTemplate.execute(
script,
Arrays.asList(stockKey, userKey),
String.valueOf(userId)
);
// 处理结果
if (result == null || result < 0) {
if (result == –1) return Result.error("商品已抢光");
if (result == –2) return Result.error("库存初始化失败");
if (result == –3) return Result.error("您已抢购过该商品");
return Result.error("抢购失败");
}
// 创建订单…
SeckillOrder order = createOrder(userId, activityId);
return Result.success(order);
}
}
4.3 缓存预热
活动开始前,将库存预热到Redis:
public void warmUpCache(Long activityId) {
SeckillActivity activity = activityMapper.selectById(activityId);
String stockKey = "seckill:stock:" + activityId;
String userKey = "seckill:users:" + activityId;
// 初始化库存
redisTemplate.opsForValue().set(stockKey, activity.getTotalStock());
// 清空已购用户集合
redisTemplate.delete(userKey);
}
五、后端API设计
5.1 Controller层
@RestController
@RequestMapping("/api/seckill")
@CrossOrigin
public class SeckillController {
@Autowired
private SeckillService seckillService;
// 获取进行中的秒杀活动
@GetMapping("/activities")
public Result<List<SeckillActivity>> getActiveActivities() {
return seckillService.getActiveActivities();
}
// 获取活动详情
@GetMapping("/detail/{id}")
public Result<SeckillActivity> getActivityDetail(@PathVariable Long id) {
return seckillService.getActivityDetail(id);
}
// 秒杀下单
@PostMapping("/order")
public Result<SeckillOrder> createSeckillOrder(@RequestBody SeckillRequest request) {
return seckillService.seckill(request);
}
// 管理端:创建活动
@PostMapping("/create")
public Result<SeckillActivity> createActivity(@RequestBody SeckillActivity activity) {
return seckillService.createActivity(activity);
}
}
5.2 API列表
| /api/seckill/activities | GET | 获取进行中的活动 |
| /api/seckill/detail/{id} | GET | 获取活动详情 |
| /api/seckill/order | POST | 秒杀下单 |
| /api/seckill/create | POST | 创建活动(管理员) |
| /api/books | GET | 图书列表 |
| /api/users/login | POST | 用户登录 |
| /api/orders | GET | 订单列表 |
六、前端实现
6.1 技术栈
{
"dependencies": {
"react": "^18.2.0",
"react-router-dom": "^6.21.0",
"zustand": "^4.4.7",
"axios": "^1.6.2",
"echarts": "^6.0.0",
"dayjs": "^1.11.10"
},
"devDependencies": {
"typescript": "^5.3.3",
"vite": "^5.0.10",
"tailwindcss": "^3.4.0"
}
}
6.2 状态管理(Zustand)
// stores/seckillStore.ts
import { create } from 'zustand';
interface SeckillState {
activities: SeckillActivity[];
loading: boolean;
fetchActivities: () => Promise<void>;
seckill: (activityId: number) => Promise<SeckillOrder>;
}
export const useSeckillStore = create<SeckillState>((set) => ({
activities: [],
loading: false,
fetchActivities: async () => {
set({ loading: true });
try {
const res = await axios.get('/api/seckill/activities');
set({ activities: res.data.data, loading: false });
} catch (e) {
set({ loading: false });
}
},
seckill: async (activityId: number) => {
const res = await axios.post('/api/seckill/order', {
activityId,
userId: localStorage.getItem('userId')
});
return res.data.data;
}
}));
6.3 秒杀按钮组件
// components/SeckillButton.tsx
import { useState } from 'react';
import { useSeckillStore } from '../stores/seckillStore';
export function SeckillButton({ activityId }: { activityId: number }) {
const [loading, setLoading] = useState(false);
const { seckill } = useSeckillStore();
const handleSeckill = async () => {
setLoading(true);
try {
const order = await seckill(activityId);
alert(`抢购成功!订单号:${order.orderNo}`);
} catch (e: any) {
alert(e.response?.data?.message || '抢购失败');
} finally {
setLoading(false);
}
};
return (
<button
onClick={handleSeckill}
disabled={loading}
className="bg-red-500 text-white px-6 py-2 rounded-lg hover:bg-red-600 disabled:opacity-50"
>
{loading ? '抢购中…' : '立即抢购'}
</button>
);
}
6.4 倒计时组件
// components/Countdown.tsx
import { useState, useEffect } from 'react';
import dayjs from 'dayjs';
export function Countdown({ endTime }: { endTime: string }) {
const [remaining, setRemaining] = useState('');
useEffect(() => {
const timer = setInterval(() => {
const diff = dayjs(endTime).diff(dayjs(), 'second');
if (diff <= 0) {
setRemaining('已结束');
clearInterval(timer);
} else {
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
const seconds = diff % 60;
setRemaining(`${hours}时${minutes}分${seconds}秒`);
}
}, 1000);
return () => clearInterval(timer);
}, [endTime]);
return <span className="text-red-500 font-bold">{remaining}</span>;
}
七、高并发优化策略
7.1 多级缓存
请求 → CDN → Nginx缓存 → Redis缓存 → MySQL
7.2 限流策略
// 使用Guava RateLimiter
@RestController
public class SeckillController {
private final RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000个请求
@PostMapping("/seckill/order")
public Result<SeckillOrder> seckill(@RequestBody SeckillRequest request) {
if (!rateLimiter.tryAcquire()) {
return Result.error("系统繁忙,请稍后重试");
}
return seckillService.seckill(request);
}
}
7.3 异步下单
// 使用消息队列异步处理订单
@Autowired
private RabbitTemplate rabbitTemplate;
public Result<String> seckill(SeckillRequest request) {
// Redis扣库存
Long result = redisTemplate.execute(luaScript, ...);
if (result < 0) return Result.error("抢购失败");
// 发送消息队列
rabbitTemplate.convertAndSend("seckill.order", request);
return Result.success("排队中,请稍后查看订单");
}
八、部署与运维
8.1 服务器配置
# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/book_seckill
username: root
password: ${DB_PASSWORD}
data:
redis:
host: localhost
port: 6379
password: ${REDIS_PASSWORD}
8.2 Nginx配置
upstream backend {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name seckill.example.com;
# 静态资源
location / {
root /var/www/frontend/dist;
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
九、性能测试结果
9.1 测试环境
| 服务器 | 阿里云 2核4G |
| 数据库 | MySQL 8.0 |
| 缓存 | Redis 7.0 |
| 并发用户 | 1000 |
9.2 测试结果
| QPS | 2000+ |
| 平均响应时间 | 50ms |
| 超卖次数 | 0 |
| 重复购买次数 | 0 |
十、踩坑经验总结
10.1 Redis连接超时
问题: 高并发下Redis连接池耗尽
解决: 增大连接池配置
spring:
data:
redis:
lettuce:
pool:
max-active: 200
max-idle: 50
10.2 数据库连接池耗尽
问题: 大量订单写入导致连接池耗尽
解决: 使用HikariCP优化配置
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
10.3 前端重复提交
问题: 用户多次点击按钮
解决: 按钮防抖 + 禁用状态
const [disabled, setDisabled] = useState(false);
const handleClick = async () => {
if (disabled) return;
setDisabled(true);
try {
await seckill(activityId);
} finally {
setTimeout(() => setDisabled(false), 1000);
}
};
十一、项目成果
11.1 功能清单
| 秒杀活动管理 | ✅ |
| 原子扣库存 | ✅ |
| 防重复购买 | ✅ |
| 用户认证 | ✅ |
| 订单管理 | ✅ |
| 管理后台 | ✅ |
| 数据统计 | ✅ |
11.2 技术亮点
十二、总结与展望
12.1 核心收获
通过这个项目,深入学习了:
- 秒杀系统的核心设计思路
- Redis高并发解决方案
- 前后端分离架构实践
12.2 未来优化方向
FAQ
Q: 为什么用Redis而不是直接操作数据库?
A: Redis是内存数据库,QPS可达10万+,MySQL只有几千。秒杀场景下,数据库扛不住瞬时流量。
Q: Lua脚本有什么优势?
A: Redis保证Lua脚本原子执行,不会被其他命令打断。避免了Java层面的并发问题。
Q: 如何保证Redis和MySQL数据一致?
A: 使用缓存失效策略:下单成功后删除缓存,下次查询时重新加载。
Q: 秒杀系统如何限流?
A: 常用方案:Nginx限流、Guava RateLimiter、Sentinel、消息队列削峰。
关于作者
一个热爱技术的全栈开发者,专注于高并发系统设计和前端工程化。如果你觉得这篇文章有帮助,欢迎点赞收藏!
项目源码:暂不支持
在线体验:暂不支持
关键词: 秒杀系统, Redis Lua, 高并发, Spring Boot, React, 原子扣库存, 电商系统



