
做演唱会在线票务预订平台我最大的感受是它跟普通电商系统的差距不是技术栈的差距而是库存模型和并发模型的差距。普通电商的库存可以抽象成一个数字字段减库存只要扣数量就行而演唱会票务的库存是座位维度的同一个座位不能卖两次两个用户不能同时锁定同一个位置用户放弃购买之后座位还得及时释放。这个差异直接决定了整个系统的走向。所以这个项目最终把架构收敛到了微服务分布式方向后端以 SpringBoot SpringCloud 做服务拆分和治理前端用 Vue 承载购票页、选座交互和订单流程再配合注册中心、网关、配置中心、分布式锁、消息队列这类基础设施把高并发、强一致、防超卖这些硬骨头一个一个啃下来。这篇文章就把我在这个平台里实际落地的方案整理出来包括服务怎么拆、扣库存为什么不用分布式锁而是用 Redis Lua、分布式事务怎么取舍、Vue 怎么跟网关配合完成选座下单以及那些文档里不会写但一定会踩的坑。1. 项目概述与架构设计思路1.1 演唱会票务场景的特殊性演唱会票务和普通电商本质上是两套不同的业务逻辑。普通电商的商品库存只是一个总量用户下单后扣减数量即可但演唱会门票的库存精确到每个座位还要区分不同票档和区域比如内场 VIP、看台 A 区、看台 B 区价格和权益完全不同。这种情况下库存操作不能简单执行stock - 1而是要精确处理“座位状态”的流转可售、锁定中、已售、已释放每个状态都由对应的业务事件触发。演唱会票务还有典型的瞬时峰值特征。普通电商的流量是长尾的一天二十四小时都有订单而演唱会开票瞬间几十万人同时挤进来抢几万张票这种洪峰流量会把任何单体应用直接打垮。更麻烦的是票务业务对一致性要求极高不能超卖不能两个用户锁同一个座位不能出现支付成功但订单状态还是待支付的情况。黄牛和脚本刷票也是长期存在的威胁限购策略、实名认证、设备指纹风控都得纳入系统设计。从技术层面拆开看真正的挑战集中在三个点第一高并发下的库存与座位状态原子更新第二订单、库存、支付三个服务之间的数据最终一致性第三用户从选座到支付的整个交互链路必须做到快速响应和状态可预期。这三个点每一个都直接关系到用户是否愿意为这个平台买单。1.2 技术选型SpringBoot Vue SpringCloud 的取舍技术选型不需要追求最新要紧的是稳定生态和团队上手成本。后端用 SpringBoot 3.2 配合 JDK 17这是目前比较稳妥的组合SpringBoot 的自动装配和生态完善度让开发效率非常高。SpringCloud 这边选择 SpringCloud Alibaba 体系Nacos 做注册中心和配置中心Spring Cloud Gateway 做统一网关Sentinel 做限流熔断配合 OpenFeign 做服务间调用。如果团队还在用 SpringBoot 2.7那 SpringCloud 选 2021.0.xNacos 用 2.x也能跑得很稳。版本这里一定要锁死不同大版本之间兼容性问题会让人抓狂。我们项目里有成员因为 SpringCloud 和 SpringBoot 版本错配网关路由一直加载不上排查了半天才发现是版本冲突。前端选 Vue 3 Vite Pinia主要原因是演唱会票务页面交互复杂座位图、倒计时、抢票按钮状态这类高频交互需要响应式能力强的框架Vue 的模板语法和状态管理比传统 jQuery 时代不知道舒服了多少。而 SpringCloud 这边微服务的注册发现、配置管理、网关统一鉴权正好解决了模块化之后的服务治理问题。选这套组合还有一个现实考量招人容易出了问题社区资料多不会卡在某个冷门使用姿势上。1.3 服务拆分的边界服务拆分没有绝对标准我的原则是按业务域拆分但不拆到数据孤岛。演唱会票务平台最终拆成六个核心服务每个服务有独立的数据库服务间只通过 API 和消息队列通信。服务名核心职责关键数据对外接口示例user-service用户注册登录、实名认证、Token 管理用户表、用户凭证表/api/user/loginshow-service演出信息、场次管理、票档与座位图演出表、场次表、座位图表/api/show/listinventory-service座位状态管理、余票锁定与释放座位状态缓存、库存流水/api/inventory/lockSeatorder-service订单创建、状态流转、超时关单订单表、订单明细表/api/order/createpayment-service支付单创建、渠道回调、退款支付单表、退款记录表/api/payment/callbacknotify-service短信、邮件、站内信通知通知记录表MQ 消费为什么把订单和库存拆成两个服务因为库存服务是最核心的并发写入口需要独立扩展和做细粒度锁而订单服务是长事务业务会有大量状态更新和查询两者拆开后可以各自做缓存和数据库优化互不干扰。但也不能拆得太细比如把支付渠道对接再拆成微信服务、支付宝服务那就过度了支付服务内部做策略模式完全够用。拆分的边界要看团队维护成本六个服务已经是这个量级项目比较舒服的粒度。2. SpringCloud 分布式架构落地的关键组件2.1 注册中心、配置中心与网关的配合服务拆分之后最直接的问题是服务之间怎么找到对方、配置怎么管理、外部请求怎么进入内部。我们用的是 Nacos 做注册中心和配置中心。Nacos 比 Eureka 好的地方在于注册中心和配置中心是同一套不用额外搭配置服务。每个服务启动时自动注册到 Nacos消费方通过服务名调用完全不用关心目标服务的 IP 和端口。网关用的是 Spring Cloud Gateway它是基于 WebFlux 的响应式网关性能比 Zuul 1.x 好很多。网关层承担三件事路由转发、统一鉴权、限流。路由配置最简单的方式是Path前缀和服务名映射比如所有/api/order/**的请求转发到order-service配置如下spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1 - id: show-service uri: lb://show-service predicates: - Path/api/show/** filters: - StripPrefix1lb://表示从负载均衡组件中按服务名寻找实例StripPrefix1表示去掉第一级前缀再转发。这样对前端来说所有请求都打到网关统一入口后端服务不直接暴露安全性也好很多。配置中心方面Nacos 上维护了application-dev.yml、application-prod.yml这类共享配置也维护了每个服务自己的配置。数据库连接、Redis 地址、消息队列地址都从配置中心拉取配合RefreshScope可以实现配置动态刷新。比如调整某个服务的超时时间改完配置发布业务代码不用重启就能生效。2.2 服务间调用与超时控制服务间调用用 OpenFeign它是声明式的 HTTP 客户端写起来非常舒服。定义一个 Feign 接口标注目标服务名和方法映射就能像调用本地方法一样调用远程服务。这里有个细节内部服务接口和对外接口一定要区分开。对外接口走网关加鉴权和参数校验内部接口用/internal/**前缀只允许服务间调用不暴露到公网。FeignClient(name inventory-service, contextId inventoryFeignClient) public interface InventoryFeignClient { PostMapping(/internal/inventory/lockSeat) ResultLockSeatVO lockSeat(RequestBody LockSeatDTO dto); PostMapping(/internal/inventory/releaseSeat) ResultVoid releaseSeat(RequestBody ReleaseSeatDTO dto); }Feign 调用最大的坑就是超时。默认连接超时只有 1 秒读取超时也不长而真实业务中一个接口可能要查数据库、扣缓存、更新状态1 秒根本不够。如果不单独配置高峰期服务一慢调用方大量线程阻塞最后会把整个线程池拖垮。我这边统一在application.yml里配置了 Feign 超时feign: client: config: default: connectTimeout: 3000 readTimeout: 10000同时每个服务的线程池要按下游接口耗时做预估简单算一下假设一个核心接口平均耗时 200ms单实例 Tomcat 默认 200 线程单个实例每秒最多处理约 1000 个请求两台实例就是 2000 QPS。压测之后如果发现线程不够优先考虑调超大线程池而不是无限增加实例。服务间调用还需要关注链路追踪。我们当时用 Micrometer Tracing 接 Zipkin把traceId贯穿整个调用链。线上排查问题的时候用户报了一个“选座失败”从前端请求到网关再到 inventory-service全程可以通过 traceId 把日志串起来直接定位到哪一步慢了、哪一步报了异常。没有链路追踪的时候这种跨服务排查基本靠猜效率太低。2.3 扣库存为什么不用分布式锁而是 Redis Lua这个点是整个项目里我最有感触的地方。很多团队一上来就想到“用 Redis 分布式锁保护扣库存”然后流程变成加锁、查询库存、判断库存是否足够、执行扣减、释放锁。这个方案逻辑上没错但性能很差而且一旦锁超时释放就会出现并发问题。真正的高并发扣库存应该用 Redis 的原子操作。把“查询库存是否足够”和“扣减库存”合并成一个 Lua 脚本在 Redis 单线程执行模型下脚本内部是绝对原子、不会被打断的。比如全局限量的余票扣减-- KEYS[1] 是余票数量对应的 key -- ARGV[1] 是本次扣减数量 if tonumber(redis.call(get, KEYS[1])) tonumber(ARGV[1]) then redis.call(decrby, KEYS[1], ARGV[1]) return 1 end return 0这个脚本返回 1 表示扣减成功返回 0 表示库存不足。整个判断和扣减过程是一次 Redis 操作不存在并发穿插的问题也就不需要加分布式锁。实际压测中单台 Redis 实例每秒能执行几万次这样的脚本性能完全够用。座位锁定的逻辑更复杂一点。用户选座时要确保这个座位没有被其他人锁定或售出。我同样用 Lua 脚本实现-- KEYS[1] 是场次座位集合 key -- ARGV[1] 是座位号 -- ARGV[2] 是锁定过期时间 if redis.call(sismember, KEYS[1], ARGV[1]) 0 then redis.call(sadd, KEYS[1], ARGV[1]) redis.call(expire, KEYS[1], ARGV[2]) return 1 end return 0这里用 Set 存储已被锁定或售出的座位号sismember判断座位是否已经被占没有则加入集合并设置过期时间。用 Set 的好处是天然去重而且可以用SREM在释放座位时精确移除。那分布式锁还有没有用有但不是用来扣库存的。比如防止同一用户短时间内重复创建订单可以用分布式锁做幂等控制锁的粒度是“用户 ID 场次 ID”而不是全局锁。真正要保护的是业务流程不是单纯的库存数字。2.4 分布式事务方案怎么取舍拆分成多个服务后最棘手的问题就是分布式事务。一个完整的购票流程涉及订单服务、库存服务、支付服务任何一个环节失败用户的资金和座位状态都可能不一致。强一致方案用的是 Seata AT 模式。Seata 会拦截 SQL记录数据变更回滚日志通过全局事务协调器统一提交或回滚。AT 模式的优点是业务代码侵入小基本不用改 SQL缺点是锁粒度比较粗性能开销大。在实际压测中AT 模式在高并发下吞吐量下降明显不太适合抢票这种瞬时流量场景。最终一致性方案用的是消息队列加本地事件表。核心逻辑是订单服务先写本地订单数据同时写入一条“订单创建事件”然后通过 RocketMQ 把事件发给库存服务和支付服务。库存服务消费消息后锁定座位支付服务消费消息后生成支付单。如果某个环节失败消息会重试配合事件状态表做最终状态核对。这里我的建议是库存扣减和座位锁定走 Redis 先行动作支付回调走消息最终一致Seata 只在退款这类低频但需要强一致的场景使用。不要试图用一个分布式事务框架解决所有问题把每个业务场景的一致性要求想清楚再决定用强一致还是最终一致。演唱会票务平台里用户最不能接受的不是“等待几秒出票”而是“支付成功但没座位”和“选了座位却下不了单”这两类问题通过最终一致性加人工对账完全可以兜住。3. 核心链路实操从选座、锁单到支付出票3.1 工程结构与 Maven 聚合模块项目采用 Maven 多模块结构父 POM 统一管理依赖版本子模块按服务拆分。Spring Boot 的 parent 负责 Spring Boot 相关依赖版本SpringCloud BOM 和 SpringCloud Alibaba BOM 负责微服务组件版本这三个版本号必须在父 POM 中严格锁定。concert-ticket-parent ├── user-service ├── show-service ├── inventory-service ├── order-service ├── payment-service ├── notify-service ├── gateway └── common ├── common-core ├── common-redis ├── common-mq └── common-securitycommon-core放统一返回结果、异常定义、工具类common-redis封装 Redis 配置和 Lua 脚本调用common-security放 JWT 工具类和网关过滤器。公共模块只放通用能力不放业务代码否则模块之间会越来越耦合丧失微服务拆分的意义。3.2 用户认证与网关鉴权联动用户认证这块我们最初想过用 Spring Security OAuth2但后来发现对于票务平台这种自研系统来说用 Sa-Token 更轻量。Sa-Token 天然支持 Redis 集成、SSO、踢人下线开发效率很高。登录成功后用户服务会生成一个 accessToken这个 Token 是无状态的网关拿到之后通过共享密钥解析解析出用户 ID 后把 userId 放进请求头转发给下游服务。public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); // 白名单登录、演出列表、座位图等接口不校验 Token if (isWhiteList(path)) { return chain.filter(exchange); } String token request.getHeaders().getFirst(Authorization); LoginUser loginUser JwtUtil.parseToken(token); if (loginUser null) { return unauthorized(exchange); } ServerHttpRequest newRequest request.mutate() .header(X-UserId, String.valueOf(loginUser.getUserId())) .build(); return chain.filter(exchange.mutate().request(newRequest).build()); } }这里必须强调一个安全细节下游服务不能无条件信任来自网关的请求头万一某个内部服务被直接暴露伪造X-UserId就能越权操作。所以内部服务在网络层面要做隔离只允许网关所在的网段访问业务服务同时网关对下游服务之间的调用做来源校验。Token 过期的问题也很常见。accessToken 有效期我设为 30 分钟refreshToken 有效期 7 天存在 Redis 中value 是用户 ID。前端 axios 响应拦截器收到 401 状态码时会用 refreshToken 静默刷新 accessToken刷新成功后再重放之前的请求。这套逻辑写好后用户基本感受不到 Token 过期。3.3 选座、锁座、下单的完整链路用户选座下单的流程是整个平台的核心链路这里把每一步的时序和状态变化都拆开看用户打开座位图前端从 show-service 获取座位图数据和座位状态快照。可售座位显示绿色已售显示灰色锁定中显示黄色。用户点击一个绿色座位前端先做本地占位然后调用 inventory-service 的锁座接口。锁座接口执行 Lua 脚本把座位标记为锁定中同时返回锁座 ID 和锁定时长比如 15 分钟。锁座成功后前端带着锁座 ID 调用 order-service 创建订单。订单服务先调用 inventory-service 的内部接口校验这个座位仍然处于当前用户锁定的状态校验通过后创建订单订单状态为待支付。支付服务生成支付单返回支付二维码或拉起支付 App。用户支付成功后支付渠道异步回调支付服务支付服务更新支付单状态并发送“支付成功”的消息到 RabbitMQ。订单服务消费支付成功消息把订单状态改为已支付同时调用 inventory-service 把座位状态从锁定中改为已售。通知服务消费同样的消息发送出票短信。这一步有个非常关键的防超卖设计锁座和创建订单之间不是完全独立的。订单服务创建订单前必须校验锁座 ID 仍然有效而且座位状态依然属于当前用户。如果用户锁座后下单失败前端会触发释放座位接口通知 inventory-service 把座位改回可售状态。PostMapping(/api/order/create) public ResultOrderVO createOrder(RequestBody CreateOrderDTO dto) { // 1. 校验锁座信息是否有效 LockSeatVO lockInfo inventoryFeignClient.checkLockSeat(dto.getLockSeatId()); if (lockInfo null || !lockInfo.getUserId().equals(dto.getUserId())) { return Result.fail(座位锁定失效请重新选座); } // 2. 幂等校验防止重复创建订单 String idempotentKey dto.getUserId() : dto.getShowId(); boolean notDuplicated redisTemplate.opsForValue() .setIfAbsent(order_idempotent: idempotentKey, 1, Duration.ofSeconds(5)); if (!notDuplicated) { return Result.fail(操作太频繁请稍后再试); } // 3. 创建本地订单 Order order orderService.createOrder(dto); // 4. 发送延迟消息15 分钟后检查订单是否未支付 mqSender.sendDelayMessage(order.getOrderId(), Duration.ofMinutes(15)); return Result.success(OrderVO.from(order)); }3.4 消息异步化订单超时与支付通知订单超时未支付需要自动关单并释放座位。实现方案很多定时任务扫表最原始但延迟精度差而且高峰期大量无效扫描会拖垮数据库。我用 RabbitMQ 的 TTL 死信队列实现延迟消息下单后发一条普通的订单消息到延迟队列消息 TTL 设为 15 分钟15 分钟之后消息进入死信队列死信队列消费者执行关单逻辑。延迟队列的配置大概是这样Configuration public class RabbitConfig { Bean public Queue orderDelayQueue() { return QueueBuilder.durable(order.delay.queue) .withArgument(x-dead-letter-exchange, order.exchange) .withArgument(x-dead-letter-routing-key, order.cancel) .build(); } Bean public Queue orderCancelQueue() { return QueueBuilder.durable(order.cancel.queue).build(); } }关单逻辑里要注意幂等订单状态已经变成已支付就不能再关单已经释放过的座位不能重复释放。判断完订单状态后更新订单状态为已取消再发送消息让库存服务释放座位。支付服务这边回调接口是重点。微信和支付宝的回调可能会重复推送所以回调处理必须幂等。支付服务先根据支付单号查库如果支付单已经是支付成功的状态直接返回成功回执给渠道方不重复处理。每笔回调都记录回调流水方便排查问题。支付成功之后座位的最终状态修改其实不需要同步等待。用户付完款前端可以立即显示“支付成功出票中”后台通过消息队列继续完成出票和短信通知。这样用户感知快系统压力也分散。演唱会场景里有些用户恨不得一秒钟就收到票短信所以通知服务要尽早消费消息短信通道选择上宁可贵一点也要保证到达率。3.5 前端 Vue 的核心细节前端这边Vue 3 Pinia 是主力。全局状态里维护了用户信息、当前选择的场次、选中的座位列表、锁座倒计时。座位图组件用 SVG 绘制每个座位是一个图形元素根据座位状态控制颜色和点击事件。用户点击座位后前端先发送请求后端锁座成功再更新本地选座状态不能让用户以为选座成功实际却没锁住。这里有一个操作体验的细节锁座有效期展示。用户锁座成功后后端返回锁定时长前端启动一个倒计时倒计时归零就自动清除选中的座位状态并提示“选座超时请重新选择”。这个功能必须做否则用户占着座位不付款座位资源会被大量浪费黄牛很容易利用这个机制锁票。const lockSeat async (seatId: string) { const res await api.inventory.lockSeat({ showId, seatId }); if (res.code 0) { selectedSeats.value.push({ seatId, lockId: res.data.lockId }); startCountdown(res.data.expireIn); } else { message.error(res.msg); refreshSeatMap(); } };路由方面用 Vue Router 的导航守卫配合用户状态实现动态路由。用户未登录只能访问演出列表和详情页进入下单或支付页面必须检查登录状态和实名认证状态。订单列表页用了滚动分页加载避免一次拉太多数据导致页面卡顿。另外演唱会平台如果要做演出回放或者直播预约前端可能会遇到 m3u8 视频流的播放问题。这个问题不复杂安装hls.js通过video标签加载.m3u8地址就可以不需要额外引入重型播放器。我建议把播放器封装成一个独立的 Vue 组件方便在不同宣发页面复用。4. 高频问题排查与避坑记录4.1 超卖是真的可能发生的而且是悄无声息的我们遇到过最典型的一次超卖问题不是发生在 Redis 扣减的时候而是发生在缓存与数据库不一致的时候。某个热门场次缓存里显示还有最后 3 张余票数据库里实际也是 3 张。两个请求同时读到缓存都判断余票充足然后都去数据库执行update stock stock - 1 where stock 1数据库层面实际上只会更新成功一条但代码逻辑里如果漏掉了对更新行数的判断就会继续下单导致出现 2 个订单抢 1 张票的结果。这个问题的根源是缓存判断只是一个前置条件最终的数据正确性必须由 MySQL 的原子更新来兜底。所以扣库存必须写成update inventory set remaining_count remaining_count - 1 where show_id #{showId} and remaining_count 1;然后检查受影响行数等于 1 才继续创建订单。如果受影响行数为 0说明库存已被其他请求抢走直接返回失败。同时Redis 扣减和 MySQL 扣减不能乱序我的建议是 Redis 先扣减成功再执行 MySQL 原子更新MySQL 失败则要补偿回滚 Redis。这套方案虽然啰嗦但在生产环境里最经得起考验。4.2 分布式锁的坑远比想象中多虽然扣库存不建议用分布式锁但防重复下单、防并发退款这类场景还是会用到。用 Redis 分布式锁时我踩过几个非常经典的坑。第一个坑是锁的过期时间太短。某个业务逻辑里要做数据库更新加外部接口调用预计耗时 3 秒锁过期时间只设了 2 秒结果锁提前释放另一个线程进入临界区出现了并发数据错乱。解决方法是使用 Redisson它的看门狗机制会自动续期默认每 10 秒检测一次只要业务线程还活着就不断续期不会出现锁提前失效的问题。第二个坑是 SETNX 和 EXPIRE 分两步执行。中间一旦服务宕机EXPIRE 没执行锁就永远不释放。正确写法是SET key value NX EX 30把加锁和过期时间合并成一条命令。Redisson 内部已经把这类细节封装好了不需要自己重复造轮子。第三个坑是锁的重入问题。同一个线程在临界区内部又调用了加锁的方法如果锁不支持重入就会自己把自己锁死。Redisson 的锁默认支持重入这也是我推荐直接用 Redisson 而不是自己实现 Redis 锁的原因。锁策略上还要注意粒度。全局锁会严重降低并发度能缩小范围就缩小范围比如锁用户维度、锁场次维度而不是锁整个库存服务的某个方法。锁粒度越细系统吞吐量越高。4.3 延迟关单和支付成功“打架”的边界演唱会抢票场景里用户可能在下单后的第 14 分钟才完成支付但延迟关单消息在第 15 分钟触发了关单逻辑。这时候订单状态已经变成已取消座位也已经释放但支付回调又来了怎么办这个边界处理不好一定会出现用户钱扣了、票没了的情况。我的处理方式是支付回调处理时先根据支付单号查出关联的订单再判断订单状态。如果订单已经处于已取消状态不能直接忽略回调也不能直接把订单改回已支付而是进入退款流程自动发起原路退款同时记录退款原因。这样用户虽然没有拿到票但资金不会受损系统也不会出现“已支付已取消”的脏状态。反向的边界也很重要支付回调还没来延迟关单消息先到了。这时候要判断订单状态为待支付理论上可以关单并释放座位但如果下一秒支付回调就到了就会触发上面的退款流程。为了减少这种“差一秒”导致的退款我的方案是把延迟关单时间设定为 18 分钟比前端展示的支付倒计时多 3 分钟这 3 分钟是为了吸收支付渠道回调的延迟。4.4 前后端联调和部署运维的隐性问题前后端联调最常见的问题是跨域。开发环境我直接用 Vite 的 proxy 配置把/api代理到网关地址不需要后端处理跨域。生产环境用 Nginx 反代前端静态文件并把/api前缀统一转发到网关。网关侧开启 CORS 配置允许前端域名访问这样一个链路下来跨域问题基本能一次性解决。还有一个容易被忽略的问题是 Token 刷新时的并发请求。accessToken 过期后前端可能同时有多个请求都返回 401如果每个请求都去刷新 TokenRedis 里的 refreshToken 会被后面的刷新操作覆盖。所以刷新逻辑要加一个队列只发一次刷新请求其他请求等待刷新完成后自动重放。否则用户会在页面点一下突然被踢下线体验很糟糕。部署层面我建议服务的启动顺序固定下来先启动 Nacos然后启动 common 相关的基础配置再启动业务服务最后启动网关。网关一定要最后启动否则网关已经就绪下游服务还在注册中请求全部报 503。服务上线的时候还要注意 Nacos 的权重配置先摘掉一台实例的流量等新版本稳定了再切回来不要直接重启所有实例。4.5 常见问题速查表最后把项目里遇到过的高频问题整理成一张速查表排查问题的时候可以先对照一遍现象可能原因解决方案明明有票却提示库存不足Redis 缓存与 MySQL 库存不同步扣减成功后写 MySQL异步同步 Redis配合定时任务对账支付成功但订单仍为待支付支付回调消息丢失或消费失败消息队列开启重试支付回调记录流水人工对账兜底用户选座后座位很快被释放锁座 TTL 设置过短锁座 TTL 设为 15 分钟前端展示同步倒计时网关频繁 503服务尚未注册完成或已下线检查 Nacos 服务列表调整部署顺序用户 Token 一会儿就失效refreshToken 并发刷新被覆盖前端对 401 请求做队列化处理只发一次刷新请求高峰期订单服务线程池打满Feign 调用下游超时时间过长配置连接超时 3 秒、读取超时 10 秒开启熔断降级座位图加载很慢座位数据全量从数据库查询座位图接口加 Redis 缓存按场次缓存过期时间 10 分钟这个项目从搭建骨架到扛过第一场真实抢票过程中最大的教训是微服务本身不是银弹它解决的是扩展性和团队协作的问题但把单体里隐藏的复杂性全部显式化了。服务一拆每个团队都只关心自己的模块但数据一致性、调用超时、幂等控制这些问题都必须有人站在全局视角去设计。如果再做一遍我会在项目启动第一天就把压测量化清楚单场演唱会预期峰值 QPS、最长链路 RT、库存写并发量这些数字直接决定服务拆分到什么粒度、哪些地方需要 Redis 前置、哪些地方可以接受最终一致。先把业务约束定死再谈架构选型才是正确的顺序。