← 返回 Java核心图解 目录

🌐 分布式与缓存图解

7大核心主题 · 7个SVG · 5个IDE代码实战 · 8道面试FAQ

① 分布式事务 4 种方案图解

🔗 跨服务的数据一致性难题

单体应用用 @Transactional 就行,但微服务下,订单服务和库存服务各自有数据库,怎么保证"扣库存+创建订单"要么全成功要么全回滚?

跨行转账类比:
你从工行转1万到建行,工行扣了钱但建行没到账怎么办?
- 2PC = 两个银行先"准备",确认都能完成再"提交"(慢但稳)
- TCC = 先冻结1万(Try),确认后扣款(Confirm),出问题解冻(Cancel)
- SAGA = 先扣工行,再存建行,存失败则反向补回工行(正向+补偿)
- 本地消息表 = 工行扣完钱,写个"待办"纸条,异步通知建行处理
分布式事务 4 种方案对比 2PC(两阶段提交) 协调者 (TM) 阶段一:准备(Prepare) 服务A: 准备OK 服务B: 准备OK 阶段二:提交(Commit) 服务A: 提交 服务B: 提交 ❌ 同步阻塞 | 单点故障 | 太慢 适合:数据库层面的XA事务 TCC(Try-Confirm-Cancel) Try:资源预留/冻结 冻结库存1件、冻结金额100元 Confirm:确认执行 扣库存、扣款(必须幂等) Cancel:取消/回滚 解冻库存、释放金额(必须幂等) ✅ 无锁 | 性能好 | 业务侵入性强 适合:对性能要求高的资金/库存场景 SAGA(长事务编排) 正向流程: 创建订单 扣减库存 扣减余额 补偿流程(某步失败时反向执行): 恢复余额 恢复库存 取消订单 ✅ 适合长事务 | 无全局锁 | 最终一致 ❌ 需要写补偿操作 | 脏读可能 框架:Seata SAGA / Axon 本地消息表(最终一致性) ① 服务A:业务操作 + 写本地消息表(同一事务) 扣库存 + INSERT INTO msg_table (同一DB事务) ② 定时任务扫描消息表 → 发送到 MQ 确保消息一定发出(失败重试) ③ 服务B:消费MQ消息 → 执行本地业务 幂等消费 + 成功后确认 ✅ 实现简单 | 最终一致 | 对业务侵入小 ❌ 定时轮询有延迟 | 消息表会膨胀

图1:分布式事务 4 种方案全景对比

方案一致性性能侵入性适用场景
2PC强一致低(阻塞)数据库XA事务
TCC最终一致高(3个接口)资金、库存
SAGA最终一致中(补偿接口)长流程业务编排
本地消息表最终一致异步通知、跨服务
OrderService.java — TCC 事务实战(Seata)
📂 order-service
OrderController.java
OrderService.java
OrderTccAction.java
📂 storage-service
StorageTccAction.java
📂 account-service
AccountTccAction.java
// ========== 分布式事务:TCC 模式实战 ========== // 场景:下单 → 扣库存 → 扣余额,三个微服务必须全成功或全回滚 @Service public class OrderService { @Autowired private StorageTccAction storageAction; // 远程:库存 @Autowired private AccountTccAction accountAction; // 远程:账户 // Seata 全局事务入口 @GlobalTransactional(timeoutMills = 60000, name = "createOrder") public Order createOrder(OrderDTO dto) { // ① 本地:创建订单(处于"待确认"状态) Order order = orderMapper.insert(dto); // ② Try:远程冻结库存 boolean frozen = storageAction.prepare( dto.getProductId(), dto.getCount()); if (!frozen) { throw new BizException("库存不足"); // → 触发全局回滚 } // ③ Try:远程冻结金额 boolean reserved = accountAction.prepare( dto.getUserId(), dto.getAmount()); if (!reserved) { throw new BizException("余额不足"); // → 触发全局回滚 } // ④ 全部 Try 成功 → Seata 自动调用各服务的 Confirm // 如果中途任何异常 → Seata 自动调用各服务的 Cancel return order; } } // ========== 库存服务的 TCC 三阶段 ========== @LocalTCC public interface StorageTccAction { // Try:冻结库存(不能直接扣,要先冻结) @TwoPhaseBusinessAction(name = "freezeStock", commitMethod = "confirm", rollbackMethod = "cancel") boolean prepare(BusinessActionContext ctx, @BusinessActionContextParameter Long productId, @BusinessActionContextParameter Integer count); // Confirm:真正扣减冻结的库存(必须幂等!) boolean confirm(BusinessActionContext ctx); // Cancel:解冻库存,恢复可用数量(必须幂等!) boolean cancel(BusinessActionContext ctx); }
TCC 三个核心坑(面试必考):
空回滚:Try 还没执行,Cancel 先到了(网络超时导致)。解决:记录全局事务ID,Cancel 时检查 Try 是否执行过
幂等性:Confirm 和 Cancel 可能被重复调用。解决:数据库加事务状态字段,只执行一次
悬挂:Cancel 比 Try 先执行,之后 Try 又来了。解决:Cancel 执行后记录该事务ID,Try 检查到则拒绝执行
Seata 的四种模式选择:
AT 模式(默认):无侵入,自动代理 SQL + 生成反向补偿SQL,但有全局锁性能较低
TCC 模式:高性能,但每个接口要写3个方法(Try/Confirm/Cancel),侵入性强
SAGA 模式:适合长流程编排,状态机驱动,适合老系统改造
XA 模式:强一致性,依赖数据库 XA 协议,性能最差

② 缓存 4 种读写模式

缓存 4 种读写模式 ① Cache Aside(旁路缓存)⭐最常用 读: 查缓存 命中→返回 未命中→查DB→写缓存 写: 先更新DB 再删除缓存(不是更新缓存!) 💡 为什么是删除而不是更新?避免并发写导致缓存与DB不一致 ② Read Through(读穿透) 应用只和缓存打交道 缓存未命中 → 缓存组件自动查DB → 回填缓存 App → Cache → (miss) → DB → Cache → App ✅ 应用代码更简洁 ❌ 缓存组件需要支持(如Guava/Caffeine) 适合:读多写少、对一致性要求不高 ③ Write Through(写穿透) 写数据时 → 同步写缓存 + 写DB App → Cache (同步) → DB 缓存和DB始终一致(强一致) 但写入延迟高(等DB写完才返回) ✅ 强一致性 ✅ 读取永远命中 ❌ 写入延迟高 ❌ 缓存组件需支持 ④ Write Behind(异步写回) 写数据时 → 只写缓存,异步批量写DB App → Cache → (异步批量) → DB 写入极快(不等DB) 但缓存宕机 → 数据丢失! ✅ 写入极快 ✅ 批量写DB减少IO ❌ 数据可能丢失 ❌ 实现复杂

图2:缓存 4 种读写模式对比

ProductService.java — Cache Aside 读写实战
📂 product-service
ProductController.java
ProductService.java
ProductMapper.java
📂 config
RedisConfig.java
// ========== Cache Aside(旁路缓存)实战代码 ========== @Service public class ProductService { @Autowired private StringRedisTemplate redis; @Autowired private ProductMapper productMapper; /** * ✅ 正确的读:先查缓存 → 未命中查DB → 回填缓存 * 注意:缓存空值防止穿透,设短TTL */ public Product getById(Long id) { String key = "product:" + id; String json = redis.opsForValue().get(key); if (json != null) { if ("NULL".equals(json)) { return null; // 防穿透:缓存了空值 } return JSON.parseObject(json, Product.class); } // 缓存未命中 → 查DB Product product = productMapper.selectById(id); if (product != null) { redis.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES); // 正常TTL 30分钟 } else { redis.opsForValue().set(key, "NULL", 2, TimeUnit.MINUTES); // 防穿透:空值短TTL 2分钟 } return product; } /** * ✅ 正确的写:先更新DB → 再删除缓存 * 注意:是"删除"不是"更新",避免并发写不一致 */ @Transactional public void updateProduct(Product product) { productMapper.updateById(product); // ① 先更新DB redis.delete("product:" + product.getId()); // ② 再删缓存 } /** * ❌ 错误的写:先删缓存 → 再更新DB * 并发场景:线程A删缓存 → 线程B读到旧DB值回填 → 线程A更新DB * 结果:缓存里还是旧数据! */ public void wrongUpdate(Product product) { redis.delete("product:" + product.getId()); // ❌ 先删缓存 productMapper.updateById(product); // ❌ 再更新DB // 并发时缓存可能被其他线程回填旧值! } }
Cache Aside 的一致性策略(面试高频):
延迟双删:更新DB前删一次缓存,更新后再延迟删一次(解决并发读写的不一致)
消息队列保证:DB更新后发MQ消息,消费者异步删缓存,保证最终一致
Canal 监听 binlog:不侵入业务代码,监听 MySQL binlog 变化自动更新/删除缓存

③ 缓存三害:穿透、击穿、雪崩

缓存三害:穿透 / 击穿 / 雪崩 💥 缓存穿透 查不存在的数据 恶意请求 id=-1(DB也没有) 缓存miss → 查DB → DB也没有 → 不断重复 → DB被打满 类比:拿假钞买东西 解决方案 1️⃣ 缓存空值(设短TTL) id=-1 → cache null, 60s 2️⃣ 布隆过滤器(前置拦截) 不存在的id直接拦住 3️⃣ 参数校验(id必须>0) ⚡ 缓存击穿 热点key过期瞬间 热点商品 key=hot_item 过期 大量请求同时miss → 全查DB → DB瞬时压力暴涨 类比:保镖不在→人群涌入 解决方案 1️⃣ 互斥锁(setnx) 只让一个请求查DB+回填 2️⃣ 逻辑过期(不设TTL) 代码判断过期→异步更新 3️⃣ 热点key永不过期 ❄️ 缓存雪崩 大量key同时过期 1000个key同一时刻过期 所有请求全打到DB → DB可能宕机 类比:挤电梯→全挤到楼梯 解决方案 1️⃣ TTL加随机值 1h + random(0~300s) 2️⃣ 多级缓存 L1本地缓存 + L2 Redis 3️⃣ 熔断降级 📋 三害速查表 维度 穿透 击穿 雪崩 原因 查不存在的数据 热点key过期 大量key同时过期 影响 绕过缓存查DB 瞬发压垮DB 持续压垮DB

图3:缓存三害对比 — 穿透/击穿/雪崩的原因、影响和解决方案

CacheDefenseService.java — 缓存三害防御实战
📂 cache-defense
CacheDefenseService.java
BloomFilterInit.java
📂 util
RedisLockUtil.java
// ========== 缓存三害的代码级防御方案 ========== @Service public class CacheDefenseService { @Autowired private StringRedisTemplate redis; @Autowired private ProductMapper productMapper; @Autowired private RBloomFilter<Long> bloomFilter; // Redisson布隆过滤器 // ========== ① 防穿透:布隆过滤器前置拦截 ========== public Product getWithBloom(Long id) { // 布隆过滤器先判断:不存在 → 直接返回,不查DB if (!bloomFilter.contains(id)) { return null; // 一定不存在 } return getByIdFromCache(id); } // ========== ② 防击穿:互斥锁只放一个请求查DB ========== public Product getWithMutexLock(Long id) { String key = "product:" + id; String json = redis.opsForValue().get(key); if (json != null) { return JSON.parseObject(json, Product.class); } // 缓存miss → 加互斥锁,只让一个线程查DB String lockKey = "lock:product:" + id; String requestId = UUID.randomUUID().toString(); try { // setnx + 过期时间(防死锁) Boolean locked = redis.opsForValue() .setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS); if (locked != null && locked) { // 获取锁成功 → 查DB → 回填缓存 Product product = productMapper.selectById(id); redis.opsForValue().set(key, JSON.toJSONString(product), 30 + new Random().nextInt(300), // ⭐防雪崩:TTL加随机 TimeUnit.SECONDS); return product; } else { // 获取锁失败 → 短暂休眠后重试(读缓存) Thread.sleep(50); return getWithMutexLock(id); // 递归重试 } } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁:Lua脚本保证原子性(判断+删除) releaseLock(lockKey, requestId); } } // ========== ③ 防雪崩:TTL 加随机偏移 ========== private long randomTtl(int baseMinutes) { // 基础TTL + 0~5分钟随机,避免大批key同时过期 return baseMinutes * 60 + new Random.nextInt(300); } // ========== ④ 布隆过滤器初始化(项目启动时预热)========== @PostConstruct public void initBloomFilter() { // 预计元素100万,误判率0.01% bloomFilter.tryInit(1000000L, 0.0001); // 批量加载所有商品ID到布隆过滤器 productMapper.selectAllIds().forEach(id -> { bloomFilter.add(id); }); } }
布隆过滤器使用注意:
有误判率:布隆过滤器说"存在"可能不存在(假阳性),但说"不存在"就一定不存在
不能删除:标准布隆过滤器不支持删除元素(位数组共享)。要删可以用 Counting Bloom Filter
启动预热:新加入的ID需要同步加入布隆过滤器,否则会误判为"不存在"导致穿透
容量预估:创建时要预估元素总量和误判率,一旦创建无法扩容

④ 限流 4 种算法图解

限流 4 种算法 ① 固定窗口 0-60s: 98/100 60-120s: 45/100 每60秒一个窗口,限100 ⚠️ 临界突刺问题 59s来100个+61s来100个 2秒内200个请求! ✅ 简单 ❌ 不平滑 适合:简单的QPS限制 ② 滑动窗口 窗口随时间滑动 统计最近60s的请求数 ✅ 解决临界突刺 窗口越细分越平滑 ✅ 比固定窗口更精确 ✅ Sentinel 默认算法 适合:大多数限流场景 ③ 漏桶 请求流入桶 匀速流出 10个/秒 不管来多快, 永远匀速输出 ✅ 输出绝对平滑 ❌ 无法应对突发流量 适合:秒杀等需要匀速 ④ 令牌桶 ⭐ 以固定速率生成令牌 🪙🪙🪙🪙🪙 桶中令牌(最多N个) 请求来了 → 拿一个令牌 有令牌 → 放行 🟢 没令牌 → 拒绝 🔴 ✅ 允许突发(桶内有积累令牌) ✅ 长期看仍限制平均速率 ✅ Guava RateLimiter 实现 适合:API限流(最推荐)

图4:限流 4 种算法 — 固定窗口/滑动窗口/漏桶/令牌桶

RateLimitDemo.java — 限流算法实战
📂 rate-limit
RateLimitDemo.java
SentinelConfig.java
📂 controller
OrderController.java
// ========== 限流算法实战:Guava + Sentinel ========== // ========== ① Guava 令牌桶(单机限流)========== @Service public class GuavaRateLimitService { // 每秒生成100个令牌 → 允许突发请求消费积累的令牌 private final RateLimiter limiter = RateLimiter.create(100.0); public String doSomething() { if (!limiter.tryAcquire()) { throw new TooManyRequestsException("限流了,请稍后重试"); } return "处理完成"; } // 预热模式:前10秒慢慢提速到100/s private final RateLimiter warmupLimiter = RateLimiter.create(100.0, 10, TimeUnit.SECONDS); } // ========== ② Sentinel 分布式限流(集群限流)========== @RestController public class OrderController { @GetMapping("/order/create") @SentinelResource(value = "createOrder", blockHandler = "createOrderBlockHandler", // 限流后的处理 fallback = "createOrderFallback") // 异常后的降级 public Result createOrder(OrderDTO dto) { return orderService.create(dto); } // 限流后的处理方法(签名必须一致 + BlockException 参数) public Result createOrderBlockHandler(OrderDTO dto, BlockException ex) { return Result.fail("系统繁忙,请稍后再试"); } // 异常降级处理 public Result createOrderFallback(OrderDTO dto, Throwable e) { return Result.fail("创建订单失败,请稍后重试"); } } // ========== ③ Sentinel 规则配置(代码方式)========== @PostConstruct public void initFlowRules() { FlowRule rule = new FlowRule(); rule.setResource("createOrder"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS限流 rule.setCount(100); // 阈值:100 QPS rule.setLimitApp("default"); // 来源限制 // 排队等待模式(匀速通过,类似漏桶效果) rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); rule.setMaxQueueingTimeMs(5000); // 最大排队5秒 FlowRuleManager.loadRules(Collections.singletonList(rule)); } // ========== ④ 自定义注解 + AOP 实现接口级限流(Redis + Lua)========== @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int qps() default 100; // 每秒允许的请求数 String message() default "请求过于频繁"; } // 使用:@RateLimit(qps = 50, message = "操作太快了")
限流方案选择决策树:
单机限流:Guava RateLimiter(令牌桶),简单无依赖,适合单体应用
集群限流:Redis + Lua(分布式计数),或 Sentinel(dashboard可视化管理)
网关限流:Sentinel Gateway 适配器 / Nginx limit_req(最外层拦截)
选型口诀:能网关限就不服务限,能集群限就不单机限,令牌桶优先于漏桶

⑤ 负载均衡策略图解

⚖️ 请求分给谁?7种策略

核心机制:
5个请求来了,4个处理节点:轮着来(轮询)/ 谁闲分给谁(最少连接)/ 性能强的多分(加权)/ 随机分(随机)/ 同一来源固定分给同一节点(IP Hash)
负载均衡策略对比 客户端 ⚖️ 负载均衡器 Nginx / Gateway 选择策略→ Server A Server B Server C 策略 原理 优点 缺点 轮询 Round Robin A→B→C→A... 简单公平 不考虑服务器差异 加权轮询 ⭐ A(5)→B(3)→C(2) 考虑服务器能力 权重需要手动调 最少连接 发给当前连接最少的 动态适配负载 需要维护连接计数 IP Hash 同一IP固定一台 Session保持 不均匀/扩缩容失效 随机 随机选一台 最简单 分布不均匀 一致性Hash ⭐ Hash环,虚拟节点 扩缩容影响小 实现复杂

图5:负载均衡 7 种策略对比

⑥ 熔断器模式图解

🔌 服务降级自救机制

当下游服务异常率过高时,不再调用它,直接走降级逻辑(返回默认值/缓存值),给下游恢复的时间。这就是熔断器模式。

保险丝类比:
家里用电过载 → 保险丝自动断开(熔断)→ 防止火灾 → 过一会再尝试合闸(半开)→ 如果还过载就继续断开,如果正常就恢复供电(关闭)。
熔断器三状态转换 🟢 关闭(Closed) 正常放行所有请求 统计失败率 失败率 > 阈值(如50%) → 转为打开状态 🔴 打开(Open) 所有请求直接降级 不再调用下游 等待冷却时间(如30s) → 转为半开状态 🟡 半开(Half-Open) 放行少量请求探测 测试下游是否恢复 成功 → 关闭(恢复) 失败 → 打开(继续熔断) 失败率高 冷却到期 探测失败→继续熔断 探测成功→恢复

图6:熔断器三状态转换 — 关闭→打开→半开→关闭

CircuitBreakerDemo.java — 熔断降级实战
📂 resilience
CircuitBreakerDemo.java
FallbackService.java
📂 config
DegradeRuleConfig.java
// ========== 熔断降级实战:Sentinel + Resilience4j ========== // ========== ① Sentinel 熔断规则(代码配置)========== @PostConstruct public void initDegradeRules() { List<DegradeRule> rules = new ArrayList<>(); // 规则1:异常比例熔断(RT超过300ms算慢调用) DegradeRule rule1 = new DegradeRule(); rule1.setResource("queryUser"); rule1.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); rule1.setCount(0.5); // 异常比例阈值 50% rule1.setTimeWindow(30); // 熔断持续时间30秒 rule1.setMinRequestAmount(20); // 最少20个请求才统计 rules.add(rule1); // 规则2:慢调用比例熔断 DegradeRule rule2 = new DegradeRule(); rule2.setResource("queryUser"); rule2.setGrade(RuleConstant.DEGRADE_GRADE_RT); rule2.setCount(300); // RT阈值300ms rule2.setSlowRatioThreshold(0.6); // 慢调用比例60% rule2.setTimeWindow(20); // 熔断20秒 rules.add(rule2); DegradeRuleManager.loadRules(rules); } // ========== ② Resilience4j 熔断器(编程式,更灵活)========== // 熔断器配置 CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) // 失败率阈值50% .slowCallRateThreshold(60) // 慢调用比例阈值60% .slowCallDurationThreshold(Duration.ofSeconds(2)) // 慢调用定义 .minimumNumberOfCalls(20) // 最少调用20次才统计 .waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断30秒 .permittedNumberOfCallsInHalfOpenState(5) // 半开时放5个 .slidingWindowSize(100) // 滑动窗口大小 .build(); CircuitBreaker cb = CircuitBreaker.of("userService", config); // ========== ③ 熔断后的降级逻辑 ========== @SentinelResource(value = "queryUser", fallback = "fallbackUser") public UserVO queryUser(Long userId) { return userService.getById(userId); // 正常调用 } // 降级方法:返回缓存或默认值(熔断时调用) public UserVO fallbackUser(Long userId, Throwable e) { UserVO cached = localCache.getIfPresent(userId); if (cached != null) { cached.setFromCache(true); // 标记是缓存数据 return cached; } return UserVO.defaultUser(); // 返回默认用户 } // ========== ④ OpenFeign + Sentinel 自动熔断 ========== @FeignClient(name = "user-service", fallback = UserClientFallback.class) public interface UserClient { @GetMapping("/user/{id}") UserVO getById(@PathVariable Long id); } // Feign 降级类:Feign调用失败/熔断时自动走这里 @Component public class UserClientFallback implements UserClient { @Override public UserVO getById(Long id) { return UserVO.defaultUser(); // 返回兜底数据 } }
熔断器 vs 限流器 vs 降级 — 三者关系:
限流器(RateLimiter):入口处拦截,控制 QPS 不超过系统承受能力。预防性
熔断器(CircuitBreaker):下游异常时自动断开,不再调用。被动触发,保护系统
降级(Fallback):核心功能不可用时的备选方案(返回缓存/默认值/友好提示)。兜底
口诀:限流防入口、熔断防扩散、降级保兜底

⑦ 雪花算法 ID 图解

❄️ 分布式唯一 ID 生成方案

分布式系统中,自增ID和UUID各有缺陷。雪花算法(Snowflake)生成 64 bit 的唯一 ID,兼具有序性和高性能。

编号含日期+分店号+序号类比:
你在连锁店开小票:20260613(日期)+ 003(分店号)+ 0001(当天第1单)= 全连锁唯一。雪花 ID 也一样:时间戳 + 机器标识 + 序列号 = 全局唯一。
雪花算法 ID 结构(64 bit) 0 1bit 时间戳(41 bit) 毫秒级精度,可用约69年 从自定义纪元开始计算 41 bit 数据中心 5 bit 32个 5 bit 机器ID 5 bit 32台/中心 5 bit 序列号(12 bit) 同一毫秒内递增 4096个/毫秒/机器 12 bit 示例:0 | 1101010110111001010111 | 00101 | 00011 | 000000010101 → 时间戳=2026-06-13 15:30:00 | 数据中心=5 | 机器=3 | 序列=21 ✅ 趋势递增(索引友好) 同一毫秒内有序,不同毫秒也递增 ✅ 不依赖数据库 本地生成,无网络开销 ⚠️ 时钟回拨问题 NTP校时可能导致ID重复

图7:雪花算法 ID 结构 — 64 bit 分布式唯一 ID

SnowflakeIdGenerator.java — 雪花算法实现
📂 common
SnowflakeIdGenerator.java
IdGeneratorConfig.java
📂 distributed
RedisWorkerIdAssigner.java
// ========== 雪花算法 ID 生成器实战 ========== public class SnowflakeIdGenerator { // 起始时间戳(2024-01-01 00:00:00) private static final long START_TIMESTAMP = 1704067200000L; // 各部分占用 bit 数 private static final long SEQUENCE_BIT = 12; // 序列号 12 bit private static final long WORKER_BIT = 5; // 机器ID 5 bit private static final long DATACENTER_BIT = 5; // 数据中心 5 bit // 最大值(位运算计算) private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT); // 4095 private static final long MAX_WORKER_ID = ~(-1L << WORKER_BIT); // 31 private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_BIT); // 31 // 各部分左移位数 private static final long WORKER_LEFT = SEQUENCE_BIT; // 12 private static final long DATACENTER_LEFT = SEQUENCE_BIT + WORKER_BIT; // 17 private static final long TIMESTAMP_LEFT = SEQUENCE_BIT + WORKER_BIT + DATACENTER_BIT; // 22 private final long workerId; private final long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; public SnowflakeIdGenerator(long workerId, long datacenterId) { if (workerId > MAX_WORKER_ID || workerId < 0) throw new IllegalArgumentException("workerId 超范围"); if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) throw new IllegalArgumentException("datacenterId 超范围"); this.workerId = workerId; this.datacenterId = datacenterId; } // 核心:生成下一个 ID(线程安全) public synchronized long nextId() { long currentTimestamp = timeGen(); // ⚠️ 时钟回拨检测(NTP校时导致当前时间 < 上次时间) if (currentTimestamp < lastTimestamp) { throw new RuntimeException( "时钟回拨!拒绝生成ID,回拨(" + (lastTimestamp - currentTimestamp) + "ms)"); } if (currentTimestamp == lastTimestamp) { // 同一毫秒内 → 序列号递增 sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == 0) { // 序列号溢出 → 等到下一毫秒 currentTimestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; // 新的毫秒 → 序列号归零 } lastTimestamp = currentTimestamp; // 拼接各部分:时间戳 | 数据中心 | 机器 | 序列号 return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT) | (datacenterId << DATACENTER_LEFT) | (workerId << WORKER_LEFT) | sequence; } private long tilNextMillis(long last) { long t = timeGen(); while (t <= last) t = timeGen(); return t; } private long timeGen() { return System.currentTimeMillis(); } } // ========== 使用示例 ========== SnowflakeIdGenerator gen = new SnowflakeIdGenerator(1, 1); long orderId = gen.nextId(); // 例:487926936723734528 // 二进制:000...010110...00001 00001 000000000000 // 时间戳部分 DC 机器 序列号
雪花 ID 的机器号分配方案(分布式部署必考):
配置文件手动指定:简单,但运维麻烦(每台机器不同配置)
Redis 自增分配:启动时 INCR,获取唯一 workerId,宕机释放
Zookeeper 临时节点:注册临时节点获取序号,宕机自动释放
数据库自增:启动时 INSERT 获取自增ID作为 workerId
百度的 UidGenerator:使用 RingBuffer 缓存生成的ID,解决时钟回拨问题
美团的 Leaf:支持号段模式和 Snowflake 模式,解决时钟回拨
时钟回拨问题的应对策略:
直接抛异常(默认):回拨超过阈值时拒绝服务
等待追上:回拨时间短(如<5ms)时等待时间追上
使用历史时间:记录上次时间戳,回拨时使用上次时间+1ms继续
Leaf-Snowflake 方案:Zookeeper 每隔一段时间上报自身时间,通过后才能提供ID服务
核心三原则速记

分布式三原则:① CAP 不可同时满足三元,BASE 是 AP 的实践落地 → ② 缓存一致性:先更新DB再删缓存,避免脏数据窗口 → ③ 分布式锁优先用 Redisson 看门狗续期,别手写 SETNX 轮询

❓ 高频面试题速答

面试官:说一下你知道的分布式事务方案?

4种主流方案,按一致性从强到弱排:

① 2PC(两阶段提交):协调者发 Prepare,所有参与者回复 OK 后发 Commit。强一致但同步阻塞、单点故障。很少用在业务层,主要用于数据库 XA

② TCC(Try-Confirm-Cancel):业务层两阶段。Try 预冻结资源 → Confirm 确认 → Cancel 回滚。高性能但侵入性强,需写3个方法。资金/库存场景首选

③ SAGA:长事务拆成多个短事务,每步都有补偿操作。失败时反向补偿。适合长流程业务编排

④ 本地消息表:业务数据和消息表在同一事务写入,定时任务扫表发MQ。实现简单,最终一致。最常用的轻量级方案

实际选型:互联网公司90%用本地消息表/事务消息(RocketMQ),资金场景用TCC(Seata),不用2PC(太慢)

面试官:缓存和数据库双写一致性问题怎么解决?

最常用的 Cache Aside 模式:读时先查缓存→miss查DB→回填缓存;写时先更新DB→再删缓存

为什么是"删除"而不是"更新"缓存?避免并发写时两个线程交替更新导致不一致。删了之后下次读自然会查DB回填最新值。

为什么是"先DB后缓存"而不是"先缓存后DB"?如果先删缓存再更新DB,并发时另一线程可能读到旧DB值回填缓存,导致缓存里是旧数据。先DB后缓存 + 延迟双删(更新后等几百ms再删一次)可以极大降低不一致概率。

终极方案:Canal 监听 MySQL binlog,异步删除缓存,实现最终一致性且不侵入业务代码。

面试官:缓存穿透、击穿、雪崩分别是什么?怎么解决?

穿透:查一个不存在的数据,每次都打到DB。可能是恶意攻击。
解决:①缓存空值(短TTL)②布隆过滤器前置拦截

击穿:一个热点key突然过期,大量请求同时打到DB。
解决:①互斥锁(setnx)只让一个请求查DB ②热点key永不过期 ③逻辑过期

雪崩:大量key同时过期(或Redis宕机),所有请求全打到DB。
解决:①TTL加随机值避免同时过期 ②多级缓存(L1本地+L2 Redis) ③Redis高可用(哨兵/Cluster)④熔断降级

记忆口诀:穿透=查不存在的→布隆过滤;击穿=热点过期→互斥锁;雪崩=批量过期→随机TTL

面试官:令牌桶和漏桶有什么区别?项目里怎么选?

漏桶:请求像水一样倒入桶中,底部以固定速率漏出。不管来多快都匀速输出。适合需要严格匀速的场景(如秒杀系统的订单消费)

令牌桶:以固定速率往桶里放令牌,请求来了拿令牌才能通过。桶可以积累令牌,所以允许一定程度的突发流量

区别核心:漏桶限制的是输出速率(绝对平滑),令牌桶限制的是平均速率(允许突发)。

选型:大多数API限流用令牌桶(Guava RateLimiter / Sentinel默认),需要严格匀速用漏桶(Sentinel 排队等待模式)

面试官:Sentinel 和 Hystrix 有什么区别?为什么选 Sentinel?

熔断策略:Hystrix 只有异常比例熔断;Sentinel 支持异常比例 + 慢调用比例(RT)两种

限流能力:Hystrix 没有限流功能(只能熔断降级);Sentinel 限流 + 熔断 + 系统自适应保护一体化

控制台:Hystrix Dashboard 只能看;Sentinel Dashboard 可以实时修改规则并推送

规则持久化:Sentinel 支持 Nacos/Zookeeper/Apollo 持久化;Hystrix 配置在代码中

维护状态:Hystrix 已停止维护(1.5.x最终版);Sentinel 阿里巴巴持续更新中

总结:新项目直接用 Sentinel,老项目可以 Hystrix → Resilience4j 迁移

面试官:雪花算法的时钟回拨问题怎么解决?

问题:NTP 校时导致系统时间回拨,回拨后生成的 ID 时间戳变小,可能出现 ID 重复。

4种方案:

抛异常拒绝服务:回拨超过阈值直接报错(最简单,但影响可用性)

等待追上:回拨时间短(如<5ms)时 sleep 等待时间追上后继续

使用扩展位:在 64bit 中留几位作为时钟回拨的序列号扩展位

Leaf-Snowflake:美团的方案,启动时通过 Zookeeper 上报时间戳并比对,确保机器时间正确后才提供服务

实际建议:用百度 UidGenerator 或美团 Leaf,它们已内置时钟回拨处理

面试官:一致性哈希是什么?解决了什么问题?

问题:普通取模 hash(hash % N)在节点数N变化时,几乎所有数据的映射都会改变,导致缓存大面积失效。

一致性哈希:把整个哈希空间组织成一个虚拟的圆环(0 ~ 2^32-1),数据和节点都映射到环上。数据顺时针找到的第一个节点就是它的归属节点。

核心优势:节点增删时只影响环上相邻的一小段数据,大部分数据映射不变。

虚拟节点:实际中节点数少时分布不均,给每个物理节点分配 150~200 个虚拟节点,让数据分布更均匀。

使用场景:Redis Cluster(16384个槽位)、Memcached、负载均衡

面试官:熔断器的三个状态是什么?半开状态的意义是什么?

三个状态:

关闭(Closed):正常状态,所有请求放行,同时统计失败率

打开(Open):失败率超过阈值后熔断,所有请求直接走降级逻辑,不再调用下游

半开(Half-Open):熔断冷却时间到了之后,放行少量请求(如5个)去试探下游是否恢复

半开状态的意义:它是"自动恢复"的探测机制。如果探测请求成功了 → 恢复正常(关闭);如果失败了 → 继续熔断(打开)。这样不需要人工干预就能自动恢复。

参数调优:熔断阈值通常设50%失败率,冷却时间30秒,半开放行5个请求

📋 本章知识速查卡

主题核心要点面试高频
分布式事务2PC强一致慢、TCC高性能高侵入、SAGA长事务、本地消息表简单⭐⭐⭐⭐⭐
缓存4种模式Cache Aside最常用、Read/Write Through缓存组件管、Write Behind异步⭐⭐⭐⭐
缓存三害穿透→布隆过滤器、击穿→互斥锁、雪崩→随机TTL⭐⭐⭐⭐⭐
限流4算法令牌桶最推荐、滑动窗口精确、漏桶平滑、固定窗口简单⭐⭐⭐⭐
负载均衡加权轮询最常用、一致性Hash扩缩容友好、最少连接动态适配⭐⭐⭐
熔断器三态转换:关闭→打开→半开→关闭;框架:Sentinel/Hystrix⭐⭐⭐⭐
雪花ID64bit=1+41+5+5+12;趋势递增;时钟回拨问题⭐⭐⭐