欢迎光临
我们一直在努力

高并发图书秒杀系统

高并发图书秒杀系统实战: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 技术亮点

  • Redis Lua脚本:原子扣库存,防止超卖
  • 缓存预热:活动开始前预热库存
  • 防重复购买:Redis Set记录已购用户
  • 前后端分离:React + Spring Boot

  • 十二、总结与展望

    12.1 核心收获

    通过这个项目,深入学习了:

    • 秒杀系统的核心设计思路
    • Redis高并发解决方案
    • 前后端分离架构实践

    12.2 未来优化方向

  • 分布式锁:Redisson实现分布式锁
  • 消息队列:RabbitMQ异步下单
  • CDN加速:静态资源CDN分发
  • 容器化部署:Docker + K8s

  • 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, 原子扣库存, 电商系统

    赞(0)
    未经允许不得转载:171主机测评 » 高并发图书秒杀系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址