场景设计题 面试题
1. 如何设计一个高并发秒杀系统
❓ 题目: 请从零开始设计一个支持百万级并发的秒杀系统,说出你的设计思路和每一步要解决的核心问题。
💡 答案:
设计一个高并发秒杀系统,核心思想是把流量削峰填谷,让绝大多数请求在离数据库最远的地方就被拦截掉。整体架构分为五层:
- 第一层是前端限流:商品详情页和按钮做静态化,按钮在秒杀开始前置灰,开始后才可点击,并且点击后立刻置灰防止重复提交。
- 第二层是网关层:使用 Nginx + Lua 或者网关插件对秒杀接口做令牌桶限流,只放行跟库存数量匹配的请求量(比如库存 1000 件,可以放行 2000-3000 个请求进入下游),其余直接返回”已售罄”。
- 第三层是服务层:用户资格校验(是否登录、是否已购买过、是否在黑名单),然后通过消息队列异步削峰,将请求写入 Kafka 让后端异步消费,前端返回”排队中”。
- 第四层是扣库存:用 Redis 做库存预扣,
DECR原子操作判断库存是否充足,扣减成功的生成预下单记录落入消息队列。 - 第五层是订单持久化:消费者从队列中拿出成功的预扣记录,异步写入数据库生成真实订单。
追问1: 秒杀系统你用 Redis 扣库存还是用数据库扣库存?各自的利弊是什么?
用 Redis 还是数据库扣库存,本质是性能与一致性的权衡。
用 Redis 扣库存的优势是性能极高——单机 Redis 可以扛 10 万+ QPS,而且 Redis 单线程模型保证了 DECR 操作的原子性,天然支持并发扣减。缺点是 Redis 数据在内存中,如果宕机未及时持久化可能丢数据,而且 Redis 和数据库之间需要异步同步,存在最终一致性问题。
用数据库扣库存的优势是强一致性——UPDATE stock SET count = count - 1 WHERE count > 0 在行锁的保护下不会超卖也不会丢数据。缺点是性能差,热点行锁会成为瓶颈,数据库连接数不够用。
业界成熟方案是”Redis 预扣 + DB 最终落单”的混合模式——Redis 承担高并发读写的压力,DB 作为最终数据源保证不丢数据。如果 Redis 挂了,降级到直接走数据库,虽然性能差但保证业务可用。
追问2: 如何保证”不超卖”?如果同时要保证”不少卖”(库存不能剩余),方案上有什么调整?
“不超卖”相对容易解决——利用 DECR 的原子性,或者数据库 UPDATE ... WHERE count > 0 加行锁。但”不少卖”(用户取消订单、超时未支付要回补库存)才是秒杀系统最难处理的点。如果在 Redis 里扣了库存、异步落库中途失败,库存已经被扣了但订单没生成——用户拿不到商品,库存也没恢复,两边都吃亏。解决方案分两步:
- 一是对”已扣 Redis 库存但未落库”的中间状态做超时释放——预扣时记录时间戳,定时任务扫描超时未支付的预下单回补库存
- 二是对落库失败做补偿——消费者处理消息如果失败,需要回滚 Redis 的扣减为了保证不会同时回滚和超时释放导致重复加库存,回补操作也用
INCR原子操作,并且设置库存上限等于初始库存。需要注意的是”不少卖”还涉及防黄牛——如果全是脚本秒杀然后取消订单,真正想买的用户抢不到。可以通过限制同一用户/IP/设备多次秒杀、要求支付后才能确认抢购成功来减少恶意占单。
📌 易错点 / 加分项:
- 秒杀不是”怎么扛最大流量”而是”怎么挡住不需要的流量”,这个思路转变很重要
- 能提到”先到先得”(Redis DECR)和”最终一致性”(异步落库)之间的平衡
- 热点账户/热点库存问题的延伸:除了 Redis 分槽(库存拆分到多个 key),还能用本地缓存预减
2. 如何设计一个短链接服务
❓ 题目: 设计一个类似 TinyURL 的短链接服务,支持生成短链接和 301 跳转,QPS 预估 10w 读 1w 写。
💡 答案:
核心架构分四层:
- API 层:提供两个接口——
POST /shorten(传入长 URL 返回短链接)、GET /{hash}(302 重定向到原始 URL)。 - 应用服务层负责生成短码、去重校验、记录映射关系。
- 缓存层:Redis 存储热点短链的映射,key 为短码、value 为原始 URL,TTL 设置较短(如 1 天),当缓存未命中时回源查数据库并写回 Redis。
- 存储层:MySQL 表结构
(id, hash, origin_url, created_at, expired_at),hash 字段建唯一索引。
短码生成用发号器(自增 ID 转 62 进制——用 0-9a-zA-Z,将数字 ID 映射为短码)比随机哈希更安全,避免碰撞,而且 ID 自增天然趋势增长对 B+ 树友好。高 QPS 读写通过 Redis 缓存承载——请求先查 Redis、再查 DB、最终缓存 Redis。写请求去重——长 URL 和短码的映射关系,可以再用一个反向索引 origin_url → hash(通过额外的 Redis key 或数据库唯一约束保证幂等)。扩容上无状态服务层水平扩展,MySQL 通过读写分离 + 分表分库(按短码 hash 分片)扩展。
追问1: 短链接的哈希算法你用什么?如何避免短链接冲突?
不建议直接用 MD5/SHA 散列进 URL 的部分——碰撞率高且有安全风险(攻击者可以反推出原始 URL 的内容摘要)。发号器方案更好:维护一个全局自增 ID generator(例如数据库自增、Redis INCR、雪花算法),将递增 ID 转为 62 进制字符串(0-9 + a-z + A-Z,共 62 个字符),十进制 1 亿转为 62 进制也就 6 位。这样短码天然不冲突(ID 全局唯一),且 ID 自增对数据库友好。如果有多机房部署,可以给每个机房分配不同的 ID 前缀或步长——比如机房 A 的 ID 从 0 开始每次加 2,机房 B 从 1 开始加 2,保证全局不冲突。62 进制也可换为其他基数(如 64、72 等)来支持更多字符,但要考虑 URL 安全(有些字符在 URL 中需要转义)。
追问2: 短链接有有效期吗?如何设计和实现过期数据的自动清理?
过期设计:如果短链接有有效期,表里加 expired_at 字段,应用服务在查 DB 时过滤 WHERE hash = ? AND (expired_at IS NULL OR expired_at >= NOW())。缓存里 TTL 设置为与过期时间一致——如果短链接在 N 天后过期,缓存的 TTL 也设为 N 天。过期数据清理:避免用定时任务直接批量删除(DELETE 大表会产生大量 undo log 且可能锁表),更推荐的做法是”软删除 + 异步清理”——如果请求时发现已过期,直接返回 404;同时后台异步标记为 EXPIRED 或写入另一个表,用一个限流删除任务在业务低峰期 DEL,每次只删少量行避免影响线上。如果数据量极大的话考虑按时间分表——每天或每月一张短链接表,过期后直接 DROP 整张表比 DELETE 高效得多。也可以将过期的短链接转移到冷存储(如对象存储)保留日志,释放生产数据库空间。
📌 易错点 / 加分项:
- 短码的生成用发号器 + 62 进制比用随机生成再查冲突更可靠——冲突检测是额外的性能开销
- 反向索引(long URL → hash)保证幂等性——即同一长 URL 无论请求多少次返回同一个短链接
- 10w 读 QPS 光用 Redis 没问题,但要考虑 Redis 宕机后的降级——降级开关可以绕过缓存直接查 DB
3. 设计一个简单的文件存储系统
❓ 题目: 设计一个类似网盘的文件存储系统,支持上传、下载、列表展示,用户量百万级。
💡 答案:
核心架构分四层:
- 接入层:Nginx 或 OSS SDK 处理上传下载请求,上传下载直接走对象存储(MinIO / 阿里 OSS / AWS S3)的 pre-sign URL,减轻应用服务压力。
- 应用层:用户管理 + 文件元数据管理——MySQL 存储文件元数据表
(file_id, user_id, file_name, file_size, content_type, storage_key, md5_hash, parent_folder_id, created_at, updated_at),配合文件夹表。 - 存储层:对象存储(不存本地文件系统,避免单机磁盘容量限制和扩容困难),文件存为
bucket/{user_prefix}/{storage_key}。 - 缓存层:Redis 存储热点文件的元数据和下载链接,避免每次列表查询都全表扫描 DB。
文件存储不进入数据库,只存 path 作为引用指针——这是存储和数据库分离的基本思想。用户量百万级下元数据表需要对 user_id 建索引 + 按 user_id 分表保证查询速度。
追问1: 大文件上传怎么处理?如果文件有几个 GB 甚至几十 GB?
大文件上传的核心方案是分块上传(Multipart Upload)。客户端将大文件切成小块(如每块 5MB),每个块独立上传到对象存储,每个块传完后返回 ETag 标识。所有块传完后,客户端调用”完成上传”接口,将分块列表和对应的 ETag 传给对象存储,对象存储按顺序拼接分块生成完整文件。好处极多:
- 一是支持断点续传——某个块传失败只需重传这个块
- 二是支持并行上传——多线程同时传不同块,极大提升大文件上传速度
- 三是降低失败成本——不会因为最后 1% 的网络问题前功尽弃
- 四是客户端可以预先计算文件 hash 判断是否去重几个 GB 的文件用分块上传是比较成熟的方案,几十 GB 级别需要注意客户端的内存和本地磁盘缓存。
追问2: 文件去重怎么做?两个人上传了完全相同的文件,要不要存两份?
文件去重用文件内容哈希。用户上传文件时先计算文件的 MD5 或 SHA-256——如果发现系统中已存在相同的哈希值,说明这个文件之前有人上传过,无需再次上传文件本身。应用层直接将这个用户的 storage_key 指向已有的文件路径,然后更新一个引用计数。这样物理上只存一份,逻辑上每个用户的文件元数据各自独立——用户 A 删文件时减少引用计数,计数归零时才真正物理删除。这个方案节省了存储空间(大文件的去重是最有价值的)但对小文件来说额外哈希计算成本不低,实际情况可以设定阈值(如低于 4KB 不去重,高于 4KB 才比较哈希)。如果要更高效,可以分块去重——将文件切块后逐块做哈希去重,类似 rsync 或 Dropbox 的方案,减少相似的重复存储。但分块去重复杂度更高,一般百万级用户用全文件哈希去重就足够了。
📌 易错点 / 加分项:
- 对象存储的 pre-sign URL 避免应用自己”搬运”文件内容,是架构设计的关键点
- 不要直接在数据库里存文件数据(BLOB),这会瞬间打爆数据库性能
- 引用计数删除有并发安全问题——删除时需要原子操作
UPDATE ... SET ref_count = ref_count - 1 WHERE ref_count > 0
4. 设计一个配置中心
❓ 题目: 设计一个配置中心(类似 Apollo 或 Nacos 的配置管理功能),支持配置的变更后实时推送到所有客户端。
💡 答案:
配置中心的核心组件分为四层。服务端:存储配置的元数据——用 MySQL 存储配置项 (app_id, namespace, key, value, version, created_at, updated_at)。每次修改配置时生成新版本号并记录变更日志(用于回滚审计)。配置的发布有灰度、全量两个阶段——可以先推给部分节点验证再全量推。客户端:启动时拉取配置的全量快照缓存到本地文件(防止配置中心挂掉后客户端无法启动),然后通过长轮询或长连接监听配置变更。实时推送的实现方案是:客户端发起一个长轮询请求(带当前配置版本号),服务端持有这个请求不放,有变更后立刻返回新配置;如果超过超时时间(如 60 秒)没有变更,返回 “无变更” 让客户端重试。
追问1: 配置变更通知你是怎么实现的?长轮询、gRPC 流、还是 MQ 广播?各自的优劣?
实时推送主要有三套方案。长轮询方案(HTTP):客户端发请求带 Notify-Time—参数,服务端在服务端挂住连接 60 秒,不在 60 秒内配置变化立刻返回新版本。优势是 HTTP 协议通用性强、穿透防火墙没问题,缺点是每次轮询都重新建连有一定资源消耗。gRPC 双向流方案:客户端建立长连接后,服务端有变化时直接通过流推送。优势是真正的”实时+低成本”,不需要定期轮询,适合配置变更频繁的场景。缺点是长连接管理复杂需要心跳保活和重连机制。MQ 广播方案:服务端变更后发消息到 MQ(Kafka/RocketMQ),所有客户端消费。优势是实现解耦、可以持久化和削峰,不丢失变更通知。不足是引入外部 MQ 组件增加了依赖。Apollo 1.x 用长轮询,Nacos 2.x 用 gRPC 流 + 长轮询作为兜底。
追问2: 如何保证配置的版本管理和回滚能力?万一推了一个错配置把系统搞挂了怎么办?
版本管理用”发布记录表”:每次配置发布生成一条发布记录 (publish_id, app_id, config_version, operator, content, created_at, status)——记录”谁在什么时间改了哪个配置”。回滚的操作本质是”用旧版本的配置内容生成一条新版本”——从发布记录中拿到上一个版本的完整内容,再插入一条新发布记录(内容=旧内容、版本号+1),和正常变更走相同的推送逻辑。安全控制上:一是发布审批流程——敏感的配置(如数据库连接串)需要二次审批才能发布;二是灰度发布——先推给 1-2 台机器观察几分钟(通过应用监控指标看是否有错误量上升),确认安全后再全量发布;三是自动回滚——对接监控系统,如果发布后错误率飙升 10% 以上,触发自动回滚按钮;四是本地兜底——客户端缓存上一次正常拉取的配置到本地文件,如果收到异常配置(比如 JSON 格式错误),拒绝该配置并使用本地备份。
📌 易错点 / 加分项:
- 配置中心和服务注册中心的功能边界不同——不要把两者混为一谈(Nacos 名字误导)
- 客户端本地缓存是容灾的保底手段——配置中心挂了应用不能也挂
- 加密敏感配置是基本要求——数据库密码、API Secret 在数据库中加密存储,客户端解密
5. 设计一个即时通讯(IM)系统
❓ 题目: 设计一个支撑千万级用户的即时通讯(IM)系统,核心功能是单聊和群聊。
💡 答案:
IM 系统的核心组件分成接入层、传输层、存储层、业务层四块。接入层:客户端和 Gateway 通过 WebSocket 或自定义 TCP 协议维护长连接,Gateway 负责连接管理、心跳保活、消息路由。Gateway 是无状态的——每个用户连接到哪个 Gateway 可用一致性哈希分配。传输层:Gateway 之间通过 RPC 或路由表定位目标用户当前的 Gateway,将消息投递到目标。存储层分为消息存储(时序数据库或 MySQL 分表,按消息时间 + 会话维度存储)、消息索引(用户 ↔ 会话关系、会话 ↔ 消息分页索引)、已读状态(每个用户在某个会话中的已读位置)。业务层负责会话管理、消息推送和离线存储逻辑。千万级用户的性能瓶颈在连接管理和消息投递延时——长连接需要做好 I/O 多路复用(Netty),单机可以支撑 50w+ 连接,配合多机横向扩展。
追问1: 消息的”已读/未读”状态如何存储和更新?群聊场景下每个人已读位置不一样怎么维护?
已读状态的存储粒度是”用户 + 会话 + 最后一条已读消息 ID”。单聊场景下两个人的已读位置独立——A 读到 msg_id=100,B 读到 msg_id=99,这是两个不同的值。群聊场景下 N 个成员 N 个已读位置——用一张”已读位置表” (session_id, user_id, last_read_msg_id, updated_at) 存储。但群聊的已读更新是频率最高的操作——500 人群每个人每看一次都更新会形成高频写入。优化方式:一是合并写入——客户端不需要每次看消息就更新已读位置,而是定时(如每 5 秒)、或者切换出会话时统一更新;二是用 Redis 的 Sorted Set——score 存 msg_id 的毫秒时间戳,member 存 user_id,表示该用户当前的已读位置,定期批量同步回 MySQL。三是用”乐观发布”——不再推送”所有人已读位置”,而是等用户主动查询时才返回精确值。大多数 IM 产品的”双蓝勾”采用类似”乐观更新”的策略。
追问2: 离线消息和消息同步机制如何设计?用户换了一台设备,历史消息如何同步?
离线消息方案:用户在线时,消息通过长连接实时投递;用户离线时,消息写入离线消息表 (user_id, msg_id, content, timestamp, status)。用户重新上线后,Gateway 通知离线消息同步模块:“该用户上线了”,模块从离线消息表批量拉取并推送。消息投递完后标记消息为已投递,超过 7 天的离线消息可清理或归档。多设备同步:每个设备独立维护一个”设备最后同步的 msg_id”——当设备 A 已收到 msg_id_150,设备 B 才挂到 msg_id_80,B 上线后需要拉取 81-150 的消息。实现方式是每个设备维护一个同步游标 (user_id, device_id, last_sync_msg_id),消息同步模块拉取这个游标之后的增量消息进行同步。历史消息的存储会随时间膨胀——一般用”冷热分离”:近 7 天的消息在 Redis + MySQL 热层快速读取,7 天以上的消息转存对象存储归档,按需加载。
📌 易错点 / 加分项:
- 消息发送的”ack 机制”——不是一条 TCP ack 就认为消息投递成功,需要上层的业务 ack
- 群聊消息的扇出问题——500 人群一条消息要扩散给 500 个人,不能串行发送需要批量
- 一致性哈希的重平衡问题——Gateway 增删节点时,用户重新分配可能导致短暂的消息路由失败,需要处理好重连逻辑
6. 设计一个接口幂等性框架
❓ 题目: 设计一个轻量级的接口幂等性框架,供各个微服务接入使用。要求支持基于 Token 的防重和基于业务键的去重两种模式。
💡 答案:
框架核心为幂等注解和幂等处理器。注解 @Idempotent 标注在需要幂等的方法上,属性包括 type(TOKEN / BUSINESS_KEY)、keyExpression(SpEL 指定业务键)、expireSeconds。
Token 模式:客户端提交前调用 GET /idempotent/token 获取 UUID token。服务端拦截 @Idempotent 方法,用 Redis SETNX idempotent:token:{token} 1 EX expireSeconds 校验——成功首次请求继续执行;失败则从 Redis 拉取之前存储的处理结果直接返回。
业务键模式:根据 SpEL 解析业务键(如订单号),用 SETNX idempotent:biz:{bizKey} REQUEST_ID EX expireSeconds。SETNX 成功继续执行并写入结果。SETNX 失败则检查 value——REQUEST_ID 说明正在处理中返回”请勿重复提交”,SUCCESS 说明已处理完直接返回成功。
📌 易错点 / 加分项:
- Token 模式要防”刷 token”——同一用户一秒钟拿一百个 token 需要频率限制
- Redis 的 SETNX 与 EXPIRE 需原子性——不可分两步,中间进程挂了 key 永不过期
- 业务键模式注意并发——首次请求还在处理中时重复请求要返回”处理中”而非”已处理”
7. 设计一个服务降级开关系统
❓ 题目: 设计一个服务降级开关系统,支持动态开启/关闭某个功能,能按百分比降级,且有自动回滚能力。
💡 答案:
核心架构:配置管理后台 + 配置中心推送 + 客户端 SDK 拦截。
降级规则表结构:(rule_id, service_name, method_path, degrade_type, degrade_value, status)。degrade_type 包括 RETURN_DEFAULT(返回默认值)、RETURN_FALLBACK(走兜底逻辑)、THROTTLE_PERCENT(按百分比降级)。配置变更时写入数据库 + 推送到 Nacos/Apollo。
客户端 SDK 提供 @DegradeSwitch 注解或 AOP 拦截。运行时先查本地缓存(Caffeine,定期从配置中心刷新)获取降级规则。匹配到已开启的降级规则后根据类型执行——RETURN_DEFAULT 返回配置的默认 JSON;THROTTLE_PERCENT 用随机数判断百分比。降级触发时记录日志 + 打 metric 给监控。
自动回滚:配置中设置”最长降级时长”——超时自动关闭。或对接监控指标——降级期间被降级的请求比例连续 5 分钟下降说明已恢复,自动关闭。定时任务扫描 WHERE status='OPEN' AND opened_at + max_duration < NOW() 自动关闭。
📌 易错点 / 加分项:
- 降级逻辑本身不能依赖被降级的下游服务——否则降级白降
- 本地缓存刷新间隔不能太长——用配置中心长轮询或 gRPC 推送实现秒级同步
- 降级开关打开和关闭都需要记录审计日志
8. 设计一个分布式异步任务框架
❓ 题目: 设计一个分布式异步任务框架,支持任务的提交、异步执行、状态追踪和失败重试。
💡 答案:
核心组件:任务提交 API + 任务调度器 + 任务执行器 + 状态存储。
任务提交:@AsyncTask(taskType="SEND_EMAIL") 注解在方法上。生成 taskId(雪花算法),将任务元数据写入 MySQL async_tasks 表,通过 MQ 或 Redis List 推送给执行器。
任务执行器:监听 MQ 或轮询 Redis,收到任务后根据 taskType 找到对应 Handler(TaskHandler 接口)。执行前插入执行记录(RUNNING),执行完更新为 SUCCESS/FAILED。失败根据重试策略(固定间隔、指数退避)计算下次重试时间。定时任务扫描 status=FAILED AND next_retry_at < NOW() AND retry_count < max_retry_count 重新投放。
状态追踪:提供 GET /task/{taskId} 查询接口,返回当前状态、创建时间、执行耗时、重试次数。超时未完成的任务由监控任务标记为 TIMEOUT 并告警。
📌 易错点 / 加分项:
- 任务的重试必须是幂等的——执行器不能保证任务只执行一次(MQ at-least-once 语义)
- 任务参数不要太大——存在数据库中,序列化后几 MB 会影响任务表性能
- 执行器的优雅关闭——收到 shutdown 信号后不再接受新任务,等当前任务完成再退出
9. 设计一个实时数据同步管道
❓ 题目: 设计一个将 MySQL 数据实时同步到 Elasticsearch 的数据管道,要求低延迟、高可用、能处理 schema 变更。
💡 答案:
核心思路是CDC(Change Data Capture)+ MQ + 消费端。
捕获阶段:用 Canal(阿里开源)或 Debezium 监听 MySQL binlog。Canal 伪装成 MySQL Slave 接收 binlog,解析 INSERT/UPDATE/DELETE 事件,封装为消息发到 Kafka——消息体包含 (操作类型, 表名, 变更前行数据, 变更后行数据)。
传输层:Kafka Topic 按表或按数据库分——cdc.db_name.table_name。Kafka 保证同表同主键的消息顺序(按主键哈希分区),这对 ES 同步的顺序性很重要——同一 ID 的 INSERT 必须在 UPDATE 之前被消费。
消费层:多个消费者订阅不同 Topic。根据操作类型——INSERT/UPDATE 调用 ElasticsearchClient.index()(ES index 是 upsert);DELETE 调用 delete()。ES index 操作天然幂等。Schema 变更——Canal 可监听 ALTER TABLE 事件,消费端调用 ES PutMapping 更新 mapping(新增字段)。破坏性变更需人工介入重建索引。
📌 易错点 / 加分项:
- Canal 的消费位点管理——Canal 需保存消费到的 binlog position,重启后从该位置继续
- ES 的
refresh_interval对实时性有影响——同步管道可设 refresh_interval=-1 暂停 refresh - 全量同步 + 增量同步两阶段——先全量导入历史数据,再切增量 CDC 补全
10. 设计一个多级缓存架构
❓ 题目: 设计一个支持热点自动发现和动态扩容的多级缓存架构,用于高并发读场景。
💡 答案:
架构分四级:浏览器缓存 → CDN/反向代理缓存 → 本地缓存(Caffeine)→ 分布式缓存(Redis)→ 数据库。
热点自动发现:在 Redis 客户端或 Proxy 层统计每个 key 的访问频率。滑动窗口(如 10 秒)统计 QPS,超过阈值的标记为”热 Key”。热 Key 列表定期同步到 Redis Set 或配置中心推送给应用服务端。
多级缓存联动:应用服务检测到热点 key 时,自动在本地 Caffeine 中缓存该 key 的值,TTL 设短(如 3 秒)——有效挡住大部分 Redis 的读压力,又保证数据不会太旧。非热点 key 直接从 Redis 读取。Redis 缓存更新策略采用”Cache Aside”——读 miss 查 DB 回写 Redis,写更新 DB 后删 Redis 缓存。
缓存一致性兜底:本地缓存的更新——用 Redis 6.0+ Client Side Caching(服务端通知失效),或用极短 TTL(3 秒)做最终一致性。更严格场景用 Canal + MQ 监听 binlog 变更,应用消费变更消息删除本地缓存——延迟百毫秒级。
📌 易错点 / 加分项:
- 本地缓存的大小必须设上限——否则热点 key 多了本地缓存会 OOM
- “先删缓存再更新 DB”和”先更新 DB 再删缓存”两种模式都有坑——后者更主流
- 不是所有数据都适合多级缓存——一致性要求高的数据(余额、库存)只走 Redis 或直读 DB