Java面试分布式案例

wen java案例 2

本文目录导读:

Java面试分布式案例

  1. 案例一:分布式系统中的“数据一致性”问题
  2. 案例二:缓存与数据库的双写一致性
  3. 案例三:分布式锁的实现与坑
  4. 案例四:分布式链路追踪与日志排查
  5. 案例五:分布式ID生成方案
  6. 案例六:分布式接口的幂等性设计
  7. 总结建议(面试策略)

下面为你整理了Java面试中关于分布式系统的高频案例,这些案例通常不会直接问“什么是分布式”,而是通过具体的业务场景来考察候选人的架构设计能力、技术深度和问题排查能力

我会按照经典业务场景分类,每个案例都包含了问题描述、核心思路、技术选型(Java生态)和追问点,方便你系统准备。


分布式系统中的“数据一致性”问题

典型场景:电商下单后,需要扣减库存、生成订单、增加积分,这三个操作分属不同的微服务(库存服务、订单服务、积分服务)。

问题:如果扣库存成功,但生成订单失败,会导致“超卖”或“幽灵订单”,如何保证数据最终一致性?

核心思路与方案

  1. 本地消息表(最终一致性)
    • 在订单服务中创建一张“本地消息表”。
    • 扣库存和写订单、写消息表放在同一个本地事务中。
    • 通过定时任务扫描消息表,将消息投递到MQ,积分服务消费后回调修改消息状态。
  2. 事务消息(RocketMQ)
    • 利用RocketMQ的半消息机制,先发送半消息,执行本地事务(扣库存),提交事务后发送确认消息。
    • 如果本地事务执行失败,回滚半消息,消费者不会收到消息。
  3. Seata 分布式事务框架(AT模式/TCC模式)
    • AT模式:基于全局锁和undo_log,适合读多写少的场景。
    • TCC模式(Try-Confirm-Cancel):适合强隔离性场景,例如扣减余额。

面试官常问追问点

  • 为什么不用2PC(两阶段提交)?—— 答:2PC是同步阻塞的,性能差,且协调者单点故障会导致阻塞。
  • 消息丢失了怎么办?—— 答:消息表/事务消息 + 对账系统 + 定时补偿。
  • 高并发追问:如果秒杀场景下,数据库扛不住扣库存压力怎么办?—— 答:引入Redis预扣库存,异步同步DB,或者使用Lua脚本保证原子性。

缓存与数据库的双写一致性

典型场景:查询商品详情,先查Redis缓存,未命中则查MySQL,并回填缓存,但更新商品信息后,如何保证缓存和数据库一致?

问题:先更新数据库还是先删缓存?为什么会导致脏数据?

核心思路与方案

  1. Cache Aside Pattern(旁路缓存)
    • :读缓存 -> 未命中 -> 读DB -> 写缓存。
    • :先更新DB,然后删除缓存(而不是更新缓存)。
  2. 延迟双删
    • 先删除缓存 -> 更新DB -> 休眠一小段时间(如500ms) -> 再次删除缓存。
    • 目的:解决并发下,旧数据回填缓存的问题。
  3. 订阅Binlog(Canal)

    通过Canal监听MySQL Binlog变更,异步解析并删除或更新Redis缓存,这是目前工业界最稳妥的方式,避免代码侵入。

面试官常问追问点

  • 为什么是“删缓存”不是“更新缓存”?—— 答:更新缓存是写操作频繁,缓存会被频繁写,且存在“写写”竞争,删除缓存是惰性加载,只有在读时候才回填。
  • 如果在“延迟双删”中,第二次删除失败了怎么办?—— 答:引入重试机制(如MQ异步重试),或者干脆靠过期时间兜底。
  • 强一致性怎么做?—— 答:并发量低可以用分布式锁(读写锁),但高并发下性能差,分布式系统通常接受最终一致性。

分布式锁的实现与坑

典型场景:多个服务实例(Java进程)同时执行定时任务,每日结账”,如何保证只有一个实例执行?

问题:Synchronized锁在分布式环境下失效,如何解决?

核心思路与方案

  1. 基于Redis(Redisson)
    • 使用RedissonClient.getLock(),底层基于Lua脚本保证加锁和设置过期时间的原子性。
    • 引入“看门狗”机制,自动续期,防止业务未执行完,锁过期被释放。
  2. 基于Zookeeper(Curator)

    使用临时顺序节点,监听前一个节点,避免了Redis锁的“锁过期”问题,且是强一致性,但性能稍逊于Redis。

面试官必问的坑(核心考点)

  • 锁误删:如果A线程的锁因为超时自动释放,B线程获取了锁,此时A线程执行完调用unlock,把B的锁删了怎么办?
    • :锁的value必须是唯一ID(UUID),在删除前比较value是否一致(Lua脚本保证原子性)。
  • 可重入性:同一个线程重入获取锁,需要计数+1。
  • Redis主从切换:主节点挂了,锁还没同步到从节点,从节点变为新主节点,导致锁丢失,怎么解决?—— 答:使用RedLock(红锁),但RedLock本身争议较大,需讲出优缺点(CAP理论偏向CP还是AP)。

分布式链路追踪与日志排查

典型场景:用户请求一次下单,经过网关 -> 订单服务 -> 库存服务 -> 支付服务,某个环节很慢,导致用户体验差,如何定位?

问题:如何快速定位是哪个服务慢,哪条SQL慢?

核心思路与方案

  1. TraceId 传递
    • 在网关生成全局唯一TraceId,通过MDC(Mapped Diagnostic Context)放入日志,通过HTTP Header(Feign的Interceptor)或MQ消息头传递。
  2. 集成框架
    • Spring Cloud Sleuth + Zipkin:采集时间戳,显示调用链路拓扑图。
    • SkyWalking:通过Java Agent字节码增强技术,对业务代码无侵入。
  3. 日志采集:ELK(Elasticsearch + Logstash + Kibana),通过TraceId聚合所有日志。

面试官常问追问点

  • 如果Header丢失,下游服务没有TraceId怎么办?—— 答:有兜底策略,如果下游发现没有TraceId,会生成新的,但会导致链路断裂。
  • 如何查看是DB慢还是代码慢?—— 答:通过Zipkin显示的Span耗时,以及集成数据库中间件(如ShardingSphere)的慢SQL日志。

分布式ID生成方案

典型场景:订单表、用户表数据量巨大,分库分表后,数据库自增主键冲突,如何生成全局唯一ID?

核心思路与方案

  1. UUID:性能最好,但是无序且太长,不适合作为聚簇索引,会导致频繁页分裂(性能灾难)。
  2. 数据库号段模式
    • 例如一张表存max_idstep,每次取1000个ID放内存,优点是简单,缺点是DB有压力。
  3. **利用Redis INCR:** 性能好,但需考虑持久化和高可用。
  4. 雪花算法(Snowflake)
    • 1bit(不用) + 41bit(毫秒时间戳) + 10bit(机器ID) + 12bit(序列号)
    • 解决时钟回拨问题:这是Java面试高频考点。
    • :如果发现当前毫秒时间戳小于上次生成的时间戳,则拒绝生成ID(抛异常或等待),或者记录回拨时间,如果回拨小于5ms,则直接等待时钟追赶。

高并发追问:单机雪花算法能生成多少ID?(答:同一毫秒支持2^12=4096个),如果超过了怎么办?(答:等待下一毫秒)。


分布式接口的幂等性设计

典型场景:前端重复点击“提交订单”,网络超时导致前端重试,或者MQ消费者消费失败后重试,导致库存扣了两次,订单建了两条。

问题:如何保证接口的幂等性?

核心思路与方案

  1. Token机制(唯一令牌)
    • 进入下单页时,后端生成Token存在Redis并返回前端。
    • 提交时,前端携带Token。
    • 后端删除Token(利用Lua脚本保证删除的原子性),删除成功则执行业务,删除失败则拒绝请求。
  2. 数据库唯一约束
    • 订单表中添加了一个business_id唯一索引(如:用户ID+商品ID+描述)。
    • 重复插入报错即代表重复请求。

面试官常问追问点

  • 如果删除Token成功,但业务执行失败了(扣库存超时),怎么办?—— 答:需要重新发放Token,或者判断失败原因,如果是超时,前端应提示重试,重试需要新Token。

总结建议(面试策略)

在面试中,回答分布式问题时,建议遵循T-R-I-F原则:

  1. T (Target):先明确这个分布式问题要解决的核心矛盾是什么(数据一致性?高可用?性能?)。
  2. R (Reason):说明为什么单机/单库解决不了,痛点在哪里。
  3. I (Implementation):给出具体的解决方案(画图说明更佳),配合中间件(如Redis、RocketMQ、Seata)。
  4. F (Fallback/Failure):主动说明如果中间件挂了怎么办如果消息丢失了怎么办如何补偿,这是拉开与普通候选人差距的关键点。

祝你面试顺利!如果你对某个具体案例(如Seata的AT模式原理)感兴趣,可以随时提问,我可以做更深入的原理解析。

抱歉,评论功能暂时关闭!