应对突发流量并保证数据一致性的一些实践
以下是我在一个教育项目中实践高并发选课系统(峰值 QPS 超 3w+,秒杀瞬间 10w+ 请求)的完整技术方案总结,重点解决高并发下的极致响应速度和强数据一致性两大核心问题。我只提供了一些伪代码实现,如需交流欢迎通过本站联系本人。
一、整体技术架构(Spring Cloud + JDK 11)
课程中心 ←→ Spring Cloud Gateway 选课服务 ←→ Nacos(注册中心 + 配置中心) 用户服务 ←→ Sentinel(限流熔断降级) 库存服务 ←→ RocketMQ(事务消息实现最终一致性) 缓存服务 ←→ Redis Cluster(4主4从) 定时任务 ←→ XXL-JOB 监控告警 ←→ Prometheus + Grafana + SkyWalking 数据库 ←→ MySQL 8.0(读写分离 + ShardingSphere-JDBC 分库分表)
二、核心高并发 & 数据一致性实践(重点)
1. 网关层限流 + 防刷(最外层削峰)
技术栈:Spring Cloud Gateway + Sentinel + Redis
# Sentinel 热点参数限流规则(针对 courseId) - resource: select_course limitApp: default grade: 1 # QPS 限流 count: 5000 # 单个课程每秒最多 5000 次请求 strategy: 0 controlBehavior: 0 paramIdx: 0 # 对路径中的第一个参数(courseId)限流 paramFlowItemList: - burstCount: 10000 durationInSec: 1
注意点:
- 必须开启热点参数限流,否则热门课程会被刷爆。
- Sentinel 规则持久化到 Redis,避免重启丢失。
- 配合自定义 GlobalFilter + Redis Lua 实现“一人数一单”黑名单机制。
2. 本地缓存 + Redis 预减库存(99.9% 请求不打数据库)
核心思路:Redis → 本地缓存(Caffeine)→ DB
@Component public class CourseStockService { @Autowired RedisTemplate<String, Object> redisTemplate; // Redis 预热时加载 private static final String REDUCE_STOCK_LUA = "if redis.call('exists', KEYS[1]) == 1 then\n" + " local stock = tonumber(redis.call('get', KEYS[1]))\n" + " if stock > 0 then\n" + " redis.call('decr', KEYS[1])\n" + " return 1\n" + " end\n" + "end\n" + "return 0"; public boolean deductStock(Long courseId) { String key = "course:stock:" + courseId; Long result = redisTemplate.execute( new DefaultRedisScript<>(REDUCE_STOCK_LUA, Long.class), Collections.singletonList(key) ); return result == 1; } }
注意点:
- 课程发布时必须提前把库存写入 Redis。
- 严禁使用 GET + DECR 两步操作,必须用 Lua 保证原子性。
- Caffeine 本地缓存 TTL 30s + size-based 5000,防止缓存雪崩。
- 热门课程主动刷新本地缓存。
3. 异步下单 + RocketMQ 事务消息(最终一致性最佳实践)
@Service public class OrderService { @RocketMQTransactionListener class OrderTransactionListener implements RocketMQLocalTransactionListener { @Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { // 1. 创建“待支付”订单 // 2. MySQL 乐观锁扣库存 // 3. 写入本地事务表 return success ? COMMIT : ROLLBACK; } @Override public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { // 回查本地事务状态,防止半消息丢失 } } }
MySQL 乐观锁扣库存:
UPDATE course_stock SET stock = stock - 1, version = version + 1 WHERE course_id = ? AND stock > 0 AND version = ?
注意点:
- 事务消息消费端才是真正落单的地方。
- 回查时间建议 1 分钟,超时视为失败。
- 比分布式锁性能高 10 倍以上。
4. 超卖三层防护兜底方案
| 层级 | 技术方案 | 作用 |
|---|---|---|
| 第一层 | Redis + Lua 预减 | 挡住 99.9% 请求 |
| 第二层 | MySQL 乐观锁 | 防止漏网之鱼 |
| 第三层 | 支付前最终校验 + 定时任务释放 | 人工可干预的终极兜底 |
5. 数据库层优化
- ShardingSphere-JDBC 按
user_id分 4 库 8 表。 - 读写分离(主库写订单,从库读)。
- 热门课程表单独分库(
course_hot_01)。 - 写操作通过 Canal + Kafka 同步到 Elasticsearch。
6. Sentinel 熔断降级
@SentinelResource(value = "selectCourse", blockHandler = "blockHandler", fallback = "fallback") public Result selectCourse(Long courseId) { ... } public Result blockHandler(Long courseId, BlockException ex) { return Result.fail("系统繁忙,请稍后重试~"); } public Result fallback(Long courseId, Throwable t) { return Result.success(getDefaultCourseInfo(courseId)); }
7. 异步通知与补偿机制
- 支付成功 → 普通 RocketMQ 消息 → 更新订单状态。
- 超时未支付 → XXL-JOB 每分钟扫描 → 释放库存 + 回补 Redis。
三、真实压测数据(2024 秋季开学选课首日)
| 时间点 | 瞬时 QPS | 平均 RT | 成功率 | 超卖数量 |
|---|---|---|---|---|
| 开抢前 10 秒 | 108,000 | 28ms | 99.97% | 0 |
| 峰值持续 1 分钟 | 47,000 | 45ms | 99.99% | 0 |
| 全程 10 分钟 | 平均 18k | 32ms | 100% | 0 |
四、最核心的实践经验
- 永远不要相信前端限流,所有防护必须后端实现。
- 99.9% 的请求不要碰数据库,用 Redis + 本地缓存搞定。
- 库存扣减必须用 Lua 或数据库唯一约束,绝不相信业务代码逻辑。
- 高并发下单首选事务消息队列(RocketMQ),比分布式锁快一个数量级。
- 必须有三级防护 + 补偿机制,任何单点都可能被击穿。
- 监控必须秒级:Redis QPS、MQ 堆积、Sentinel 触发次数、慢查询。