Skip to content

分布式系统详解

一、为什么需要分布式?

1.1 单机架构的极限

单机架构存在物理上限:单台服务器 CPU 核心数有限、内存最大几百 GB、磁盘 IOPS 有瓶颈、网络带宽有上限。当业务增长到一定规模,单机无法承载:

瓶颈类型单机上限(典型值)突破方式
CPU128 核分布式计算,多机并行
内存1-2 TB分布式缓存,数据分片
磁盘 IO10万 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 对比:

维度ACIDBASE
核心思想强一致性最终一致性
代表场景传统关系型数据库分布式系统、NoSQL
一致性要求事务完成后立即一致允许短暂不一致,最终一致
可用性可能因加锁而降低优先保证可用性
典型实现MySQL InnoDB, OracleCassandra, 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();  // 生成全局唯一订单 ID

Snowflake 优缺点:

优点缺点
本地生成,无网络开销,性能极高强依赖机器时钟,时钟回拨可能导致 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 为什么需要分布式锁?

在单机环境下,synchronizedReentrantLock 可以保证线程安全。但在分布式环境下,多个服务实例操作同一共享资源(如扣减库存),需要跨进程的锁机制。

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

维度NacosApollo
配置格式YAML, Properties, JSON, XMLProperties, 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 预留资源