高并发票务系统实战:Redis分布式锁与限流架构设计
大型线下活动票务系统在面临高并发抢购时,如何保证系统稳定、公平公正,同时处理好各种异常场景,是技术团队必须面对的挑战。本文将以一次实际的大型展会票务销售事件为背景,深入分析从技术选型、架构设计到具体代码实现的全流程,为开发类似高并发系统的工程师提供可落地的解决方案。
1. 理解高并发票务系统的核心挑战
1.1 业务场景分析
大型展会活动票务销售通常面临几个典型特征:瞬时高并发、库存有限、用户操作频繁、交易实时性要求高。以BW(Bilibili World)这类大型展会为例,数万张门票可能在几分钟内售罄,同时有数十万用户在线抢票。
技术层面需要解决的核心问题包括:
- 库存超卖问题:如何保证不会卖出超过实际库存数量的票
- 系统稳定性:如何应对瞬间流量峰值而不宕机
- 公平性问题:如何防止黄牛利用技术手段抢票
- 用户体验:如何减少用户等待时间,提供清晰的反馈
1.2 技术架构选型考量
在技术架构选择上,需要平衡性能、成本和开发复杂度。常见的方案包括:
- Redis集群+分布式锁:适合中等并发场景,实现相对简单
- 消息队列削峰填谷:适合流量波动大的场景,但会增加系统复杂度
- 数据库乐观锁:实现简单,但在极高并发下性能较差
- 令牌桶限流:控制入口流量,保护下游系统
在实际项目中,往往采用多种技术组合的方案。下面我们重点分析基于Redis的分布式锁方案。
2. 环境准备与核心技术栈
2.1 开发环境要求
为了完整复现票务系统,需要准备以下环境:
服务器环境配置:
- Linux服务器(CentOS 7.6+或Ubuntu 18.04+)
- JDK 1.8或11
- Redis 6.0+集群模式
- MySQL 8.0或PostgreSQL 12+
- Nginx 1.18+用于负载均衡
关键依赖版本:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.17.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.0</version> </dependency> </dependencies>2.2 Redis集群配置
票务系统的核心是Redis集群的合理配置。以下是生产环境推荐的配置:
# application-redis.yml spring: redis: cluster: nodes: - 192.168.1.101:6379 - 192.168.1.102:6379 - 192.168.1.103:6379 max-redirects: 3 lettuce: pool: max-active: 1000 max-wait: -1ms max-idle: 10 min-idle: 5 timeout: 2000ms3. 核心业务逻辑实现
3.1 数据库表设计
合理的数据库设计是系统稳定的基础。票务相关核心表结构如下:
CREATE TABLE ticket_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_name VARCHAR(100) NOT NULL COMMENT '活动名称', total_tickets INT NOT NULL COMMENT '总票数', sold_tickets INT DEFAULT 0 COMMENT '已售票数', start_time DATETIME NOT NULL COMMENT '开售时间', end_time DATETIME NOT NULL COMMENT '结束时间', status TINYINT DEFAULT 1 COMMENT '状态:1-待开始 2-进行中 3-已结束', version INT DEFAULT 0 COMMENT '乐观锁版本号', created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE ticket_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号', user_id BIGINT NOT NULL COMMENT '用户ID', event_id BIGINT NOT NULL COMMENT '活动ID', ticket_count INT NOT NULL COMMENT '购票数量', amount DECIMAL(10,2) NOT NULL COMMENT '订单金额', status TINYINT DEFAULT 1 COMMENT '状态:1-待支付 2-已支付 3-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME COMMENT '支付时间', INDEX idx_user_id (user_id), INDEX idx_event_id (event_id) );3.2 分布式锁实现抢票逻辑
使用Redisson实现分布式锁,确保在高并发下库存扣减的原子性:
@Service @Slf4j public class TicketService { @Autowired private RedissonClient redissonClient; @Autowired private TicketEventMapper ticketEventMapper; @Autowired private TicketOrderMapper ticketOrderMapper; public ApiResult purchaseTicket(Long eventId, Long userId, Integer count) { // 参数校验 if (eventId == null || userId == null || count == null || count <= 0) { return ApiResult.error("参数错误"); } // 获取分布式锁,锁粒度细化到具体活动 String lockKey = "ticket_purchase_lock:" + eventId; RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待3秒,锁持有时间30秒 boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { return ApiResult.error("系统繁忙,请稍后重试"); } // 查询活动信息 TicketEvent event = ticketEventMapper.selectById(eventId); if (event == null) { return ApiResult.error("活动不存在"); } // 检查活动状态 if (event.getStatus() != 2) { return ApiResult.error("活动未开始或已结束"); } // 检查库存 int availableTickets = event.getTotalTickets() - event.getSoldTickets(); if (availableTickets < count) { return ApiResult.error("库存不足"); } // 生成订单 TicketOrder order = createOrder(event, userId, count); // 扣减库存(使用乐观锁防止超卖) int updateCount = ticketEventMapper.updateSoldTickets( eventId, count, event.getVersion()); if (updateCount == 0) { // 乐观锁冲突,说明其他请求已经修改了库存 return ApiResult.error("库存不足,请重新尝试"); } // 订单创建成功 return ApiResult.success("抢票成功", order.getOrderNo()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("抢票过程被中断", e); return ApiResult.error("系统异常"); } catch (Exception e) { log.error("抢票异常", e); return ApiResult.error("系统繁忙"); } finally { // 释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } private TicketOrder createOrder(TicketEvent event, Long userId, Integer count) { TicketOrder order = new TicketOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setEventId(event.getId()); order.setTicketCount(count); order.setAmount(calculateAmount(event, count)); order.setStatus(1); // 待支付 ticketOrderMapper.insert(order); return order; } private String generateOrderNo() { return "T" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999); } }3.3 库存预热与缓存策略
在抢票开始前,将库存信息预热到Redis中,减少数据库压力:
@Component public class TicketCacheService { private static final String TICKET_STOCK_KEY = "ticket:stock:"; private static final String TICKET_EVENT_KEY = "ticket:event:"; @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 活动开始前预热库存到Redis */ public void preheatStock(Long eventId) { TicketEvent event = ticketEventMapper.selectById(eventId); if (event != null) { String stockKey = TICKET_STOCK_KEY + eventId; String eventKey = TICKET_EVENT_KEY + eventId; // 设置库存 redisTemplate.opsForValue().set(stockKey, event.getTotalTickets() - event.getSoldTickets()); // 设置活动信息,过期时间设置为活动结束后1小时 long expireTime = calculateExpireTime(event.getEndTime()); redisTemplate.opsForValue().set(eventKey, event, expireTime, TimeUnit.SECONDS); } } /** * 从缓存中获取库存 */ public Integer getStockFromCache(Long eventId) { String key = TICKET_STOCK_KEY + eventId; Object value = redisTemplate.opsForValue().get(key); return value != null ? Integer.parseInt(value.toString()) : null; } /** * 扣减缓存中的库存 */ public boolean decreaseStock(Long eventId, Integer count) { String key = TICKET_STOCK_KEY + eventId; Long result = redisTemplate.opsForValue().decrement(key, count); return result != null && result >= 0; } }4. 高并发优化与限流策略
4.1 网关层限流配置
使用网关层限流保护后端服务,以下是基于Spring Cloud Gateway的配置:
spring: cloud: gateway: routes: - id: ticket-service uri: lb://ticket-service predicates: - Path=/api/ticket/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 # 每秒允许的请求数 redis-rate-limiter.burstCapacity: 200 # 瞬时最大请求数 key-resolver: "#{@userKeyResolver}" - name: StripPrefix=1对应的KeyResolver配置:
@Bean KeyResolver userKeyResolver() { return exchange -> { String userId = exchange.getRequest().getQueryParams().getFirst("userId"); if (StringUtils.isEmpty(userId)) { // 如果没有userId,使用IP限流 return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } return Mono.just(userId); }; }4.2 服务层限流与降级
在服务层使用Resilience4j实现更细粒度的限流和熔断:
@Service public class TicketOrderService { private final RateLimiter rateLimiter; private final CircuitBreaker circuitBreaker; public TicketOrderService() { // 每秒钟最多处理50个请求 RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom() .limitRefreshPeriod(Duration.ofSeconds(1)) .limitForPeriod(50) .timeoutDuration(Duration.ofMillis(500)) .build(); this.rateLimiter = RateLimiter.of("ticketRateLimiter", rateLimiterConfig); // 熔断器配置:失败率超过50%时熔断 CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(30)) .ringBufferSizeInClosedState(100) .ringBufferSizeInHalfOpenState(10) .build(); this.circuitBreaker = CircuitBreaker.of("ticketCircuitBreaker", circuitBreakerConfig); } @RateLimiter(name = "ticketService") @CircuitBreaker(name = "ticketService", fallbackMethod = "purchaseFallback") public ApiResult purchaseWithRateLimit(Long eventId, Long userId, Integer count) { return purchaseTicket(eventId, userId, count); } // 降级方法 private ApiResult purchaseFallback(Long eventId, Long userId, Integer count, Exception e) { log.warn("票务服务降级,eventId: {}, userId: {}", eventId, userId, e); return ApiResult.error("当前排队人数过多,请稍后重试"); } }5. 异常处理与事务一致性
5.1 分布式事务处理
在分布式环境下,需要保证订单创建和库存扣减的事务一致性:
@Service public class TicketTransactionService { @Transactional(rollbackFor = Exception.class) public ApiResult purchaseWithTransaction(Long eventId, Long userId, Integer count) { try { // 1. 检查库存 TicketEvent event = checkStock(eventId, count); // 2. 生成订单 TicketOrder order = createOrder(event, userId, count); // 3. 扣减库存 decreaseStock(eventId, count); // 4. 发送订单创建消息 sendOrderCreatedMessage(order); return ApiResult.success("下单成功", order.getOrderNo()); } catch (BusinessException e) { // 业务异常,直接抛出触发回滚 throw e; } catch (Exception e) { log.error("下单过程异常", e); throw new RuntimeException("系统异常,订单创建失败"); } } /** * 使用消息队列保证最终一致性 */ @Async public void sendOrderCreatedMessage(TicketOrder order) { try { OrderMessage message = new OrderMessage(); message.setOrderNo(order.getOrderNo()); message.setUserId(order.getUserId()); message.setEventId(order.getEventId()); message.setCreateTime(new Date()); // 发送延迟消息,15分钟后检查支付状态 rocketMQTemplate.asyncSend("ORDER_CREATED_TOPIC", message, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { log.info("订单创建消息发送成功: {}", order.getOrderNo()); } @Override public void onException(Throwable throwable) { log.error("订单创建消息发送失败: {}", order.getOrderNo(), throwable); // 消息发送失败,记录日志并人工处理 } }, 3000, 3); // 延迟3秒,重试3次 } catch (Exception e) { log.error("发送订单消息异常", e); } } }5.2 常见异常处理方案
在实际运行中,需要针对不同异常类型制定处理策略:
| 异常类型 | 现象描述 | 处理方案 | 预防措施 |
|---|---|---|---|
| 库存超卖 | 实际售出票数超过库存 | 人工核对,补偿用户 | 使用分布式锁+乐观锁 |
| 重复下单 | 同一用户短时间内重复下单 | 检查用户最近订单 | 前端防重复提交+后端校验 |
| 网络超时 | 请求响应时间过长 | 自动重试机制 | 优化SQL,增加缓存 |
| 系统宕机 | 服务不可用 | 快速故障转移 | 集群部署,健康检查 |
6. 监控与日志排查
6.1 关键指标监控
建立完善的监控体系,及时发现系统问题:
@Component public class TicketMetrics { private final MeterRegistry meterRegistry; private final Counter purchaseSuccessCounter; private final Counter purchaseFailCounter; private final Timer purchaseTimer; public TicketMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.purchaseSuccessCounter = Counter.builder("ticket.purchase.success") .description("成功购票次数") .register(meterRegistry); this.purchaseFailCounter = Counter.builder("ticket.purchase.fail") .description("购票失败次数") .register(meterRegistry); this.purchaseTimer = Timer.builder("ticket.purchase.duration") .description("购票处理时间") .register(meterRegistry); } public void recordPurchaseSuccess(long duration) { purchaseSuccessCounter.increment(); purchaseTimer.record(duration, TimeUnit.MILLISECONDS); } public void recordPurchaseFail(String reason) { purchaseFailCounter.increment(); Tags tags = Tags.of("reason", reason); Counter.builder("ticket.purchase.fail.reason") .tags(tags) .register(meterRegistry) .increment(); } }6.2 日志排查策略
制定清晰的日志规范,便于问题排查:
@Slf4j @Aspect @Component public class TicketLogAspect { @Around("execution(* com.example.ticket.service.*.*(..))") public Object logServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); long startTime = System.currentTimeMillis(); String traceId = MDC.get("traceId"); if (traceId == null) { traceId = generateTraceId(); MDC.put("traceId", traceId); } try { log.info("【Ticket】方法开始: {}, 参数: {}, traceId: {}", methodName, Arrays.toString(args), traceId); Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); log.info("【Ticket】方法结束: {}, 耗时: {}ms, traceId: {}", methodName, (endTime - startTime), traceId); return result; } catch (Exception e) { log.error("【Ticket】方法异常: {}, 错误: {}, traceId: {}", methodName, e.getMessage(), traceId, e); throw e; } finally { MDC.clear(); } } private String generateTraceId() { return "TICKET_" + System.currentTimeMillis() + "_" + ThreadLocalRandom.current().nextInt(1000, 9999); } }7. 生产环境部署建议
7.1 架构部署方案
对于大型票务系统,推荐采用多机房部署方案:
前端负载均衡层:Nginx + LVS (主备) 应用服务层:Spring Boot微服务集群 (多机房部署) 缓存层:Redis集群 (主从架构) 数据库层:MySQL主从复制 + 分库分表 消息队列:RocketMQ集群 监控告警:Prometheus + Grafana + 告警中心7.2 压测与容量规划
在上线前必须进行充分的压力测试:
压测关键指标:
- 单机QPS:2000+
- 响应时间:P99 < 500ms
- 错误率:< 0.1%
- 系统资源:CPU < 70%,内存 < 80%
容量规划公式:
所需服务器数量 = 预期峰值QPS / 单机可承受QPS × 安全系数(1.5-2.0) Redis内存需求 = 活动数量 × 每个活动缓存大小 × 副本数 × 安全系数 数据库连接数 = 应用实例数 × 每个实例最大连接数7.3 应急预案
制定完善的应急预案,包括:
- 流量激增预案:自动扩容触发条件与流程
- 系统故障预案:故障转移与数据恢复方案
- 数据不一致预案:对账与补偿机制
- 安全攻击预案:DDoS防护与业务风控策略
大型票务系统的技术实现需要综合考虑并发控制、系统稳定性、数据一致性和用户体验等多个维度。在实际项目中,建议先从小规模场景开始验证,逐步优化和完善各项技术方案,最终形成适合自身业务特点的高可用架构。