第二章:系统总体架构:分层架构设计

好,咱们进入正题。票务系统听起来简单,不就是卖票吗?但真正做过的人都知道,这里面的门道深着呢。我个人习惯,拿到一个系统需求,第一件事就是画分层图。为什么?因为分层能帮我们把复杂问题拆解开。

轨道交通票务系统,说白了就是一套处理「谁、在哪儿、什么时候、怎么付钱、能不能过闸」的规则引擎。我参与过几个城市的票务系统建设,踩过不少坑。今天就把这套分层架构掰开了讲。

2.1 为什么必须分层?

你想想看,一个票务系统要面对什么?

  • 前端有各种闸机、售票机、手机APP
  • 中间要处理票种规则、计费逻辑、优惠策略
  • 后端要管账户、交易记录、黑名单

如果不分层,所有代码揉在一起。嗯,那维护起来就是噩梦。我曾经接手过一个老系统,业务逻辑和数据库查询写在一个函数里,改一个优惠规则要翻三天代码。所以,分层不是炫技,是保命。

核心原则:每一层只关心自己的事。接入层不管业务逻辑,业务层不碰数据库,数据层不处理界面。

2.2 三层架构详解

我们通常把票务系统分成三层:接入层、业务层、数据层。每一层都有明确的职责。

2.2.1 接入层(也叫网关层、接口层)

这一层是系统的「门面」。所有外部请求都先到这里。

  • 协议转换:闸机用的是串口或TCP,手机APP用的是HTTP/HTTPS,售票机可能是WebSocket。接入层要把这些五花八门的协议统一成内部RPC调用。
  • 安全校验:设备认证、签名校验、频率限制。我记得有一次,某个站点闸机固件有bug,疯狂重发请求,差点把业务层打挂。后来我们在接入层加了限流,才稳住。
  • 会话管理:乘客在售票机上选票、支付、出票,这是一个会话。接入层要维护这个状态。
// 接入层伪代码示例:统一请求处理
public class AccessHandler {
    public Response handle(Request req) {
        // 1. 设备认证
        if (!authenticate(req.getDeviceId(), req.getSign())) {
            return Response.fail(401, "设备未授权");
        }
        // 2. 限流检查
        if (rateLimiter.isOverLimit(req.getDeviceId())) {
            return Response.fail(429, "请求过于频繁");
        }
        // 3. 协议转换,转发到业务层
        return serviceProxy.call(req.getBizType(), req.getPayload());
    }
}

我的经验:接入层不要做任何业务判断。比如「这个乘客能不能用学生票」——这不是接入层该管的事。接入层只负责「谁来了,是不是合法设备」,至于「来了之后干什么」,交给业务层。

2.2.2 业务层(也叫服务层、逻辑层)

这是整个系统的大脑。所有规则、策略、流程都在这里。

业务层通常拆成多个子模块:

模块名称 职责说明 典型场景
票务引擎 处理票种定义、有效期、使用规则 单程票、储值卡、日票、月票
计费模块 计算乘车费用、换乘优惠、封顶逻辑 进站扣预授权,出站算实际费用
账户模块 管理乘客余额、交易流水、退款 充值、扣款、挂失
黑名单模块 管理挂失卡、异常卡、逃票记录 刷卡时检查是否在黑名单中
清分结算 多线路、多运营商之间的收入分配 A线路进站,B线路出站,钱怎么分

这里有个关键点:业务层是无状态的。什么意思?就是业务层不保存任何会话数据。每次请求都是独立的,需要什么数据,去数据层拿。这样做的好处是方便水平扩展。我见过一个反面案例,业务层把乘客的购物车信息存在本地内存里,结果一扩容,乘客的购物车丢了,投诉电话被打爆。

2.2.3 数据层(也叫持久层、存储层)

数据层负责「把东西存下来,并且能快速找回来」。

票务系统的数据有几个特点:

  • 高并发写入:早晚高峰,每秒几千笔交易流水
  • 强一致性要求:扣款不能多扣,也不能少扣
  • 数据量大:一个城市一年几十亿条交易记录

所以数据层通常不是单一数据库,而是一个组合:

  • 关系型数据库(如MySQL/Oracle):存账户、票种、黑名单等核心数据。用主从复制保证高可用。
  • 缓存(如Redis):存热点数据,比如票价表、设备信息。我建议把票价表全量加载到缓存里,查询速度能快两个数量级。
  • 消息队列(如Kafka/RocketMQ):处理异步任务,比如交易流水入库、清分结算。为什么要异步?因为高峰期直接写库,数据库扛不住。
  • 归档存储:超过一定时间的交易记录,迁移到HBase或OSS,降低主库压力。

避坑指南:我曾经遇到过一个问题——数据层的事务边界没划清楚。业务层调用数据层时,把多个表的更新放在一个事务里,结果因为锁冲突导致接口响应超时。后来我们规定:业务层一个请求,最多只能跨两个数据源的事务。超过的,用消息队列做最终一致性。

2.3 模块交互流程:一次完整的乘车过程

光讲理论太枯燥。咱们走一遍实际流程:乘客用手机APP扫码过闸。

  1. 进站:
    • 闸机扫码,把二维码数据发给接入层。
    • 接入层校验设备合法性,解析出乘客ID和票种信息。
    • 接入层调用业务层的「进站处理」接口。
    • 业务层检查:乘客账户是否正常?票种是否有效?黑名单有没有?
    • 业务层调用数据层:扣一笔预授权金额(比如2元),记录进站事件。
    • 数据层返回成功,业务层通知接入层,接入层告诉闸机「开闸」。
  2. 出站:
    • 闸机再次扫码,接入层同样处理。
    • 业务层收到出站请求,计算实际费用(比如3元)。
    • 业务层调用数据层:释放预授权,扣掉实际费用(3元 - 2元 = 再扣1元)。
    • 数据层更新账户余额,记录出站事件。
    • 业务层返回成功,闸机开闸放行。
  3. 异步处理:
    • 交易流水通过消息队列异步写入归档库。
    • 清分结算模块定时拉取流水,计算各线路的分账金额。

你看,整个过程涉及三层协作。每一层各司其职,互不干扰。这就是分层架构的魅力。

总结一下:接入层管「连接」,业务层管「规则」,数据层管「存储」。这三层像一条流水线,每个环节只做自己的事。这样做的好处是——哪一层出了问题,就只修哪一层,不影响其他层。我经历过一次数据库切换,业务层代码一行没改,只改了数据层的配置,就完成了迁移。

嗯,下一章咱们聊聊接入层的具体设计,包括设备管理、协议适配、高并发接入这些实战内容。到时候我会分享一些压测数据,看看一个接入层节点到底能扛多少并发。