← 返回 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)
// ========== 分布式事务: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 读写实战
// ========== 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 — 缓存三害防御实战
// ========== 缓存三害的代码级防御方案 ==========
@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 — 限流算法实战
// ========== 限流算法实战: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 — 熔断降级实战
// ========== 熔断降级实战: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 — 雪花算法实现
// ========== 雪花算法 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 ⭐⭐⭐⭐
雪花ID 64bit=1+41+5+5+12;趋势递增;时钟回拨问题 ⭐⭐⭐
↑