应对突发流量并保证数据一致性的一些实践

以下是我在一个教育项目中实践高并发选课系统(峰值 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,00028ms99.97%0
峰值持续 1 分钟47,00045ms99.99%0
全程 10 分钟平均 18k32ms100%0

四、最核心的实践经验

  1. 永远不要相信前端限流,所有防护必须后端实现。
  2. 99.9% 的请求不要碰数据库,用 Redis + 本地缓存搞定。
  3. 库存扣减必须用 Lua 或数据库唯一约束,绝不相信业务代码逻辑。
  4. 高并发下单首选事务消息队列(RocketMQ),比分布式锁快一个数量级。
  5. 必须有三级防护 + 补偿机制,任何单点都可能被击穿。
  6. 监控必须秒级:Redis QPS、MQ 堆积、Sentinel 触发次数、慢查询。