Java 热门面试题200道之分布式相关(13 题)
(1)什么情况下需要使用分布式事务,有哪些方案?
一般在跨多个数据库、或者不同服务的情况下需要用到分布式事务,比如订单服务和库存服务,下订单和扣库存属于不同服务的方法,因此本地事务无法保证一致性,需要引入分布式服务。
分布式事务是由多个本地事务组成的,分布式事务跨越了多设备,之间又经历了复杂的网络,可想而知想要实现严格的事务道路阻且长。
我们就先来看看常见的分布式事务方案:2PC、3PC、TCC、本地消息表、可靠消息服务(如MQ事务)。
2PC
2PC,Two-phase commit protocol,即两阶段提交协议。 它引入了一个事务协调者角色,来管理各个参与者(就是各数据库资源)。
整体分为两个阶段,分别是准备阶段和提交/回滚阶段。
我们先来看看第一个阶段,即准备阶段。

由事务协调者给每个参与者发送准备命令,每个参与者收到命令之后会执行相关事务操作,你可以认为除了事务的提交啥都做了。
然后每个参与者会返回响应告知协调者自己是否准备成功。
协调者收到每个参与者的响应之后就进入第二阶段,根据收集的响应,如果有一个参与者响应准备失败那么就向所有参与者发送回滚命令,反之发送提交命令。

这个协议其实很符合正常的思维,就像我们大学上课点名的时候,其实老师就是协调者的角色,我们都是参与者。
老师一个一个的点名,我们一个一个的喊到,最后老师收到所有同学的到之后就开始了今天的讲课。
而和点名有所不同的是,老师发现某几个学生不在还是能继续上课,而我们的事务可不允许这样。
事务协调者在第一阶段未收到个别参与者的响应,则等待一定时间就会认为事务失败,会发送回滚命令,所以在 2PC 中事务协调者有超时机制。
2PC 的优点是能利用数据库自身的功能进行本地事务的提交和回滚,也就是说提交和回滚实际操作不需要我们实现,不侵入业务逻辑由数据库完成,在之后讲解 TCC 之后相信大家对这点会有所体会。
2PC 主要有三大缺点:同步阻塞、单点故障和数据不一致问题。
同步阻塞
可以看到在第一阶段执行了准备命令后,我们每个本地资源都处于锁定状态,因为除了事务的提交之外啥都做了。
所以这时候如果本地的其他请求要访问同一个资源,比如要修改商品表 id 等于 100 的那条数据,那么此时是被阻塞住的,必须等待前面事务的完结,收到提交/回滚命令执行完释放资源后,这个请求才能得以继续。
所以假设这个分布式事务涉及到很多参与者,然后有些参与者处理又特别复杂,特别慢,那么那些处理快的节点也得等着,所以说效率有点低。
单点故障
可以看到这个单点就是协调者,如果协调者挂了整个事务就执行不下去了。
如果协调者在发送准备命令前挂了还行,毕竟每个资源都还未执行命令,那么资源是没被锁定的。
可怕的是在发送完准备命令之后挂了,这时候每个本地资源都执行完处于锁定状态了,都杵着了,这就很僵硬了,如果是某个热点资源都阻塞了,这估计就要完蛋了。
数据不一致问题
因为协调者和参与者之间的交流是经过网络的,而网络有时候就会抽风的或者发生局部网络异常。
那么就有可能导致某些参与者无法收到协调者的请求,而某些收到了。比如是提交请求,然后那些收到命令的参与者就提交事务了,此时就产生了数据不一致的问题。
小结一下 2PC
2PC ,它是一个同步阻塞的强一致性两阶段提交协议,分别是准备阶段和提交/回滚阶段。
2PC 的优势在于对业务没有侵入,可以利用数据库自身机制来进行事务的提交和回滚。
它的缺点:是一个同步阻塞协议,会导致高延迟和性能的下降,并且存在协调者单点故障问题,极端情况下会有数据不一致的问题。
当然这只是协议,具体的落地还是可以变通了,比如协调者单点问题,我就搞个主从来实现协调者,对吧。
XA(eXtended Architecture)
XA 是一个由 X/Open 和 Open Group 提出的标准,用于在分布式事务环境中管理多个资源管理器(如数据库或消息队列)的事务性操作。XA 提供了一种协议,使得这些资源管理器能够参与到一个全局事务中,并保证事务的 ACID 特性(原子性、一致性、隔离性和持久性)。
主要特点和机制:
- 两阶段提交(2PC):XA 协议的核心机制是两阶段提交。在两阶段提交中,事务协调者(Transaction Coordinator)协调多个参与者(Participants)的资源管理器,确保所有参与者要么都提交事务,要么都回滚事务,从而保证事务的一致性。
- 全局事务管理:XA 提供了一个全局事务管理的框架,允许应用程序在多个不同的数据库或资源上执行操作,并以全局的方式进行事务管理。
适用场景:
- 需要跨多个数据库或资源的事务操作,例如在订单和库存之间保持一致性。
- 需要严格的 ACID 特性和数据一致性保证。
优点:
- 提供了强一致性和可靠性的事务处理能力。
- 标准化的接口和协议,方便使用和实现。
缺点:
- 性能损耗较大,特别是在分布式环境下网络延迟较高时。
- 存在单点故障问题,事务协调者故障会导致整个系统的事务受影响。
TCC
TCC 是一种基于补偿事务的分布式事务解决方案,主要用于解决分布式系统中的数据一致性问题。TCC 的核心思想是将事务分解为三个阶段:尝试(Try)、确认(Confirm)和取消(Cancel),每个阶段对应一个操作。
主要特点和机制:
- 三阶段处理:TCC 事务通过 Try、Confirm 和 Cancel 三个阶段来实现:
- Try:预留必须的资源,执行业务检查。
- Confirm:确认执行,提交事务,释放资源。
- Cancel:取消操作,释放预留的资源,回滚事务。
- 补偿机制:TCC 通过补偿操作来保证最终一致性,即使在部分参与者失败的情况下也可以进行处理。
适用场景:
- 需要高并发和低延迟的分布式系统。
- 需要较大的灵活性和容错能力,例如电商交易系统中的订单操作和库存扣减。
- 业务逻辑相对复杂,不适合使用传统的两阶段提交协议。
优点:
- 高并发和低延迟,适合大规模分布式系统。
- 弹性和灵活性高,能够处理各种复杂的业务场景。
缺点:
- 实现和维护成本较高,需要额外的补偿逻辑来保证最终一致性。
- 对业务代码有一定要求,需要开发者显式地定义 Try、Confirm 和 Cancel 三个操作。
区别和选择:
- 一致性级别:
- XA 提供了强一致性,适合需要严格 ACID 特性的场景。
- TCC 提供了最终一致性,适合需要高并发和灵活性的场景。
- 适用场景:
- 如果应用需要确保强一致性和数据的原子性操作,可以选择 XA。
- 如果应用可以容忍最终一致性,并且需要高并发和灵活的事务处理能力,可以选择 TCC。
- 实现复杂度:
- XA 的实现相对较复杂,需要使用两阶段提交协议,存在性能损耗和单点故障风险。
- TCC 的实现相对灵活,但需要开发者实现补偿逻辑来保证最终一致性。
本地消息:将需要执行的操作写到本地消息表中,并由一个独立的进程异步地去处理这些消息,适用于需要最终一致性的场景,例如异步扣库存。
可靠消息服务(MQ 事务):利用消息队列来实现分布式事务,如RocketMQ的事务消息,在发送消息和执行本地事务时保证一致性。
(2)如何设计一个秒杀功能?
针对高并发秒杀场景,需要考虑瞬时流量承接、防止超卖、预防黑产、避免对正常服务影响以及兜底方案等问题。在面试时,可以先点出这些关键挑战,再从前端和后端两个角度阐述解决方案。前端侧重于利用CDN缓存静态资源、优化前端性能(如预加载、骨架屏)和实施前端限流策略,以应对大量用户的访问。后端则需要设计高效的接口、使用缓存削峰填谷、采用分布式锁防止超卖、区分黑产流量并结合熔断和降级机制来保障服务可用性。
核心问题点
在开始设计前,应先与面试官明确需要解决的关键问题:
- 瞬时流量的承接: 如何在高并发下,合理分散和吸收短时间内涌入的大量用户请求。
- 防止超卖: 如何确保在商品数量有限的情况下,不被超量购买。
- 预防黑产: 如何识别和阻止恶意爬虫、刷单等黑产行为。
- 避免对正常服务的影响: 如何隔离秒杀场景的压力,防止其影响到其他正常业务。
- 兜底方案: 如何在主流程出现问题时,有一套可以快速执行的备用方案来保证业务基本可用。
前端视角
- 利用CDN缓存静态资源: 将秒杀页面的 HTML、CSS、JavaScript 等静态资源部署在CDN上,加速访问,减少对源站的压力。
- 前端性能优化:
- 预加载和骨架屏: 提前加载关键资源,提高用户体验,使用骨架屏让用户感知到页面正在加载。
- 优化JS和CSS: 减少不必要的请求和资源,进行压缩和代码合并。
- 前端限流: 客户端进行简单的流量控制,例如通过点击按钮后的防抖、节流操作,防止用户短时间内重复提交请求。
- 异步数据请求: 秒杀商品信息等动态数据通过异步接口请求,避免阻塞页面加载。
后端视角
- 削峰填谷与缓存:
- 使用分布式缓存: 在内存中缓存秒杀商品信息,提高读取效率,减少数据库压力。
- 异步消息队列: 将用户请求放入消息队列,异步处理,从而削峰填谷。
- 商品接口优化与安全:
- 商品详情页优化: 秒杀接口只返回秒杀所需的核心信息,减少数据量。
- 接口防刷: 通过验签、IP限制、设备指纹等技术,识别和过滤掉黑产流量。
- 超卖的解决方案:
- 分布式锁: 在进行库存扣减前,使用分布式锁(如Redis的SETNX命令或ZooKeeper的分布式锁)来保证同一时刻只有一个请求能修改库存。
- 预减库存: 可以在请求进入时先在Redis中预减库存,成功后才去数据库扣减,这样可以大幅减少对数据库的压力。
- 流量隔离与熔断降级:
- 服务隔离: 采用微服务架构,将秒杀服务独立部署,防止其他服务的故障影响秒杀。
- 熔断和限流: 在后端对秒杀接口进行严格的限流,当流量过大或服务不稳定时,及时进行熔断,保护核心服务。
- 兜底方案与数据一致性:
- 最终一致性: 结合事务和消息队列,在秒杀成功后生成订单,然后异步通知库存系统,处理少量不一致的情况,确保商品最终不超卖。
- 补偿机制: 在秒杀结束后,对订单和库存进行核对,对不一致的数据进行补偿,保证数据最终准确。
(3)让你设计一个分布式 ID 发号器,怎么设计?
设计分布式ID发号器,核心目标是在多节点(多服务器、多应用实例)的分布式系统中,生成全局唯一、符合业务需求的ID。作为Java程序员,我们可以从“解决什么问题”和“怎么实现”两个角度,用通俗的方式理解这件事。
先明确:分布式ID需要满足哪些基本要求?
- 唯一性:这是底线,任何两个节点生成的ID绝对不能重复。
- 有序性:部分场景需要ID能体现顺序(比如数据库分表分库时,按ID排序),至少要“趋势递增”(整体越来越大)。
- 高性能:生成ID的速度要快,不能成为系统瓶颈(比如每秒生成10万+ID也不卡顿)。
- 高可用:发号器本身不能单点故障,否则整个系统可能卡壳。
- 可理解性(可选):ID中最好能包含一些业务信息(比如时间、来源节点),方便排查问题。
常见的分布式ID方案,各自的思路和优缺点
1. 最“简单粗暴”的方案:UUID/GUID
- 思路:用算法生成一个32位的字符串(比如550e8400-e29b-41d4-a716-446655440000),本质是通过“随机+网卡MAC/时间戳”保证唯一性。
- 优点:本地生成,不依赖任何外部系统,性能极高,永不重复(理论上)。
- 缺点:无序(字符串乱序)、太长(32位),不适合作为数据库主键(会导致索引效率低),也无法体现业务含义。
- 适用场景:只需要唯一标识,不关心顺序和长度的场景(比如日志ID、临时会话ID)。
2. 基于数据库的“改进版自增ID”
- 思路:利用数据库的自增主键(AUTO_INCREMENT),但单库单表会有瓶颈,所以扩展为“多库多表”:
比如搞3个数据库,每个库的自增ID起始值分别为1、2、3,步长都是3(即库1生成1,4,7…;库2生成2,5,8…;库3生成3,6,9…)。
应用需要ID时,随机选一个库取ID,保证全局唯一。 - 优点:实现简单(复用数据库特性),ID是数字且有序。
- 缺点:依赖数据库,性能受限于数据库(单库每秒最多几千ID);扩展麻烦(步长固定后,想加新库要重新调整所有库的规则)。
- 适用场景:并发量不高(每秒几千以内),且需要数字有序ID的场景。
3. 基于Redis的自增ID
- 思路:利用Redis的INCR命令(原子自增),比如让Redis维护一个计数器,每次生成ID就调用INCR key,返回的结果就是新ID。
- 优点:性能比数据库高(Redis单实例每秒几万ID没问题),ID是数字且严格递增,支持分布式。
- 缺点:依赖Redis(如果Redis挂了,发号器就瘫了);需要考虑Redis持久化(否则重启后计数器归零会重复);Redis集群时要保证“单key自增”(比如用主从+哨兵,避免分片导致的key分散)。
- 适用场景:并发量中等,能接受依赖Redis,且需要严格递增ID的场景。
4. 最经典的方案:雪花算法(Snowflake)
- 思路:把ID设计成一个64位的Long型数字(Java中Long正好64位),结构拆分如下:
- 前41位:时间戳(记录从某个固定时间点到现在的毫秒数)—— 保证“趋势递增”。
- 中间10位:机器ID(最多支持1024个节点)—— 区分不同服务器,避免重复。
- 最后12位:序列号(0-4095)—— 同一毫秒内,一个节点最多生成4096个ID,解决并发问题。
- 优点:本地生成(不依赖外部系统),性能极高(每秒百万级);ID是数字且趋势递增;包含时间信息,方便追溯。
- 缺点:需要提前分配“机器ID”(避免冲突,可手动配置或用ZooKeeper自动分配);依赖系统时钟,如果时钟回拨(比如服务器同步时间时突然倒退),会生成重复ID(需要特殊处理,比如等待时钟追上、跳过回拨时段)。
- 适用场景:高并发(每秒10万+)、需要本地生成ID、能接受轻微时钟问题的场景(几乎所有中大型分布式系统都在用,比如Twitter、美团、阿里)。
5. 开源方案:美团Leaf、百度UidGenerator
这些是在雪花算法基础上的优化版,解决了雪花算法的一些痛点:
- 美团Leaf:支持“号段模式”和“雪花模式”。号段模式是从数据库批量取一段ID(比如一次取1000个),本地缓存用完再取,减少数据库压力,适合超高并发。
- 百度UidGenerator:优化了机器ID的分配(用ZooKeeper自动生成),并解决了时钟回拨问题(通过记录最后一次生成ID的时间,回拨时直接报错或等待)。
总结:怎么选?
- 简单场景(低并发、无序可接受):用UUID。
- 中等并发、需有序数字ID:用Redis的INCR。
- 高并发、本地生成、趋势有序:用雪花算法或其开源优化版(Leaf、UidGenerator)。
核心原则是:满足业务需求的前提下,越简单越好。作为Java程序员,雪花算法及其衍生方案是最值得掌握的,因为它平衡了性能、可用性和有序性,且能天然适配Java的Long类型。
(4)让你设计一个短链系统,怎么设计?
设计短链系统,核心是解决“长网址太长不好传播,把它变成短网址,用户点短网址能跳转到原长网址”的问题。就像给长网址办了个“缩写身份证”,既好记又好传播。作为Java程序员,我们可以从“怎么把长链变断链”“怎么存映射关系”“怎么让跳转更快”三个核心环节来拆解。
第一步:长链怎么变成短链?核心是生成“唯一短码”
短链的格式通常是“域名+短码”(比如https://t.cn/abc123),其中“abc123”就是关键的短码(一般6-8个字符)。生成短码的核心要求是:唯一不重复(否则两个长链会指向同一个短链)、足够短(太长就失去意义了)。
常用的生成方式有两种:
- 方式一:自增ID转短码(推荐)
思路像“编号缩写”:给每个长链分配一个唯一的数字ID(比如1、2、3…),再把这个数字转成“62进制”(0-9、a-z、A-Z共62个字符)。比如数字100转成62进制是“1C”,数字123456转成62进制可能是“abc12”。
这样做的好处是:ID唯一则短码必唯一(不会冲突),且短码长度可控(6位62进制能表示约560亿个不同短码,完全够用)。
数字ID可以用数据库自增主键,或分布式ID生成器(如雪花算法),确保分布式系统下ID不重复。 - 方式二:哈希算法取片段(需处理冲突)
对长链做哈希计算(比如MD5、SHA-1),得到一个很长的字符串(比如MD5生成32位),然后取前6位作为短码。
但哈希可能“撞车”(两个不同长链算出同一短码),所以必须检查:生成短码后,先查数据库有没有这个短码,有就换几位再取(比如取7-12位),直到拿到唯一短码。这种方式稍复杂,不如第一种常用。
第二步:短码和长链的关系怎么存?要“查得快”
生成短码后,必须存下“短码→长链”的映射关系,否则用户访问短码时,系统不知道该跳转到哪里。存储设计要兼顾“容量”和“查询速度”:
- 核心存储:数据库(比如MySQL)
建一张表(比如short_url),至少包含这些字段:- 短码(short_code):主键或唯一索引(确保查得快);
- 长链(long_url):存原网址;
- 过期时间(expire_time):可选,支持“这个短链7天后失效”;
- 生成时间、访问次数等(用于统计)。
- 加速查询:Redis缓存
用户访问短码时,系统需要快速查到长链。如果每次都查数据库,高并发下会很慢。可以把“短码→长链”的映射缓存到Redis(Key是短码,Value是长链),过期时间和数据库一致。
访问流程就变成:查Redis→有就直接用;没有就查数据库,查到后同步到Redis,再用。这样90%以上的请求都能从缓存走,速度极快。
第三步:用户点短链后,怎么跳转到长链?要“跳得快”
用户点击https://t.cn/abc123后,系统的处理流程要简单高效:
- 解析短码:从URL里提取出短码“abc123”;
- 查询长链:按上面说的“先查Redis,再查数据库”的逻辑,拿到对应的长链(比如https://www.xxx.com/very/long/url/…);
- 跳转:用HTTP的302重定向(或301永久重定向,根据业务选),把用户浏览器导向长链。
整个过程要尽量“轻量”:跳转服务不用复杂业务逻辑,拿到短码查缓存/数据库,直接返回重定向响应,确保毫秒级完成。
进阶功能:让短链系统更实用
- 过期控制:生成短链时可以设置过期时间(比如“24小时有效”),跳转时检查如果已过期,返回“链接已失效”。
- 访问统计:每次跳转时,记录访问时间、IP、设备等信息,供用户查看“这个短链被点了多少次,来自哪些地区”。统计数据可以用消息队列(如RabbitMQ)异步写入数据库,避免影响跳转速度。
- 防滥用:限制同一IP生成短链的频率(比如1分钟最多生成5个),过滤恶意长链(比如用黑白名单,或调用第三方接口检测是否为钓鱼网址)。
- 自定义短码:允许用户自己指定短码(比如https://t.cn/mylove),前提是这个短码没被占用(查数据库/Redis确认)。
高并发场景的优化
如果短链访问量极大(比如热门活动的短链,每秒几万次访问):
- 跳转服务集群部署,用Nginx做负载均衡;
- Redis做集群(主从+哨兵),避免缓存单点故障;
- 数据库分库分表(按短码哈希分片),应对海量映射数据;
- 静态资源CDN加速,短链域名的DNS解析和跳转服务入口都用CDN,减少网络延迟。
总结:短链系统就像“网址中转站”
- 长链→短码:给每个长链发一个唯一的“缩写身份证”(自增ID转62进制最可靠);
- 存映射:用数据库记“身份证号→原地址”,Redis当“快速查询窗口”;
- 跳转:用户凭“身份证号”来中转站,中转站快速查到原地址,指引用户过去。
作为Java程序员,实现时用Spring Boot搭跳转和生成服务,MySQL存映射,Redis做缓存,RabbitMQ处理统计,这些常用技术组合起来,就能搭建一个稳定高效的短链系统。
(5)分布式锁一般都怎样实现?
分布式锁,简单说就是在多台服务器(分布式系统)上,保证同一时间只有一个“操作”能执行的工具。就像多个人抢一个厕所,分布式锁就是那个“有人正在使用”的牌子,确保同一时间只有一个人能用。
作为Java程序员,常见的实现方式有三种,各有各的“脾气”:
1. 基于数据库的分布式锁(最直接但稍显笨重)
思路很简单:用一张数据库表当“锁表”,表里有个字段存“锁的名字”(比如lock_name),再加个唯一索引(确保这个名字只能存一次)。
- 加锁:想拿锁时,就往表里插一条记录(比如lock_name=‘order’)。如果插成功了,说明拿到锁;插失败(因为唯一索引冲突),说明锁被别人拿走了。
- 解锁:操作完成后,删除这条记录,相当于把锁放回去。
- 防死锁:给记录加个“过期时间”字段,配合定时任务,超时自动删记录(比如任务执行一半服务器崩了,锁不会一直占着)。
优点:实现简单,不用额外学中间件。
缺点:性能一般(数据库是瓶颈),高并发下容易冲突,适合并发量不高的场景。
2. 基于Redis的分布式锁(最常用,性能好)
利用Redis的set命令特性(原子操作),把锁存在Redis里,就像把“牌子”挂在Redis这个公共区域。
- 加锁:用set key value NX PX 30000命令(NX表示“只有key不存在时才设置”,PX 30000表示30秒后自动过期)。成功设置就表示拿到锁,失败就等一会儿再试。
- 解锁:不能直接del key(怕删了别人的锁),要先判断“value是不是自己的”(value可以用UUID当唯一标识),是自己的再删。这两步要用Lua脚本保证原子性(避免判断完还没删,锁就过期被别人拿走了)。
- 防超时:如果任务执行时间超过锁的过期时间,可能会释放别人的锁。可以搞个“看门狗”线程,任务没执行完就自动续期(比如每10秒给锁加30秒过期时间)。
优点:性能好(Redis快),实现不算复杂,Java里有Redisson框架直接封装了这些逻辑,拿来就能用。
缺点:依赖Redis的可用性,如果Redis集群主从切换,可能短暂出现“锁丢失”(可以用Redisson的红锁机制缓解,多台Redis同时加锁成功才算拿到锁)。
3. 基于ZooKeeper的分布式锁(最可靠,稍复杂)
ZooKeeper是个天生适合做分布式协调的工具,它的“临时有序节点”特性特别适合做锁。
- 加锁:在ZooKeeper里建一个“锁目录”(比如/locks/order),每个想拿锁的服务器,都在这个目录下建一个“临时有序子节点”(名字带序号,比如/locks/order/0000001)。然后看自己是不是目录里序号最小的节点,是就拿到锁;不是就监听前一个节点(比如序号001监听000)。
- 解锁:任务完成后,删除自己的子节点。这时,被监听的节点消失,下一个节点会收到通知,然后检查自己是不是最小的,依次类推。
- 防死锁:因为是“临时节点”,如果服务器崩了,ZooKeeper会自动删节点,锁就释放了,不会死锁。
优点:可靠性高(不会丢锁),天然支持等待队列(不用轮询,效率高)。
缺点:实现比Redis复杂,性能略低(ZooKeeper是强一致性,写操作要同步到集群),适合对可靠性要求极高的场景。
总结:怎么选?
- 简单场景、并发不高:用数据库锁(省事)。
- 高并发、性能优先:用Redis锁(配合Redisson,省心)。
- 高可靠、不怕麻烦:用ZooKeeper锁(适合金融等核心场景)。
核心原则是:分布式锁必须满足“互斥性”(同一时间只有一个持有者)、“安全性”(不能释放别人的锁)、“可用性”(避免死锁)。作为Java程序员,Redis锁是日常开发中性价比最高的选择,尤其是Redisson已经帮我们处理了大部分细节,不用重复造轮子。
(6)如何设计一个点赞系统?
设计点赞系统,核心是解决“用户对内容(文章、评论、视频等)的点赞/取消点赞行为”,同时要满足高并发、实时性、数据准确三个关键需求。可以类比成“给朋友圈动态点赞”的场景——用户点一下,数字立刻+1;再点一下,数字-1,而且要记住“谁点过”,避免重复点赞。
一、先明确核心功能:用户和内容的“互动记录+计数”
点赞系统看起来简单,实则要处理三个核心问题:
- 记录“谁对什么内容点了赞”:避免用户重复点赞(比如同一用户不能对同一篇文章点两次赞)。
- 实时更新“内容的点赞数”:用户点赞后,前端要立刻看到数字变化(比如从10变成11)。
- 支持“取消点赞”:用户点错了能撤回,点赞数要相应减少。
二、数据怎么存?分“关系记录”和“计数”两类
点赞的数据可以拆成两部分,分别用不同的存储方式,兼顾性能和准确性:
1. 点赞关系:“谁赞了谁”——用“用户ID+内容ID”做唯一标识
需要记录“用户A对内容X点了赞”,这是一种“一对一”的关系(同一用户对同一内容只能有一次有效点赞)。
- 存储选择:
- 高频查询用Redis:用“Set集合”存,比如键是“content:like:X”(X是内容ID),值是点赞的用户ID集合。这样判断“用户A是否赞过X”,只需要查Set里有没有A,速度极快(O(1)复杂度)。
- 持久化用MySQL:存一张“点赞关系表”,字段包括用户ID、内容ID、点赞时间。用于数据备份(防止Redis崩了丢数据),以及后续可能的统计(比如“某用户点赞过的所有内容”)。
关键点:这部分数据必须保证“唯一性”——Redis的Set天然去重,MySQL表要建“用户ID+内容ID”的联合唯一索引,防止重复存储。
2. 点赞计数:“内容有多少赞”——用计数器实时更新
每个内容的点赞数(比如“文章X有123个赞”)需要实时展示,且支持快速增减。
- 存储选择:
- 优先用Redis的“计数器”(比如String类型):键是“content:like:count:X”,值是点赞数。Redis支持“原子操作”(比如INCR加1、DECR减1),就算同时有1000个用户点赞,也不会算错(避免“并发计数不准”的坑)。
- MySQL同步:定期(比如每5分钟)把Redis的计数同步到MySQL的“内容表”(比如文章表有个like_count字段),保证持久化。同步时要处理“增量”(比如Redis比MySQL多了10个赞,就加10,而不是覆盖,避免数据冲突)。
三、核心流程:点赞/取消点赞的“三步走”
用户点击“点赞”按钮时,系统要做三件事,确保“快且准”:
1. 点赞操作:
- 第一步:查Redis的Set(“content:like:X”),看用户A是否已点赞。
- 若已点赞:直接返回(避免重复操作)。
- 若未点赞:继续下一步。
- 第二步:更新Redis:
- 往Set里加用户A(SADD content:like:X A),标记“用户A赞了X”。
- 计数器+1(INCR content:like:count:X),实时更新点赞数。
- 第三步:异步同步到MySQL:
- 用消息队列(比如RabbitMQ)发一个“点赞事件”,后端异步消费,往“点赞关系表”插一条记录(用户A、内容X、时间)。
- 定期同步计数器到内容表的like_count字段。
2. 取消点赞操作:
和点赞类似,只是步骤相反:
- 查Set确认用户A已点赞。
- 从Set里删用户A(SREM content:like:X A)。
- 计数器-1(DECR content:like:count:X)。
- 异步通知MySQL删除“点赞关系表”的记录,同步计数器。
四、解决高并发和特殊场景的“小技巧”
- 防刷点赞:同一用户短时间内频繁点赞/取消,用Redis做限流(比如1分钟内最多操作5次),避免恶意攻击。
- 热点内容优化:比如一篇爆款文章,瞬间有10万用户点赞,Redis可能压力大。可以给计数器加“本地缓存”(比如Java的ConcurrentHashMap),先在本地累计,再批量同步到Redis,减少网络请求。
- 点赞列表展示:用户可能想看看“谁赞了这篇文章”,一般只需要显示最近10个点赞的用户。这时候用Redis的“有序集合(ZSet)”,键是“content:like:users:X”,值是用户ID,分数是点赞时间戳,按时间排序,查最新10个用户时直接取ZSet的前10条,速度极快。
- 数据一致性兜底:如果Redis崩了,从MySQL的“点赞关系表”重新统计点赞数(count(*)),再同步回Redis,保证数据不丢。
总结:核心是“Redis扛并发,MySQL保持久,异步同步解耦”
点赞系统的设计,本质是用Redis的“快”(支持高频读写、原子操作)处理实时交互,用MySQL的“稳”(持久化、支持复杂查询)做数据兜底,再通过异步同步(消息队列)化解两者的速度差异。对Java开发来说,重点是用好Redis的Set和计数器,处理好并发场景下的原子性,以及设计可靠的异步同步机制——这样既保证用户点完赞“数字立刻变”的体验,又能确保数据长期准确。
(7)让你设计一个 RPC 框架,怎么设计?
设计一个RPC框架,核心目标是让“远程服务调用”像“本地方法调用”一样简单——比如在订单服务里想调用库存服务的reduceStock()方法,不用手动写网络请求、解析响应,直接像调用本地方法一样写stockService.reduceStock(1001, 2),框架会自动帮你完成远程通信。
作为Java程序员,我们可以从“一次远程调用的完整流程”入手,拆解设计思路,就像拆解“打电话”的过程:拨号(找服务)→ 说什么(传数据)→ 对方听懂(处理)→ 回话(返回结果)。
一、一次RPC调用的基本流程:就像“打电话”
假设订单服务(客户端)要调用库存服务(服务端)的reduceStock(商品ID, 数量)方法,整个过程分四步:
- 客户端发起调用:程序员在订单服务里写stockService.reduceStock(1001, 2),看起来是调用本地接口,实际触发的是框架的“代理逻辑”。
- 客户端打包请求:框架把“调用的方法名(reduceStock)、参数(1001, 2)、目标服务标识”打包成“请求消息”,就像把“要说的话”转换成“电信号”。
- 网络传输:请求消息通过网络发送到库存服务所在的服务器,类似“电信号通过电话线传输”。
- 服务端处理并返回:库存服务的框架收到请求,找到对应的reduceStock方法执行,得到结果后打包成“响应消息”,再通过网络传回订单服务。
- 客户端接收结果:订单服务的框架解析响应消息,把结果返回给程序员,就像“对方的回话通过电话线传回来,你听到结果”。
二、核心组件设计:让“远程调用”自动化
要实现上面的流程,需要几个关键“角色”协同工作:
1. 客户端代理(Proxy):“替你拨号并说话的人”
客户端程序员调用的stockService其实不是真正的服务端对象,而是框架生成的“代理对象”(类似“电话亭”,帮你处理拨号和通话)。
- 作用:拦截本地方法调用,把“方法名、参数”偷偷换成“远程请求”。
- 实现:用Java的动态代理技术(JDK代理或CGLIB),在代理对象的invoke()方法里,偷偷执行“打包请求、发网络请求、等响应、解析结果”的逻辑。程序员完全感知不到远程调用的存在,以为在调本地方法。
2. 服务端处理器(Processor):“接电话并办事的人”
服务端需要一个“处理器”,专门接收客户端的请求,找到对应的方法执行,就像“接线员接到电话后,转给对应的部门处理”。
- 作用:
- 监听网络端口(比如8080),接收客户端发来的请求消息;
- 解析消息,拿到“要调用的方法名、参数”;
- 找到本地真正的服务对象(比如StockServiceImpl),反射调用对应的方法(reduceStock(1001, 2));
- 把方法返回的结果打包成响应消息,发回客户端。
3. 序列化器(Serializer):“把话转成电信号”
内存中的方法名、参数(比如Java对象)不能直接在网络上传输,必须转成字节流(就像“声音转成电信号”),这个转换过程叫“序列化”;服务端收到字节流后,再转成对象,叫“反序列化”。
- 要求:快(序列化/反序列化耗时少)、小(字节流体积小,省带宽)、兼容(客户端和服务端用同一种格式)。
- 常用选择:
- 简单场景用JSON(易读,但体积大、速度慢);
- 高性能场景用Protobuf(二进制格式,体积小、速度快,适合Java这类强类型语言)。
4. 网络传输层(Transport):“电话线/信号塔”
负责在客户端和服务端之间传输序列化后的字节流,需要高效、可靠。
- 实现:基于TCP协议(可靠,适合数据传输),用Netty这类NIO框架(Java里常用)来处理网络通信。Netty能高效处理大量并发连接(比如同时有1万个客户端调用),支持异步IO,避免线程阻塞。
- 通信格式:定义一个简单的“消息协议”,比如消息开头用4个字节表示消息长度,后面跟着具体的序列化数据(方便接收方知道什么时候读完一条消息)。
5. 服务注册中心(Registry):“通讯录”
客户端怎么知道服务端在哪里(IP和端口)?比如库存服务可能部署在192.168.1.100:8080和192.168.1.101:8080两个节点,客户端需要知道这些地址才能发请求。
- 作用:
- 服务端启动时,把自己的“服务名(比如stockService)+ IP:端口”注册到注册中心(就像“新公司开业,把地址录入通讯录”);
- 客户端启动时,从注册中心查询“stockService”对应的所有服务端地址(就像“查通讯录找公司地址”);
- 服务端下线时,注册中心会自动移除它的地址(避免客户端调用无效节点)。
- 常用选择:ZooKeeper、Nacos(支持动态更新,服务上下线时能通知客户端)。
三、进阶功能:让RPC更稳定、高效
1. 负载均衡(Load Balancer):“选哪个电话亭拨号”
如果服务端有多个实例(比如库存服务部署在3台机器),客户端需要选一个调用,避免某台机器压力过大。
- 策略:
- 轮询(依次选1、2、3号机);
- 随机(随机挑一个);
- 权重(给性能好的机器分配更高权重,多接请求)。
2. 超时与重试:“电话没接通怎么办”
网络可能卡断,客户端发了请求但一直没收到响应,不能一直等。
超时控制:客户端设置超时时间(比如3秒),超过时间没收到响应就报错,避免线程一直阻塞。
重试机制:如果超时是因为网络抖动(不是服务端故障),可以自动重试1-2次(但要确保服务端方法是“幂等的”,比如reduceStock不能重试,否则会多减库存;查询类方法可以重试)。
3. 容错机制:“对方占线/关机怎么办”
如果调用的服务端节点宕机了,客户端需要有备用方案。
- 策略:
- 快速失败(直接返回错误,让上层处理);
- 失败重试(换一个服务端节点再试);
- 熔断(如果某服务连续失败多次,暂时“拉黑”它,一段时间内不调用,避免无效请求浪费资源)。
总结:RPC框架的核心是“隐藏远程调用细节”
就像打电话时,你只需要拨号、说话、听回话,不用关心“电信号怎么传输”“基站怎么工作”——RPC框架让程序员只关心“调用哪个方法、传什么参数”,把“网络传输、序列化、服务发现”这些复杂细节全部隐藏起来。
作为Java程序员,实现时可以组合这些技术:用JDK动态代理做客户端代理,Netty处理网络传输,Protobuf做序列化,ZooKeeper当注册中心,再加上负载均衡和容错策略,就能搭出一个可靠的RPC框架(比如Dubbo、Spring Cloud OpenFeign的核心思路就是这样)。核心目标始终是:让远程调用和本地调用一样简单、可靠。
(8)什么是限流?限流算法有哪些?
1. 什么是限流?
通俗说,限流就是给系统“设个门槛”,限制单位时间内的请求数量,避免太多请求同时涌入把系统压垮。
比如一家奶茶店,最多同时招待10位客人点单,再多就会让制作台、服务员忙不过来,导致所有人都等很久甚至订单出错。限流就像门口的保安,超过10人就拦住,让后面的人排队,保证店里能正常运转。
对系统来说,限流能保护服务器、数据库等资源不被过载,让系统在可控范围内稳定工作。
2. 常见的限流算法有哪些?怎么实现的?
不同场景需要不同的限流策略,常见的算法有4种,核心都是控制“单位时间内的请求数”,但思路各有侧重:
(1)固定窗口限流(又称“计数器限流”)
思路:把时间切成固定的“窗口”(比如1秒一个窗口),每个窗口里最多允许N个请求,超过就拒绝。
举例:规定1秒内最多100个请求。
- 0-1秒内,第101个请求来的时候,直接拒绝。
- 到1秒整,计数器清零,1-2秒又能接受新的100个请求。
优点:简单好实现,用一个计数器+时间戳就行。
缺点:有“窗口临界问题”。比如第1秒的最后100毫秒来了100个请求,第2秒的前100毫秒又来100个请求,这200毫秒内实际有200个请求,超过了1秒100的限制,可能压垮系统。
(2)滑动窗口限流
思路:把固定窗口拆成更小的“子窗口”,每次时间推进,窗口像“滑动”一样移动,综合多个子窗口的请求数来判断是否限流。
举例:1秒的窗口拆成10个100毫秒的子窗口,每个子窗口最多10个请求。
- 当判断当前是否允许请求时,会计算最近1秒内(即当前子窗口+前9个子窗口)的总请求数,超过100就拒绝。
- 这样能避免固定窗口在临界时间点的突发流量问题。
优点:比固定窗口更精确,能平滑流量。
缺点:子窗口拆分越细,计算越复杂,需要记录每个子窗口的请求数,消耗更多内存。
(3)漏桶算法
思路:把请求想象成“水流”,系统是一个“漏桶”——水流进桶里,桶以固定速度向外漏水(处理请求)。如果进水太快,桶满了就会溢出(拒绝请求)。
举例:系统每秒最多处理10个请求(漏水速度固定),如果突然来了20个请求,桶里先存10个,剩下10个直接拒绝,然后每秒处理1个存着的请求。
优点:能强行控制请求的处理速度,避免系统被突发流量冲击(不管进来多快,处理速度始终平稳)。
缺点:不够灵活。比如系统偶尔能处理更快的请求,但漏桶会强行把多余的请求拒掉,可能浪费资源。
(4)令牌桶算法
思路:系统会按固定速度往“令牌桶”里放令牌(比如每秒放100个),请求来了必须拿到令牌才能被处理。如果桶里没令牌,请求就被拒绝或排队。
举例:桶最多存100个令牌,每秒新增10个。
- 平时请求少,令牌会攒在桶里(最多100个)。
- 突然来了150个请求,桶里的100个令牌能先处理100个,剩下50个等新令牌(每秒来10个,5秒后处理完)。
优点:既控制了平均速度,又能应对短时间的突发流量(利用桶里攒的令牌),比漏桶更灵活,实际中用得最多(比如API网关限流常用)。
总结
- 限流的核心是“保护系统不被过载”,本质是控制单位时间的请求量。
- 固定窗口简单但有临界问题,滑动窗口更精确但复杂;
- 漏桶强制平稳处理,令牌桶能应对突发流量(更常用)。
Java里可以用Guava的RateLimiter(基于令牌桶)快速实现限流,也可以自己基于Redis(用计数器+过期时间)实现分布式限流,核心都是上述算法的思路落地。
(9)负载均衡算法有哪些?
负载均衡有多种核心算法,轮询算法(Round Robin)将请求按顺序分配给后端服务器,适用于服务器性能相近的场景。加权轮询算法(Weighted Round Robin)在轮询基础上为服务器分配权重,性能更强的服务器会处理更多请求,适用于服务器性能不均的情况。其他常见算法还包括IP哈希(IP Hash)用于会话保持、最少连接数(Least Connections)适合长连接场景,以及最短响应时间(Least Response Time)用于对响应时间有要求的场景。
轮询算法 (Round Robin)
- 原理:按顺序依次将请求分配给后端服务器,循环往复。
- 适用场景:后端服务器处理能力相近,且请求负载相对均衡。
- 缺点:不考虑服务器的实际负载情况,可能导致某些服务器过载,而另一些服务器空闲。
加权轮询算法 (Weighted Round Robin)
- 原理:给后端服务器分配不同的权重,权重越高的服务器会获得更多的请求。
- 适用场景:后端服务器性能不均,需要根据性能差异分配流量,提高整体效率。
其他常见算法
- IP哈希 (IP Hash):根据客户端IP地址的哈希值将请求分配给特定的服务器,保证同一个客户端的请求会话始终发送到同一台服务器。
- 最少连接数 (Least Connections):将请求分配给当前活动连接数最少的服务器,适合处理长连接的场景。
- 最短响应时间 (Least Response Time):将请求分配给响应时间最短的服务器,适合对响应时间有严格要求的应用。
如何选择算法
选择哪种负载均衡算法需要综合考虑多种因素,包括:
- 业务场景:是否需要会话保持?对响应时间是否有要求?
- 服务器性能:后端服务器的性能是否一致?
- 流量特征:请求是短连接还是长连接?
(10)分布式和微服务有什么区别?
1. 概念范畴不同
- 分布式系统是一个宏观的技术概念,它描述的是一种系统构建方式:系统的不同组件部署在网络中的多台计算机上,这些计算机通过消息传递进行通信和协调,从用户视角看仍然是一个完整的系统。
- 微服务是一种具体的软件架构风格,它关注的是如何将一个大型的单体应用程序拆分成一组小型、自治的服务,每个服务都围绕着特定的业务能力进行构建。
2. 核心目标不同
- 分布式系统的核心目标是解决技术层面的挑战,包括:
- 提升系统的计算能力和存储容量
- 增强系统的可靠性和可用性
- 实现系统的水平扩展性
- 微服务的核心目标是解决开发和运维层面的挑战,包括:
- 降低大型系统的复杂性
- 提高团队开发的并行度和独立性
- 实现服务的独立部署和快速迭代
3. 拆分原则不同
-
在分布式系统中,拆分通常是技术导向的:
- 按照技术层次进行拆分(如Web服务器、应用服务器、数据库服务器)
- 按照资源需求进行拆分(如计算密集型任务、存储密集型任务)
-
在微服务架构中,拆分是业务导向的:
- 按照业务领域进行拆分(如用户服务、订单服务、支付服务)
- 每个服务都对应一个明确的业务能力
4. 数据管理方式不同
- 分布式系统强调数据的集中管理和一致性:
- 通常使用统一的分布式数据库或缓存
- 关注数据的强一致性和事务完整性
- 微服务架构强调数据的去中心化和自治:
- 每个服务拥有自己独立的数据库
- 通过最终一致性来保证数据的同步
- 服务之间通过API进行数据交互
5. 通信方式不同
- 分布式系统的通信更多关注技术效率:
- 可能使用底层的RPC、消息队列等通信机制
- 通信协议可能对业务开发人员不透明
- 微服务的通信更多关注业务表达的清晰性:
- 通常使用HTTP/REST、gRPC等标准化协议
- 接口设计面向业务概念,便于理解和使用
6. 演进关系
微服务架构是分布式系统的一种具体实现形式:
- 所有的微服务架构本质上都是分布式系统
- 但并非所有的分布式系统都采用微服务架构
- 微服务为分布式系统的构建提供了一套方法论和最佳实践
总结
简单来说,分布式关注的是"系统如何在多台机器上运行",而微服务关注的是"应用如何按业务拆分成小服务"。分布式是解决技术扩展性问题的手段,微服务是解决业务复杂性问题的方法。一个采用微服务架构的系统必然是一个分布式系统,但一个分布式系统不一定采用微服务架构。
(11)什么是服务熔断?
服务熔断是一种快速的故障恢复机制,当某个被依赖的服务持续失败时,系统会主动“熔断”对其的调用,直接返回一个预定的降级结果,避免故障蔓延和资源耗尽。
核心思想:电路断路器模式
它模仿了电路中的保险丝:
- 正常状态(Closed):请求正常发送到目标服务。
- 触发熔断(Open):当失败率超过阈值,熔断器“跳闸”。在接下来的时间内,所有对此服务的调用立即失败,不再发出真实请求。
- 半开状态(Half-Open):经过一个时间窗口后,熔断器允许少量试探请求通过。如果这些请求成功,熔断器关闭,恢复常态;如果仍然失败,则继续保持熔断状态。
要解决的问题(为什么需要它?)
- 防止雪崩效应:避免一个服务的故障导致整个系统连锁崩溃。
- 减少资源浪费:避免无谓地等待一个已经不可用的服务响应(节省线程、连接等资源)。
- 提供快速失败:给用户一个明确的、快速的(即使是错误的)响应,优于长时间的等待。
核心目的
牺牲局部(一个故障服务)的可用性,保全整体系统的可用性和稳定性。
(12)什么是服务降级?
服务降级是在系统资源紧张或某些服务不可用时,主动关闭或简化非核心业务功能,从而保证核心业务继续可用并释放资源,是一种“弃车保帅”的策略。
核心思想与场景
- 目的:确保核心业务流程的可用性,保障系统整体稳定。
- 场景:例如在“双十一”大促期间,为了保障下单、支付等核心链路的顺畅,系统可能会暂时关闭商品评价、个性化推荐等非核心功能。
与服务熔断的关键区别
为了让你理解得更透彻,这是它和服务熔断的核心区别:
| 特性 | 服务降级 | 服务熔断 |
|---|---|---|
| 触发原因 | 主动决策,通常基于整体系统负载或预案。 | 被动触发,因为某个依赖服务持续失败。 |
| 目标范围 | 通常是自己负责的、非核心的功能。 | 是针对外部依赖的、核心或非核心的服务。 |
| 管理层次 | 系统层面的战略决策,是一种业务取舍。 | 服务层面的故障隔离机制,是一种技术手段。 |
| 最终状态 | 返回一个预设的、可接受的降级结果(如默认值、空数据、友好提示)。 | 返回一个快速的、明确的失败。 |
简单比喻:
- 服务熔断:就像你常去的餐馆后厨着火了(服务故障),经理立刻在门口挂出“暂停营业”的牌子(熔断),不再接待任何新客人(请求)。
- 服务降级:就像餐馆在高峰期人手不足(资源紧张)时,主动告知顾客“今天不提供免费的餐前小菜和饭后甜点(非核心功能)”,但依然全力保障主菜(核心功能)的供应。
总结
服务降级是一种以业务为导向的、主动的“舍弃”策略,目的是在压力下确保大局稳定;而服务熔断是一种以技术为导向的、被动的“隔离”机制,目的是防止故障扩散。
(13)什么是服务雪崩?
服务雪崩是指在分布式系统中,一个服务的失败引发连锁反应,最终导致整个系统不可用的灾难性故障现象。
核心过程(多米诺骨牌效应)
它的发生通常遵循一个清晰的“死亡链条”:
- 起点:某个服务A因故障(如资源耗尽、Bug、机器宕机)而变慢或不可用。
- 蔓延:依赖服务A的服务B,在调用A时会大量阻塞(例如,线程在等待A的响应),最终导致服务B自身的资源(如线程池、连接)被耗尽,服务B也随之崩溃。
- 扩散:同样的过程会继续向上传递,影响依赖服务B的服务C、服务D……
- 崩溃:就像多米诺骨牌一样,故障在系统中迅速扩散,最终导致所有相互依赖的服务链全部瘫痪,整个系统崩溃。
根本原因
- 服务间强依赖:服务之间存在着复杂的调用链。
- 无故障隔离:没有使用服务熔断等机制来阻止调用已失败的服务。
- 资源耗尽:关键资源(如线程、数据库连接)被等待的请求占满,无法处理新请求。
解决方案
为了防止服务雪崩,业界形成了三大核心防线:
- 服务熔断:快速失败,及时“熔断”对故障服务的调用,避免资源被拖垮。
- 服务降级:弃车保帅,暂时牺牲非核心功能,释放资源保障核心业务。
- 流量控制:釜底抽薪,使用限流、削峰填谷等手段,从源头控制流量,防止系统被冲垮。
总结
服务雪崩的本质是故障在分布式系统中不受控制地层层放大。而微服务架构的高可用性设计,其核心目标之一就是通过熔断、降级、限流等手段,预防和阻止服务雪崩的发生。
更多推荐


所有评论(0)