直播这个场景,后端要面对的压力和普通 CRUD 项目完全是两个物种。你可能会说,不就是个聊天室加推流吗?但真到了开播瞬间,上万人在线、弹幕刷屏、礼物飘屏、连麦状态同步一起涌过来的时候,再回头看那些增删改查接口,真的就是小儿科。我最初开始碰直播高并发后端,也是从接手一套互动系统开始,一边被线上告警追着跑,一边把架构一点点拆明白。这篇文章就是我从一个后端小白的角度,手搓一套直播高并发环境的完整笔记,从整体设计、技术选型,到具体搭建、压测调参,再到踩坑复盘,全部展开。适合刚入行后端、想搞明白高并发到底高在哪里的朋友,也适合前端同学想在前后端分离项目里补一补后端视角,看完之后至少知道直播系统里每一条弹幕是怎么流转的。
1. 直播高并发后端,先拆清这三个压力点
1.1 直播互动不是看视频,是消息洪峰
很多人第一次接触直播,第一反应是“视频流难”,毕竟推流、拉流、转码、CDN 一整套链路听起来就复杂。但如果你只关注后端互动系统,真正的难点根本不是视频流,而是消息洪峰。视频流走的是 CDN 和媒体服务,后端更多时候负责的是信令和互动消息,例如:弹幕、礼物、点赞、关注、上麦下麦、在线人数变化。
看视频是单向的,观看者只管从 CDN 拉流,后端不参与每个画面帧的转发。但弹幕就不一样了,它是多对多的实时消息,任意用户发一条弹幕,直播间里所有其他人要基本同时看到。一万人在线,一个用户发弹幕,剩下九千九百九十九个人都要收到这条数据。如果每秒有几百人同时发弹幕,那后端要处理的写入量和推送量就会瞬间变成几十万甚至上百万次转发。这不是普通 web 应用的请求模型,普通 web 是“客户端请求一次、服务端响应一次、连接就结束了”,直播互动是“连接长时间保持、服务端主动往客户端推数据”。所以你在设计系统的时候,如果还抱着“一个接口一个 Controller、查询一次数据库然后返回 JSON”的思路,绝对会被压垮。
把直播互动抽象成三块就好理解了:连接管理、消息流转、状态维护。连接管理负责维护大量在线用户的网络连接;消息流转负责把弹幕、礼物这些消息从发送者传到所有接收者;状态维护负责在线人数、礼物榜、房间热度这些一直在变的数据。把这三个问题拆开,整个系统就清晰了。
1.2 后端小白的第一个高并发项目,为什么选直播场景
我有个挺直观的感受:后端新手学高并发,最容易陷入“背八股”的怪圈。JUC 的并发工具、线程池参数、并发三要素背得滚瓜烂熟,但一碰到真实的线上并发场景,还是会懵。原因很简单,没有把并发知识挂载到一个完整业务链路上。直播这个场景天生就适合拿来练手高并发,因为它的业务规则简单,但流量模型很复杂。
为什么说业务规则简单?弹幕就是“发消息、收消息”,礼物就是“记录赠送、更新榜单、推送特效”,在线人数就是“连接进来加一、连接断开减一”。没有像电商那种状态扭转、库存扣减、支付回调之类的复杂流程。正因为它简单,你可以把大部分精力放在性能和稳定性上,而不是纠缠业务逻辑。
但流量模型又很复杂,包含长连接、高吞吐写入、热 key 读取、实时推送、水平扩容、负载均衡等等。这些恰好是高并发后端最重要的基本功。而且直播场景不用花一分钱就能模拟,你本地起一个 Spring Boot 服务,再用脚本模拟几千个 WebSocket 客户端,就能真真切切感受到连接数上涨之后的内存变化、GC 压力和线程池占用。这种“眼见为实”的学习效率,比看十篇理论文章都高。
1.3 一套直播高并发环境由哪几块组成
如果你把它画成一个简化的架构图,从上到下大概是这样的:
- 接入层:用 Nginx 做反向代理和负载均衡,客户端统一连到 Nginx,再由它转发给后端服务。这一层还要处理 WebSocket 的升级协议。
- 应用层:Spring Boot 服务集群,这里跑真正的业务逻辑,比如用户认证、弹幕发送、礼物赠送,以及维持 WebSocket 连接。
- 状态层:Redis,用来存在线用户列表、礼物榜、房间维度缓存,以及通过发布订阅模式做跨节点的消息广播。
- 数据层:MySQL,存用户信息、礼物记录、弹幕回放等结构化数据。但要注意,热路径上的高并发读写尽量避免直接打 MySQL。
这套组合是我比较推荐新手第一次搭直播后端时使用的。没有引入太复杂的消息队列,没有上微服务全家桶,也没有搞 Service Mesh,原因很简单:一个新项目如果一开始就铺开十几个组件,还没开始写业务先被运维和部署绊倒了。直播高并发环境的本质是让连接管理和消息推送能撑住量,而这些用 Spring Boot、WebSocket、Redis、Nginx 四个核心件就能完整跑通。后面如果消息量真的大到 Redis Pub/Sub 扛不住,再考虑引入 Kafka、RocketMQ 等专业消息队列也不迟。
2. 新手选型指南:Spring Boot、WebSocket、Redis、Nginx 怎么配合
2.1 主体框架选 Spring Boot 3 + Java 17
主体框架我选了 Spring Boot 3 + Java 17,原因很朴素:生态成熟,资料多,出了问题搜起来快。做直播高并发环境这种项目,最怕用的是“看起来很炫但没人用过”的框架。Spring Boot 的自动配置和 starter 机制,能让一个没有任何历史包袱的新工程在几分钟内跑起来,而且它对 WebSocket、Redis、Web 开发的支持都非常完整,所有这些在前后端分离项目里也都是常识操作。
Java 17 是当前比较稳的长期支持版本,虚拟线程在 Java 21 才真正好用,但 Spring Boot 3 对 Java 17 的支持已经很成熟。选 Java 17 还有一个好处,它的垃圾回收器(默认 G1)在多数延迟敏感场景下表现很稳,尤其是直播弹幕这种大量短生命周期对象的场景。虽然很多人说 Java 写网络服务太重,但实际上用 Netty 和虚拟线程之前,Spring Boot + Tomcat 的异步处理能力足够支撑中小型直播互动场景。你真正要担心的不是语言,而是有没有把连接和线程模型搞清楚。
2.2 实时通道用 WebSocket 还是 Netty,别纠结
我在做选型的时候,在 WebSocket 和 Netty 之间纠结过一阵子。诚然,Netty 是目前 Java 网络编程的底层王者,Spring WebFlux 底层的 Reactor Netty、RPC 框架里的通信层都基于它。用 Netty 手写一个直播推送网关,性能和可定制性都会更强,但代码量和对网络编程的理解门槛也更高。对一个后端小白第一次搭直播高并发环境来说,直接用 Spring 自带的 WebSocket 抽象,反而更容易把逻辑讲清楚。
这里给一个很实际的选型建议:如果你是想快速跑通业务,验证直播互动方案是否可行,直接选 Spring Boot 的 WebSocket,它底层也是 Tomcat 或 Netty 实现,起步快、代码简洁;如果你们公司已经有大流量的业务,或者你明确知道要支撑百万级长连接,那再去深入研究 Netty 手写网关也不迟。我搭这套环境的时候用的是 Spring WebSocket,压测到单机五六千连接没什么大问题,对新手来说已经是一个很好的起步量级。
| 对比项 | Spring Boot WebSocket | Netty 自研 |
|---|---|---|
| 上手难度 | 低,注解加配置即可 | 高,需要理解 Pipeline、EventLoop |
| 扩展性 | 中等,适合中小规模 | 高,适合大规模定制 |
| 调试成本 | 低,社区资料多 | 偏高 |
| 适合阶段 | 从 0 到 1、MVP 验证 | 从 1 到 100、大规模优化 |
在技术选型上,最忌讳的是“为了炫技而选复杂方案”,直播高并发不只是连接管理一件事,你的主要精力应该放在业务链路和数据一致性上。
2.3 Redis 在直播场景里的状态设计
Redis 在我这套环境里承担了三类工作:缓存、计数、消息转发。缓存是最直观的,直播间的基础信息、热门礼物列表、用户基本信息都可以放在 Redis 里,避免每个请求都去查数据库。计数则是直播场景最核心的数据操作,比如在线人数、点赞总数、送礼总值,这些数据特点就是高频写、低频读或者高频写、也要实时读,用 MySQL 行锁根本顶不住。
在线人数推荐用 Redis 的 Set 或 ZSet 来维护,直接SADD live:room:{roomId} {userId},用户下线时SREM,需要查在线人数时SCARD一下就出来了。之所以不用简单的 INCR/DECR,是因为直播间可能进进出出,用计数方式容易出现重复计数、少计等脏数据,而 Set 天然幂等,同一个用户重复加入只算一次。
礼物榜用 ZSet 是最自然的,ZINCRBY live:gift:rank:{roomId} {score} {userId},每次有人送礼就把分值累加进去。直播间的排行榜需要实时更新,ZSet 的 Skip List 结构能保证排序性能,就算一个房间有数万人,取 Top 50 的耗时也在毫秒级。
还有一类消息转发场景,我采用的是 Redis 发布订阅(Pub/Sub),这部分我会在消息链路里详细说。Redis 做状态层最大的好处是单线程模型天然避免并发竞争,坏处是你必须设计好 key 的过期策略,避免“永不失效的脏 key”把内存撑爆。
2.4 Nginx 接入层与负载均衡的配置思路
高并发环境不可能只靠单台后端硬扛,水平扩容是必经之路,而扩容之后必须有负载均衡把流量分散开。Nginx 在这里承担两个职责:普通 HTTP API 的反向代理,以及 WebSocket 的接入转发。
对于普通 HTTP API,只需要配置proxy_pass到后端集群地址即可。但 WebSocket 比普通 HTTP 多一个握手升级过程,客户端发的是包含Upgrade: websocket头部的 HTTP 请求,所以 Nginx 转发时必须保留这些头部,同时设置一个足够长的proxy_read_timeout,不然连接没有任何消息传输时会被 Nginx 掐断。
有人可能会问,WebSocket 是长连接,会不会因为 Nginx 轮询导致同一个用户的消息被转发到不同后端?放心,Nginx 的负载均衡只在建立连接时生效,连接建立之后就一直绑定在某一台后端节点上。只要后端节点能通过 Redis Pub/Sub 收到其他节点的消息,用户的消息就不会丢,这也是我坚持用 Redis 做节点间消息同步的原因。
3. 最核心的链路实现:弹幕、礼物、在线人数与鉴权
3.1 弹幕和礼物的推送链路:从接口到 Redis Pub/Sub
弹幕和礼物这类互动消息,我采用的方案是“HTTP 发送 + WebSocket 推送”。听上去有点绕,但实际用起来很舒服:客户端往一个普通的 REST 接口 POST 一条弹幕内容,后端先校验用户权限、过滤敏感词、落库存档,然后通过 Redis Pub/Sub 把这条消息发布到对应直播间的 channel 上,所有订阅了这个 channel 的后端节点收到消息后,再推送给连接在自己节点上的客户端。
这样做的好处是发送动作可以走普通 HTTP 接口,方便做拦截、校验、限流;而接收动作走 WebSocket 长连接,保证实时性。有人会觉得那为什么不直接把弹幕内容也通过 WebSocket 发到后端,省一次 HTTP 请求?从技术上讲完全可行,但把发送入口做成 HTTP 接口,对客户端更友好,也更容易做统一的参数校验和审计日志。在直播间里,发弹幕本身就不是超高频率的动作,真正超高频率的是广播推送,所以把耗时的校验逻辑放在 HTTP 请求里,把广播放在 WebSocket 里,是更合理的分工。
Redis Pub/Sub 的用法也很简单。发布端调用convertAndSend("live:room:1001", payload),订阅端监听live:room:*这个 pattern,收到消息后解析出房间号,再查看本地节点的 session 池中是否维护了这个房间的连接,有就批量发送。这里有个坑:Redis Pub/Sub 的消息不会持久化,发布时如果没有订阅者在线,消息会直接丢失。对弹幕这种“发出去就完事”的场景可以容忍,但如果将来要做离线消息、补拉历史弹幕,就需要换用 Stream 或者 Kafka。
3.2 在线人数统计别碰数据库,Redis 够用了
在线人数是直播场景最典型的“状态型数据”,几乎每秒钟都在变化。开播瞬间大量用户涌入,人数从几千快速涨到几万,如果每个进来的人都去数据库 UPDATE 一个人数计数器,数据库非常容易变成瓶颈。而且这种数据的一致性要求没那么高,它不是金额,不需要强一致,允许有几秒的偏差,但必须实时展示在直播间的角标上。
我用的方案是双写:WebSocket 连接建立时,SADD到房间在线集合,同时把 userId 与房间号的映射关系写入本地内存;连接断开时,SREM从集合移除。查询在线人数就是一个SCARD key,很快,一秒内可以执行几万次。这里要配合心跳机制,因为网络异常断开时服务端可能感知不到连接已经失效,所以客户端每 30 秒发送一个心跳包,服务端超过 60 秒没有收到心跳就主动断开这个连接,清理 Redis 和本地 session。这套机制在直播高并发环境下非常实用,能避免大量“僵尸连接”把在线人数和系统资源都拖垮。
3.3 前后端分离下,WebSocket 连接怎么鉴权
前后端分离的项目,用户信息通常通过 Token 传递。HTTP 接口在 Header 里放一个Authorization: Bearer xxx很自然,但 WebSocket 握手时不能自定义 Header,这是浏览器原生 WebSocket API 的限制。所以实际操作中,常用的做法是在连接 URL 上带参数取,比如ws://xxx/live/ws?roomId=1001&token=xxx。
在手写握手鉴权时要注意,不要直接把 token 放到日志里,不然用户的登录凭证就泄露了。Spring WebSocket 提供了HandshakeInterceptor,在握手前拦截请求并校验 token,校验通过才把 userId 放到 attributes 中传给 WebSocketHandler。我之前看到很多新手写法是到了 WebSocketHandler 里再解析 token、再查用户信息,其实通过拦截器在握手阶段就完成鉴权和使用者是更优雅的方式,还能避免无效连接占着资源。
还有一点,直播间权限校验也很重要。有些直播间是付费场或者密码房,必须在握手时校验用户是否拥有进入权限,而不能只验证 token 有效。我把这层逻辑也放在HandshakeInterceptor里,校验失败直接返回 401,客户端会收到一个握手失败的响应,不会建立 WebSocket 连接。
4. 从 0 到 1 搭建实录:工程初始化、WebSocket、Nginx、压测
4.1 环境准备:JDK、Maven、Redis、Docker
先把基础环境准备好。我本地用的是 macOS,但这套东西在 Linux 服务器上更接近生产环境,所以后期我全程在 CentOS 7 虚拟机里跑。需要准备的东西如下:
- JDK 17,配置好
JAVA_HOME - Maven 3.8+
- Redis 6.2+,单机版足够演示
- Docker 和 Docker Compose,用来快速启动 MySQL、Redis,减少本地环境污染
- Nginx 1.20+
- 一个方便压测的 WebSocket 客户端,我用了 Python 的
websockets库,比 GUI 工具更灵活
新手容易忽略的是系统的文件描述符限制。Linux 默认单个进程能打开的 fd 数量是 1024,换成 WebSocket 连接就是最多同时在线 1024 人,这个数量级对直播来说肯定不够。需要修改ulimit -n,或者更稳妥的做法是在/etc/security/limits.conf里写入* soft nofile 102400和* hard nofile 102400。我一开始没改这个参数,压测到一千个连接直接就报Too many open files,排查了半天才意识到是这个底层限制。
4.2 初始化 Spring Boot 工程与核心配置
我用 Spring Initializr 生成的工程,依赖只勾了Spring Web、WebSocket、Data Redis Reactive、Validation。主流 Java 后端项目基本都是这套组合,再加上 MySQL 驱动和 MyBatis-Plus,就覆盖了直播业务需要的几乎所有基础能力。
核心的application.yml配置大概是这样的:
server: port: 8080 tomcat: max-threads: 200 accept-count: 1000 max-connections: 10000 spring: redis: host: 127.0.0.1 port: 6379 timeout: 3000ms datasource: url: jdbc:mysql://127.0.0.1:3306/live?useUnicode=true&characterEncoding=utf8 username: root password: root123 hikari: maximum-pool-size: 20Tomcat 参数这里只说一个重点:max-threads是处理请求的线程数,不要盲目调到几千,线程数过高会导致频繁上下文切换,性能反而下降。200 左右在不开启虚拟线程的情况下是比较合理的起点,后续压测时再根据 CPU 利用率进行调整。accept-count是请求队列长度,给多一点能缓冲瞬时峰值流量,不至于一来高峰就直接拒绝请求。
4.3 接入 WebSocket 处理器与 Redis 订阅转发
WebSocket 配置类,核心代码大概长这样:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(liveWebSocketHandler(), "/live/ws") .addInterceptors(new LiveAuthInterceptor()) .setAllowedOrigins("*"); } @Bean public WebSocketHandler liveWebSocketHandler() { return new LiveWebSocketHandler(); } }LiveWebSocketHandler继承TextWebSocketHandler,维护了一个并发安全的 session 池。因为同一个直播间可能有很多连接,所以我会用两层 Map:外层是ConcurrentHashMap<WebSocketSession, String>之类的映射,更准确一点,用ConcurrentHashMap<String, Set<WebSocketSession>>按直播间维度管理连接。每次有消息发往某个直播间,只需要遍历这个直播间对应的 session 集合,逐个发送即可。
Redis 订阅部分,我实现了MessageListener接口,订阅 pattern 是live:room:*。收到 Redis 消息后,解析出房间号,从本地 session 池取出对应连接,批量推送。之所以要 Redis 中转,是因为用户可能连接在 A 节点,但发送弹幕的 HTTP 请求被 Nginx 转发到了 B 节点。B 节点把消息发布到 Redis,A 节点订阅后推给自己的客户端,这样不管客户端连在哪个节点,消息都不会断。一个小优化是发布消息时带上消息序号和时间戳,这样客户端做去重和排序就有依据了。
4.4 Nginx 负载均衡配置实战
Nginx 的配置我单独列一段,因为 WebSocket 转发比普通 HTTP 多几个关键参数:
upstream live_backend { server 192.168.1.10:8080 max_fails=3 fail_timeout=30s; server 192.168.1.11:8080 max_fails=3 fail_timeout=30s; keepalive 64; } server { listen 80; server_name live.example.com; location /live/ws { proxy_pass http://live_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } location /api/ { proxy_pass http://live_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }有两点需要特别注意。第一,proxy_http_version必须设成 1.1,不然长连接无法生效。第二,WebSocket 的proxy_read_timeout不能太短,否则客户端长时间不讲话,连接就被 Nginx 给断了。我一开始图省事只设置了默认的 60 秒,结果每隔一分钟连接就断一次,客户端疯狂重连,把后端连接池都打满了。后来改成 3600 秒,这个坑才算填上。
4.5 压测验证:连接数、吞吐量与参数调整
压测是整个搭建过程里最有成就感的环节,也是暴露问题的关键环节。我用了一段 Python 脚本,模拟多个客户端同时连接 WebSocket,然后定时发弹幕,观察服务端进程的 CPU、内存和文件连接数。
import asyncio import websockets async def mock_client(i): uri = f"ws://127.0.0.1:8080/live/ws?roomId=1001&token=test{i}" async with websockets.connect(uri) as ws: for j in range(20): await ws.send(f"message from user {i}") await asyncio.sleep(1) async def main(): tasks = [mock_client(i) for i in range(3000)] await asyncio.gather(*tasks) asyncio.run(main())用ulimit -n 102400放开文件描述符限制之后,单机跑 3000 个 WebSocket 连接是比较轻松的。当连接数到 5000 时,能观察到 Tomcat 的连接线程占用明显上升,所以我用了 Spring WebSocket 的异步发送模式,避免大量网络阻塞卡住业务线程。另一个压测结论是,单条弹幕广播给 5000 人会有几十毫秒的延迟,这个延迟主要花在遍历 session 和网络发送上,属于可接受范围。如果想继续优化,可以引入用户分片和批量发送,但那就属于进阶话题了。
5. 直播后端上线前,把这些问题都排查一遍
5.1 连接数打满,服务直接拒绝握手怎么办
我第一次压测到四千多连接时,新连接握手开始大量失败,日志里出现Connection refused和Too many open files。排查时先看系统 fd 限制,发现虽然设置了ulimit -n 102400,但 Spring Boot 进程是用 systemd 启动的,systemd 服务文件里默认的LimitNOFILE还是 1024。这个问题非常典型,你在 shell 里调了 ulimit 只管当前 shell 和它的子进程,换到 systemd 启动、docker 启动,又是另一套配置。
解决方法是同时检查三层:操作系统层、systemd 服务文件层、JVM 进程层。systemd 服务文件里加LimitNOFILE=102400,然后 daemon-reload。Docker 部署时则要在 docker run 参数里加--ulimit nofile=102400:102400。把这些都处理好,连接数才真正能上去。
5.2 内存飙升与 GC 频繁
长连接场景下,最容易被忽视的是内存里堆积了大量 WebSocketSession 和相关业务对象。前期我发现在线人数只有五千,但堆内存占用已经超过了 1G,GC 也变得很频繁。排查之后发现有一个地方写得很粗糙:每当用户进入直播间,我就往 session 的 attributes 里塞了一个很大的用户信息对象,里面有头像 URL、历史记录、订阅关系等一堆用不上的字段。一个两个无所谓,五千个连接叠加起来就很可观。
解决思路是 attributes 里只放必要信息,比如 userId、roomId、nickname。其他信息按需再查 Redis。另一个大招是开启 Tomcat 的连接器回收,同时给 JVM 设置合理的堆内存上下限:
java -Xms512m -Xmx1024m -jar live-backend.jar对于这种场景,不要给 JVM 分配超过物理内存的堆,否则系统会用大量 swap,GC 时间会变得很难看。我在 4G 内存的虚拟机上,用-Xms512m -Xmx1g跑 3000 连接基本稳得住。
5.3 Redis 热点 key 引发的连锁故障
直播场景里最容易出问题的 Redis 用法是:所有人都集中读写同一个 key。比如超级头部主播开播,几万人同时看同一个直播间的在线人数、点赞数、礼物榜,这个房间的所有 key 都变成了热点 key。哪怕 Redis 单机 QPS 能达到十万,但热点 key 所在的 Redis 实例 CPU 一旦被打满,整个系统的消息转发也会跟着卡顿。
我采用的缓解方案是降级加本地缓存。在线人数允许几秒的延迟,所以本地缓存十秒更新一次,而不是每次请求都穿透到 Redis。礼物榜这种实时性要求高的数据,则把它拆分到多个 key 里,比如按用户 ID 哈希分成 10 个分片,查询时把 10 个分片的结果聚合起来。这种分片策略会增加代码复杂度,但对于直播高并发环境,这是躲不掉的手段。热点 key 问题在流量小时感受不到,一旦上了真实直播,瞬间就能把你打懵。
5.4 跨域、Token、部署层面的典型坑
前后端分离项目里,跨域和后端鉴权是绕不开的两个话题。WebSocket 的setAllowedOrigins("*")确实能解决跨域连接问题,但生产环境里最好把允许来源固定成自己的前端域名,否则任何网站都能往你的 WebSocket 服务灌消息,风险极高。Token 方面,要在 WebSocket 握手阶段校验,不能等服务端和客户端已经建立了长连接再慢慢校验,否则恶意用户可以通过不携带 token 的方式占用大量连接资源。
部署方面,因为我最终要用 Docker 部署,发现容器里的时间默认是 UTC,日志里排查问题会非常别扭。解决办法是在 Dockerfile 里设置时区,同时在 Nginx 层的 access log 用nginx -V确认好日志格式。这些看起来是小事,但线上排查问题时,时间和日志对不上会耽误很长时间。
最后再分享一个我踩过最值的坑:永远不要在长连接消息里打印完整消息内容。弹幕业务文本可能很短,但万一哪个用户发了一段超长字符,你的日志系统可能直接被打满。我后来在打印日志时只保留前 64 个字符,配合消息长度和用户 ID 就够定位问题了。这个习惯一直保留到现在,每次做实时消息系统我都会提醒自己先想清楚日志和监控该怎么设计,再开始写业务代码。