如果让你设计 12306 如何入手

193 阅读7分钟

一、业务场景与核心挑战

1. 高并发压力

春运期间,12306 系统每秒可能会收到数百万次的请求,这对系统的并发处理能力提出了极高的要求。2023 年春运期间,12306 系统最高峰日处理售票请求达 15 亿次,平均每秒处理请求超过 17000 次。

2. 票额库存管理

火车票的库存管理需要保证原子性和一致性,防止超卖现象。例如,一张票不能同时售给多个用户。

3. 防止黄牛与爬虫

黄牛党和恶意爬虫会通过自动化工具大量抢票,破坏票务系统的公平性。据统计,12306 系统每天要拦截数千万次的恶意爬虫请求。

4. 预售与候补机制

预售期的设置需要精确控制,候补购票则需要高效的排队和通知机制。目前,12306 的预售期为 15 天,候补购票成功率超过 70%。

5. 分布式事务处理

票务系统涉及多个服务的协同操作,如订单创建、库存扣减、支付处理等,需要保证分布式事务的一致性。

二、技术架构设计

1. 整体架构

12306 抢票系统采用微服务架构,将系统拆分为多个独立的服务,如用户服务、票务服务、订单服务、支付服务等,每个服务可以独立部署和扩展。

系统前端采用前后端分离架构,使用 Vue.js 等框架构建高性能的 Web 界面;后端采用 Spring Cloud 微服务框架,实现服务的注册与发现、负载均衡、配置管理等功能。

2. 高并发处理方案

(1)负载均衡

使用 Nginx 和 LVS 进行四层和七层负载均衡,将请求均匀分发到多个应用服务器。同时,采用 CDN 加速静态资源的访问,减轻服务器压力。

(2)限流与熔断

在网关层使用 Sentinel 或 Hystrix 进行限流和熔断,防止系统被恶意请求或突发流量压垮。例如,对单个用户的请求频率进行限制,对异常服务进行熔断。

// 使用Sentinel进行限流的示例代码
@SentinelResource(value = "getTicket", blockHandler = "blockHandler")
public Ticket getTicket(String trainNumber, String date) {
    // 查询车票信息的业务逻辑
}

public Ticket blockHandler(String trainNumber, String date, BlockException ex) {
    // 限流或熔断后的处理逻辑
    return new Ticket("系统繁忙,请稍后再试");
}

(3)异步处理

对于非核心业务逻辑,如短信通知、日志记录等,采用消息队列(如 RabbitMQ 或 Kafka)进行异步处理,提高系统的吞吐量。

3. 库存管理方案

(1)分布式锁

使用 Redis 分布式锁或 ZooKeeper 实现对票额库存的原子操作,确保同一时间只有一个请求能够修改库存。

java

// 使用Redis分布式锁的示例代码
public boolean deductStock(String trainNumber, String seatType, int count) {
    String lockKey = "stock_lock:" + trainNumber + ":" + seatType;
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS);
    if (!locked) {
        return false; // 获取锁失败
    }
    try {
        // 查询库存
        int stock = getStock(trainNumber, seatType);
        if (stock >= count) {
            // 扣减库存
            boolean result = updateStock(trainNumber, seatType, stock - count);
            return result;
        }
        return false;
    } finally {
        // 释放锁
        redisTemplate.delete(lockKey);
    }
}

(2)本地缓存与多级缓存

对于热门车次的票额信息,采用本地缓存(如 Caffeine)和分布式缓存(如 Redis)相结合的方式,减少对数据库的访问压力。

4. 防止黄牛与爬虫方案

(1)验证码机制

采用滑动拼图、点击图标等复杂验证码,增加自动化工具识别的难度。12306 的验证码系统采用了深度学习技术,不断优化验证码的难度和用户体验。

(2)用户行为分析

通过分析用户的访问频率、操作模式等行为特征,识别并拦截可疑用户。例如,对短期内频繁查询同一车次的用户进行限流或封禁。

(3)IP 黑名单与代理检测

维护 IP 黑名单,对已知的黄牛 IP 进行封禁。同时,检测用户是否使用代理服务器,对可疑代理进行拦截。

5. 预售与候补机制实现

(1)定时任务

使用 Quartz 或 Spring Task 实现预售车票的定时上架功能。例如,每天早上 6 点准时开放新一天的车票预订。

// 使用Spring Task实现定时任务的示例代码
@Component
public class TicketReleaseTask {

    @Scheduled(cron = "0 0 6 * * ?") // 每天早上6点执行
    public void releaseTickets() {
        // 释放新一天的车票库存
        ticketService.releaseTickets();
    }
}

(2)候补队列

使用 Redis 的有序集合(Sorted Set)实现候补队列,根据用户提交候补的时间进行排序。当有退票或余票时,按照队列顺序通知用户。

6. 分布式事务处理

采用 TCC(Try-Confirm-Cancel)或 Saga 模式处理分布式事务。例如,在订单创建时,先冻结库存(Try),支付成功后确认订单并扣减库存(Confirm),支付失败则回滚库存(Cancel)。

// TCC模式处理订单创建的示例代码
public class OrderService {

    @Transactional
    public boolean createOrder(String userId, String trainNumber, String seatType) {
        // Try阶段:冻结库存
        boolean frozen = stockService.freezeStock(trainNumber, seatType, 1);
        if (!frozen) {
            return false;
        }
        
        try {
            // 创建订单
            Order order = new Order(userId, trainNumber, seatType);
            orderRepository.save(order);
            
            // 调用支付服务进行支付
            boolean paid = paymentService.pay(order.getId(), order.getAmount());
            
            if (paid) {
                // Confirm阶段:确认订单和扣减库存
                orderService.confirmOrder(order.getId());
                stockService.confirmStock(trainNumber, seatType, 1);
                return true;
            } else {
                // Cancel阶段:回滚订单和库存
                orderService.cancelOrder(order.getId());
                stockService.cancelStock(trainNumber, seatType, 1);
                return false;
            }
        } catch (Exception e) {
            // Cancel阶段:回滚订单和库存
            stockService.cancelStock(trainNumber, seatType, 1);
            return false;
        }
    }
}

三、数据存储设计

1. 关系型数据库

使用 MySQL 存储用户信息、订单信息、车次信息等结构化数据。采用主从复制和分库分表技术提高数据库的读写性能。

2. 非关系型数据库

使用 Redis 存储热点数据,如票额库存、用户会话信息等,提高数据访问速度。使用 MongoDB 存储日志数据和非结构化数据。

3. 缓存策略

采用多级缓存策略,本地缓存(Caffeine)存储最近访问的数据,分布式缓存(Redis)存储全局热点数据。设置合理的缓存过期时间和淘汰策略,保证数据的一致性。

四、高可用与灾备设计

1. 集群部署

所有服务均采用集群部署,避免单点故障。应用服务器、数据库、缓存等组件都部署多个实例,通过负载均衡器进行调度。

2. 熔断与降级

在服务间调用时使用熔断机制,当某个服务出现异常时,自动熔断对该服务的调用,并进行降级处理,保证系统的可用性。

3. 数据备份与恢复

定期对数据库进行全量备份和增量备份,确保数据的安全性。同时,设计完善的灾备方案,当主数据中心出现故障时,能够快速切换到备用数据中心。

五、性能优化与监控

1. 性能优化

  • 数据库索引优化:合理设计数据库索引,提高查询效率。
  • SQL 优化:避免慢查询,对复杂 SQL 进行性能分析和优化。
  • 异步处理:将非核心业务逻辑异步化,提高系统吞吐量。
  • 代码优化:减少锁的粒度,避免死锁,优化算法复杂度。

2. 监控系统

建立完善的监控系统,实时监控系统的性能指标、服务状态和业务指标。使用 Prometheus 和 Grafana 搭建监控平台,对系统的 CPU、内存、磁盘 IO、请求响应时间等进行监控和预警。

// 使用Micrometer和Prometheus进行指标监控的示例代码
@Service
public class TicketService {

    private final Counter ticketQueryCounter;
    private final Timer ticketQueryTimer;

    public TicketService(MeterRegistry registry) {
        // 计数器:统计车票查询次数
        this.ticketQueryCounter = Counter.builder("ticket.query.count")
                .description("Number of ticket queries")
                .register(registry);
        
        // 计时器:统计车票查询耗时
        this.ticketQueryTimer = Timer.builder("ticket.query.time")
                .description("Time taken to query tickets")
                .register(registry);
    }

    public List<Ticket> queryTickets(String trainNumber, String date) {
        // 记录查询次数
        ticketQueryCounter.increment();
        
        // 记录查询耗时
        return ticketQueryTimer.record(() -> {
            // 实际查询逻辑
            return ticketRepository.findByTrainNumberAndDate(trainNumber, date);
        });
    }
}

六、总结与展望

12306 抢票系统的设计是一个复杂的系统工程,需要综合考虑高并发、数据一致性、安全性、可用性等多个方面的因素。通过采用微服务架构、分布式缓存、异步处理、分布式锁等技术手段,可以有效应对系统面临的各种挑战。