八股文-场景题
秒杀场景
【问题】: 如何设计一个秒杀功能?
【答案】: 秒杀系统需要解决五个核心问题:瞬时流量承接、防止超卖、预防黑产、避免影响正常服务、兜底方案。整体架构分三层:
前端层面:1)CDN缓存静态资源(商品详情页、图片),减少源站压力;2)按钮防重复点击(前端加loading状态+本地标记),防止用户连点;3)倒计时控制(服务端校验秒杀时间,未到时间直接拒绝);4)答题/验证码分散请求(先答题再抢购,拉长请求时间窗口,让流量更平滑)。
后端层面:1)独立秒杀服务与主业务隔离,秒杀服务挂了不影响正常浏览下单;2)Nginx+网关层限流(令牌桶,放出不超过库存数的令牌),多余请求直接返回”抢购太火爆”;3)Redis预减库存:秒杀开始前将库存加载到Redis(SET stock:1001 1000),请求到达时DECR原子扣减,返回负数说明卖完了直接拒绝;4)消息队列异步下单:扣减成功后发MQ消息,订单服务异步消费创建订单,前端轮询订单状态;5)防超卖双保险:Redis层分布式锁防并发扣减(SETNX lock:1001:userId NX PX 3000,3秒内同一用户只能抢一次),数据库层乐观锁兜底(UPDATE stock SET count=count-1 WHERE id=1001 AND count>0);6)风控防刷:同一用户/IP限频、黑名单过滤、设备指纹识别。
兜底方案:1)限流兜底:Sentinel配置QPS阈值,超限直接拒绝;2)熔断兜底:下游服务超时/异常时熔断,返回默认提示;3)降级兜底:极端情况直接关闭秒杀入口,展示”活动已结束”。
【大白话解释】: 秒杀就像春运抢火车票,几千万人同一秒抢有限的票。核心思路:前端别让人乱点,后端别让数据库直接扛流量。用Redis先挡一波(预减库存,Redis内存操作微秒级),用消息队列慢慢消化(异步下单,削峰填谷),最后到数据库时已经是可控的量了。超卖问题就像不能把同一张票卖给两个人,Redis用分布式锁保证同一用户只能抢一次,数据库用乐观锁保证库存不会扣成负数(count>0才扣)。
【扩展知识讲解】: 架构演进路线:数据库直接扣减(简单但性能差,TPS几百)→ Redis缓存扣减(高性能,TPS数万)→ MQ异步下单(削峰填谷,TPS可到十万+)→ 容器化弹性扩缩容(大促前自动扩容)。
Redis预减库存的关键细节:1)库存预热:秒杀开始前用SET命令将库存数写入Redis,不能用INCR从0递增因为无法回滚;2)DECR返回值判断:返回值>=0说明扣减成功,<0说明卖完了需INCR恢复并拒绝请求;3)库存与订单一致性:Redis扣减成功但MQ消费失败怎么办?用本地消息表记录扣减事件,定时补偿;4)Redis集群方案:用Redis Cluster分片,不同商品库存分布在不同节点,避免单节点热点。
数据库乐观锁细节:UPDATE stock SET count=count-1 WHERE id=1001 AND count>0,返回affected rows=1说明扣减成功,=0说明库存已为0。比悲观锁(SELECT FOR UPDATE)性能好很多,因为不阻塞读。但高并发下重试次数多,需控制重试次数。
Sentinel限流配置:@SentinelResource(value=”seckill”, blockHandler=”handleBlock”),QPS阈值设为库存数的1.5倍(考虑部分请求会失败),超出直接返回”活动太火爆”。还可结合Sentinel的预热模式,秒杀开始瞬间QPS从0逐步提升到阈值,避免瞬间冲击。
系统设计场景
【问题】: 如何设计一个点赞系统?
【答案】: 核心在于高并发写入和数据一致性的平衡,设计思路:缓存抗流量、异步削峰、批量落库。三层架构:
1)接入层(Redis):用户点赞请求先打到Redis。点赞计数用String结构,key为like:count:{动态ID},INCR原子操作累加;点赞状态用SET结构,key为like:status:{动态ID},SADD添加用户ID标记已点赞,SISMEMBER判断是否点过(O(1)复杂度),SREM取消点赞。响应时间控制在10ms以内;
2)异步层(MQ):Kafka把点赞事件投递给消费者,消息体包含动态ID、用户ID、操作类型(点赞/取消)、时间戳。同一动态ID路由到同一分区保证顺序性;
3)持久层(DB):消费者批量将点赞数据落库,每500条或每2秒批量INSERT一次。点赞计数表和点赞明细表分开:计数表存动态ID+点赞总数,明细表存动态ID+用户ID+时间戳。
幂等处理:用户重复点赞不会重复计数,SET的SADD返回0表示已存在即重复点赞直接返回成功。
【大白话解释】: 不可能每点一次赞就往数据库写一条记录,数据库扛不住。先用Redis快速记下来告诉用户”点赞成功了”,然后通过消息队列慢慢把数据写到数据库里。就像餐厅先给你号牌说”点好了”,后厨再慢慢做。
【扩展知识讲解】: 还需考虑:1)点赞数Redis和DB最终一致性:定时任务每5分钟将Redis计数同步到DB,以DB为准修正Redis偏差;2)防重复点赞:Redis SET结构SISMEMBER判断,数据库层加唯一索引(动态ID+用户ID)兜底;3)取消点赞的幂等处理:先SREM移除再DECR减1,DECR前需判断SREM返回值是否为1(1表示确实移除了才减);4)大V动态点赞量可能达百万级:点赞状态SET需分片(按用户ID取模分到多个key),或改用Bitmap(用户ID作为offset)节省内存;5)热点动态限流:单动态点赞QPS超过阈值(如1万)则排队异步处理。
朋友圈点赞可用ZSet,key为like:list:{动态ID},score为时间戳,value为用户ID。ZREVRANGE按时间倒序获取点赞列表,ZCARD获取总数。朋友圈和普通点赞的区别:朋友圈需要展示点赞列表和顺序,所以用ZSet而非SET。还需考虑:共同好友可见点赞列表(查询时过滤非好友)、点赞通知推送、大V朋友圈点赞量可能达百万级需分页查询。
【问题】: 让你设计一个消息队列,怎么设计?
【答案】: 1)明确角色:生产者、消费者、Broker(消息服务器)、NameServer/注册中心(服务发现); 2)数据流转:生产者发消息至Broker暂存,消费者从Broker拉取消费,NameServer负责路由注册与发现; 3)通信协议:基于Netty自定义协议(RocketMQ协议),包含消息头(消息ID、Topic、QueueId、长度等)+消息体,比HTTP更轻量高效; 4)存储模型:消息持久化到磁盘,用顺序写提升性能(6块机械盘顺序写可达600MB/s),CommitLog追加写+ConsumeQueue逻辑队列读,实现读写分离。消息按Topic分区(Queue),每个Queue有序; 5)高可用:主从复制(同步/异步双写)、分区机制(Topic下多个Queue分布在不同Broker)、故障自动切换(Dledger选举); 6)消息可靠性:生产端确认(同步双写/异步刷盘)、消费端ACK、消息重试(默认16次)、死信队列兜底; 7)消息去重:生产者生成全局唯一msgId(IP+进程号+时间戳+序列号),Broker端维护去重表。
【大白话解释】: 消息队列就像快递中转站,寄件人把包裹送到中转站暂存,收件人来取。关键是包裹不能丢、不能重复、不能乱序。存包裹用顺序写磁盘(像记流水账)比随机写快得多。
【扩展知识讲解】: 还需考虑:1)消息确认机制(ACK):消费者消费成功后发ACK,Broker标记该消息已消费,未ACK的消息会重投;2)消息回溯:按时间戳重新消费历史消息(Broker保留CommitLog文件),适合数据修复场景;3)死信队列:消费重试16次仍失败进入%DLQ%Topic,需人工介入;4)消息积压处理:增加消费者实例、临时扩容消费端、跳过非关键消息;5)消息顺序性:同一Queue内FIFO,多Queue间不保证全局有序。全局有序需单Queue但吞吐受限; 6)事务消息:RocketMQ支持半消息机制——生产者先发半消息到Broker,执行本地事务后发Commit/Rollback,保证本地事务与消息发送的原子性; 7)延迟消息:RocketMQ内置18个延迟级别(1s~2h),开源版不支持任意延迟,需用时间轮或Redis ZSet补充。
主流MQ对比:RocketMQ(阿里,事务消息+高吞吐+Java生态)、Kafka(大数据场景,超高吞吐+持久化)、RabbitMQ(Erlang,功能丰富+延迟低)、Pulsar(下一代,存算分离+多租户)。
【问题】: 让你设计一个短链系统,怎么设计?
【答案】: 短链系统核心三件事:生成短链、存储映射关系、重定向跳转。用户输入短链后,请求打到短链服务,根据URL找到对应长链,返回302重定向响应,浏览器自动跳转。
生成短链的三种方式:1)哈希算法(MurmurHash):对长链做哈希取前6-8位作短链码,冲突时加盐重新哈希。优点简单,缺点需处理冲突;2)自增ID+进制转换:数据库自增ID,转成62进制(0-9a-zA-Z)。ID=1→”1”,ID=61→”Z”,ID=62→”10”。6位62进制可表示568亿个短链,足够用;3)发号器方案:用Redis INCR或雪花算法生成全局唯一ID再转62进制,避免数据库自增的单点瓶颈。
存储设计:MySQL存映射(短链码、长链、创建时间、过期时间),短链码建唯一索引。高并发读用Redis缓存热点映射(key=短链码,value=长链),设置和短链相同的过期时间。
【大白话解释】: 短链就像给长网址起了个昵称。你输入昵称,系统帮你找到真名跳转过去。生成昵称可以用自增编号转成字母数字组合,比如第1个链接就是”1”,转成62进制就是”1”,第62个就是”10”。
【扩展知识讲解】: 短链系统还需考虑:1)布隆过滤器防重复:新短链生成前查布隆过滤器判断长链是否已有短链,避免重复生成。存在误判但可接受(误判时查DB确认);2)缓存热点短链映射:Redis缓存+本地缓存二级,短链读多写少缓存命中率>99%;3)分库分表存储:按短链码首位字符分128张表,单表控制在500万行以内;4)高可用部署:短链服务无状态水平扩展,Redis Cluster高可用;5)跳转用302而非301:301浏览器会缓存,后续请求不经过服务端无法统计点击量;302每次都经过服务端便于统计分析;6)预生成短链:提前生成一批短链码放入池中,用的时候直接取,避免实时生成的性能瓶颈。
【问题】: 让你设计一个RPC框架,怎么设计?
【答案】: RPC框架核心几点:1)动态代理:消费端通过JDK动态代理或Javassist生成代理对象,屏蔽底层网络调用细节,让远程调用像本地调用一样简单;2)序列化:把Java对象转成二进制进行网络传输。方式对比:JDK序列化(慢且大,不推荐)、JSON(可读但体积大,适合调试)、Hessian2(中等,Dubbo默认)、Protobuf(快且小,gRPC默认,需IDL定义);3)协议设计:自定义二进制协议,包含魔数(2B)+版本号(1B)+消息类型(1B)+序列化方式(1B)+请求ID(4B)+数据长度(4B)+数据体,总共16字节头部。自定义协议比HTTP更轻量,避免HTTP头部开销;4)网络传输:Netty作为底层通信框架,NIO模型,一个Selector多线程处理连接,高性能且稳定;5)注册中心:服务启动时注册IP:端口+服务名到注册中心(Nacos/ZooKeeper),消费端从注册中心订阅服务列表,本地缓存+定时刷新;6)负载均衡:从多个提供者中选一个调用,策略有随机、轮询、加权轮询、一致性Hash、最小活跃数;7)容错机制:失败自动重试(默认2次)、失败自动切换其他提供者、失败快速返回错误、失败安全忽略;8)超时控制:每个调用设超时时间,超时后中断请求防止线程阻塞。
【大白话解释】: RPC就像打电话,你只需拨号(调用方法),不用关心信号怎么传输的。代理帮你拨号,序列化把你说的转成电信号,协议就是通话规则,Netty是电话线路。
【扩展知识讲解】: 常见RPC框架对比:Dubbo(阿里,Java生态,功能丰富,SPI扩展性强)、gRPC(Google,跨语言,Protobuf+HTTP/2,但服务治理弱)、Spring Cloud OpenFeign(声明式HTTP客户端,集成Ribbon+Hystrix,微服务全家桶)。选型:纯Java项目用Dubbo,多语言项目用gRPC,Spring Cloud体系用OpenFeign。
序列化深入对比:JDK序列化存在安全漏洞且不支持跨语言;JSON可读性好但无法序列化复杂对象(如循环引用);Protobuf需写.proto文件用protoc生成代码,性能最优但开发体验差;Hessian2是折中选择,Dubbo默认方案。序列化性能:Protobuf > Hessian2 > JSON > JDK,体积:Protobuf < Hessian2 < JSON < JDK。
注册中心选型:ZooKeeper(CP强一致,但性能一般,不适合大规模服务注册)、Nacos(AP/CP可切换,性能好,推荐)、Consul(CP,支持健康检查)、Eureka(AP,已停更)。
【问题】: 让你设计一个分布式ID发号器,怎么设计?
【答案】: 核心问题:多机器多进程环境下生成全局唯一且趋势递增的ID。常见方案:1)雪花算法:64位结构——1位符号位+41位时间戳(可用69年)+10位机器ID(1024台机器)+12位序列号(单机每毫秒4096个ID);2)数据库号段模式:每次从数据库取一个号段(如1-1000),用完再取,减少数据库压力。
【大白话解释】: 分布式ID就像全国身份证号,14亿人不能重号。雪花算法把ID拆成时间戳+机器编号+序号,就像身份证的省市区+出生日期+顺序码。号段模式就像去银行取号,一次取一批号回来慢慢发。
【扩展知识讲解】: 雪花算法时钟回拨问题:机器时钟回退会导致ID重复,解决方案:回拨时间短则等待、回拨时间长则拒绝服务或采用备用workerId。美团的Leaf方案结合了号段模式和雪花算法的优点。
【问题】: 让你设计一个购物车功能,怎么设计?
【答案】: 主要功能:加购、商品列表展示、结算下单。数据至少包括:商品SKUID、数量、加购时间、勾选状态。
未登录用户:用Cookie或LocalStorage暂存,Cookie存JSON字符串(限制4KB),LocalStorage存5-10MB。缺点是换浏览器数据丢失。
登录用户:同步到数据库支持多端同步。表设计:cart(user_id, sku_id, quantity, checked, add_time),联合唯一索引(user_id, sku_id)防止重复加购。读写频繁场景可用Redis缓存:Hash结构key=cart:{userId},field=skuId,value=JSON(数量+勾选状态+加购时间),HSET时若field已存在则quantity累加。
商品列表展示:实时获取最新价格和库存状态(调商品服务接口批量查询),价格变动频繁不能缓存。结算时校验库存和价格,防止加购后价格变化或库存不足。
【大白话解释】: 购物车就像超市推车,没登录时推车贴身上(浏览器本地存),登录后推车挂名下(数据库存),换台机器也能找到你的推车。展示时实时查价格,因为价格随时可能变。
【扩展知识讲解】: 购物车数据存Redis还是MySQL?读写频繁用Redis,但需考虑数据一致性——Redis与MySQL双写,先写Redis再异步写MySQL(MQ或定时同步),以Redis为准。纯MySQL方案在高并发下性能差但数据可靠。
合并逻辑:用户登录时需将本地购物车与数据库购物车合并,同商品数量叠加,不同商品直接插入。合并时机:登录接口中同步执行,或登录后前端调合并接口。
大促期间优化:1)购物车数据可设过期时间(如7天)减少Redis存储压力;2)未勾选商品不参与结算查询,减少批量查询数据量;3)购物车数量上限(如50个SKU),防止恶意加购占内存。
【问题】: 商家想要知道自己店铺卖的最好的top 50商品,如何实现?
【答案】: 核心是实时TopK问题,用Redis的ZSet最合适。详细设计: 1)数据写入:每个商家对应一个ZSet,key=rank:sold:{merchantId},member=商品ID(skuId),score=销量。每卖出一件商品用ZINCRBY rank:sold:1001 1 sku123把对应商品的score加1; 2)查询排行榜:ZREVRANGE rank:sold:1001 0 49 WITHSCORES获取Top50商品及销量,ZREVRANGE按score从大到小排序,时间复杂度O(log(N)+M); 3)不能用数据库ORDER BY的原因:每秒几十单甚至几百单,每次更新销量都UPDATE+SELECT排序,数据库扛不住。ZINCRBY是O(log(N))操作,单线程无并发问题。
定时落库:每5分钟将Redis排行榜快照写入MySQL的排行榜表(merchant_id, sku_id, sold_count, rank_date),支持历史趋势查询。MySQL做兜底,Redis故障时从MySQL查。
【大白话解释】: 排行榜就像学生成绩排名,每卖一件商品就给这个商品加1分,随时查谁分最高。用Redis的ZSet天生就是干这个的,不用每次都从数据库排序,那样太慢了。
【扩展知识讲解】: 数据量极大时的优化:1)定时任务周期性从数据库聚合统计结果写入Redis,而非每笔订单实时ZINCRBY(订单量大时Redis写入压力大);2)HyperLogLog做UV统计(同一用户多次浏览只算一次),PFADD uv:{merchantId}:{date} userId,PFCOUNT获取UV数;3)Bloom Filter做商品去重:用户浏览记录去重,避免重复计数;4)排行榜实时性与准确性权衡:实时排行榜(每次ZINCRBY)数据准确但Redis压力大,准实时排行榜(每分钟聚合一次)降低压力但有短暂延迟。5)多维度排行榜:按销量/按金额/按评分分别建ZSet,互不影响。
【问题】: 让你设计一个文件上传系统,怎么设计?
【答案】: 核心点:支持超大文件上传、避免重复存储、限流。大文件分片上传流程: 1)前端把文件切成固定大小分片(如每片2MB),计算整个文件MD5摘要作为唯一标识; 2)先问后端摘要存不存在(接口:/check?fileMd5=xxx),存在就秒传(返回已有文件URL); 3)不存在则请求上传:后端返回可用的分片序号列表(断点续传,已上传的分片跳过); 4)前端逐片或并发上传,每片带序号(chunkIndex)、文件标识(fileMd5)、总分片数(chunkTotal); 5)后端收到分片后存到临时目录:/temp/{fileMd5}/chunk_{index}; 6)所有分片上传完毕,前端调合并接口,后端按序拼接分片为完整文件,计算合并后MD5校验完整性,存到最终位置(本地/OSS/MinIO),删除临时文件。
并发控制:前端限制并发上传3-5个分片,避免浏览器连接耗尽。断点续传:后端记录已上传分片序号,前端重新上传时跳过已完成分片。
【大白话解释】: 大文件上传就像搬家,一件件搬。先看看有没有一样的家具已搬过(秒传),没有就把大件拆成小件搬(分片),搬到新家再组装。中间断了不用从头来,接着搬没搬完的就行(断点续传)。
【扩展知识讲解】: 还需考虑:1)分片大小选择:2-5MB为宜,太小HTTP请求多开销大,太大单次传输耗时长容易超时;2)文件校验:前端算MD5耗时长(大文件可能要几十秒),可用Web Worker后台计算不阻塞UI,或用SparkMD5分片计算;3)存储方案:本地磁盘(小规模)、阿里OSS/腾讯COS(大规模,自带分片上传SDK)、MinIO(私有化部署);4)清理过期临时分片:定时任务扫描/temp目录,删除超过24小时未合并的分片;5)上传限流:按用户限制并发上传任务数(如同时最多3个),防止单用户占满带宽;6)大文件MD5计算优化:采样计算(取文件头+尾+中间各2KB算MD5),速度提升10倍但有一-定碰撞风险。
【问题】: 如何设计一个OAuth2.0授权服务?Token如何管理?
【答案】: 四个角色:资源所有者(用户)、客户端(第三方应用)、授权服务器、资源服务器。授权码模式流程:1)用户点击”使用微信登录”跳转授权服务器;2)用户登录授权;3)授权服务器返回授权码code;4)客户端用code换取access_token和refresh_token;5)用access_token访问资源。Token管理:短期access_token+长期refresh_token,支持撤销。
【大白话解释】: OAuth2.0就像住酒店,你(用户)给前台(授权服务器)出示身份证,前台给你一张房卡(access_token),你拿房卡开门(访问资源)。房卡有时效,过期了用长住卡(refresh_token)换新房卡,不用重新登记。
【扩展知识讲解】: 四种授权模式:授权码模式(最安全推荐)、隐式模式(已不推荐)、密码模式(高度信任场景)、客户端凭证模式(服务间调用)。Token安全:HTTPS传输、短期有效期、签名防篡改、黑名单支持主动失效。
【问题】: 如果要实现一个抢红包的功能,红包金额是如何计算的?
【答案】: 核心是公平性和总额控制。两种红包类型: 1)普通红包:总金额/人数=固定金额,用整数分计算避免浮点精度问题; 2)随机红包(二倍均值法):每次抢的金额=随机(0.01, 剩余金额/剩余人数×2)。推导:每次抢的期望=剩余金额/剩余人数,所以上限设2倍均值保证公平性。举例:100元10人抢,第1人随机(0.01, 100/10×2)=(0.01, 20)元,假设抢12元;第2人随机(0.01, 88/9×2)=(0.01, 19.56)元…最后一人拿剩余金额。
技术实现:预先生成所有金额存入Redis列表(拆红包时RPOP),避免实时计算。并发抡红包用Redis的DECR原子操作扣减剩余数量,DECR返回值>=0说明抢到了,<0说明没了直接返回”已抢光”。金额精度用整数分计算(100元=10000分),避免BigDecimal舍入问题。
【大白话解释】: 二倍均值法就像分蛋糕,每次切时先算平均大小的两倍,在这个范围内随机切一块。先抢的不会太大(有上限),后抢的不会太小(剩余钱够分),数学上期望值相等,公平。
【扩展知识讲解】: 微信红包优化:预生成金额方案——红包创建时根据二倍均值法预计算所有金额,存入Redis列表key=redpacket:{id},抡红包时RPOP弹出金额,原子操作保证不超发。
并发处理:1)DECR扣减剩余数量+RPOP弹出金额,两个操作用Lua脚本保证原子性;2)Redis单线程保证顺序,先到先得;3)防重复抢:Redis SET记录已抢用户ID,SISMEMBER判断+SETNX保证同一用户只能抢一次。
红包过期退回:创建红包时设24小时过期时间,定时任务扫描过期未抢完红包,剩余金额退回发送者账户。防刷:同一用户同一红包只能抢一次,同一用户每天抡红包次数受限。
【问题】: 如何在附近100w的商户中,快速找到离你最近的5家商户?
【答案】: 利用空间索引加速地理位置查询:1)Redis的Geo数据结构:GEOADD添加商户位置,GEORADIUS查找指定半径内商户,按距离排序,底层用GeoHash+ZSet实现;2)MySQL/MongoDB的R-tree索引:将商户经纬度组织成树结构加速查询。
【大白话解释】: 找附近商户就像在地图上画圈,看圈里有哪些店。Redis的Geo功能天生干这个,你告诉它你的位置和搜索半径,它瞬间返回最近的店。底层是把经纬度编码成字符串(GeoHash),前缀相同说明距离近。
【扩展知识讲解】: GeoHash编码:将二维经纬度映射为一维字符串,前缀匹配实现邻近搜索。6位GeoHash精度约±0.61km。边界问题:GeoHash在边界处可能漏掉附近点,需查询周围8个格子补充。数据量极大时可按城市分片。
【问题】: 分布式锁一般都怎样实现?
【答案】: 主流方案用Redis实现。核心是SETNX命令:SET lockKey lockValue NX PX 30000,NX保证只有第一次设置成功(互斥性),PX设30秒过期防死锁(客户端崩溃锁也能自动释放)。lockValue用UUID+线程ID,释放锁时先判断value是否是自己设的再删除,防止误删别人的锁。
释放锁必须用Lua脚本保证原子性:if redis.call(‘get’,KEYS[1]) == ARGV[1] then return redis.call(‘del’,KEYS[1]) else return 0 end。因为GET+DEL不是原子操作,中间可能被其他客户端抢先设置。
Redisson封装了看门狗机制:默认锁30秒过期,但后台线程每10秒(1/3过期时间)检查并续期,业务未完成锁不会过期,业务完成自动释放。还支持可重入锁(Hash结构记录重入次数)、联锁(MultiLock同时锁多个key)、读写锁。
ZooKeeper实现:创建临时顺序节点,节点序号最小的获得锁,其他节点监听前一个节点的删除事件。天然防死锁(会话断开临时节点自动删除),支持公平锁(按创建顺序排队),但性能不如Redis。
【大白话解释】: 分布式锁就像公共厕所门锁,进去的人锁门,出来开锁。SETNX就是”锁不存在就上锁”,设过期时间防人不出来(死锁)。开门前要看清楚是不是自己上的锁,别把别人的锁开了——Lua脚本保证一气呵成。
【扩展知识讲解】: Redis锁的三大问题及解决方案:1)锁过期但业务未完成:Redisson看门狗自动续期解决;2)主从切换锁丢失:主节点加锁后未同步到从节点就宕机,从节点升为主后锁丢失。RedLock算法解决——向5个独立Redis实例加锁,超过半数成功才算加锁成功,但争议大(Martin Kleppmann质疑);3)锁可重入:Redisson用Hash结构,field=客户端ID+线程ID,value=重入次数,每次加锁value+1,解锁-1,到0删除key。
选型建议:一般场景用Redis+Redisson(性能好、功能全),强一致性要求用ZooKeeper(CP特性保证锁不会丢)。不推荐自己造轮子,Redisson已经处理了大部分边界情况。
【问题】: 如果让你统计每个接口每分钟调用次数怎么统计?
【答案】: 方案看精度要求:1)小系统内存计数:ConcurrentHashMap+AtomicInteger,AOP切面拦截,定时任务每60秒落库;2)大系统日志采集+ES:请求日志写入文件,Filebeat采集到ES,按时间聚合统计;3)MQ+时序数据库:请求事件发MQ,消费者写入InfluxDB,支持实时查询和可视化。
【大白话解释】: 统计接口调用次数就像商场统计每小时客流量。小店放个计数器手动记(内存计数),大商场装摄像头自动统计(日志+ES),大型连锁用总部统一汇总分析(MQ+时序数据库)。
【扩展知识讲解】: 实际项目通常组合使用:内存计数做实时限流、日志做离线分析、时序数据库做监控大盘。还需考虑:分布式环境下多实例计数汇总、统计维度扩展(按接口/用户/IP)、数据采样降低存储压力。
【问题】: 朋友圈点赞功能如何实现?
【答案】: 朋友圈点赞分两层:点赞计数和点赞列表。
点赞计数:用Redis的String结构,key=like:count:{动态ID},INCR原子操作累加,DECR取消点赞时减1。但需注意DECR前要先判断是否已点过赞。
点赞列表:用Redis的ZSet,key=like:list:{动态ID},score为点赞时间戳(毫秒级),value为用户ID。ZADD添加点赞记录,ZREM取消点赞,ZREVRANGE按时间倒序获取点赞列表,ZCARD获取点赞总数,ZSCORE判断某用户是否点过赞(返回nil未点过)。
取消点赞流程:1)ZSCORE判断是否已点赞;2)ZREM移除记录;3)ZREM返回1才DECR减1(防止重复取消导致计数为负);4)发MQ异步消息更新数据库。
异步落库:通过消息队列将点赞事件发送到消费者批量写入数据库。点赞明细表(moments_id, user_id, like_time, status),点赞计数表(moments_id, like_count)。
【大白话解释】: 朋友圈点赞就像班级投票,谁点了赞、点赞顺序都要记。计数器记总数(INCR),ZSet记谁点的和顺序(按时间排)。取消点赞就是从名单里划掉名字、总数减一。数据库不用每次都写,攒一批再写。
【扩展知识讲解】: 朋友圈点赞和普通点赞的区别:朋友圈需要展示点赞列表和顺序,所以用ZSet而非SET。还需考虑:1)共同好友可见点赞列表:查点赞列表后过滤非好友,或用Redis SET存好友关系sismember判断;2)点赞通知推送:点赞后发MQ消息,通知服务推送给动态作者(App推送/站内信);3)防重复点赞幂等处理:ZADD已存在的member只会更新score不会重复添加,天然幂等;4)大V朋友圈点赞量可能达百万级:ZSet存储百万member约需100MB,需分页查询ZRANGE按页获取,或改用计数+只存最近N条点赞列表;5)点赞与评论的联动:点赞数和评论数一起展示,可合并到一个Hash缓存。
【问题】: 如果项目需要实现敏感词过滤功能,如何实现?
【答案】: 主流方案是DFA(确定有限自动机)算法:将敏感词库构建成字典树,文本过滤时逐字符匹配,沿树遍历,遇到结束标记即匹配成功。优点:时间复杂度O(n),与敏感词数量无关。还可结合:1)前缀匹配+后缀匹配提高准确率;2)白名单机制防误杀;3)谐音/拆字/拼音变体需额外处理。
【大白话解释】: 敏感词过滤就像查违禁品,把所有违禁品名称建一棵树(字典树),检查时沿着树走,走到底就找到了。比如”赌博”,先找到”赌”节点再找”博”节点,走到底就是敏感词。一棵树检查所有词,速度不受词库大小影响。
【扩展知识讲解】: 工程实践:词库通常上万条,DFA构建后常驻内存,定期热更新。难点在变体处理:”赌-博”、”dubo”、”赌 博”(加空格)等。方案:预处理时去除特殊字符、转拼音后再匹配。开源工具:ToolGood.Words(C#)、DFA-filter(Java)。
【问题】: 可以用几行代码实现一个负载均衡器吗?
【答案】: 最简单的负载均衡器就是一个请求分发器:维护服务实例列表,每次请求按策略选一个实例转发。常见策略: 1)轮询:AtomicInteger计数取模,int index = counter.getAndIncrement() % instances.size(),简单均匀但不考虑服务器性能差异; 2)随机:Random随机选,简单但分布不均匀; 3)加权轮询:按权重分配,权重大的实例获得更多请求。Nginx的平滑加权轮询(SWRR)算法:每个实例维护currentWeight,每次选择currentWeight最大的实例,选中后currentWeight减去totalWeight,所有实例currentWeight加上自身weight,实现平滑分配; 4)一致性Hash:按请求特征(如用户ID)Hash映射到固定实例,相同请求总是到同一实例,解决会话保持问题; 5)最小连接数:选当前活跃连接数最少的实例,适合请求处理时间差异大的场景。
核心代码逻辑:while(true) { Instance inst = select(strategy, instances); Response resp = forward(inst, request); },就是一个循环+策略选择+HTTP转发。
【大白话解释】: 负载均衡器就像餐厅领位员,客人来了按规则安排座位。轮询是按顺序一张张安排,随机是随便指一个,加权是VIP多安排几个位置。核心逻辑就这么简单,几行代码搞定。
【扩展知识讲解】: 生产级负载均衡器还需考虑:1)健康检查:定时HTTP/TCP探测实例是否存活,不可用的从列表剔除,恢复后自动加回。检查间隔5-10秒,连续3次失败标记不可用;2)平滑加权轮询(SWRR):Nginx的默认算法,解决普通加权轮询的请求集中问题——比如A:B=5:1,普通轮询会连续5个请求都到A,SWRR会均匀打散为A-A-B-A-A-A;3)最小连接数策略:适合长连接或请求耗时差异大的场景(如文件上传);4)会话保持:同一用户请求路由到同一实例,可用IP Hash或Cookie植入实现。但微服务架构不推荐会话保持,应设计无状态服务;5)预热(Slow Start):新实例上线后权重从0逐步提升到目标值,避免瞬间涌入大量请求导致新实例崩溃。Nginx商业版支持slow_start参数。
开源实现:Nginx(七层)、HAProxy(四层+七层)、Ribbon/Spring Cloud LoadBalancer(客户端负载均衡)。
【问题】: 把单体服务拆成多个微服务,这些服务之间如何自动发现彼此?
【答案】: 通过注册中心实现服务发现。流程:1)服务启动时将自己的IP、端口、服务名注册到注册中心;2)消费方从注册中心订阅所需服务的实例列表;3)注册中心通过心跳检测剔除不可用实例;4)实例变化时推送通知消费方更新列表。常见注册中心:Nacos(AP/CP可切换)、Eureka(AP)、ZooKeeper(CP)、Consul(CP)。
【大白话解释】: 服务发现就像公司通讯录,新员工入职登记(注册),离职自动移除(心跳检测),找人就查通讯录(服务发现)。通讯录更新了还会群通知(推送变更)。没有通讯录就得挨个问谁在哪个工位。
【扩展知识讲解】: CAP理论在注册中心的选择:Eureka优先可用性(AP),网络分区时仍可提供服务发现但不保证数据一致;ZooKeeper优先一致性(CP),网络分区时不可用但数据不会错。Nacos支持临时实例(AP)和持久实例(CP)。微服务场景一般选AP,因为短暂的数据不一致好过服务不可用。
【问题】: 项目中同样的功能需要适配多种数据库,如何实现?
【答案】: 核心是抽象+策略模式。1)定义统一的数据访问接口;2)为每种数据库实现各自的策略类(如MySQLStrategy、OracleStrategy);3)通过配置或运行时动态选择策略。MyBatis Plus的多数据源+动态表名也能解决。Spring中可用AbstractRoutingDataSource实现动态数据源切换。
【大白话解释】: 适配多种数据库就像万能充电器,定义一个标准接口(充电口),每种手机配一个转接头(策略类),用哪个插哪个。程序运行时根据配置选转接头就行,业务代码完全不用改。
【扩展知识讲解】: 实际难点在SQL方言差异:分页语法不同(MySQL的LIMIT vs Oracle的ROWNUM)、数据类型不同、函数不同。解决方案:用ORM框架屏蔽差异、或用数据库无关的SQL构建器。多数据源事务需用分布式事务或分库路由。
【问题】: 从网关再到各个后端服务,如何设置RPC的超时时间?
【答案】: 超时设置遵循链路递减原则:网关超时 > 下游服务超时 > 底层服务超时。比如网关3s > 服务A 2s > 服务B 1s。原因是每个环节需要留出自己处理的时间。还需考虑:1)读操作超时可长一些,写操作超时要短;2)重试次数×超时时间不能超过上游超时;3)设置熔断器防止级联超时。
【大白话解释】: 超时就像办事时限,大领导给3天,你分给下属2天,下属给自己1天,层层递减。你总不能给下属3天,万一他3天才回你,大领导那边早超时了。留点时间给自己处理结果。
【扩展知识讲解】: 常见问题:超时链路配置不合理导致读超时重试风暴。比如网关超时2s但下游设置3s,下游还在处理网关就重试了,请求量翻倍。建议:下游超时 < 上游超时 × 0.8,重试次数 ≤ 2次,非幂等接口不重试。
【问题】: 当前有个本地操作A,远程操作B,需要保证A和B事务的一致性,如何实现?
【答案】: 这是典型的分布式事务场景。三种主流方案:
1)本地消息表(最实用):A操作和写消息表在同一个本地事务中——BEGIN; 扣钱SQL; INSERT INTO msg_table(id, biz_type, biz_id, params, status, retry_count) VALUES(…); COMMIT; 消息表记录B的调用参数和状态(待处理/已处理/失败)。定时任务每5秒扫描status=待处理的消息,调用B服务,成功则更新status=已处理,失败则retry_count+1,超过5次告警人工介入。优点:实现简单、不依赖额外中间件、保证最终一致性。缺点:消息表和业务表耦合、定时任务有延迟。
2)TCC:A执行Try阶段预留资源(冻结金额而非扣除),B执行Try预留资源;全部Try成功则Confirm提交(扣除冻结金额),任一Try失败则Cancel回滚(解冻金额)。需为每个服务编写Try/Confirm/Cancel三个接口。优点:强一致性。缺点:开发量大、需处理空回滚和幂等、资源冻结有性能开销。
3)Saga:A和B各定义正向操作和补偿操作。正向流程:A扣钱→B加钱;任一步失败执行已成功步骤的补偿:B加钱失败→A补偿回加钱。适合业务流程长、参与方多的场景。
【大白话解释】: 本地操作和远程操作要一致,就像你转钱给朋友,你扣钱和朋友到账必须同时成功或同时失败。本地消息表方案:你扣钱的同时写条记录”待转账给朋友”,后台任务确保朋友到账后删记录。相当于你先记账,再慢慢确认,不怕断电丢记录。
【扩展知识讲解】: 本地消息表注意事项:1)消息表需定期清理已处理消息,避免表过大影响查询性能;2)定时任务需保证幂等,同一消息不能重复处理(消费端也需幂等);3)长时间未完成的消息需告警(如retry_count>5),人工介入排查;4)消息表和业务表在同一个库才能用本地事务保证一致性。
Seata框架:阿里开源的分布式事务框架,支持AT(自动补偿,对业务无侵入,一阶段拦截SQL生成回滚日志,二阶段自动提交/回滚)、TCC(手动编写三个接口)、Saga(长事务编排)、XA(强一致但性能差)四种模式。生产中AT模式用得最多,但对SQL有限制(不支持多表关联更新等)。实际项目常用本地消息表+最终一致性替代完整的分布式事务框架。
【问题】: 公司有海外业务,从国内将数据发送到海外延迟比较大,如何处理?
【答案】: 1)就近部署:在海外机房部署服务,用户请求直接打到海外服务,避免跨洋传输;2)CDN加速:静态资源用海外CDN节点;3)数据同步:核心数据通过专线或消息队列异步同步到海外,非核心数据按需拉取;4)压缩传输:减少数据量降低延迟;5)协议优化:用HTTP/2或gRPC减少连接开销。
【大白话解释】: 海外延迟大就像从北京寄快递到美国,太慢了。解决方案:在美国开分店(就近部署),畅销品提前运过去(数据同步),快递压缩打包(压缩传输),走航空专线(协议优化)。
【扩展知识讲解】: 数据同步方案:Canal监听binlog同步MySQL数据、消息队列跨地域复制。注意合规:GDPR等法规要求数据不能随意跨境传输,需脱敏或本地化存储。跨国网络可用AWS Direct Connect或阿里云高速通道。
【问题】: 用Bitmap存储100个用户ID,但ID范围很大,会有什么问题?如何解决?
【答案】: 问题:Bitmap大小由最大ID值决定,如果ID范围是1-10亿,即使只有100个用户也需要约120MB内存,空间浪费严重。解决方案:1)ID映射:将稀疏ID映射到连续的密集编号(如1-100),用映射表维护对应关系;2)RoaringBitmap:高效压缩位图,将32位空间分桶,稀疏桶用数组存,密集桶用位图存;3)用Set替代:数据量小时直接用Set更省内存。
【大白话解释】: Bitmap就像一排灯,每个用户ID对应一盏灯,有人就亮。但ID范围太大就像给10亿盏灯只亮100盏,浪费电。解决办法:重新编号(ID映射),或用智能灯控(RoaringBitmap),亮灯多的区域用位图,少的区域只记编号。
【扩展知识讲解】: RoaringBitmap原理:将32位整数空间分成65536个桶(高16位),每个桶内:元素≤4096个用有序数组存储,>4096个用位图存储。这样既节省空间又保持高效位运算。Redis 2.8.9+支持BITFIELD命令操作大偏移量。
【问题】: 项目上用了分布式锁,加锁后并发度不就降低了吗?
【答案】: 确实会降低并发度,这是锁的本质——用串行化换取正确性。关键是要把锁粒度做到最小:1)锁对象细分:别锁整个订单,锁具体订单ID即可;2)锁范围缩小:只锁临界区代码,不要把非临界操作包进去;3)分段锁:如ConcurrentHashMap的思路,不同段并行;4)读写锁:读操作共享锁,写操作独占锁;5)乐观锁替代:用版本号CAS代替加锁。
【大白话解释】: 加锁就像给门上锁,确实一次只能进一个人。但你可以把大门换成小门(锁细分),一个房间一把锁,不同房间可以同时进。也可以用刷卡门禁(乐观锁),先试试不锁门,冲突了再重来。
【扩展知识讲解】: 生产中分布式锁的优化:1)锁续期避免业务未完成锁过期(Redisson看门狗);2)锁可重入减少嵌套加锁开销;3)联锁(MultiLock)同时锁多个资源;4)红锁(RedLock)多节点防单点故障。核心原则:能用乐观锁不用悲观锁,能用细粒度锁不用粗粒度锁。
【问题】: 单体项目QPS到1万,要微服务化拆分吗?
【答案】: 不一定需要拆分。1万QPS单体完全能扛,一个优化良好的Spring Boot应用单机可达数千QPS,2-3个实例加负载均衡即可。拆分微服务的判断标准不在QPS而在:1)团队规模:超过2个披萨团队(8-10人)协作困难;2)业务复杂度:模块边界清晰、独立迭代需求强;3)部署效率:某模块频繁发版影响全局;4)技术栈差异:不同模块需要不同技术栈。
【大白话解释】: QPS 1万就像日客流量1万的商场,一个大楼完全装得下,没必要拆成10个小店。除非:管理团队太大(团队协作困难)、某些区域经常装修影响整体(部署问题)、餐饮和服装需要不同管理方式(技术栈差异)。
【扩展知识讲解】: 微服务不是银弹,引入后带来分布式事务、服务间通信、链路追踪、运维复杂度等问题。单体优先原则:能单体解决就不微服务。拆分时机:代码库已无法有效管理、团队沟通成本高于开发成本、发布周期被拖慢。拆分策略:先从最独立的模块开始,逐步拆分。
组件设计场景
【问题】: 让你设计一个HashMap,怎么设计?
【答案】: 核心三件事:hash函数怎么算、碰撞了怎么办、满了怎么扩。底层是数组加链表结构,数组每个位置是一个桶,存放链表头节点。
put流程:1)对key算hash值:h = key.hashCode() ^ (key.hashCode() »> 16)(高16位异或低16位的扰动函数,减少碰撞);2)定位桶:(n-1) & hash(n是数组长度,等价于取模但更快,所以数组长度必须是2的幂);3)桶为空直接放入;4)桶不为空遍历链表,key相等则覆盖value,否则挂链表尾部;5)链表长度超过8且数组长度>=64转红黑树(TreeNode),查找从O(n)降为O(log n);6)数组元素超过阈值(capacity * loadFactor,默认16*0.75=12)就扩容为2倍。
get流程:1)算hash定位桶;2)桶为空返回null;3)桶头节点key匹配直接返回;4)否则遍历链表/红黑树查找。
为什么数组长度是2的幂:(n-1) & hash等价于hash % n,但位运算比取模快10倍以上。扩容时也是2倍,方便rehash优化。
【大白话解释】: HashMap就像一排储物柜,拿钥匙(key)算出柜子编号,直接往里放东西。两个钥匙算出同编号(哈希冲突)就排成链子挂柜子里。链子太长(超8)换红黑树加速查找,柜子快满了就换一排两倍长的柜子。
【扩展知识讲解】: 设计要点:1)hash扰动:高16位异或低16位,让高位也参与定位计算,减少碰撞。没有扰动的话,低位相同高位不同的key会全部碰撞;2)负载因子0.75的权衡:太大(如1.0)空间省但碰撞多查找慢,太小(如0.5)查找快但浪费空间。0.75是时间和空间的最佳平衡点,来自泊松分布的统计结果;3)扩容rehash优化:JDK8不需要重新计算hash定位,用hash & oldCap判断新增的那一位是0还是1,0留在原位,1移到原位+oldCap位置,避免全量rehash;4)线程不安全原因:并发put时如果两个线程同时触发扩容,链表可能成环导致死循环(JDK7头插法),JDK8改尾插法不会成环但仍然会丢数据;5)链表转红黑树阈值8的选择:泊松分布计算,负载因子0.75时链表长度达到8的概率仅亿分之六,正常情况不会转树。
【问题】: 让你设计一个线程池,怎么设计?
【答案】: 线程池本质是生产者消费者模型。四个核心问题:
1)线程管理:核心线程数(corePoolSize)和最大线程数(maximumPoolSize)控制。核心线程长期存活即使空闲,非核心线程空闲超过keepAliveTime后被回收。线程创建时机:请求来时先创建核心线程,核心线程满了任务入队列,队列满了创建非核心线程,非核心线程也满了执行拒绝策略;
2)任务存储:BlockingQueue阻塞队列存储待执行任务。队列选择:SynchronousQueue(不存储,直接交接,CachedThreadPool用)、LinkedBlockingQueue(无界队列,FixedThreadPool用,注意OOM风险)、ArrayBlockingQueue(有界队列,推荐)、PriorityBlockingQueue(优先级队列);
3)满了怎么办:四种拒绝策略——AbortPolicy(抛异常,默认)、CallerRunsPolicy(调用者线程执行任务,不丢弃也不抛异常,起到限流效果)、DiscardPolicy(默默丢弃最新任务)、DiscardOldestPolicy(丢弃队列头部最旧任务再提交新任务)。生产推荐CallerRunsPolicy或自定义策略(记录日志+持久化到DB);
4)监控指标:活跃线程数、队列大小、已完成任务数、拒绝任务数、平均耗时等。ThreadPoolExecutor提供了getActiveCount()、getQueue().size()、getCompletedTaskCount()等方法。
【大白话解释】: 线程池像出租车公司,核心线程是正式员工(一直在岗),非核心线程是临时工(忙时叫来闲时走人)。任务队列是排队等车的乘客,车全出去了乘客还排着队就按策略处理:直接拒绝、让叫车人自己跑、默默丢掉、丢掉排队最久的。
【扩展知识讲解】: 线程池参数配置原则:1)CPU密集型任务(计算、加密):核心线程数=CPU核数+1,多1个线程是为了在某个线程偶尔因为缺页中断暂停时仍能利用CPU;2)IO密集型任务(网络请求、数据库查询):核心线程数=CPU核数×2,或用公式线程数=CPU核数×(1+IO等待时间/CPU时间)。实际中需压测调优。
Executors创建的坑:1)FixedThreadPool和SingleThreadPool内部用LinkedBlockingQueue(默认Integer.MAX_VALUE容量),任务堆积会OOM;2)CachedThreadPool的maximumPoolSize=Integer.MAX_VALUE,高并发下可能创建数万线程导致OOM。生产环境必须用ThreadPoolExecutor手动创建,明确指定队列容量和最大线程数。
动态调参:Hippo4j等开源项目支持线程池参数动态调整,无需重启应用。核心思路:修改配置中心的参数值→监听变更→调用ThreadPoolExecutor的setCorePoolSize()等方法热更新。
【问题】: 在Java中,不使用锁如何实现一个线程安全的单例?
【答案】: 四种方式:1)静态内部类:利用JVM类加载机制,外部类加载时不加载内部类,调用getInstance时才触发内部类加载初始化实例,JVM保证线程安全且延迟加载。代码:public class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } };2)枚举单例:public enum Singleton { INSTANCE; },JVM保证枚举实例唯一且线程安全,天然防反射破坏(反射创建枚举会抛IllegalArgumentException)和防序列化破坏;3)饿汉式:类加载时直接初始化private static final Singleton INSTANCE = new Singleton(),简单但没有延迟加载;4)CAS无锁:用AtomicReference的compareAndSet实现,无锁但竞争时多次重试浪费CPU。
【大白话解释】: 不用锁实现单例就像不用门锁也能保证房间只有一个人——利用JVM类加载机制天然保证(静态内部类),或用枚举(JVM天生保证枚举实例唯一),或用CAS乐观锁(先试试进去,有人就算了再试)。
【扩展知识讲解】: 静态内部类原理:外部类加载时不立即加载内部类,调用getInstance时才触发内部类加载,JVM的类初始化阶段有
为什么枚举单例最推荐?Effective Java作者Joshua Bloch推荐。原因:1)防反射:反射的Constructor.newInstance()内部检查如果是枚举类型直接抛异常;2)防序列化:枚举的readResolve()直接返回已有实例,不会创建新对象;3)代码最简洁。
双重检查锁(DCL)也是常见方式:if(instance==null) { synchronized(Singleton.class) { if(instance==null) instance = new Singleton(); } },必须加volatile防止指令重排序(new Singleton()分3步:分配内存→初始化→引用赋值,重排序可能导致引用赋值先于初始化,其他线程拿到未初始化的对象)。
【问题】: 让你实现一个分布式单例对象,如何实现?
【答案】: 普通单例是进程内唯一,分布式单例要跨进程唯一。实现思路:1)用分布式锁控制创建过程,保证同一时刻只有一个进程能创建;2)把对象存到外部存储让所有进程都能访问。创建流程:多进程同时获取分布式锁,只有一个抢到;抢到的先检查外部存储里有没有,没有就创建并序列化存进去;最后释放锁。
【大白话解释】: 分布式单例就像全国只有一个主席,多个地方同时选,用分布式锁保证同一时间只有一个地方在选,选出来的结果公布到公告栏(外部存储),所有人看公告栏就知道主席是谁了。
【扩展知识讲解】: 外部存储可选Redis、ZooKeeper、数据库。注意点:分布式锁要设超时防死锁、对象序列化与反序列化的性能开销、外部存储的高可用保障。对象有状态变更时还需考虑状态同步问题。
【问题】: WebSocket知道吗?如果让你设计一个IM协议,你会考虑什么?
【答案】: WebSocket是基于TCP的双向通信协议,通过一次HTTP升级握手建立持久连接。设计IM协议考虑:1)高效传输层(WebSocket);2)轻量可扩展的消息结构(JSON或Protobuf);3)消息可靠性(确认机制、重传);4)消息顺序性;5)安全性(加密传输);6)心跳保活机制。
【大白话解释】: HTTP像发短信一问一答,WebSocket像打电话接通后随时能说。设计IM协议就像设计电话规则:用什么编码(Protobuf)、怎么确认对方听到了(ACK)、怎么说才有顺序(消息序号)、怎么保证偷听不到(加密)、怎么确认还在线(心跳)。
【扩展知识讲解】: 还需考虑:消息存储(离线消息拉取)、多端同步(同一账号多设备消息一致)、群聊消息扇出优化、已读回执、输入状态提示。长连接保活:应用层心跳(30s一次),TCP KeepAlive作兜底。
【问题】: HashMap是不是线程安全的?让你实现一个线程安全的HashMap你怎么设计?不用加锁呢?
【答案】: HashMap非线程安全。线程安全方案: 1)Collections.synchronizedMap:内部用synchronized包装所有方法(put/get/size等),锁粒度是整个Map对象,性能差,相当于串行访问; 2)ConcurrentHashMap:JDK7用分段锁(Segment数组,默认16段,每段一把锁,不同段可并行),JDK8用CAS+synchronized优化到桶级别——put时桶为空用CAS写入(无锁),桶不为空用synchronized锁住头节点(只锁一个桶),其他桶不受影响; 3)不加锁方案:CAS+不可变数据结构(如PersistentHashMap,每次修改创建新版本,读旧版本不受影响),或CopyOnWrite思路——写时复制一份新Map改完再替换引用,读无锁。CopyOnWrite适合读多写极少场景(如配置表),写多时复制开销太大。
推荐ConcurrentHashMap,性能和安全兼顾。
【大白话解释】: HashMap像公共储物柜没锁,多人同时用就乱。加锁就是给整个房间装门锁(synchronizedMap),或给每个柜子装锁(ConcurrentHashMap)。不加锁的思路:读时用快照(CopyOnWrite),写时复制一份改完再替换。
【扩展知识讲解】: ConcurrentHashMap JDK8精髓:1)put时桶为空用CAS写入,失败则自旋重试;桶不为空用synchronized锁住头节点,锁粒度极小(只锁一个桶),其他桶完全并行;2)size()方法用baseCount+CounterCell数组分散计数,类似LongAdder思路——多线程并发更新不同CounterCell避免CAS竞争,最终汇总baseCount+所有CounterCell的值。比JDK7分段锁size()需要锁所有Segment高效得多;3)扩容支持多线程协助:一个线程触发扩容后,其他正在put的线程可以帮忙迁移数据(helpTransfer),加速扩容过程。
线程安全HashMap的选择:读多写少且数据量小→ConcurrentHashMap;读极多写极少→CopyOnWrite(如配置/元数据);需弱一致性迭代→ConcurrentHashMap(迭代器不会抛ConcurrentModificationException)。
【问题】: 假设有一个1G大的HashMap,用户请求刚好触发扩容,会怎样?如何优化?
【答案】: 1G的HashMap扩容需新增2倍大小数组并搬运所有元素,当前线程会被长时间阻塞。优化方案:借鉴Redis渐进式rehash,维护新旧两个数组,扩容时不一次性迁移,而是把迁移分摊到后续每次读写操作中,每次迁移一小部分,直到全部完成。
【大白话解释】: 一次性扩容就像搬家当天要把所有东西一口气搬完,累死人还耽误事。渐进式rehash就像每天搬一点,该上班上班该吃饭吃饭,每次顺手搬几样,不知不觉搬完了。期间查东西先查老房子再查新房子。
【扩展知识讲解】: Redis渐进式rehash:维护ht[0](旧表)和ht[1](新表),每次CRUD操作时顺带迁移ht[0]中rehashidx位置的元素到ht[1]。查找时先查ht[0]再查ht[1],新增只往ht[1]写。全部迁移完成后ht[1]变为ht[0]。
缓存场景
【问题】: 线上发现Redis机器爆了,如何优化?
【答案】: 1)首先排查根因:通过监控查看Redis的CPU、内存、命令执行时间或带宽指标;2)针对性解决:内存耗尽→临时升配再优化,CPU过高→排查慢查询或复杂命令;3)长期优化:数据分片、设置合理过期策略、优化数据结构。
【大白话解释】: Redis爆了就像水管爆了,先关闸止损(扩容),再查哪里漏水(监控排查),最后修好管子(优化)。别上来就优化代码,先把线上问题稳住再说。
【扩展知识讲解】: Redis内存优化:用Hash替代多个String(小Hash用ziplist编码极省内存)、设置maxmemory和淘汰策略、定期清理无用Key、大Key拆分。监控工具:Redis Info命令、Redis Exporter+Prometheus、redis-cli –bigkeys扫描大Key。
【问题】: MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?
【答案】: 核心思路是淘汰策略+热点探测。Redis配好maxmemory,选LRU或LFU淘汰策略,内存满了冷数据自动被踢掉。光靠淘汰策略不够,还需热点探测:通过埋点统计访问频率,主动把高访问量数据加载到Redis,如京东开源的hotkey能毫秒级探测出热点Key。
【大白话解释】: 就像书店只摆最畅销的20本书,怎么保证摆的都是畅销书?两个办法:一是书架满了自动把最久没人翻的撤掉(淘汰策略),二是统计哪些书卖得好主动摆上去(热点探测)。两个结合才靠谱。
【扩展知识讲解】: LRU vs LFU:LRU按最近访问时间淘汰,LFU按访问频率淘汰。LFU更适合热点数据场景,因为偶尔访问一次的冷数据不会挤掉频繁访问的热数据。Redis 4.0+支持LFU策略(volatile-lfu、allkeys-lfu)。
【问题】: 如果发现Redis内存溢出了?你会怎么做?
【答案】: 第一时间止损:立即扩容增加内存保证业务正常。然后排查:1)检查是否有大Key;2)检查过期策略是否合理;3)检查是否有过期Key未及时清除;4)优化数据结构选择(如用Hash替代多个String)。长期方案:设置maxmemory和淘汰策略、数据分片、监控告警。
【大白话解释】: Redis内存溢出像家里东西太多放不下。先租个仓库(扩容),然后看看是不是有几件特别占地方的大件(大Key),是不是该扔的旧东西没扔(过期Key没清),以后买东西设个规矩——满了先扔最不用的(淘汰策略)。
【扩展知识讲解】: 大Key问题:一个Key存上百MB的Value,删除时主线程阻塞(DEL是同步的),应用UNLINK异步删除。内存碎片:Redis实际使用内存可能远大于数据所需,可开启activedefrag自动碎片整理。
【问题】: 项目用了Redis缓存来提升并发度,假设Redis挂了怎么办?
【答案】: 三个层面应对:1)高可用保障:主从复制+哨兵(自动故障转移,主节点挂了哨兵选举新主)或Redis Cluster(6节点起步,3主3从,自动分片+故障转移),主节点挂了自动切换,故障转移时间约30秒;2)流量防护:Redis挂了请求全部涌向数据库,必须有限流和熔断兜底。Sentinel配置规则:QPS超过数据库承受能力(如5000)直接拒绝,数据库连接池满则熔断返回默认值;3)兜底降级:读请求走数据库(性能下降但可用,数据库需提前做好连接池优化),写请求暂存本地消息表或MQ,Redis恢复后回写。还可以用本地缓存(Caffeine/Guava Cache)做二级缓存,Redis不可用时查本地缓存,命中率虽然不如Redis但好过直接查库。
【大白话解释】: Redis挂了就像高速堵车,得有备用路线。平时多修几条路(主从集群),堵了就分流;设收费站限流(熔断),别一窝蜂挤小路;实在不行走国道(数据库直查),慢点但能到。
【扩展知识讲解】: 缓存雪崩、击穿、穿透的区别与应对: 1)缓存雪崩:大量Key同时过期,请求全部打到数据库。应对:过期时间加随机值(如2小时+随机0-30分钟)、热点Key永不过期+异步刷新、Redis集群高可用、限流降级兜底; 2)缓存击穿:某个热点Key过期,大量并发请求同时穿透到数据库。应对:互斥锁(SETNX只允许一个请求查库回写缓存,其他请求等待)、热点Key永不过期(逻辑过期——存一个过期时间字段,到期后异步刷新)、提前续期(定时任务在Key过期前刷新); 3)缓存穿透:查询不存在的数据,缓存没命中数据库也没命中,每次都穿透。应对:布隆过滤器(将所有可能的数据哈希到位图,查询前先过布隆过滤器,不存在直接拒绝)、缓存空值(查不到也缓存null,设短过期时间如5分钟)、接口参数校验。
多级缓存架构:浏览器缓存→CDN缓存→Nginx本地缓存→Redis集群→数据库。每一层都能拦截部分流量,减少下游压力。
【问题】: 现在让你负责一个核心系统,需要实现核心数据的缓存预热,如何做?
【答案】: 缓存预热的本质是在流量打过来之前把热点数据提前灌到缓存。核心流程:1)系统启动时预热服务从数据库加载热点数据;2)批量写入Redis;3)用户请求到来时直接命中缓存。预热策略:全量预热(数据量小)、增量预热(定时同步变化数据)、按访问频率预热(统计热点Key)。
【大白话解释】: 缓存预热就像开店前把畅销商品摆上货架,客人来了直接买,不用现从仓库取。全量预热是小店全摆出来,增量预热是大店每天补新货,按频率预热是精明老板只摆卖得好的。
【扩展知识讲解】: 预热时机:应用启动时、缓存清空后、大促活动前。注意事项:预热不能阻塞启动流程、数据量大需分批加载、预热期间数据库压力大需控制并发。可结合canal监听binlog实现缓存自动更新。
高并发与限流场景
【问题】: 什么是限流?限流算法有哪些?怎么实现的?
【答案】: 限流就是限制到达系统的并发请求数,超出的直接拒绝或排队。四种限流算法:
1)计数器(固定窗口):维护一个计数器,窗口时间内计数器<阈值则通过,>=阈值则拒绝,窗口结束后重置。问题:临界突刺——窗口末尾和下一窗口开头各通过阈值个请求,2倍流量瞬间涌入;
2)滑动窗口:记录窗口内每个请求的时间点,统计最近一个窗口时间内的请求数是否超限。解决临界突刺问题但内存开销大(需记录每个请求时间)。Sentinel的滑动窗口实现:将窗口细分为多个小格子(如1秒的窗口分2个500ms格子),每个格子独立计数,滑动时只统计当前窗口覆盖的格子;
3)漏桶:请求先进入漏桶排队,以固定速率从桶底流出处理。不管来多少请求,处理速率恒定,完美平滑流量。但无法应对合理突发——即使系统空闲也只能匀速处理;
4)令牌桶:以固定速率往桶里放令牌,桶满则丢弃令牌,请求需拿令牌才能通过,没拿到则等待或拒绝。允许一定突发:桶里有积攒的令牌时可以瞬间处理一批请求。Guava RateLimiter就是令牌桶实现。
【大白话解释】: 计数器像出租车限载4人,到4人就拒载。滑动窗口更精细,看你近1分钟上了多少人。漏桶像水龙头滴水,不管来多少水都匀速流出。令牌桶像游乐场发手环,每隔一秒发一个,有手环才能玩,允许攒着一起玩。
【扩展知识讲解】: 工程实践中最常用令牌桶算法:1)Sentinel默认用滑动窗口+令牌桶,支持QPS限流和线程数限流;2)Guava RateLimiter是单机令牌桶实现,SmoothBursty允许突发(积攒令牌),SmoothWarmingUp有预热期(QPS从0逐步提升到目标值);3)Nginx的limit_req模块用漏桶算法,配置limit_req zone=api burst=20 nodelay。
分布式限流:单机限流管不了全局QPS,需Redis+Lua脚本实现集群限流。Lua脚本原子执行:1)INCR计数;2)判断是否超阈值;3)设过期时间。也可用Redis的CELL模块(Redis 7.0+内置令牌桶)。网关层限流更优:在Nginx/Kong/APISIX层限流,请求还没到后端就被拦截,减少后端压力。
限流后的处理策略:直接拒绝(返回429 Too Many Requests)、排队等待(令牌桶等待获取令牌)、降级返回默认值(返回缓存数据或兜底数据)。
【问题】: 两百万个生产者发送消息,仅一个消费者,如何高效设计锁?
【答案】: 这道题考察内存队列的锁设计,不是消息中间件方案。极高并发写入场景下的锁优化:1)分段锁:将队列分成多个段,每段一把锁,降低锁竞争;2)CAS无锁队列:用AtomicReference实现无锁入队操作;3)Disruptor框架:基于环形缓冲区和多生产者序列器的无锁设计。
【大白话解释】: 两百万生产者抢一把锁就像两百万人在一个收银台排队,太慢了。分段锁就像开多个收银台,CAS就像自助结账不用排队,Disruptor就像环形跑道——跑到一圈终点就是起点,不用锁,大家按序号占位就行。
【扩展知识讲解】: Disruptor核心设计:1)环形缓冲区(RingBuffer):预分配数组避免GC,用序号取模定位;2)无锁设计:多生产者用CAS争抢序号,消费者各自跟踪消费序号;3)缓存行填充消除伪共享。单线程吞吐可达数千万ops/s。
【问题】: 当客户端有重连机制,服务意外宕机重新部署启动时,怎么避免流量洪峰?
【答案】: 重连风暴问题:服务挂了所有客户端等着重连,服务一起来瞬间涌入大量请求又被打死。解决方案:1)客户端随机退避:重连间隔加随机抖动(如5s±2s)避免同时重连;2)服务端限流:启动后先限制并发数逐步放开;3)注册中心延迟注册:服务完全就绪后再注册;4)预热:启动后先加载缓存再放流量。
【大白话解释】: 就像演唱会散场,所有人同时挤向出口会踩踏。解决办法:通知大家错峰走(随机退避),门口设栏杆慢慢放人(限流),检票员就位后再开门(延迟注册),开门前先检查设备(预热)。
【扩展知识讲解】: 注册中心延迟注册在Dubbo中叫delay属性,Spring Cloud可用@PostConstruct预热完成后才注册。Nacos支持临时实例延迟注册。客户端指数退避公式:wait = base * 2^retry + random,避免惊群效应。服务端限流可用Sentinel配置启动后QPS从0逐步提升到目标值的预热模式。
订单支付场景
【问题】: 如果一笔订单,用户在微信和支付宝同时支付,会怎么样?
【答案】: 会产生重复支付问题。防范措施: 1)支付幂等:同一笔订单同一支付渠道只能支付一次。实现:支付前检查订单状态,只有”待支付”才能发起支付,用乐观锁UPDATE order SET status=’PAYING’ WHERE id=? AND status=’UNPAID’,affected rows=0说明状态已变不能支付; 2)支付锁定:用户选择支付方式后锁定订单,其他渠道不允许发起支付。在订单表加pay_channel和pay_lock_time字段,选择支付方式时设置锁定,15分钟未支付自动释放; 3)对账兜底:定时对账发现重复支付自动退款。每天凌晨跑对账任务,将支付流水与订单状态比对,发现同一订单多条成功支付流水则触发自动退款流程; 4)核心是订单状态机:待支付→支付中→已支付,只有”待支付”状态才能发起支付,任何状态变更都通过状态机校验。状态机代码可用状态模式或枚举实现,非法状态转换直接拒绝。“待支付”状态才能发起支付。
【大白话解释】: 同一笔订单两个渠道支付就像同时用两张银行卡付同一笔账单,可能扣两次钱。解决办法:选了一种支付方式就锁定不让选其他,就像抢到票就别再抢了。万一真的付了两次,对账发现后自动退款。
【扩展知识讲解】: 支付状态机设计:待支付→支付中(锁定)→已支付/已取消/支付失败。支付中状态需设超时自动回滚(如15分钟),否则用户选了支付方式但没付款,订单就一直锁着。分布式锁+状态机双重保障更可靠。
【问题】: 一笔订单,在取消的那一刻用户刚好付款了,怎么办?
【答案】: 这是经典的并发状态冲突。解决方案:1)状态机+乐观锁:取消操作用UPDATE … WHERE status=待支付,如果状态已变则取消失败;2)补偿机制:如果取消成功了但支付回调也到了,触发退款流程;3)延迟取消:取消不是立即生效,设置短暂缓冲期(如3秒),观察是否有支付回调。
【大白话解释】: 取消和付款撞车就像退票和出票同时发生。用乐观锁:出票时看票还在不在,在就出票成功,退票就失败。万一真的又退又出了,那就退钱(补偿退款)。
【扩展知识讲解】: 实际处理优先级:支付成功 > 取消订单,因为钱已收不能轻易退。支付回调处理:先更新订单状态为已支付,再发消息通知其他系统。取消订单操作需检查支付状态,已支付则转退款流程。建议用分布式锁保证同一订单操作串行化。
【问题】: 让你实现一个订单超时取消功能,怎么设计?
【答案】: 三种方案:1)延迟消息:下单时发一条延迟消息到MQ(如RocketMQ延迟消息或RabbitMQ死信队列),到期消费检查订单状态,未支付则取消;2)定时任务轮询:定时扫描超时未支付订单执行取消,简单但有延迟;3)时间轮:Netty的HashedWheelTimer,适合短时间高精度场景。
【大白话解释】: 订单超时取消就像餐厅预约,超过时间没来就取消预约。延迟消息最优雅:下单时设个闹钟,时间到了自动检查。定时轮询像每小时看一遍预约本,简单但不及时。时间轮像秒表计时,精度高但适合短时间。
【扩展知识讲解】: RocketMQ延迟消息支持特定延迟级别(1s/5s/10s/30s/1m/…/2h),不支持任意时间。RabbitMQ需借助TTL+死信队列模拟。定时轮询的优化:只扫描最近一段时间的数据,配合索引提高查询效率。Redis的ZSet也能实现:score存过期时间,定时任务ZRANGEBYSCORE扫描过期订单。
【问题】: 如何避免用户重复下单(多次下单未支付,占用库存)?
【答案】: 1)前端防重复点击:按钮置灰+防抖;2)后端幂等:下单接口基于用户ID+商品ID+时间窗口生成幂等Key,同一Key的请求返回相同结果;3)库存预扣+超时释放:下单时锁定库存,超时未支付释放库存;4)限制未支付订单数:同一用户未支付订单不超过N个。
【大白话解释】: 重复下单就像在食堂打饭,一个人拿5个盘子占5份菜,别人没得吃。前端按钮点一次就灰掉(防手贱),后端看同一人同一菜几分钟内只能点一份(幂等Key),超时不付钱盘子收走(库存释放)。
【扩展知识讲解】: 幂等Key实现:可用Redis的SETNX,Key为orderId:userId:skuId,过期时间5分钟。库存预扣在Redis中用DECR原子操作,超时释放用延迟消息或定时任务。注意:库存预扣可能导致恶意占库存攻击,需结合用户限流和风控。
【问题】: 每秒200笔订单请求到支付服务,支付服务需调用第三方支付,限流100笔每秒。如何设计支付服务满足FIFO且用满第三方100笔配额?
【答案】: 核心是流量整形+队列缓冲。设计:1)请求入队:200笔请求先入消息队列(如Kafka分区保证FIFO);2)消费端限速:消费者按100 TPS匀速消费,可用令牌桶或RateLimiter控制消费速率;3)结果回调:第三方支付结果通过回调或轮询获取。关键点:队列分区保证顺序、消费者精确限速、第三方调用失败重试回队列头部。
【大白话解释】: 200人同时冲向检票口但检票口只能每秒过100人,直接冲会堵死。办法:排队取号(入队),检票员匀速放人(限速消费),先进先出。不能用数据库队列,扛不住这个并发量。
【扩展知识讲解】: 精确限速实现:Guava RateLimiter(单机)或Redis+Lua令牌桶(分布式)。FIFO保证:Kafka单分区天然有序,多分区用相同订单ID做分区键。第三方支付超时处理:设合理超时时间,超时后查询第三方确认状态再决定重试还是取消。注意幂等:第三方支付接口调用必须幂等。
【问题】: 针对支付宝八折优惠事故,说说如何避免类似事件?
【答案】: 事故本质是运营配置错误未经验证直接上线。防范措施:1)配置审核:运营配置需双人审核;2)灰度发布:优惠活动先小范围验证再全量;3)预算上限:设置活动总预算和单用户优惠上限,超限自动停用;4)实时监控:优惠金额异常波动自动告警;5)紧急熔断:发现异常一键关闭活动。
【大白话解释】: 八折事故就像商场搞促销,本来只打9折但系统设成了8折,一开门就亏大了。防范:设折扣需两个人确认(审核),先在一个柜台试试(灰度),设置亏本上限(预算),亏钱速度异常马上报警(监控),出问题一秒关门(熔断)。
【扩展知识讲解】: 金融级配置管理:配置变更有版本号和回滚机制、A/B测试验证配置正确性、配置变更记录完整审计日志。优惠系统架构:规则引擎+预算控制+风控校验三层架构,任何一层拦截异常配置。
数据库场景
【问题】: MySQL中使用索引一定有效吗?如何排查索引效果?
【答案】: 索引不一定有效:1)索引失效场景:对索引列使用函数、隐式类型转换、LIKE左模糊、OR条件中有无索引列、不满足最左前缀;2)即使走了索引,如果回表代价大(筛选率低),优化器可能选择全表扫描。排查方法:EXPLAIN查看执行计划,关注type(是否走索引)、rows(扫描行数)、Extra(是否用了覆盖索引)。
【大白话解释】: 索引就像书的目录,但不是每次查目录都快。查”姓张的人”目录有用,查”名字有’明’的人”目录帮不上忙(左模糊)。有时候优化器发现用目录还不如翻书快(回表代价大),就干脆不用目录了。
【扩展知识讲解】: EXPLAIN关键字段:type从好到差为system>const>eq_ref>ref>range>index>ALL。Extra的Using index表示覆盖索引(不回表),Using filesort表示额外排序,Using temporary表示用了临时表。优化索引的核心原则:选择性高的列放前面、尽量覆盖索引避免回表、联合索引遵循最左前缀。
【问题】: MySQL中如何解决深度分页的问题?
【答案】: 深度分页(如LIMIT 1000000,10)问题:MySQL需扫描前100万行再丢弃,极慢。解决方案:1)游标分页:用WHERE id > 上次最大id LIMIT 10,只往后翻不往前;2)子查询优化:先查主键再回表SELECT * FROM t WHERE id IN (SELECT id FROM t LIMIT 1000000,10);3)覆盖索引:查询列都在索引中避免回表;4)ES分页:大数据量场景用Elasticsearch。
【大白话解释】: 深度分页就像看书翻到第10万页,得从前一页页翻。游标分页像书签,记住上回看到哪直接跳过去,但不能往回翻。子查询优化先只查页码(主键),再根据页码找内容,比翻页快。
【扩展知识讲解】: 游标分页局限:不支持跳页(只能上一页下一页),社交App瀑布流常用此方案。子查询优化原理:子查询走覆盖索引只扫描索引不回表,拿到主键后再精确回表取10条。业务层优化:限制最大翻页数(如百度最多76页)。
【问题】: 项目上需要导入一个几百万数据的Excel文件到数据库,有哪些注意点?
【答案】: 核心问题:内存溢出、导入耗时、锁表风险。解决方案:1)流式读取:用EasyExcel/SXSSF逐行读取,不一次加载到内存;2)分批插入:每500-1000条一批INSERT,减少数据库交互次数;3)关闭自动提交:手动事务,一批一提交;4)导入前关索引和约束,导入后重建;5)异步导入:后台任务执行,前端轮询进度。
【大白话解释】: 几百万行Excel就像几百万封信,一次全搬到邮局会把邮局压垮(OOM)。一车一车搬(分批),搬的时候别每封都盖邮戳(关自动提交),搬完再统一盖(批量提交)。大门口太窄就先拆门框(关索引),搬完再装上(重建索引)。
【扩展知识讲解】: EasyExcel原理:SAX模式解析Excel,逐行回调处理,内存占用极小。LOAD DATA INFILE:MySQL原生批量导入命令,比INSERT快10倍以上。导入验证:数据格式校验、重复数据检测、异常数据记录到错误文件。并发导入多张表需注意数据库连接池大小。
【问题】: MySQL中select * from一个有1000万行的表,内存会飙升吗?
【答案】: 会的,但飙升的是MySQL服务端缓冲区而非客户端内存。MySQL默认逐行发送结果集,服务端需将结果写入net_buffer(默认16KB)发送给客户端。如果没有WHERE条件,MySQL必须扫描全部1000万行,服务端CPU和IO飙升,网络带宽也被大量数据占满。客户端如果一次性接收全部结果集才会OOM。
【大白话解释】: select * 1000万行就像把整个图书馆的书全搬出来看一遍,搬的、看的都累死。不是你脑子(客户端内存)爆,是搬运工(服务端)先累垮。而且你根本不需要看所有书,加个WHERE只看需要的就好。
【扩展知识讲解】: 避免全表扫描:加WHERE条件缩小范围、只SELECT需要的列、加LIMIT限制返回行数。JDBC的fetchSize参数控制每次从服务端拉取的行数(Oracle默认10行,MySQL需设置useCursorFetch=true才生效)。大数据量导出必须分页流式处理。
【问题】: 有一张表5000W数据,如何统计流量最大时有多少条数据?
【答案】: 思路是将时间维度聚合。1)按时间粒度分组统计:如果需要分钟级粒度,用DATE_FORMAT(开始时间,’%Y-%m-%d %H:%i’)分组COUNT,取最大值;2)优化:在开始时间和结束时间上建联合索引;3)预计算:如果查询频繁,用定时任务每隔5分钟统计当前活跃数写入汇总表,查询直接查汇总表。
【大白话解释】: 统计流量峰值就像统计高速路上同时最多的车数。把时间切成小段(每分钟),数每段里在路上的车,取最多的那段。5000万条数据不能每次都现数,提前算好存起来(预计算),查的时候直接看。
【扩展知识讲解】: 活跃数计算:某时刻的活跃数=开始时间≤该时刻且结束时间≥该时刻的记录数。精确统计需扫描大量数据,近似统计可用采样或HyperLogLog。预计算方案类似数据仓库的Cube聚合,适合固定维度查询。
【问题】: 如何实现数据库的不停服迁移?
【答案】: 双写方案:1)新库建表,老库不变;2)应用层双写:写操作同时写老库和新库,读仍走老库;3)数据全量同步:用DataX或Canal将老库历史数据同步到新库;4)数据校验:比对老库和新库数据一致性;5)灰度切读:先切10%读流量到新库验证,逐步加到100%;6)停止双写:确认新库稳定后停写老库。
【大白话解释】: 不停服迁移就像搬家不耽误住,先在新家摆好家具(建新库+同步数据),然后每天用的东西两个家都放一份(双写),确认新家没问题了再彻底搬过去(切流量),最后退租老房子(停双写)。
【扩展知识讲解】: 双写一致性:先写老库再写新库,中间失败需补偿。Canal方案更优:监听binlog自动同步,应用层无需改造。灰度切读可用网关层按用户ID路由。迁移窗口选择低峰期,全程可回滚。
【问题】: 单表数据量很大,如何优化?
【答案】: 不考虑分库分表的优化方案:1)索引优化:添加合适索引、覆盖索引减少回表;2)冷热分离:热数据留在主表,冷数据归档到历史表;3)分区表:按时间或ID范围分区,查询只扫相关分区;4)读写分离:主库写从库读;5)缓存层:热点查询结果缓存到Redis;6)字段优化:拆分大字段到扩展表、适当冗余减少JOIN。
【大白话解释】: 单表数据大优化就像大房间整理:先把常用和不常用的分开(冷热分离),给东西贴标签方便找(索引),按区域分区管理(分区表),一个人整理一个人取东西(读写分离),常用的放手边(缓存),大件家具拆开存放(字段优化)。
【扩展知识讲解】: 分区表注意事项:分区键必须包含在唯一索引中、跨分区查询效率可能更差、分区数量有上限。冷热数据分离方案:按时间分表(如按月),热数据在主表,冷数据归档到历史表。读写分离:主库写、从库读,需处理主从延迟问题。
【问题】: 数据库表上新增一个字段,如果这个表正在进行读写操作,如何处理才能不影响现有读写操作?
【答案】: 1)在线DDL:MySQL 5.6+的ALGORITHM=INPLACE/ALGORITHM=INSTANT,不锁表;2)影子表迁移:先创建新结构的影子表并同步数据,低峰期通过RENAME TABLE原子切换;3)gh-ost工具:GitHub开源的在线表结构变更工具,通过binlog同步增量数据,最终原子切换;4)pt-online-schema-change:Percona工具,原理类似。
【大白话解释】: 在线加字段就像高速公路上修路,不能封路。INSTANT方式最快,只改元数据瞬间完成;INPLACE方式在旁边修好再切换;影子表方式像在旁边修一条新路,修好后把路牌一换(RENAME),车辆无感切换。
【扩展知识讲解】: MySQL 8.0的INSTANT ADD COLUMN:只修改表的元数据,不重建表,秒级完成。但有局限性:只能加在表末尾、不能加主键。gh-ost vs pt-osc:gh-ost基于binlog无触发器、对主库压力更小;pt-osc用触发器同步、使用更广泛。大表变更务必在低峰期执行。
【问题】: 设计数据库表时,关联表和在一个表中加冗余字段各有什么优势?
【答案】: 关联表(范式设计):优点是数据一致性好、更新只需改一处、存储空间小;缺点是多表JOIN性能差、查询SQL复杂。冗余字段(反范式设计):优点是查询快(单表即可)、SQL简单;缺点是更新需同步多处、可能数据不一致、存储空间大。选择依据:读多写少用冗余、写多用关联、核心数据用关联保证一致性。
【大白话解释】: 关联表像正式档案,一份修改全局生效,但查档案要跑多个部门。冗余字段像复印件放在手边,查起来方便但改了原件得记得同步所有复印件。频繁查的放复印件(冗余),少改的重要的存档案(关联)。
【扩展知识讲解】: 实际项目中常用适度冗余:如订单表中冗余商品名称和价格(快照),避免每次JOIN商品表。但商品价格实时变动不影响已下订单的快照价格。这就是”读时冗余、写时一致”的思路。分库分表后跨库JOIN很困难,冗余字段几乎是必须的。
【问题】: 项目上需要存储IP地址,数据库应该用什么类型来存储?
【答案】: 推荐用INT UNSIGNED存储(4字节),用INET_ATON()将IP转为整数存入,用INET_NTOA()将整数转回IP。对比:VARCHAR(15)存点分十进制占7-15字节且无法范围查询,INT只需4字节且支持范围查询(如查某个网段)。IPv6地址用VARBINARY(16)存储,用INET6_ATON/INET6_NTOA转换。
【大白话解释】: 存IP就像存电话号码,存数字比存字符串省空间又好查询。”192.168.1.1”转成数字3232235777,只占4个字节,查”192.168”开头的IP直接用数字范围查,比LIKE快多了。
【扩展知识讲解】: INT存储IP的优势:1)节省空间(4字节 vs 7-15字节);2)支持范围查询(BETWEEN 167772160 AND 167772415查10.0.0.*);3)索引效率更高(整数比较比字符串快)。注意:MySQL的INET_ATON只支持IPv4,IPv6需用INET6_ATON。
【问题】: 现在有40亿个QQ号,给你1G内存,如何实现去重?
【答案】: 40亿QQ号用HashSet约需64GB内存,1G内存装不下。方案:1)Bitmap:QQ号为整数,用位图标记是否存在,40亿QQ号约需500MB,1G内存够用;2)分治+外部排序:数据分成多个小文件,每个文件去重后合并;3)Bloom Filter:用布隆过滤器判断是否重复,有少量误判但省内存。
【大白话解释】: 40亿个号码1G内存去重,就像用1个便签本记住40亿个人名。直接记名字肯定记不下(64GB),但每个人画个格子打勾(Bitmap)只要500MB。或者用特殊印章(布隆过滤器),盖过章的就是重复的,偶尔看错但不影响大局。
【扩展知识讲解】: Bitmap方案细节:QQ号范围假设0-43亿,需要4300000000bit≈512MB。Java中可用BitSet或直接用byte数组。Bloom Filter方案:1G内存可容纳约80亿bit,误判率约0.7%,适合允许少量误判的场景。分治方案:按QQ号范围哈希分到多个文件,每个文件内存去重,最后合并。
【问题】: 现在有500G数据需要排序,但只有4G内存,如何实现?
【答案】: 外部排序。步骤:1)分段读取:每次读4G内存能装下的数据量到内存中排序;2)写入临时文件:排好序的数据写成有序小文件;3)多路归并:用小顶堆对所有小文件的当前最小值归并,每次取堆顶写入最终文件。归并时只需每个文件读一条数据到内存,内存占用极小。
【大白话解释】: 500G排序就像500本书按字母排,桌子只能放4本。每次拿4本排好放一边(分段排序),最后每次从每堆里取最前面的比较,取最小的放到最终书架上(多路归并)。桌子只需放每堆当前第一本,不需要放全部。
【扩展知识讲解】: 多路归并优化:败者树比小顶堆少比较次数。归并路数k不宜太大,k路归并每轮需log(k)次比较,通常分多级归并。磁盘IO优化:顺序读写、缓冲区预读。MapReduce的Sort阶段就是这个原理。
【问题】: 会员表中有500万条会员数据,如何对即将过期的会员提前7天进行提醒?
【答案】: 1)定时任务扫描:每天定时查询7天内到期的会员发送提醒,需在到期时间字段建索引;2)延迟消息:会员开通时发一条延迟消息,到期前7天消费触发提醒;3)时间轮+日历:按到期日期分桶,每天扫对应桶即可。推荐方案1,简单可靠,500万数据加索引查询很快。
【大白话解释】: 会员到期提醒就像生日提醒,每天看谁7天内过生日发短信。500万人不用每次全扫,给生日建个索引(到期时间索引),瞬间查出来。也可以开通时就设个7天前的闹钟(延迟消息),到点自动提醒。
【扩展知识讲解】: 定时任务方案优化:只扫描到期时间在7天后的那条记录,避免重复提醒需记录提醒状态。延迟消息方案适合精确时间触发但MQ需支持长延迟。数据量极大时可按到期时间分表,每天扫一张表。
【问题】: MySQL数据库一亿条数据,怎么快速加索引?
【答案】: 1)在线DDL:MySQL 8.0的ALGORITHM=INPLACE加索引,不锁表但耗时长;2)gh-ost/pt-osc工具:在影子表上加索引后切换,对线上影响小;3)从库先加:主从架构中先在从库加索引,切换主从后原主库再加;4)临时关闭核心业务:低峰期执行,减少锁等待。
【大白话解释】: 一亿条数据加索引就像一亿本书贴标签,不能停业搞。用在线DDL边营业边贴(慢但不停),用影子表在旁边贴好再换(最稳妥),或让助手先贴(从库先加)。
【扩展知识讲解】: ALGORITHM=INPLACE加索引原理:按主键排序后批量构建索引,无需全表随机IO。一亿数据建索引可能需数十分钟到数小时,期间会占用IO和CPU资源。建议:在从库先加索引验证查询计划,确认有效后再在主库操作。加索引前用PT-Duplicate-Key-Checker检查是否有冗余索引。
【问题】: ArrayList里有1亿条数据,要怎么去重?
【答案】: 1)HashSet去重:new HashSet<>(list),利用Hash去重,时间O(n),但1亿数据内存开销大;2)Stream去重:list.stream().distinct().collect(Collectors.toList()),底层用LinkedHashSet;3)先排序再去重:Collections.sort后遍历相邻比较,省内存但O(nlogn);4)BitSet去重:如果是整数且范围可控,用BitSet最省内存。
【大白话解释】: 1亿条去重就像1亿张票查重,直接丢进HashSet让它自动去重最快但最费内存。排序后相邻比较像把票排好序,一样的挨在一起一眼看出来,省内存但慢。如果是数字类型,用BitSet最省:每个数字对应一个格子,来过就标记。
【扩展知识讲解】: 1亿数据内存估算:ArrayList约800MB,HashSet约2GB+。如果内存不够,需分批处理:分成多块分别去重后合并。大数据量去重考虑用外部排序+归并,或用Bloom Filter粗筛后再精确去重。BitSet方案:1亿个bit约12MB,极其省内存。
消息队列场景
【问题】: 线上消息队列故障,兜底改造方案是什么?
【答案】: MQ故障时不能让业务中断,需多级兜底:1)降级写本地:消息暂写到本地消息表或本地文件,MQ恢复后补偿发送;2)同步调用降级:MQ不可用时改为同步直接调用下游服务,牺牲性能保可用性;3)备用MQ:部署备用集群快速切换;4)限流保护:MQ故障后请求全部同步调用,需限流防级联故障。
【大白话解释】: MQ故障就像快递公司罢工,包裹不能停发。先存自家仓库(本地消息表),等快递恢复再发。急件自己送(同步调用),不急的等恢复。或者直接换一家快递公司(备用MQ)。
【扩展知识讲解】: 本地消息表设计:包含消息ID、内容、状态(待发送/已发送/发送失败)、重试次数、创建时间。定时任务扫描待发送消息重发,失败次数超限告警人工介入。需注意:本地消息表数据量控制(定期清理已发送消息)、同一消息不能重复消费(幂等)。
【问题】: 第三方上游接口接入做异步处理,是否可以使用MQ?如果不用MQ还有什么方式?
【答案】: 可以用MQ,但需考虑:1)消息可靠性:上游接口结果不能丢;2)消息顺序:有些场景需保序;3)消息幂等:重复消费不影响结果。不用MQ的替代方案:1)线程池异步:简单场景用CompletableFuture+线程池,不引入MQ中间件;2)数据库轮询:写状态表,定时任务扫描处理;3)事件总线:Guava EventBus或Spring ApplicationEvent,进程内异步。
【大白话解释】: 异步处理就像餐厅后厨做菜,订单进来不傻等,丢给厨师(MQ)慢慢做。但小餐馆不用雇传菜员(MQ),厨师直接接单(线程池),或者写在单子上排队(数据库轮询)。规模大了再雇传菜员更高效。
【扩展知识讲解】: MQ vs 线程池:MQ支持跨进程、可持久化、有重试机制,但引入额外组件和复杂度;线程池简单但进程重启任务丢失、不支持跨服务。选型原则:单机异步用线程池、跨服务异步用MQ、最终一致性要求高用MQ。
【问题】: 如何用消息队列实现延迟任务?比如订单30分钟未支付自动取消?
【答案】: 1)RocketMQ延迟消息:内置18个延迟级别,下单时发指定级别的延迟消息,到期消费检查订单状态;2)RabbitMQ TTL+死信队列:消息设TTL过期后进入死信队列,消费死信队列触发检查;3)Redis ZSet:score存执行时间戳,定时任务ZRANGEBYSCORE扫描到期任务;4)时间轮:适合高精度短延迟场景。
【大白话解释】: 延迟任务就像设闹钟,30分钟后响铃检查订单。RocketMQ自带闹钟功能(延迟消息),RabbitMQ需要把闹钟设在另一个房间(死信队列),Redis像一张时间表定时看哪些到期了。
【扩展知识讲解】: RocketMQ延迟级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。RabbitMQ死信方案需注意:同一队列不同TTL的消息,前一条未过期后一条无法进入死信,需每条消息一个队列或用插件。Redis ZSet方案需注意定时任务的扫描频率和精度。
【问题】: 什么是死信队列?在什么场景下会用到?如何设计死信队列的处理机制?
【答案】: 死信队列(DLQ)是存放无法被正常消费的消息的队列。消息进入死信的条件:1)消费失败且重试次数耗尽;2)消息过期(TTL超时);3)队列满无法投递。设计处理机制:1)死信队列单独存储,不占用正常消费队列;2)人工介入:提供死信查看和重发界面;3)自动重试:降低频率定期重试消费死信;4)告警:死信堆积超过阈值触发告警。
【大白话解释】: 死信队列就像邮局的”退件箱”,送不出去的信都放这里。原因可能是地址不对(消费失败)、信过期了(TTL)、收件箱满了(队列满)。定期检查退件箱,能重发就重发,不行就通知寄件人(告警)。
【扩展知识讲解】: RabbitMQ死信队列配置:正常队列设置x-dead-letter-exchange和x-dead-letter-routing-key指向死信交换机。Kafka没有内置死信队列,需自行实现:消费失败的消息发送到指定Topic。RocketMQ内置重试机制:默认重试16次后进入%DLQ%ConsumerGroup死信Topic。
登录认证与安全场景
【问题】: 你项目用的JWT,那如何保证安全性?别人拿到Token不就可以直接登录你的账号了吗?
【答案】: JWT的安全防护多层设计:1)传输安全:只在HTTPS下传输,防止中间人窃取;2)有效期短:access_token有效期设15-30分钟,降低被盗风险;3)refresh_token:长期token只在刷新时使用,减少暴露;4)绑定指纹:Token中或服务端绑定客户端指纹(IP+UA),不一致则拒绝;5)黑名单机制:Token可主动失效。
【大白话解释】: JWT就像房卡,丢了谁都能开门。防护:只在监控走廊使用(HTTPS),房卡有效期短(短期token),丢了立刻挂失(黑名单)。高级酒店还会核对入住人信息(客户端指纹),不对就拒接。
【扩展知识讲解】: JWT无状态的矛盾:JWT优势是不查库验证,但黑名单需要查库,变成了有状态。折中方案:用Redis存黑名单(只存已注销的token),正常请求不查Redis,注销/修改密码时加入黑名单。黑名单TTL设为token剩余有效期,过期自动清理。
【问题】: 如果线上接口被恶意刷流量了,要如何解决?
【答案】: 多层防护:1)网关层限流:Nginx或API网关按IP/用户限流;2)WAF防护:识别并拦截恶意请求(如SQL注入、爬虫);3)验证码:人机校验,滑动验证码或图形验证码;4)IP黑名单:识别异常IP加入黑名单;5)业务层限流:Sentinel按接口/用户维度限流;6)CDN防护:隐藏源站IP,DDoS攻击由CDN扛。
【大白话解释】: 恶意刷流量就像有人雇人排队堵店门。解决:门口限流(网关限流),保安查身份证(验证码),黑名单上的不让进(IP黑名单),大流量交保安公司处理(CDN防护)。
【扩展知识讲解】: 识别恶意流量:请求频率异常、UA特征异常、请求路径规律性。爬虫防护:动态生成接口签名、请求参数加密、接口时间戳校验(防重放)。DDoS防护需专业服务(CloudFlare、阿里云DDoS高防)。
【问题】: 有人恶意攻击注册功能,伪造手机号大量下发验证码,除了限流之外如何解决?
【答案】: 1)图形验证码前置:发短信前先过图形验证码,增加机器操作成本;2)号码有效性校验:对接运营商API验证手机号真实存在;3)IP维控:同一IP每天限制发送次数;4)设备指纹:同一设备每天限制发送次数;5)收费限制:注册验证码由业务方承担费用,单日预算超限自动熔断;6)风控模型:根据行为特征判断是否恶意。
【大白话解释】: 短信轰炸就像有人疯狂用你的座机打骚扰电话,电话费你出。防护:先回答个问题再拨号(图形验证码),只拨已知的真实号码(号码校验),同一部电话每天限打10个(IP/设备限制),话费超预算停机(预算熔断)。
【扩展知识讲解】: 短信验证码安全:验证码6位数字、5分钟有效、同一号码1分钟内只能发1次、验证码输错3次失效。防短信轰炸需多维度组合防护,单靠限流不够。阿里云/腾讯云短信服务自带频率限制和防刷功能。
【问题】: 设计一个将已登录用户踢下线的功能,如何实现?
【答案】: 核心思路是让已发放的Token失效。方案:1)黑名单机制:将用户ID或Token加入Redis黑名单,每次请求校验时检查是否在黑名单中;2)Token版本号:用户表维护token_version,签发Token时包含版本号,踢下线时版本号+1,旧Token版本号不匹配自动失效;3)强制重新登录:修改用户密码或状态,所有已签发Token自然失效。
【大白话解释】: 踢下线就像老板换锁,之前的钥匙(Token)全部作废。黑名单是记一张”作废钥匙清单”每次核对;版本号是给钥匙编版本,换锁后旧版本钥匙都开不了门。版本号方案更优雅,不用每次查清单。
【扩展知识讲解】: Token版本号方案:JWT payload加version字段,服务端Redis存userId→version映射。踢下线时version+1,校验Token时比较version是否一致。支持踢单个设备:用deviceId+userId做版本号。支持踢所有设备:只改userId的version。
【问题】: 把单体项目进行了多机部署,多台服务器如何共享用户登录信息?
【答案】: 1)Session共享:Tomcat Session复制(不推荐,性能差)或Spring Session+Redis集中存储Session;2)Token方案(推荐):用户登录后签发JWT Token,客户端携带Token,每台服务器独立验证Token无需共享状态;3)粘性Session:Nginx按IP哈希路由到固定服务器,但不够灵活。
【大白话解释】: 多台服务器共享登录就像连锁店共享会员卡信息。方案一:每家店都拷贝一份会员册(Session复制,太慢);方案二:会员卡自带信息自证身份(JWT Token,最好);方案三:每个会员只去固定那家店(粘性Session,不方便)。
【扩展知识讲解】: Spring Session+Redis原理:用Filter包装HttpServletRequest,getSession()时从Redis读取Session数据。JWT方案优势:无状态、天然支持多服务端、不依赖存储。但JWT无法主动失效,需配合黑名单机制。大型系统一般JWT+Redis结合。
【问题】: 如何实现一个单点登录(SSO)系统?
【答案】: SSO核心:一次登录,多系统通行。实现流程(CAS模式):1)用户访问子系统A,未登录则302跳转SSO认证中心;2)SSO认证中心展示登录页面,用户登录成功后写全局Cookie(TGC)并302回子系统A带ticket;3)子系统A用ticket向SSO验证,验证通过写本地Session/Cookie;4)访问子系统B时跳转SSO,SSO发现TGC有效直接签发ticket回B,无需再次登录。
【大白话解释】: SSO就像商场的总服务台,进任何专柜先查你的总会员卡(TGC)。有了总会员卡,每个专柜直接给你发临时通行证(ticket),不用每家都登记。只需在总服务台登记一次。
【扩展知识讲解】: CAS(Central Authentication Service)是SSO经典实现。OAuth2.0的授权码模式也可实现SSO。跨域SSO需处理Cookie跨域问题(设置domain为顶级域名)。移动端SSO一般用Token方案替代Cookie。CAS框架开源实现:Apereo CAS。
【问题】: SSO系统中,如果认证中心挂了怎么办?如何保证已登录用户不受影响?
【答案】: 1)认证中心高可用:集群部署+负载均衡,避免单点故障;2)本地缓存:子系统缓存已验证的ticket和用户信息,认证中心短暂不可用时不影响已登录用户;3)Token自包含:用JWT替代ticket,子系统可独立验证Token不依赖认证中心;4)降级策略:认证中心不可用时不允许新登录,已登录用户正常使用。
【大白话解释】: 认证中心挂了就像总服务台关门了。已经拿到会员卡的人(已登录)不受影响继续逛,新来的(新登录)暂时进不去。所以总服务台要多开几个窗口(集群部署),且会员卡自带信息(JWT)不需要每次回服务台核实。
【扩展知识讲解】: JWT方案下认证中心只负责签发Token,验证由各子系统独立完成(验签名),认证中心挂了已签发Token仍然有效。但Token吊销需要依赖认证中心,可用短期Token+长期refresh机制,refresh时才访问认证中心。
【问题】: 微信扫码登录是如何实现的?
【答案】: 流程:1)PC端请求微信登录,展示带唯一key的二维码;2)手机微信扫码,微信服务器将key与用户身份绑定并通知PC端;3)PC端轮询或WebSocket监听扫码结果,发现扫码成功获取授权码code;4)PC端用code向微信服务器换取access_token;5)用access_token获取用户信息完成登录。底层是OAuth2.0授权码模式。
【大白话解释】: 扫码登录就像用手机扫门禁二维码开门。PC显示二维码(门禁),手机扫一下(授权),门禁识别到手机扫了就开门(登录成功)。手机相当于你的身份证,扫一下就证明是你本人。
【扩展知识讲解】: 二维码关键设计:二维码内容包含唯一key+过期时间,key存Redis设置5分钟过期。PC端轮询间隔1-2秒,扫码后改为WebSocket推送状态更实时。手机端确认登录后才签发code,防止误扫。微信开放平台接入需注册应用获取AppID和AppSecret。
性能优化场景
【问题】: 项目上有个导出Excel场景发现很慢,怎么优化?
【答案】: 1)流式写入:用EasyExcel或SXSSFWorkbook流式写Excel,不在内存中构建完整Excel对象;2)分批查询:数据库分页查询而非一次加载全部数据;3)异步导出:大数据量导出改为异步任务,完成后通知下载;4)减少数据量:只导出必要字段、限制单次导出行数;5)SQL优化:加索引、避免SELECT *、用覆盖索引。
【大白话解释】: 导出慢就像抄笔记,一次抄完一本书太慢。边查边写(流式写入),每次查几页(分批查询),太厚了就让别人帮你抄(异步导出),只抄重点(减少数据量)。
【扩展知识讲解】: EasyExcel原理:基于SAX模式解析和写入Excel,内存占用极低。SXSSFWorkbook是POI的流式API,数据写入磁盘临时文件而非内存。异步导出方案:请求发MQ或线程池异步执行,结果存OSS/MinIO,前端轮询或WebSocket通知下载链接。单次导出建议限制10万行以内,超过则分批导出。
【问题】: 一条1s请求的响应,怎么优化成1ms?
【答案】: 分步骤分析1s耗时在哪:1)网络传输:压缩响应体、减少HTTP请求、CDN加速;2)服务处理:缓存热点数据(Redis)、并行调用(CompletableFuture)、异步化非核心逻辑;3)数据库:加索引、优化SQL、读写分离、分库分表;4)JVM:JIT预热、减少GC停顿。通常最大的优化空间在缓存和数据库。
【大白话解释】: 1秒变1毫秒就是提速1000倍,就像走路变坐飞机。先看时间花在哪:路上慢就换交通工具(网络优化),办事慢就提前备好(缓存),查资料慢就建索引(数据库优化),多件事同时办(并行调用)。
【扩展知识讲解】: 性能优化方法论:先监控定位瓶颈(Arthas、SkyWalking),再针对性优化。常见误区:没定位就优化、过度优化非瓶颈点。优化优先级:架构级优化(缓存/异步/并行)> 代码级优化(算法/数据结构)> 配置级优化(JVM/连接池参数)。
【问题】: 除了代码,还有哪些地方可以优化性能?
【答案】: 性能优化不只是代码的事:1)架构层:引入缓存、异步化、读写分离、CDN;2)数据库层:索引优化、SQL调优、分库分表、连接池调参;3)JVM层:垃圾收集器选择、堆大小调整、JIT优化;4)操作系统层:文件描述符限制、TCP参数调优(TIME_WAIT复用);5)网络层:HTTP/2多路复用、gzip压缩、长连接;6)硬件层:SSD替代HDD、增加内存、多核CPU。
【大白话解释】: 性能优化就像提速,不只看发动机(代码)。道路规划(架构)、变速箱调校(数据库)、换机油(JVM)、修路(操作系统)、换高速通道(网络)、换跑车(硬件),每个环节都能提速。
【扩展知识讲解】: 性能优化全栈思维:前端(懒加载/CDN/压缩)→ 网络(HTTPS/HTTP2/DNS)→ 网关(限流/缓存)→ 服务(并行/异步/缓存)→ 数据库(索引/分库分表)→ 操作系统(内核参数)。木桶效应:最慢的环节决定整体性能。
【问题】: 1000个任务,每个任务0.1s,最大响应时间1s,线程池参数怎么设置?
【答案】: 单线程串行需100s,1s内完成需并发。计算:1000×0.1s=100s总工作量,1s内完成至少需100个线程。参数配置:核心线程数100、最大线程数120(留余量)、队列容量0(直接创建线程)、拒绝策略CallerRunsPolicy(不丢弃任务)。注意每个任务0.1s包含CPU和IO时间,纯CPU任务线程数不超过CPU核数。
【大白话解释】: 1000个活每个干0.1秒,总共100秒的工作量。要求1秒干完,至少雇100个人同时干。雇太多浪费资源,雇太少干不完。留点余量雇120人防意外。
【扩展知识讲解】: 线程数计算公式:CPU密集型线程数=CPU核数+1,IO密集型线程数=CPU核数×2或用公式线程数=CPU核数×(1+IO等待时间/CPU时间)。此题假设是IO密集型任务。实际生产中线程数还需考虑:数据库连接池大小(线程数≤连接池大小)、下游服务承受能力、内存开销(每线程约1MB栈空间)。
【问题】: 如何设计一个接口签名验证机制?防止参数被篡改和重放攻击?
【答案】: 签名验证三要素:1)签名算法:将请求参数按key排序拼接后加盐(appSecret)做HMAC-SHA256生成签名sign;2)防篡改:服务端用相同算法验签,参数被改签名就不匹配;3)防重放:请求加timestamp(5分钟有效)和nonce(一次性随机数),服务端校验timestamp是否在有效期内、nonce是否已使用(Redis存5分钟)。
【大白话解释】: 接口签名就像快递签收的密码,寄件时根据包裹内容生成密码(签名),收件时核验密码是否对得上。防篡改:包裹被动过密码就对不上。防重放:给密码加时效(timestamp)和一次性编号(nonce),过期或重复的密码无效。
【扩展知识讲解】: 签名流程:客户端将参数按ASCII排序→key=value&拼接→末尾加&key=appSecret→HMAC-SHA256→转大写作为sign参数。服务端同样流程计算签名比对。nonce去重用Redis的SETNX,TTL与timestamp有效期一致。微信支付、支付宝都采用类似签名机制。
故障排查场景
【问题】: 线上数据库连接池爆满问题如何排查?
【答案】: 1)查看当前连接:SHOW PROCESSLIST看当前连接数和状态;2)定位慢查询:慢查询日志或SHOW PROCESSLIST中Time大的连接;3)检查连接泄漏:应用是否未关闭连接(如try-catch中未finally close);4)检查连接池配置:maxActive是否太小、timeout是否合理;5)检查是否长事务:大事务占用连接不释放;6)检查是否死锁:SHOW ENGINE INNODB STATUS查看死锁信息。
【大白话解释】: 连接池爆满就像停车场满了,得查谁占着车位不走。看监控(SHOW PROCESSLIST)找出长时间停的车辆(慢查询),检查有没有车停了不开走(连接泄漏),停车场是不是太小(配置问题),有没有车互相堵住(死锁)。
【扩展知识讲解】: HikariCP连接池监控:getActiveConnections()、getIdleConnections()、getThreadsAwaitingConnection()。预防措施:设置连接超时时间(connectionTimeout)、最大生命周期(maxLifetime)、空闲超时(idleTimeout)。Druid连接池内置监控页面可实时查看连接状态。
【问题】: 线上业务服务器CPU飙高如何排查?
【答案】: 四步排查法:1)定位进程:top命令找出CPU占用最高的Java进程,记下PID;2)定位线程:top -Hp
实际排查中经常用Arthas一键定位:thread -n 3直接看CPU最高的3个线程及堆栈,无需手动jstack+grep。
【大白话解释】: CPU飙高就像发动机过热,先看哪台车过热(定位进程),再看哪个气缸问题(定位线程),打开引擎盖看哪个零件坏了(定位代码),最后分析原因(死循环/频繁GC等)。
【扩展知识讲解】: Arthas常用排查命令:1)thread -n 3查看CPU最高的3个线程及堆栈;2)thread -b查看死锁线程;3)dashboard实时查看线程、内存、GC等面板;4)profiler start生成火焰图,profiler stop停止并输出SVG火焰图文件。
GC导致的CPU飙高:1)jstat -gcutil
线上排查注意事项:1)jstack在JVM安全模式下可能需要jstack -F强制打印;2)生产环境慎用jmap dump,可能导致应用暂停数秒甚至数十秒;3)建议加-XX:+HeapDumpOnOutOfMemoryError参数,OOM时自动dump;4)大堆(>8GB)dump文件很大,需确保磁盘空间充足。
【问题】: 怎么分析JVM当前的内存占用情况?OOM后怎么分析?
【答案】: 内存分析:1)jstat -gcutil
OOM分析:1)前提:加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/参数,OOM时自动dump堆快照;2)用MAT(Memory Analyzer Tool)打开dump文件;3)MAT分析步骤:Dominator Tree看占用内存最大的对象→Leak Suspects自动检测泄漏嫌疑→Thread Overview看线程栈和局部变量→OQL查询特定对象;4)重点关注:大对象(单个对象占几十MB)、集合类泄漏(HashMap/ArrayList不断增长不释放)、ClassLoader泄漏(动态代理生成的类未卸载)、ThreadLocal泄漏(线程池中ThreadLocal未remove)。
常见OOM类型及解决:Java heap space→增大堆或优化对象生命周期;Metaspace→增大Metaspace或排查动态生成类;GC overhead limit exceeded→同Java heap space,GC回收效率太低;Direct buffer memory→限制堆外内存-XX:MaxDirectMemorySize。
【大白话解释】: 内存分析就像体检,jstat是验血(看指标),jmap是B超(看详细情况),MAT是X光(深入分析大对象)。OOM后分析就像事故调查,先拍照留证(dump文件),再分析哪根柱子塌了(大对象/泄漏对象)。
【扩展知识讲解】: MAT核心功能详解:1)Dominator Tree:按对象Retained Size(对象及其支配的所有对象的总大小)排序,快速定位最大内存消耗者;2)Leak Suspects:自动分析出可能的内存泄漏点,给出泄漏对象和引用链;3)OQL(Object Query Language):类似SQL查询堆中的对象,如SELECT * FROM java.lang.String WHERE value.length > 1000查找超过1000字符的String;4)GC Roots引用链:从GC Roots到泄漏对象的引用路径,帮助理解为什么对象无法被回收。
线上内存泄漏排查流程:1)发现内存持续增长(监控告警);2)连续dump两次堆(间隔5分钟);3)用MAT对比两次dump,找出新增对象;4)分析新增对象的GC Roots引用链,找到泄漏根因。
【问题】: Java应用的内存持续性增长,但监控显示堆内存没什么变化,可能的原因有哪些?
【答案】: 堆内存不变但总内存增长,问题出在堆外:1)直接内存:NIO的DirectByteBuffer未释放;2)Metaspace:动态生成的类(如CGLIB代理)未卸载;3)线程栈:线程数持续增加未销毁;4)JNI/native内存:native代码的内存泄漏;5)内存映射文件:MappedByteBuffer未关闭。
【大白话解释】: 堆内存没变但总内存涨,就像家里客厅没多东西但整栋楼变重了。肯定是其他房间出了问题:地下室(直接内存)、阁楼(Metaspace)、走廊(线程栈)、邻居家的东西(JNI/native内存)。
【扩展知识讲解】: 排查工具:pmap查看进程内存映射、NativeMemoryTracking(JVM参数-XX:NativeMemoryTracking=summary)跟踪JVM原生内存分配、jcmd
【问题】: 线上CPU Load飙高如何排查?
【答案】: CPU Load和CPU使用率不同:Load高说明排队运行的进程多,不一定是CPU使用率高。排查:1)vmstat 1看r列(运行队列)和b列(阻塞队列);2)如果是CPU使用率高:按CPU飙高排查流程处理;3)如果是IO Wait高:磁盘IO阻塞导致进程排队,用iostat -x 1查看磁盘IO;4)如果是进程数过多:ps -ef统计进程数,排查是否有进程泄漏。
【大白话解释】: CPU Load就像银行排队人数,人多了不一定窗口都在忙,可能很多人等某个慢窗口(IO阻塞)。排查先看是窗口真忙(CPU高)还是等的人多(IO高),或者窗口开太多了(进程数过多)。
【扩展知识讲解】: Load Average三个值分别代表1分钟、5分钟、15分钟的平均负载。健康标准:Load < CPU核数。IO Wait高常见原因:磁盘慢查询、日志写入过猛、swap频繁(用free -h查看)。长期Load高的根因往往是架构问题,需从架构层面优化。
【问题】: 系统上线后,发现某个接口响应很慢,如何定位可能的原因?
【答案】: 分层排查:1)网络层:ping/telnet检查网络延迟和连通性;2)网关层:看网关日志是否有转发延迟;3)应用层:APM链路追踪(SkyWalking/Zipkin)定位慢在哪个方法;4)数据库层:慢查询日志、EXPLAIN分析SQL执行计划;5)外部依赖:第三方接口是否超时。逐层缩小范围找到瓶颈。
【大白话解释】: 接口慢就像送快递慢,得查哪个环节慢:路上堵不堵(网络)、中转站效率如何(网关)、打包慢不慢(应用处理)、找仓库慢不慢(数据库)、第三方卖家发货慢不慢(外部依赖)。链路追踪就是给包裹装GPS,每一步都看得清。
【扩展知识讲解】: 链路追踪原理:每个请求生成唯一traceId,经过每个服务/方法生成spanId,记录耗时。SkyWalking无侵入Java Agent接入,Zipkin需代码埋点。Arthas的trace命令可在线追踪方法调用链耗时:trace com.xxx.Service method -n 5。
【问题】: 横跨十几个分布式服务的慢请求如何排查?
【答案】: 1)链路追踪:SkyWalking/Zipkin/Jaeger通过traceId串联整个调用链,定位哪个服务/方法耗时最长;2)日志关联:所有服务日志输出traceId,ELK中按traceId搜索完整链路日志;3)拓扑分析:从调用拓扑图看是否有异常路径或环路;4)逐步下钻:找到最慢的服务后深入排查其内部瓶颈。
【大白话解释】: 十几个服务的慢请求就像快递经停十几个中转站,哪个站耽误了?给包裹贴条形码(traceId),每个站扫码记录时间,最终看哪站停得最久就是瓶颈。不能瞎猜,得用数据说话。
【扩展知识讲解】: 分布式链路追踪标准:OpenTelemetry(合并了OpenTracing和OpenCensus)。核心概念:Trace(一次完整请求)、Span(一个操作)、Context Propagation(跨服务传递traceId,通常通过HTTP Header)。采样策略:全量采样(性能开销大)、概率采样、自适应采样。
【问题】: 接口变慢了应该如何排查?导致接口变慢的原因有哪些?
【答案】: 排查思路:先确认是持续慢还是偶发慢。持续慢:代码变更(新版本引入慢逻辑)、数据量增长(SQL变慢)、资源泄漏(连接池耗尽);偶发慢:GC停顿、锁竞争、数据库锁等待、网络抖动。常见原因:1)数据库慢查询;2)GC频繁;3)线程池满;4)缓存未命中;5)下游服务超时;6)大对象序列化。
【大白话解释】: 接口变慢像交通变堵,先看是一直堵还是偶尔堵。一直堵可能是新路修了(代码变更)、车变多了(数据量增长)、路口坏了(资源泄漏)。偶尔堵可能是红绿灯太多(GC)、排队等车位(锁竞争)、前面出事故了(下游超时)。
【扩展知识讲解】: 排查工具箱:Arthas(trace/profiler)、SkyWalking(链路追踪)、Grafana+Prometheus(监控指标)、JProfiler(性能分析)。GC日志分析:-Xlog:gc*打印GC日志,GCEasy在线分析。数据库锁等待:SHOW ENGINE INNODB STATUS查看锁信息。
【问题】: 系统每天晚上都会有一小时左右的时间瘫痪,你觉得可能的原因是什么?
【答案】: 定时任务导致:1)大任务执行消耗资源:凌晨跑批处理、数据同步、报表生成等占用大量CPU/IO/数据库连接;2)数据库备份:mysqldump在低峰期执行但锁表导致业务阻塞;3)日志轮转:大日志文件压缩时CPU飙升;4)缓存过期:大量Key同一时间过期导致缓存雪崩。排查:crontab -l看定时任务、查看凌晨时段的监控和日志。
【大白话解释】: 每晚瘫痪像每晚同一时间停电,肯定是有人在干耗电的大活。查查是不是夜班工人在搬货(批处理)、维护设备(数据库备份)、清理垃圾(日志轮转)、统一关灯(缓存同时过期)。看crontab就知道谁在搞鬼。
【扩展知识讲解】: 批处理优化:分批执行、降级非核心逻辑、独立节点执行不影响线上。数据库备份优化:用XtraBackup热备替代mysqldump、从库备份不影响主库。缓存雪崩防护:过期时间加随机值、永不过期+异步刷新。
【问题】: 调用第三方接口应注意哪些问题?
【答案】: 1)超时设置:必须设连接超时和读超时,防止线程阻塞;2)重试策略:幂等接口可重试,非幂等不重试,重试需指数退避;3)熔断降级:连续失败触发熔断,降级返回默认值;4)限流:控制调用频率不超过对方限制;5)异常处理:网络异常、业务异常分别处理;6)日志记录:请求和响应完整记录便于排查;7)幂等设计:确保重复调用不产生副作用。
【大白话解释】: 调用第三方就像委托别人办事,不能傻等(超时),办不成换个方式(重试/降级),对方忙就别一直催(限流),出了岔子要有后手(异常处理),每次委托留个底(日志记录),同一件事别让人家办两遍(幂等)。
【扩展知识讲解】: HTTP客户端选型:Apache HttpClient(功能全)、OkHttp(轻量高效)、Spring RestTemplate/WebClient。熔断器:Resilience4j(轻量推荐)、Sentinel(功能丰富)。调用链路可观测性:traceId透传、指标监控(成功率/耗时/P99)。
【问题】: 新项目要上线了,你会关注哪些指标?
【答案】: 分四类:1)应用指标:QPS、响应时间(P50/P99/P999)、错误率、线程池状态;2)JVM指标:堆内存使用率、GC频率和耗时、线程数、类加载数;3)基础设施:CPU使用率、内存使用率、磁盘IO、网络带宽;4)业务指标:核心业务成功率、订单量、支付成功率。告警阈值:错误率>1%、P99>3s、CPU>80%、内存>85%。
【大白话解释】: 新项目上线就像新车上路,仪表盘要看:车速和转速(QPS/响应时间)、油量和水温(内存/GC)、轮胎和刹车(CPU/磁盘IO)、乘客满意度(业务成功率)。红灯亮了要立刻停车检查。
【扩展知识讲解】: 监控体系:Prometheus采集+Grafana展示+AlertManager告警。日志体系:ELK(Elasticsearch+Logstash+Kibana)。链路追踪:SkyWalking。开源一栈式方案:Spring Boot Admin+Actuator暴露健康检查和指标端点。上线前压测验证容量规划。
【问题】: CDN流量异常暴增,可能原因和解决方案?
【答案】: 原因分析:1)被DDoS攻击:大量恶意请求消耗CDN带宽;2)爬虫抓取:搜索引擎或恶意爬虫高频抓取;3)资源未缓存:CDN回源率高导致流量暴增;4)大文件被频繁下载:如安装包被盗链。解决方案:1)WAF防护:配置CC防护规则;2)防盗链:Referer白名单+签名URL;3)缓存策略:调整缓存过期时间减少回源;4)流量限制:CDN层配置带宽上限;5)离线分析日志定位异常请求源。
【大白话解释】: CDN流量暴增就像水费突然飙升,得查哪里漏水。被人攻击了(DDoS)、有人偷水(盗链)、水龙头关不紧(缓存未命中回源多)、大管子一直流水(大文件频繁下载)。加锁防盗(防盗链)、修水龙头(缓存策略)、装水表监控(流量限制)。
【扩展知识讲解】: CDN监控:流量曲线、回源率、命中率、状态码分布。CloudFlare/阿里云CDN提供实时监控和告警。防盗链签名:URL加时间戳+签名,过期自动失效。CC攻击防护:人机识别(JS Challenge/CAPTCHA)+ 请求频率限制。
分布式架构场景
【问题】: 如何设计电商系统的订单数据同步方案(同步到数仓)?要求数据准确、性能高。
【答案】: 核心方案:Canal监听MySQL binlog实时同步。详细流程:1)Canal伪装为MySQL从节点订阅binlog(MySQL需开启binlog格式为ROW);2)解析binlog获取数据变更事件(INSERT/UPDATE/DELETE),每条变更包含表名、变更类型、变更前后的行数据;3)写入Kafka消息队列缓冲,key为表名+主键值保证同一行数据的变更路由到同一分区(保序);4)Flink/Spark消费Kafka数据写入数仓,Flink用upsert方式写入(主键存在则更新,不存在则插入)。
保证数据准确:全量+增量结合。全量用DataX定期同步做基线(如每天凌晨全量同步一次),增量用Canal实时同步当天变更。如果Canal位点丢失,可以从上次全量同步的时间点重新消费binlog。
保证性能:1)Kafka缓冲削峰:双十一期间写入量暴增,Kafka作为缓冲层防止下游数仓被打崩;2)批量写入数仓:Flink配置checkpoint间隔30秒,每30秒批量写入一次数仓,减少小文件和IO压力;3)Canal并行解析:多Canal实例订阅不同数据库实例。
【大白话解释】: 订单同步到数仓就像把门店销售记录抄到总部账本。Canal像自动抄表员,只要店里出单子(binlog)就自动抄录,通过快递(Kafka)送到总部,文员(Flink)整理入册。全量是每月盘点一次,增量是每天实时抄录。
【扩展知识讲解】: Canal高可用:多个Canal Server竞争成为某个MySQL实例的订阅者(ZooKeeper选举),同一时刻只有一个Canal Server在运行,避免重复消费。主Canal挂了备Canal自动接管。
数据校验:1)定期比对待同步表和数仓的记录数(SELECT COUNT)和关键字段(MD5哈希比对);2)校验不一致时以MySQL为准修复数仓数据;3)Canal位点与MySQL位点差值监控,延迟超过5分钟告警。
数仓选型对比:1)Hive:离线分析,T+1数据,查询慢(分钟级)但存储便宜,适合历史数据分析;2)ClickHouse:实时OLAP,查询快(毫秒级),适合实时报表和大屏;3)Doris:实时+离线一体化,支持高并发点查和批量导入,适合中小规模。数据量<1TB选Doris,>10TB选ClickHouse+Hive组合。
【问题】: 设计一个实时数据同步系统,将MySQL数据实时同步到数据仓库?
【答案】: 架构:MySQL → Canal → Kafka → Flink → 数仓。关键设计:1)变更捕获:Canal监听binlog,INSERT/UPDATE/DELETE事件分别处理;2)顺序保证:同一主键的变更事件路由到Kafka同一分区,保证消费顺序;3)断点续传:记录消费位点(offset),重启后从断点继续;4)幂等写入:数仓层用REPLACE INTO或主键去重;5)监控告警:同步延迟、消费堆积、异常数据告警。
【大白话解释】: 实时同步就像直播,现场发生了什么观众立刻看到。MySQL是现场,Canal是摄像机(拍binlog),Kafka是直播平台,Flink是剪辑师,数仓是观众。每个环节都要快、不能断、不能乱序。
【扩展知识讲解】: Canal vs Debezium:Canal是阿里开源的MySQL binlog解析工具,Debezium是RedHat开源的支持多种数据库的CDC平台。Flink CDC可直接内置Debezium引擎,无需额外部署Canal和Kafka,架构更简单。数据一致性保证:至少一次语义(at-least-once)+ 幂等写入 = 精确一次效果。
【问题】: 使用LIMIT OFFSET分页同步数据时,发现数据丢失了,可能是什么原因?如何解决?
【答案】: 原因:同步过程中新插入或删除数据导致偏移量错位。例如:同步第1页时新插入一条记录,第2页的OFFSET跳过了本应同步的记录;或删除一条记录后,OFFSET指向了重复数据。解决方案:1)游标分页:用WHERE id > 上次最大id替代OFFSET,不受数据变动影响;2)快照隔离:用MVCC在事务中读取一致性快照;3)时间戳字段:记录每次同步的最大更新时间,增量同步。
【大白话解释】: OFFSET分页同步就像排队拍照,拍照时有人插队或离开,后面的人位置全变了,照片可能漏人或重复。解决办法:给每个人编号(主键),每次记住最后拍到谁,从下一个人继续拍,不受插队离开影响。
【扩展知识讲解】: 游标分页的局限:不支持跳页和总数统计,但对数据同步场景足够。MVCC快照:START TRANSACTION WITH CONSISTENT SNAPSHOT开启一致性读视图,整个事务中看到的数据不变。分页同步最佳实践:按主键范围分片(如id 1-10000, 10001-20000),每片独立同步。
【问题】: 数据同步过程中出现OOM内存溢出,如何排查和解决?
【答案】: 排查:1)定位OOM位置:heap dump分析大对象;2)常见原因:一次性加载过多数据到内存、大对象未及时释放、内存泄漏。解决:1)流式处理:逐条或小批量读取处理,不一次性加载;2)限制批次大小:每批1000条处理完释放;3)增加内存:临时增大JVM堆内存;4)对象复用:避免频繁创建大对象;5)Off-Heap:使用堆外内存处理大数据。
【大白话解释】: 同步OOM就像搬家车太小装不下,把所有家具一次往上搬(全量加载)就超载了。改成一件件搬(流式处理),或一批批搬(分批处理),或换辆大车(增大内存)。不是车的问题就检查是不是有东西忘卸了(内存泄漏)。
【扩展知识讲解】: 流式处理框架:Flink天然支持流式处理,背压机制防止数据积压OOM。Spark需配置maxRecordsPerBatch控制批次大小。JVM调优:-XX:+UseG1GC减少大对象对老年代的冲击、-XX:MaxDirectMemorySize限制堆外内存。监控:Prometheus+Grafana监控JVM内存使用趋势。
【问题】: 双十一期间订单量暴增,实时数据同步系统出现消息堆积,如何应对?
【答案】: 1)消费者扩容:增加消费者实例数量,Kafka通过rebalance自动分配分区;2)临时队列:堆积严重时创建临时Topic分流,后续慢慢消费;3)跳过非关键数据:只同步核心字段或核心表,非核心数据延迟同步;4)批量写入:提高单条消费的写入效率;5)限流生产端:Canal侧限流降低采集速率,保同步不崩溃比全量同步更重要。
【大白话解释】: 消息堆积就像快递爆仓,包裹堆成山。多雇几个快递员(消费者扩容),大件先不管只送小件(跳过非关键数据),把包裹分到多个仓库(临时队列分流),加快送货速度(批量写入),实在送不过来就慢点收件(限流生产端)。
【扩展知识讲解】: Kafka消费者扩容限制:消费者数不能超过分区数,否则多余消费者空闲。分区预规划:大促前按预估流量提前增加分区。背压机制:Flink的背压自动调节消费速率,但可能导致Canal位点延迟增大。堆积监控:Kafka Lag指标,Lag > 10万需告警。
【问题】: 数据同步任务执行到一半失败了,如何保证数据的一致性?
【答案】: 1)断点续传:记录同步进度(如最后同步的ID或binlog位点),重启后从断点继续;2)幂等写入:目标端支持幂等(REPLACE INTO或主键冲突更新),重复执行不产生错误数据;3)事务保证:每批数据在事务中写入,失败则整批回滚;4)补偿机制:定时校验源端和目标端数据一致性,发现差异自动补偿。
【大白话解释】: 同步失败就像搬家搬了一半下大雨,需要记住搬到哪里了(断点续传),搬过的东西再搬一次不会多出来(幂等写入),每箱东西要么全搬要么不搬(事务保证),最后清点一遍发现漏的补上(补偿机制)。
【扩展知识讲解】: 断点存储:binlog位点存ZooKeeper或数据库,每次成功写入后更新。幂等设计:根据业务主键做UPSERT(存在则更新不存在则插入)。数据校验:定期COUNT对比、关键字段HASH对比。最终一致性:允许短暂不一致,通过补偿机制保证最终一致。
【问题】: 让你设计一个API网关,需要考虑哪些核心功能?如何实现动态路由和限流?
【答案】: 核心功能:1)路由转发:根据请求路径/头信息路由到后端服务;2)负载均衡:多个后端实例轮询/加权;3)限流:按IP/用户/接口维度限流;4)鉴权:统一认证和权限校验;5)熔断降级:下游故障时返回默认值;6)日志监控:请求日志和指标采集。动态路由:路由规则存Nacos/Redis,网关监听配置变更热更新,无需重启。限流:Redis+Lua脚本实现分布式令牌桶。
【大白话解释】: API网关就像公司前台,来客先登记(鉴权),按部门指引方向(路由),人多时排队等(限流),某个部门下班了就告诉来访者(熔断降级)。路由规则像公司通讯录,有人换办公室前台马上更新(动态路由)。
【扩展知识讲解】: 开源网关:Spring Cloud Gateway(Java生态)、Kong(OpenResty/Lua)、APISIX(Apache,OpenResty)。动态路由实现:Nacos配置中心推送变更→网关订阅变更→内存路由表热更新。限流算法:令牌桶(允许突发)、漏桶(匀速)、滑动窗口(精确计数)。生产级网关还需考虑:灰度发布、协议转换、请求/响应改写。
【问题】: API网关如何实现接口版本管理?同时支持v1、v2、v3多个版本如何设计?
【答案】: 三种方式:1)URL路径版本:/api/v1/user、/api/v2/user,网关按路径路由到不同服务版本;2)Header版本:请求头加Api-Version: v2,网关按Header路由;3)参数版本:/api/user?version=v2。推荐URL路径方式,直观且利于缓存。网关层:路由表维护版本→服务映射,新版本上线时新增路由规则,旧版本设置过期时间。
【大白话解释】: 接口版本管理就像软件更新,v1是旧版v2是新版,两个版本同时运行。最简单的方式:不同版本走不同URL(/v1/xxx、/v2/xxx),就像Windows和Mac版软件分开发布。旧版给个过期时间,到期下线。
【扩展知识讲解】: 版本共存策略:蓝绿部署(新旧版本并行)、灰度发布(新版本先放小流量)。版本下线流程:标记deprecated → 文档通知 → 监控调用量 → 调用量为0后下线。网关层可做版本路由权重:90%流量走v1、10%走v2,逐步切换。
【问题】: 网关层如何防止接口重放攻击?
【答案】: 防重放三要素:1)timestamp:请求中带时间戳,网关校验与服务器时间差不超过5分钟;2)nonce:每次请求带唯一随机数,网关用Redis SETNX记录nonce,5分钟内重复nonce拒绝;3)签名:对请求参数+timestamp+nonce做HMAC签名,防篡改和伪造。三者结合:timestamp防长期重放、nonce防短期重放、签名防伪造。
【大白话解释】: 重放攻击就像有人拿你的旧车票重复坐车。防重放:车票加日期(timestamp过期作废)、每张票唯一编号(nonce用过的作废)、车票有防伪标记(签名防伪造)。三个加起来就万无一失了。
【扩展知识讲解】: nonce存储优化:用Bloom Filter替代Redis SETNX节省内存(允许极小误判率)。签名算法选择:HMAC-SHA256安全且性能好,RSA签名更安全但性能差。HTTPS本身防窃听但不防重放,应用层签名+时间戳+nonce是必要补充。
【问题】: 微服务架构中,如何设计一个配置中心?配置变更后如何实时通知各个服务?
【答案】: 配置中心核心功能:配置存储、版本管理、灰度发布、实时推送。架构:1)存储层:MySQL存储配置数据,Redis缓存热点配置;2)长连接推送:服务端与客户端维持长轮询或gRPC流,配置变更时主动推送;3)客户端缓存:本地缓存配置,配置中心不可用时用缓存兜底。Nacos的推拉结合:变更时UDP推送通知,客户端收到后HTTP拉取最新配置。
【大白话解释】: 配置中心就像公司公告栏,统一发布通知所有人。关键是怎么让大家第一时间看到:广播通知(推送),或大家定时来看(轮询)。Nacos的做法:贴公告时发微信通知(推送),员工收到后去公告栏看详情(拉取)。公告栏看不了时用手机截图的缓存(本地缓存)。
【扩展知识讲解】: Nacos配置中心原理:客户端与服务器建立长轮询(Long Polling),默认30秒超时。服务器在超时前如果配置变更,立即返回响应;否则等超时返回空。这种方式兼顾实时性和性能。Apollo配置中心用长轮询+Release Message机制。Spring Cloud Config需配合Spring Cloud Bus(MQ推送)实现实时更新。
【问题】: 如果配置中心挂了,各个微服务应该如何保证可用性?
【答案】: 1)本地缓存兜底:客户端启动时从配置中心拉取配置缓存到本地文件,配置中心不可用时读本地缓存;2)容灾降级:只使用已缓存的配置,不拉取新配置;3)高可用部署:配置中心集群部署,至少3个节点+数据库主从;4)只读模式:数据库可用但配置中心不可用时,服务直接读数据库获取配置;5)配置预检:启动时检查配置是否齐全,缺少关键配置拒绝启动。
【大白话解释】: 配置中心挂了就像公告栏倒了,但员工手机里有之前拍的照片(本地缓存),照常干活。只要之前同步过配置,短期故障不影响。关键是平时做好”拍照备份”(本地缓存),不能100%依赖公告栏。
【扩展知识讲解】: Nacos容灾机制:客户端本地缓存目录${user.home}/nacos/config/,包含配置快照和failover文件。failover文件优先级最高,可用于紧急修复配置。Apollo的容灾:Config Service无状态可水平扩展,Admin Service和Portal挂了只影响配置发布不影响服务运行。
【问题】: 如何设计一个灰度发布系统?如何控制流量逐步切换到新版本?
【答案】: 灰度发布核心:按规则将部分流量路由到新版本。详细设计: 1)流量标记:网关层给请求打标签,标签来源可以是用户ID(如尾号为1的走新版本)、城市(如北京用户走新版本)、设备类型(如iOS走新版本)、Cookie中的灰度标识(如gray=true); 2)路由规则:根据标签将指定比例的流量路由到新版本服务。网关层维护路由表:serviceA→v1(weight=90) + v2(weight=10),请求到达时按权重随机选择版本; 3)规则引擎:灰度规则存Nacos配置中心,格式如{service:”order”, rule:{type:”PERCENT”, value:10}},支持热更新无需重启网关; 4)监控对比:新旧版本的核心指标(错误率/P99响应时间/QPS)实时对比,新版本错误率>1%或P99>3s自动告警; 5)回滚机制:发现问题一键回滚——将灰度规则改为新版本weight=0,流量全部回到旧版本,秒级生效。
流量切换方式:按百分比逐步切换(5%→10%→50%→100%),每步观察15-30分钟无异常再继续。或按用户特征(内部员工→白名单用户→活跃用户→全量用户),逐步扩大范围。
【大白话解释】: 灰度发布就像新菜试卖,先让10%的顾客尝(5%流量),没人投诉再增加到50%,全部没问题就全面上新。出问题立刻换回老菜(回滚)。网关是服务员,按规则把客人领到新/老餐桌。
【扩展知识讲解】: 灰度发布策略:金丝雀发布(最简单,1台新版本验证)、蓝绿部署(新旧两套环境切换)、A/B测试(按用户特征分两组对比)。Istio Service Mesh支持基于权重和请求特征的精细流量管理。Spring Cloud Gateway+Nacos配置灰度规则可实现轻量级灰度。
【问题】: 灰度发布过程中,如何保证新旧版本数据的一致性?如果新版本有数据库结构变更怎么办?
【答案】: 数据库兼容性原则:新版本代码必须兼容旧数据库结构。策略:1)先扩后缩:先加新字段/新表(兼容旧代码),全量发布后再删旧字段;2)双向兼容:新旧版本都能读写新旧字段;3)数据迁移:灰度期间新版本写新字段,后台任务异步迁移旧数据到新字段;4)验证完毕后:全量切到新版本,下线旧代码,最后删旧字段。
【大白话解释】: 灰度期间数据库变更就像公路拓宽,新车道先修好(加新字段),新旧车道都能跑(双向兼容),车子慢慢转到新车道(灰度切换),最后旧车道拆掉(删旧字段)。千万别还没修新车道就把旧车道拆了(删旧字段旧代码就崩了)。
【扩展知识讲解】: 数据库变更12条原则:1)只加不删(先加新字段,旧字段暂保留);2)新字段设默认值;3)改字段用新字段替代;4)禁用RENAME COLUMN;5)禁用修改字段类型。工具:Flyway/Liquibase管理数据库版本变更。灰度期间的回滚方案:旧版本代码回滚即可,因为旧数据库结构还在。
【问题】: 用户下单后如何保证订单、库存、支付三个服务的数据一致性?请设计一个分布式事务方案。
【答案】: 推荐Saga模式+本地消息表组合方案:
主流程:1)下单服务创建订单(状态:待确认),同时写本地消息表记录”待扣库存”;2)库存服务消费消息扣减库存(正向操作),扣减成功写本地消息表记录”待扣款”,失败则执行补偿(订单状态改为已取消,释放库存预扣);3)支付服务消费消息发起支付(正向操作),支付成功写本地消息表记录”待确认订单”,失败则执行补偿(回加库存,订单状态改为已取消);4)订单服务消费确认消息,更新订单状态为已确认。
任何一步失败,按反序执行已完成步骤的补偿操作。用MQ编排事务流程,每个服务消费消息执行操作并发布下一条消息。每个服务通过本地消息表保证自身操作与消息发送的原子性。
为什么不用TCC:电商场景业务流程长、参与方多(可能还有积分服务、优惠券服务),TCC需每个服务写Try/Confirm/Cancel三个接口开发量大,且资源冻结(冻结库存不卖给别人)影响用户体验。Saga无资源预留阶段,直接执行+补偿,更实用。
【大白话解释】: 分布式事务就像组团旅行,订酒店、买机票、租车三件事要么全成要么全取消。Saga模式:先订酒店→成功→买机票→成功→租车→失败了→退机票→取消酒店,反序撤回已完成的操作。不要求同时完成,但最终要么全成要么全退。
【扩展知识讲解】: Saga vs TCC深度对比:1)一致性:TCC强一致(Try成功则Confirm一定成功),Saga最终一致(补偿可能有短暂不一致窗口);2)性能:TCC需冻结资源(如冻结库存),Saga直接操作资源,Saga吞吐更高;3)开发量:TCC每个服务写3个接口(Try/Confirm/Cancel),Saga写2个(正向+补偿),开发量少1/3;4)适用场景:TCC适合强一致性要求高、资源争抢激烈的场景(如转账、秒杀),Saga适合业务流程长、参与方多的场景(如订单创建、旅行预订)。
Seata框架支持AT、TCC、Saga、XA四种模式。AT模式最常用:一阶段拦截SQL生成回滚日志(undo_log表),二阶段自动提交/回滚。但对SQL有限制(不支持多表关联更新、不支持存储过程)。实际项目常用本地消息表+最终一致性替代完整的分布式事务框架——简单、可靠、不依赖额外中间件。
【问题】: 分布式缓存中,如何实现一致性Hash算法?相比普通Hash解决了什么问题?
【答案】: 一致性Hash将整个Hash空间组织成虚拟的圆环(0~2^32-1),每个节点和Key都Hash到环上,Key顺时针找最近的节点。
普通Hash的问题:节点增减时几乎所有Key的映射都会变化。比如mod N变mod N+1,当N从4变成5时,约80%的Key需要迁移到不同节点,导致大面积缓存失效。一致性Hash只需迁移受影响节点的相邻区间Key,不影响其他节点,数据迁移量约1/N。
虚拟节点:每个物理节点对应多个虚拟节点(如150个),解决数据倾斜问题。没有虚拟节点时,如果只有3个物理节点,Hash分布可能极不均匀(某个节点承担50%+的Key)。加入虚拟节点后,虚拟节点均匀分布在环上,物理节点的虚拟节点承担的Key数量趋于均衡。虚拟节点命名:node1#1, node1#2, …, node1#150, node2#1, …
Java实现:TreeMap<Integer, String> hashRing,put时hash(nodeName+i)定位环上位置,get时tailMap(hash(key))找顺时针第一个节点。
【大白话解释】: 普通Hash就像按编号分班,加一个班所有人重新分。一致性Hash像时钟,每个人站在钟面上(Hash到环上),顺时针找最近的老师(节点)。加一个老师只影响他前面那一小段学生,其他人不动。虚拟节点就像一个老师占好几个位置,让学生更均匀分布。
【扩展知识讲解】: 一致性Hash在Redis Cluster中的应用:Redis Cluster没有用传统一致性Hash环,而是用Hash Slot(16384个槽)实现类似效果。每个节点负责一部分槽位(如3节点各负责约5461个槽),Key的槽位=CRC16(key) % 16384。节点增减时只需迁移对应槽位的数据,不需要全量迁移。
一致性Hash vs Hash Slot:1)一致性Hash环:虚拟节点数不固定,数据分布依赖Hash函数均匀性,可能出现数据倾斜;2)Hash Slot:固定16384个槽位,手动或自动分配槽位给节点,更易控制数据分布均衡。新增节点时只需迁移部分槽位,操作更可控。
Ketama算法是一致性Hash的经典实现,被memcached客户端广泛使用。核心优化:虚拟节点Hash值用MD5算法生成,分布更均匀。Nginx的一致性Hash模块也基于Ketama算法。