第二章:系统总体架构:分层架构设计
好,咱们进入正题。票务系统听起来简单,不就是卖票吗?但真正做过的人都知道,这里面的门道深着呢。我个人习惯,拿到一个系统需求,第一件事就是画分层图。为什么?因为分层能帮我们把复杂问题拆解开。
轨道交通票务系统,说白了就是一套处理「谁、在哪儿、什么时候、怎么付钱、能不能过闸」的规则引擎。我参与过几个城市的票务系统建设,踩过不少坑。今天就把这套分层架构掰开了讲。
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扫码过闸。
- 进站:
- 闸机扫码,把二维码数据发给接入层。
- 接入层校验设备合法性,解析出乘客ID和票种信息。
- 接入层调用业务层的「进站处理」接口。
- 业务层检查:乘客账户是否正常?票种是否有效?黑名单有没有?
- 业务层调用数据层:扣一笔预授权金额(比如2元),记录进站事件。
- 数据层返回成功,业务层通知接入层,接入层告诉闸机「开闸」。
- 出站:
- 闸机再次扫码,接入层同样处理。
- 业务层收到出站请求,计算实际费用(比如3元)。
- 业务层调用数据层:释放预授权,扣掉实际费用(3元 - 2元 = 再扣1元)。
- 数据层更新账户余额,记录出站事件。
- 业务层返回成功,闸机开闸放行。
- 异步处理:
- 交易流水通过消息队列异步写入归档库。
- 清分结算模块定时拉取流水,计算各线路的分账金额。
你看,整个过程涉及三层协作。每一层各司其职,互不干扰。这就是分层架构的魅力。
总结一下:接入层管「连接」,业务层管「规则」,数据层管「存储」。这三层像一条流水线,每个环节只做自己的事。这样做的好处是——哪一层出了问题,就只修哪一层,不影响其他层。我经历过一次数据库切换,业务层代码一行没改,只改了数据层的配置,就完成了迁移。
嗯,下一章咱们聊聊接入层的具体设计,包括设备管理、协议适配、高并发接入这些实战内容。到时候我会分享一些压测数据,看看一个接入层节点到底能扛多少并发。