Appearance
分布式系统详解
一、为什么需要分布式?
1.1 单机架构的极限
单机架构存在物理上限:单台服务器 CPU 核心数有限、内存最大几百 GB、磁盘 IOPS 有瓶颈、网络带宽有上限。当业务增长到一定规模,单机无法承载:
| 瓶颈类型 | 单机上限(典型值) | 突破方式 |
|---|---|---|
| CPU | 128 核 | 分布式计算,多机并行 |
| 内存 | 1-2 TB | 分布式缓存,数据分片 |
| 磁盘 IO | 10万 IOPS | 分布式存储,读写分离 |
| 网络带宽 | 10-25 Gbps | 负载均衡,多机分担 |
| 可用性 | 单点故障 | 多副本,故障转移 |
分布式系统的定义: 一组独立的计算机通过网络互联,协同工作,对外呈现为一个统一的整体。其核心目标是通过横向扩展(Scale Out)替代纵向扩展(Scale Up),用廉价机器集群替代昂贵的大型机。
1.2 分布式的代价
分布式的引入不是免费的,它带来了显著的复杂性:
| 挑战 | 描述 |
|---|---|
| 网络不可靠 | 网络延迟、丢包、分区,无法假设网络是可靠的 |
| 时钟不同步 | 各节点时钟可能不一致,无法依赖全局时钟排序 |
| 部分失败 | 不是所有节点都失败,而是部分节点失败,处理更复杂 |
| 数据一致性 | 多副本之间如何保持一致,CAP 定理的约束 |
| 运维复杂度 | 部署、监控、日志收集、链路追踪都需要分布式支持 |
二、CAP 定理
2.1 定理定义
CAP 定理(Brewer's Theorem)由 Eric Brewer 于 2000 年提出:一个分布式系统最多只能同时满足以下三个特性中的两个:
| 特性 | 含义 | 举例 |
|---|---|---|
| Consistency(一致性) | 所有节点在同一时刻看到相同的数据 | 写完后立即读,一定能读到最新值 |
| Availability(可用性) | 每个请求都能获得非错误的响应(不保证是最新数据) | 任何时候都能访问,不会超时或报错 |
| Partition Tolerance(分区容错性) | 系统在任意网络分区故障下仍能正常运作 | 节点间网络断开,系统仍能工作 |
2.2 为什么只能三选二?
+-----------+
/ \
/ CA \
/ (单机系统) \
/ \
/ CP AP \
/ (强一致) (高可用) \
+---------------------------+
P (分区容错性 - 必须选择)关键认知: 在分布式系统中,网络分区(P)是不可避免的。网络总是可能出问题,所以 P 必须选择。真正的选择是在 CP 和 AP 之间:
- CP without A: 发生分区时,牺牲可用性,保证数据一致。例如 Zookeeper 在 Leader 选举期间拒绝服务。
- AP without C: 发生分区时,牺牲强一致性,保证系统可用。例如 Eureka 的自我保护模式,允许读取过期数据。
2.3 实际场景选择
| 场景 | 选择 | 原因 | 技术 |
|---|---|---|---|
| 银行转账、余额查询 | CP | 宁可不可用也不能数据错 | Zookeeper, Etcd, Consul |
| 社交动态、商品浏览 | AP | 看到旧数据总比看不到好 | Eureka, Nacos(AP模式), Cassandra |
| 注册中心 | AP | 服务发现宁可数据旧,不能不可用 | Eureka, Nacos(默认AP) |
| 配置中心 | CP | 配置错误会导致业务故障 | Nacos(CP模式), Apollo |
三、BASE 理论
BASE 理论是 CAP 定理中 AP 方案的实践延伸,由 eBay 架构师 Dan Pritchett 提出:
| 缩写 | 全称 | 含义 |
|---|---|---|
| Basically Available | 基本可用 | 系统出现故障时,允许损失部分可用性(如响应时间变长、功能降级) |
| Soft State | 软状态 | 允许系统中的数据存在中间状态,该状态不影响系统整体可用性 |
| Eventually Consistent | 最终一致性 | 系统中的所有数据副本经过一段时间后,最终达到一致状态 |
BASE 与 ACID 对比:
| 维度 | ACID | BASE |
|---|---|---|
| 核心思想 | 强一致性 | 最终一致性 |
| 代表场景 | 传统关系型数据库 | 分布式系统、NoSQL |
| 一致性要求 | 事务完成后立即一致 | 允许短暂不一致,最终一致 |
| 可用性 | 可能因加锁而降低 | 优先保证可用性 |
| 典型实现 | MySQL InnoDB, Oracle | Cassandra, DynamoDB, 基于 MQ 的异步架构 |
最终一致性实践:
java
// 电商下单:最终一致性模式
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional
public void createOrder(CreateOrderRequest request) {
// 1. 创建订单(状态:待支付)
Order order = new Order();
order.setStatus(OrderStatus.PENDING_PAYMENT);
orderDao.save(order);
// 2. 发送订单创建事件(异步通知库存服务扣库存)
// 此时库存可能还没扣,但最终会一致
OrderCreatedEvent event = new OrderCreatedEvent(order);
rocketMQTemplate.convertAndSend("order-created", event);
// 3. 返回成功(不必等库存扣完)
// 如果库存扣减失败,通过补偿机制处理
}
}四、分布式 ID 生成
4.1 为什么需要分布式 ID?
在分库分表场景下,数据库自增 ID 会重复,需要全局唯一的 ID 生成方案。
方案对比:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| UUID | 随机生成 128 位 | 本地生成,无网络开销 | 无序,字符串长,索引效率差 | 小规模,非主键 |
| 数据库自增 | 利用数据库自增 ID | 简单,单调递增 | 单点瓶颈,性能有限 | 小型项目 |
| 数据库号段 | 批量预取号段 | 性能好,灵活 | 依赖数据库 | 中大型项目 |
| Snowflake | 时间戳 + 机器 ID + 序列号 | 趋势递增,高性能 | 依赖时钟同步 | 大型分布式系统 |
| Redis 自增 | INCR 原子操作 | 简单,性能好 | 需要持久化,数据丢失风险 | 对 ID 无严格连续性要求 |
4.2 Snowflake(雪花算法)详解
Twitter 开源的分布式 ID 算法,生成 64 位 Long 型 ID:
+-----------------------------------------------------------+
| 1bit | 41bit | 10bit | 12bit |
| 符号 | 时间戳 | 机器 ID | 序列号 |
| 位 | (毫秒级) | (5+5) | (同一毫秒内) |
+-----------------------------------------------------------+
未使用 可用 69 年 1024 节点 每毫秒 4096 个java
/**
* Snowflake 分布式 ID 生成器实现
*/
public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01 00:00:00)
private static final long START_TIMESTAMP = 1577836800000L;
// 各部分占用的位数
private static final long DATA_CENTER_ID_BITS = 5L; // 数据中心 ID 位数
private static final long WORKER_ID_BITS = 5L; // 机器 ID 位数
private static final long SEQUENCE_BITS = 12L; // 序列号位数
// 最大值
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); // 31
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 31
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
// 位移量
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 12
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 17
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; // 22
private final long dataCenterId;
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long workerId) {
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException("Data center ID must be 0-31");
}
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("Worker ID must be 0-31");
}
this.dataCenterId = dataCenterId;
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 回拨不超过 5ms,等待追上
try {
Thread.sleep(offset << 1);
timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards after waiting");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Clock moved backwards by " + offset + "ms");
}
}
if (timestamp == lastTimestamp) {
// 同一毫秒内,序列号递增
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// 序列号用完,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
// 使用示例
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
long orderId = idGenerator.nextId(); // 生成全局唯一订单 IDSnowflake 优缺点:
| 优点 | 缺点 |
|---|---|
| 本地生成,无网络开销,性能极高 | 强依赖机器时钟,时钟回拨可能导致 ID 重复 |
| 趋势递增,数据库索引友好 | 递增特性可能暴露业务量(竞品可推算) |
| 可灵活配置位数,适应不同规模 | 机器 ID 需要手动分配 |
4.3 数据库号段模式
java
/**
* 号段模式:每次从数据库批量获取一段 ID,内存中分配
*/
@Service
public class SegmentIdGenerator {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取下一个号段
* UPDATE id_generator SET max_id = max_id + step WHERE biz_type = 'order'
* 返回更新后的 max_id,号段范围为 (max_id - step, max_id]
*/
public Segment getNextSegment(String bizType) {
// 原子更新:获取号段
String sql = "UPDATE id_generator SET max_id = max_id + ? WHERE biz_type = ?";
jdbcTemplate.update(sql, 1000, bizType); // step = 1000
// 查询新的 max_id
Long maxId = jdbcTemplate.queryForObject(
"SELECT max_id FROM id_generator WHERE biz_type = ?", Long.class, bizType
);
return new Segment(maxId - 1000 + 1, maxId);
}
}五、分布式锁
5.1 为什么需要分布式锁?
在单机环境下,synchronized 或 ReentrantLock 可以保证线程安全。但在分布式环境下,多个服务实例操作同一共享资源(如扣减库存),需要跨进程的锁机制。
5.2 方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 数据库乐观锁 | UPDATE ... SET version = version + 1 WHERE version = ? | 简单,无需额外组件 | 性能差,不适合高并发 |
| Redis 分布式锁 | SET key value NX PX 30000 | 性能高,使用广泛 | 锁过期问题,RedLock 有争议 |
| Zookeeper 临时顺序节点 | 创建临时顺序节点,序号最小的获得锁 | 可靠性高,自动释放 | 性能不如 Redis |
5.3 Redis 分布式锁
java
/**
* Redis 分布式锁实现(单节点)
*/
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_SUCCESS = "OK";
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 加锁
* @param lockKey 锁的 key
* @param requestId 请求标识(UUID),用于解锁时验证身份
* @param expireTime 过期时间(秒),防止死锁
*/
public boolean lock(String lockKey, String requestId, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
RedisStringCommands.SetOption option =
RedisStringCommands.SetOption.ifAbsent();
RedisStringCommands.Expiration expiration =
RedisStringCommands.Expiration.seconds(expireTime);
byte[] result = connection.set(
lockKey.getBytes(),
requestId.getBytes(),
expiration,
option
);
return LOCK_SUCCESS.equals(new String(result));
});
}
/**
* 解锁(Lua 脚本保证原子性)
* 必须校验 requestId,防止误删别人的锁
*/
public boolean unlock(String lockKey, String requestId) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), requestId);
return result != null && result == 1L;
}
}
// 使用示例
@Service
public class InventoryService {
@Autowired
private RedisDistributedLock lock;
public void deductStock(Long productId, int quantity) {
String lockKey = "stock:lock:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试加锁,最多等待 10 秒
boolean locked = lock.lock(lockKey, requestId, 30);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 执行业务逻辑
int stock = stockDao.getStock(productId);
if (stock < quantity) {
throw new BusinessException("库存不足");
}
stockDao.deduct(productId, quantity);
} finally {
// 释放锁
lock.unlock(lockKey, requestId);
}
}
}5.4 Redisson 分布式锁(推荐)
java
// Redisson 封装了完整的分布式锁实现,支持自动续期、可重入、公平锁
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
@Service
public class InventoryService {
@Autowired
private RedissonClient redissonClient;
public void deductStock(Long productId, int quantity) {
RLock lock = redissonClient.getLock("stock:lock:" + productId);
try {
// 尝试加锁,等待 10 秒,锁自动过期 30 秒
// Redisson 的 Watchdog 会自动续期(默认每 10 秒续期到 30 秒)
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙");
}
// 业务逻辑
int stock = stockDao.getStock(productId);
if (stock < quantity) {
throw new BusinessException("库存不足");
}
stockDao.deduct(productId, quantity);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁(自动判断是否为当前线程持有)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}5.5 Zookeeper 分布式锁
java
/**
* Zookeeper 分布式锁原理:
* 1. 所有客户端在同一个锁路径下创建临时顺序节点
* 2. 序号最小的节点获得锁
* 3. 未获得锁的客户端 watch 前一个节点
* 4. 前一个节点释放(删除)时,下一个节点被通知获得锁
*/
@Component
public class ZkDistributedLock {
private final CuratorFramework client;
private final ThreadLocal<String> lockPath = new ThreadLocal<>();
public ZkDistributedLock(CuratorFramework client) {
this.client = client;
}
public boolean lock(String lockKey, long timeoutMs) throws Exception {
// 创建临时顺序节点
String path = client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath("/locks/" + lockKey + "/lock-");
lockPath.set(path);
// 获取所有子节点,按序号排序
List<String> children = client.getChildren()
.forPath("/locks/" + lockKey);
Collections.sort(children);
String currentNode = path.substring(path.lastIndexOf('/') + 1);
int index = children.indexOf(currentNode);
if (index == 0) {
// 序号最小,获得锁
return true;
}
// 监听前一个节点
String prevNode = "/locks/" + lockKey + "/" + children.get(index - 1);
CountDownLatch latch = new CountDownLatch(1);
client.getData().usingWatcher((Watcher) event -> {
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
latch.countDown();
}
}).forPath(prevNode);
return latch.await(timeoutMs, TimeUnit.MILLISECONDS);
}
public void unlock() throws Exception {
String path = lockPath.get();
if (path != null) {
client.delete().forPath(path);
lockPath.remove();
}
}
}六、分布式事务
6.1 方案全景
| 方案 | 一致性 | 性能 | 复杂度 | 侵入性 | 适用场景 |
|---|---|---|---|---|---|
| 2PC(两阶段提交) | 强一致 | 低 | 低 | 低 | 传统数据库跨库事务 |
| TCC(Try-Confirm-Cancel) | 最终一致 | 高 | 高 | 高 | 金融、支付核心链路 |
| Saga 模式 | 最终一致 | 高 | 中 | 中 | 长事务、跨服务工作流 |
| 本地消息表 | 最终一致 | 高 | 中 | 低 | 通用异步场景 |
| 可靠消息最终一致 | 最终一致 | 高 | 中 | 低 | 基于 MQ 的异步场景 |
6.2 TCC 模式详解
TCC = Try + Confirm + Cancel
Try: 预留资源(如冻结库存、预扣余额)
Confirm: 确认提交(使用预留的资源)
Cancel: 取消回滚(释放预留的资源)java
/**
* TCC 分布式事务:下单扣库存扣余额
* 使用 Seata TCC 模式实现
*/
// ========== 库存服务 TCC 接口 ==========
@LocalTCC
public interface InventoryTccAction {
/**
* Try:冻结库存
*/
@TwoPhaseBusinessAction(name = "inventoryTcc", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(@BusinessActionContextParameter("productId") Long productId,
@BusinessActionContextParameter("quantity") Integer quantity);
/**
* Confirm:确认扣减冻结库存
*/
boolean confirm(BusinessActionContext context);
/**
* Cancel:释放冻结库存
*/
boolean cancel(BusinessActionContext context);
}
// ========== 库存服务 TCC 实现 ==========
@Service
public class InventoryTccActionImpl implements InventoryTccAction {
@Autowired
private InventoryFreezeDao freezeDao; // 冻结记录表
@Autowired
private InventoryDao inventoryDao; // 库存表
@Override
@Transactional
public boolean tryDeduct(Long productId, Integer quantity) {
// 1. 检查可用库存(总库存 - 冻结库存)
int availableStock = inventoryDao.getAvailableStock(productId);
if (availableStock < quantity) {
throw new BusinessException("库存不足");
}
// 2. 冻结库存(插入冻结记录)
InventoryFreeze freeze = new InventoryFreeze();
freeze.setXid(RootContext.getXID()); // 全局事务 ID
freeze.setProductId(productId);
freeze.setQuantity(quantity);
freeze.setStatus(FreezeStatus.FROZEN);
freezeDao.save(freeze);
return true;
}
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
String xid = context.getXid();
Long productId = Long.valueOf(context.getActionContext("productId").toString());
Integer quantity = Integer.valueOf(context.getActionContext("quantity").toString());
// 1. 更新冻结记录状态
freezeDao.updateStatus(xid, FreezeStatus.CONFIRMED);
// 2. 真正扣减库存
inventoryDao.deduct(productId, quantity);
return true;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
// 1. 释放冻结库存(更新冻结记录状态)
freezeDao.updateStatus(xid, FreezeStatus.CANCELLED);
return true;
}
}
// ========== 订单服务:调用 TCC 各参与方 ==========
@Service
public class OrderService {
@Autowired
private InventoryTccAction inventoryTccAction;
@Autowired
private AccountTccAction accountTccAction;
@GlobalTransactional // Seata 全局事务注解
@Transactional
public void createOrder(CreateOrderRequest request) {
// 1. 创建订单(本地事务)
orderDao.save(buildOrder(request));
// 2. TCC Try:冻结库存
inventoryTccAction.tryDeduct(request.getProductId(), request.getQuantity());
// 3. TCC Try:冻结余额
accountTccAction.tryFreeze(request.getUserId(), request.getAmount());
// 如果全部 Try 成功,Seata 自动调用各参与方的 Confirm
// 如果任何一步失败,Seata 自动调用各参与方的 Cancel
}
}6.3 Saga 模式
Saga 模式将长事务拆分为多个本地事务,每个本地事务有对应的补偿操作:
正向:创建订单 → 扣减库存 → 扣减余额 → 创建物流单
补偿:取消订单 → 恢复库存 → 恢复余额 → 取消物流单java
/**
* Saga 模式:基于状态机实现
*/
@Service
public class OrderSagaService {
@Autowired
private OrderDao orderDao;
@Autowired
private InventoryService inventoryService;
@Autowired
private AccountService accountService;
@Transactional
public void createOrder(CreateOrderRequest request) {
// 1. 创建订单(状态:初始化)
Order order = new Order();
order.setStatus(OrderStatus.INIT);
orderDao.save(order);
try {
// 2. 扣减库存
inventoryService.deduct(request.getProductId(), request.getQuantity());
order.setStatus(OrderStatus.STOCK_DEDUCTED);
orderDao.update(order);
// 3. 扣减余额
accountService.debit(request.getUserId(), request.getAmount());
order.setStatus(OrderStatus.PAID);
orderDao.update(order);
} catch (Exception e) {
// 补偿:根据当前状态回滚
compensate(order);
throw e;
}
}
private void compensate(Order order) {
switch (order.getStatus()) {
case STOCK_DEDUCTED:
// 已扣库存,恢复库存
inventoryService.rollback(order.getProductId(), order.getQuantity());
break;
case PAID:
// 已扣余额,恢复余额 + 恢复库存
accountService.rollback(order.getUserId(), order.getAmount());
inventoryService.rollback(order.getProductId(), order.getQuantity());
break;
}
order.setStatus(OrderStatus.CANCELLED);
orderDao.update(order);
}
}七、分布式 Session
7.1 问题
在负载均衡环境下,用户的多次请求可能被分发到不同的服务器。如果 Session 存储在本地内存,后续请求会丢失登录状态。
请求1 → 服务器A(登录,创建 Session) → 返回 SessionID
请求2 → 服务器B(无 Session,需要重新登录) → 用户体验差7.2 解决方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Session 粘滞(Sticky) | Nginx ip_hash 将同一 IP 打到同一台机器 | 简单,不改代码 | 机器故障 Session 丢失,负载不均 |
| Session 复制 | 各服务器间广播同步 Session | 应用无感知 | 网络开销大,不适合大规模集群 |
| 集中存储(推荐) | Session 存到 Redis | 高性能,高可用 | 需要 Redis 集群 |
| 无状态 Token(JWT) | 不用 Session,用 JWT Token | 服务端无状态 | Token 无法主动失效 |
7.3 Spring Session + Redis 实现
java
// Spring Session + Redis 集中存储 Session
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 分钟过期
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
// 使用 Jackson 序列化,替代默认的 JDK 序列化
return new GenericJackson2JsonRedisSerializer();
}
}
// application.yml
spring:
session:
store-type: redis
redis:
host: 127.0.0.1
port: 6379八、分布式配置中心
8.1 Nacos vs Apollo
| 维度 | Nacos | Apollo |
|---|---|---|
| 配置格式 | YAML, Properties, JSON, XML | Properties, XML, JSON, YAML |
| 配置生效 | 实时推送 + 客户端轮询 | 实时推送(HTTP 长轮询) |
| 灰度发布 | 支持(按 IP 灰度) | 支持(多维度灰度) |
| 权限管理 | 较简单 | 完善(团队、应用、环境) |
| 部署复杂度 | 简单(单进程) | 较复杂(Config + Admin + Portal) |
| 注册中心 | 内置 | 需要配合 Eureka |
| 适用场景 | 中小型微服务,注册+配置一体化 | 大型企业级配置管理 |
8.2 Nacos 配置中心核心代码
java
// 动态刷新配置
@RestController
@RefreshScope
public class ConfigDemoController {
@Value("${app.name:default}")
private String appName;
@Value("${feature.flag.discount:false}")
private boolean discountEnabled;
@Value("${rate.limit.qps:100}")
private int rateLimitQps;
@GetMapping("/config/info")
public Map<String, Object> getConfig() {
return Map.of(
"appName", appName,
"discountEnabled", discountEnabled,
"rateLimitQps", rateLimitQps
);
}
}
// 监听配置变更
@Component
public class ConfigChangeListener {
@NacosConfigListener(dataId = "application.properties", groupId = "DEFAULT_GROUP")
public void onConfigChange(String newConfig) {
log.info("配置已更新:{}", newConfig);
// 执行自定义逻辑,如刷新本地缓存
}
}九、面试高频问题
Q1: CAP 定理中,为什么不能同时满足三点?
因为网络分区(P)发生时,系统必须在 C 和 A 之间做选择。如果选择 C,则必须拒绝某些请求以保证数据一致;如果选择 A,则必须接受可能返回旧数据。没有网络分区时,C 和 A 可以同时满足,但分布式系统中网络分区是不可避免的。
Q2: BASE 理论中的"软状态"和"最终一致性"有什么区别?
软状态是手段,最终一致性是目标。软状态指允许存在中间状态(如订单状态为"处理中"),最终一致性指经过一段时间后所有数据副本达到一致(如"处理中"最终变为"已完成"或"已取消")。
Q3: Snowflake 算法如何解决时钟回拨问题?
- 短时间回拨(< 5ms):等待追平时间
- 长时间回拨:抛异常,人工介入
- 备用方案:使用额外的回拨计数器位
- 实践:使用 NTP 平滑同步,避免跳跃式调整
Q4: Redis 分布式锁的锁过期问题如何解决?
锁过期(业务还没执行完,锁就过期了)的解决方案:
- Redisson 的 Watchdog 机制:自动续期,默认每 10 秒续期到 30 秒
- 合理设置过期时间:根据业务最长执行时间设置
- 不建议手动设置过长过期时间:会导致死锁恢复慢
Q5: 为什么 RedLock 算法有争议?
Redisson 的 RedLock 方案(多节点独立加锁,多数派成功才算加锁成功)被 Martin Kleppmann 和 Redis 作者 Antirez 争论过。争议点:时钟跳跃可能导致锁安全性问题;GC 停顿可能导致锁过期时间判断失效。实际生产环境中,Redis 单节点或 Sentinel 方案的分布式锁已经足够绝大多数场景。
Q6: TCC 和 Saga 的核心区别是什么?
- TCC 是两阶段:Try 预留资源,Confirm 提交 / Cancel 回滚。需要实现三个接口,侵入性强,但一致性好。
- Saga 是一阶段:每个步骤直接执行,失败时反向补偿。实现简单,但可能出现中间状态被读到。
- 选择:金融场景(资金安全第一)选 TCC;长流程场景(如订单履约)选 Saga。
Q7: 分布式 ID 如何保证全局唯一?
- UUID:通过随机算法保证极低碰撞概率
- Snowflake:通过机器 ID + 时间戳 + 序列号保证唯一
- 数据库号段:通过数据库行锁保证号段不重复
- 关键:不同方案的核心都是划分命名空间,避免冲突
Q8: 分布式系统的一致性问题可以通过哪些方式解决?
- 强一致性:2PC、Paxos/Raft 共识算法
- 最终一致性:消息队列、本地消息表、定时对账、补偿机制
- 读取一致性:主从同步延迟时,强制读主库(Hint:
@Master注解) - 业务容错:幂等设计、状态机驱动、TCC 预留资源
