Java 分布式理论与高并发编程

文章目录

    • 写在前面
    • 一、分布式理论基础
      • [1.1 CAP 定理:为什么必须做取舍](#1.1 CAP 定理:为什么必须做取舍)
      • [1.2 BASE 理论:AP 的工程落地版](#1.2 BASE 理论:AP 的工程落地版)
      • [1.3 五种一致性模型](#1.3 五种一致性模型)
      • [1.4 分布式系统的两个基础设施](#1.4 分布式系统的两个基础设施)
    • 二、分布式锁:三种实现与选型
      • [2.1 为什么本地锁失效](#2.1 为什么本地锁失效)
      • [2.2 Redis 分布式锁:基础版](#2.2 Redis 分布式锁:基础版)
      • [2.3 Redisson:生产环境首选](#2.3 Redisson:生产环境首选)
      • [2.4 ZooKeeper 分布式锁:临时顺序节点](#2.4 ZooKeeper 分布式锁:临时顺序节点)
      • [2.5 三种方案怎么选](#2.5 三种方案怎么选)
    • 三、分布式事务:六种方案的取舍
      • [3.1 问题的本质](#3.1 问题的本质)
      • [3.2 2PC 两阶段提交](#3.2 2PC 两阶段提交)
      • [3.3 TCC:业务层两阶段](#3.3 TCC:业务层两阶段)
      • [3.4 Saga:长事务的正向执行 + 反向补偿](#3.4 Saga:长事务的正向执行 + 反向补偿)
      • [3.5 本地消息表:最朴素的可靠方案](#3.5 本地消息表:最朴素的可靠方案)
      • [3.6 RocketMQ 事务消息:MQ 帮你保证原子性](#3.6 RocketMQ 事务消息:MQ 帮你保证原子性)
      • [3.7 Seata AT:零侵入方案](#3.7 Seata AT:零侵入方案)
      • [3.8 六种方案横向对比](#3.8 六种方案横向对比)
    • 四、消息队列:异步解耦与可靠投递
      • [4.1 三个核心作用](#4.1 三个核心作用)
      • [4.2 三大 MQ 怎么选](#4.2 三大 MQ 怎么选)
      • [4.3 消息不丢的三道防线](#4.3 消息不丢的三道防线)
      • [4.4 顺序消息](#4.4 顺序消息)
    • 五、并发编程基础:三个必懂的点
      • [5.1 volatile:解决可见性,不解决原子性](#5.1 volatile:解决可见性,不解决原子性)
      • [5.2 synchronized 锁升级:从偏向锁到重量级锁](#5.2 synchronized 锁升级:从偏向锁到重量级锁)
      • [5.3 CAS 与 ABA 问题](#5.3 CAS 与 ABA 问题)
    • [六、JUC 并发包:核心组件拆解](#六、JUC 并发包:核心组件拆解)
      • [6.1 AQS:JUC 的地基](#6.1 AQS:JUC 的地基)
      • [6.2 ReentrantLock vs synchronized](#6.2 ReentrantLock vs synchronized)
      • [6.3 三大并发工具类](#6.3 三大并发工具类)
      • [6.4 ConcurrentHashMap:从分段锁到 CAS + synchronized](#6.4 ConcurrentHashMap:从分段锁到 CAS + synchronized)
      • [6.5 线程池:七个参数和执行流程](#6.5 线程池:七个参数和执行流程)
    • 七、高并发架构:四个关键设计
      • [7.1 缓存策略:Cache Aside 是标准答案](#7.1 缓存策略:Cache Aside 是标准答案)
      • [7.2 缓存三大问题:穿透、击穿、雪崩](#7.2 缓存三大问题:穿透、击穿、雪崩)
      • [7.3 限流:三种算法](#7.3 限流:三种算法)
      • [7.4 熔断降级:Sentinel 三态机](#7.4 熔断降级:Sentinel 三态机)
      • [7.5 分库分表:ShardingSphere 配置](#7.5 分库分表:ShardingSphere 配置)
    • 八、实战:秒杀系统完整架构
      • [8.1 分层架构全景](#8.1 分层架构全景)
      • [8.2 第一层:接口限流 + Redis 预扣](#8.2 第一层:接口限流 + Redis 预扣)
      • [8.3 第二层:MQ 消费者落库](#8.3 第二层:MQ 消费者落库)
      • [8.4 设计要点总结](#8.4 设计要点总结)
    • [九、常见问题 FAQ](#九、常见问题 FAQ)
    • 十、下一步怎么学
    • 总结

写在前面

面试被问到 CAP 定理只能背"三者选二",写代码时用 synchronized 却挡不住分布式并发,缓存加了还是被击穿打垮数据库------这些问题的根源往往不是知识点缺失,而是知识点之间没有连成体系。

这篇文章把 Java 分布式理论和高并发编程串成一条完整链路:从理论(CAP/BASE)→ 基础设施(分布式锁/分布式事务/MQ)→ 语言层(JUC/线程池)→ 架构层(缓存/限流/熔断/分库分表)→ 落地(秒杀系统)。每个知识点都配了可运行的 Java 代码和方案对比表,看完能直接用到项目里。

环境版本:JDK 17 / Spring Boot 3.x / Redis 7 / RocketMQ 5 / Seata 2

适合谁读:有 Java 基础、想系统补齐分布式与并发知识体系的开发者。全文约 1.2 万字,建议先收藏,按需跳读。


一、分布式理论基础

1.1 CAP 定理:为什么必须做取舍

CAP 由 UC Berkeley 的 Eric Brewer 在 2000 年提出,是分布式系统的理论基石。它指出一个分布式系统最多只能同时满足三个特性中的两个:

特性 全称 含义
C Consistency 强一致性 所有节点在同一时刻看到相同的数据
A Availability 可用性 每一次请求都能在有限时间内返回非错误响应
P Partition Tolerance 分区容错性 网络分区(节点间通信中断)时系统仍能运作

核心矛盾:网络分区在现实中不可避免(光纤被挖断、机房网络抖动、跨可用区延迟),所以 P 必须保留。真正的选择是在 C 和 A 之间权衡。

选择 放弃 典型场景 代表系统
CP A 数据不能不一致,容忍超时 ZooKeeper、etcd、HBase
AP C 服务必须可用,允许短期不一致 Eureka、Cassandra、DynamoDB
CA P 理论存在,仅单机 传统 RDBMS

CP 系统的写入代价可以直观感受到------ZooKeeper 必须等到过半节点 ACK 才算成功:

java 复制代码
// CP 系统示例:ZooKeeper 写入流程
// 客户端写入时必须等待过半节点 ACK 才返回成功
// 如果网络分区导致不到半数节点可达,写入会阻塞或失败
Stat stat = zk.setData("/config", newData, -1, (rc, path, ctx, stat1) -> {
    if (rc == Code.OK.intValue()) {
        callback.onSuccess();          // 写入成功:过半节点已持久化
    } else {
        callback.onFailure(KeeperException.create(rc));  // 不够半数节点
    }
}, null);

一句话总结:CAP 不是让你三选二,而是提醒你------既然 P 躲不掉,就得明确回答"网络出问题时,我要数据一致还是要服务可用"。

1.2 BASE 理论:AP 的工程落地版

选了 AP 之后,一致性怎么办?答案是 BASE,由 Dan Pritchett 提出:

缩写 全称 含义
BA Basically Available 基本可用 故障时允许损失部分可用性(响应变慢、降级页面)
S Soft State 软状态 允许系统存在中间状态,不要求每时每刻强一致
E Eventually Consistent 最终一致性 保证最终达成一致,但不需要实时强一致

演进逻辑:CAP 选 AP(放弃强 C)→ 用 BASE 填补空缺 → 不追求实时强一致,而是靠异步同步、补偿机制,在约定时间内达到最终一致。

1.3 五种一致性模型

实际选型时,比"强一致/最终一致"更细的划分是这五种:

模型 含义 典型场景
强一致性 写完成后,后续读立即可见最新值 银行转账、库存扣减
最终一致性 写完成后,延迟一段时间可见 社交动态、消息已读
因果一致性 有因果关系的操作保证顺序 评论回复链
读己之写 自己的写操作,自己后续读一定可见 个人资料修改
单调读 读到某值后,不会再读到更旧的值 信息流刷新

1.4 分布式系统的两个基础设施

服务注册与发现------服务启动时注册,消费方通过注册中心拿到实例列表:

java 复制代码
// 服务注册
@Service
public class OrderService {

    @NacosValue(value = "${server.port}", autoRefreshed = true)
    private int port;

    @PostConstruct
    public void registerService() {
        // 启动时自动注册到 Nacos
        // Nacos 客户端通过定时心跳维持注册状态
    }
}

// 服务发现 + 负载均衡
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    UserDTO getUser(@PathVariable Long id);
}

配置中心------配置变更无需重启:

java 复制代码
@RestController
@RefreshScope  // 配置变更后自动刷新
public class ConfigController {

    @Value("${order.timeout:3000}")
    private int orderTimeout;  // 运行时通过 Nacos 控制台修改,自动生效

    @GetMapping("/timeout")
    public int getTimeout() {
        return orderTimeout;
    }
}

二、分布式锁:三种实现与选型

2.1 为什么本地锁失效

单机环境下 synchronized 够用。但分布式系统中,多个服务实例跑在不同 JVM 里,各自持有独立的锁对象:

复制代码
用户请求 → 负载均衡 → 实例A (JVM-1, synchronized)  ←挡不住→  实例B (JVM-2, synchronized)

两个实例同时扣同一件商品的库存,各扣一次,超卖。这就是分布式锁存在的理由。

2.2 Redis 分布式锁:基础版

核心是 SET key value NX EX ------ 不存在才设置,并带过期时间:

java 复制代码
public class RedisLock {

    private final StringRedisTemplate redis;
    private static final String LOCK_PREFIX = "lock:";
    private static final Duration DEFAULT_EXPIRE = Duration.ofSeconds(30);

    public boolean tryLock(String key, String requestId) {
        // NX:key 不存在才设置成功;EX:自动过期防死锁
        Boolean result = redis.opsForValue()
            .setIfAbsent(LOCK_PREFIX + key, requestId, DEFAULT_EXPIRE);
        return Boolean.TRUE.equals(result);
    }

    public boolean unlock(String key, String requestId) {
        // 必须用 Lua 脚本保证「判断归属 + 删除」的原子性
        String script =
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "  return redis.call('del', KEYS[1]) " +
            "else " +
            "  return 0 " +
            "end";
        Long result = redis.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(LOCK_PREFIX + key),
            requestId
        );
        return result != null && result == 1;
    }
}

为什么要 Lua:如果分成"先 get 判断、再 del"两步,会出现经典事故------A 的锁到期释放 → B 抢到锁 → A 执行到 del,把 B 的锁删了。Lua 脚本在 Redis 里单线程原子执行,杜绝这个窗口。

2.3 Redisson:生产环境首选

手写 Redis 锁有两个坑:锁续期(业务没跑完锁就过期)、可重入。Redisson 都解决了:

java 复制代码
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useClusterServers()
            .addNodeAddress("redis://127.0.0.1:6379")
            .setLockWatchdogTimeout(30000);   // 看门狗超时 30 秒
        return Redisson.create(config);
    }
}

@Service
public class StockService {

    @Autowired
    private RedissonClient redissonClient;

    public boolean deductStock(Long productId) {
        String lockKey = "stock:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 最多等 5 秒,锁 30 秒后自动释放
            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (!locked) {
                return false;   // 拿锁失败,快速失败
            }

            int stock = getStock(productId);
            if (stock <= 0) {
                return false;
            }
            updateStock(productId, stock - 1);
            return true;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            // 必须判断是否当前线程持有,避免误删别人的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

看门狗机制:锁默认 30 秒过期,Redisson 后台每 10 秒检查一次,业务还没结束就自动续期到 30 秒。这样既避免死锁,又不怕业务超时。

2.4 ZooKeeper 分布式锁:临时顺序节点

ZooKeeper 用另一种思路------临时顺序节点天然有序,且会话断开自动删除,不存在"锁过期"的问题:

复制代码
/locks
  /lock_00000001   ← 实例A 创建,序号最小,持有锁
  /lock_00000002   ← 实例B 创建,监听 00000001
  /lock_00000003   ← 实例C 创建,监听 00000002
java 复制代码
public class ZkDistributedLock {

    private final InterProcessMutex mutex;

    public ZkDistributedLock(CuratorFramework client, String lockPath) {
        this.mutex = new InterProcessMutex(client, lockPath);
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws Exception {
        return mutex.acquire(timeout, unit);
    }

    public void unlock() {
        mutex.release();
    }
}

2.5 三种方案怎么选

维度 Redis ZooKeeper 数据库乐观锁
性能 高(内存操作) 中(写盘 + 集群协商) 低(行锁竞争)
可靠性 中(主从切换可能丢锁) 高(临时节点 + 顺序监听) 高
实现复杂度 低 中 低
适用场景 高性能、容忍极小概率丢锁 强一致要求高 低并发、简单场景

结论:秒杀、抢券这类高性能场景用 Redisson;选主、配置变更这类强一致场景用 ZooKeeper;日常小并发直接用数据库乐观锁最省事。


三、分布式事务:六种方案的取舍

3.1 问题的本质

单体应用一个数据库连接就能保证 ACID。拆成微服务后,订单、库存、账户各有独立数据库,本地事务跨不过去:

复制代码
下单 → 订单服务(MySQL-1) → 库存服务(MySQL-2) → 支付服务(MySQL-3)

任何一步失败,前面已成功的操作怎么回滚?这是分布式事务要解决的问题。

3.2 2PC 两阶段提交

阶段一 Prepare :协调者问所有参与者"能不能提交",参与者执行但不提交,锁住资源。

阶段二 Commit/Rollback:全部同意就 Commit,任一拒绝就 Rollback。

复制代码
协调者                 参与者A          参与者B
   |--- Prepare ------->|                |
   |<-- Yes ------------|                |
   |--- Prepare ------------------------>|
   |<-- Yes -----------------------------|
   |--- Commit -------->|                |
   |--- Commit ------------------------->|

致命缺陷:同步阻塞(Prepare 后资源全程锁定)、协调者单点故障、部分节点 Commit 失败导致数据不一致。XA 协议是 2PC 的工业实现,性能是硬伤。

3.3 TCC:业务层两阶段

TCC 把两阶段搬到业务层:Try 预留资源,Confirm 确认,Cancel 释放。

阶段 动作 转账示例
Try 预留资源 冻结 100 元
Confirm 确认操作 扣冻结金额,加到对方账户
Cancel 取消操作 解冻,恢复可用余额
java 复制代码
@LocalTCC
public interface TransferTccAction {

    @TwoPhaseBusinessAction(name = "transferTry",
        commitMethod = "transferConfirm",
        rollbackMethod = "transferCancel")
    boolean transferTry(BusinessActionContext context,
                        @BusinessActionContextParameter("from") String from,
                        @BusinessActionContextParameter("to") String to,
                        @BusinessActionContextParameter("amount") BigDecimal amount);

    boolean transferConfirm(BusinessActionContext context);

    boolean transferCancel(BusinessActionContext context);
}

// Try:冻结金额
@Override
public boolean transferTry(BusinessActionContext ctx, String from, String to, BigDecimal amount) {
    return accountMapper.freezeAmount(from, amount) > 0;
}

// Confirm:确认转账
@Override
public boolean transferConfirm(BusinessActionContext ctx) {
    String from = ctx.getActionContext("from").toString();
    String to = ctx.getActionContext("to").toString();
    BigDecimal amount = new BigDecimal(ctx.getActionContext("amount").toString());
    accountMapper.deductFrozen(from, amount);   // 扣冻结
    accountMapper.addBalance(to, amount);       // 加对方余额
    return true;
}

// Cancel:解冻
@Override
public boolean transferCancel(BusinessActionContext ctx) {
    String from = ctx.getActionContext("from").toString();
    BigDecimal amount = new BigDecimal(ctx.getActionContext("amount").toString());
    return accountMapper.unfreeze(from, amount) > 0;
}

TCC 的坑:三个方法都要自己写,还要处理空回滚、幂等、悬挂。业务侵入性最高,但一致性和性能都最好,适合资金类场景。

3.4 Saga:长事务的正向执行 + 反向补偿

Saga 没有 Try 阶段,直接执行,失败就反向补偿:

复制代码
正向:T1(创建订单) → T2(扣库存) → T3(扣余额)
补偿:C1(取消订单) ← C2(回库存) ← C3(回余额)
java 复制代码
@SagaOrchestration
public class OrderSaga {

    @SagaStep(compensateMethod = "rejectOrder")
    public boolean createOrder(Order order) {
        orderMapper.insert(order);
        return true;
    }

    @SagaStep(compensateMethod = "restoreStock")
    public boolean deductStock(Long productId, int qty) {
        return stockMapper.deduct(productId, qty) > 0;
    }

    @SagaStep(compensateMethod = "refundBalance")
    public boolean deductBalance(String userId, BigDecimal amount) {
        return accountMapper.deduct(userId, amount) > 0;
    }

    // 补偿方法
    public void rejectOrder(Order order) { orderMapper.updateStatus(order.getId(), "CANCELLED"); }
    public void restoreStock(Long productId, int qty) { stockMapper.add(productId, qty); }
    public void refundBalance(String userId, BigDecimal amount) { accountMapper.add(userId, amount); }
}

3.5 本地消息表:最朴素的可靠方案

不需要任何事务框架,靠"业务 + 消息同一本地事务"保证原子性:

复制代码
1. 业务操作 + 写消息表(同一本地事务,要么都成功要么都失败)
2. 定时任务扫描消息表,发送到 MQ
3. 消费者消费消息,执行业务
4. 消费成功 ACK,消息表标记为已完成
java 复制代码
@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);          // 1. 写订单

        LocalMessage message = new LocalMessage();   // 2. 写消息表(同一事务)
        message.setBizKey(order.getId().toString());
        message.setTopic("order-created");
        message.setPayload(JSON.toJSONString(order));
        message.setStatus("PENDING");
        messageMapper.insert(message);
        // 事务提交 → 订单和消息一起落库
    }
}

// 定时扫描待发送消息
@Component
public class MessageScheduler {

    @Scheduled(fixedDelay = 5000)
    public void sendPendingMessages() {
        List<LocalMessage> messages = messageMapper.selectPending(100);
        for (LocalMessage msg : messages) {
            try {
                rocketMQTemplate.convertAndSend(msg.getTopic(), msg.getPayload());
                messageMapper.updateStatus(msg.getId(), "SENT");
            } catch (Exception e) {
                messageMapper.incrementRetry(msg.getId());   // 失败下次重试
            }
        }
    }
}

3.6 RocketMQ 事务消息:MQ 帮你保证原子性

RocketMQ 原生支持半消息,把"发消息"和"本地事务"绑成原子操作:

java 复制代码
@Service
public class OrderTransactionListener implements RocketMQTransactionListener {

    @Autowired
    private OrderMapper orderMapper;

    // 执行本地事务
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Order order = JSON.parseObject(new String(msg.getBody()), Order.class);
        try {
            orderMapper.insert(order);
            return RocketMQLocalTransactionState.COMMIT;     // 成功 → 提交消息
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;   // 失败 → 回滚消息
        }
    }

    // 事务回查:Broker 定期检查未决消息
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(MessageExt msg) {
        Order order = JSON.parseObject(new String(msg.getBody()), Order.class);
        Order existing = orderMapper.selectById(order.getId());
        return existing != null
            ? RocketMQLocalTransactionState.COMMIT
            : RocketMQLocalTransactionState.ROLLBACK;
    }
}

// 发送事务消息
public void createOrderWithTx(Order order) {
    Message<String> msg = MessageBuilder
        .withPayload(JSON.toJSONString(order))
        .setHeader(RocketMQHeaders.KEYS, order.getId())
        .build();
    rocketMQTemplate.sendMessageInTransaction("order-tx-group", "order-created", msg, null);
}

3.7 Seata AT:零侵入方案

业务只加一个注解,Seata 自动记录 undo log 并处理回滚:

java 复制代码
@Service
public class OrderService {

    @GlobalTransactional   // 就这一行
    public void createOrder(Order order) {
        orderMapper.insert(order);                              // 订单库
        stockClient.deduct(order.getProductId(), order.getQuantity());   // 库存库
        accountClient.deduct(order.getUserId(), order.getAmount());      // 账户库
        // 任一步失败,Seata 根据 undo log 自动回滚已执行的本地事务
    }
}

原理:阶段一执行业务 SQL 同时记录 undo log,本地事务直接提交(不锁资源);阶段二全局提交则异步删 undo log,全局回滚则用 undo log 反向补偿。

3.8 六种方案横向对比

方案 一致性 侵入性 性能 复杂度 推荐场景
2PC/XA 强一致 低 差 中 传统金融,强一致刚需
TCC 强一致 高(写3个方法) 好 高 资金、支付核心链路
Saga 最终一致 中 好 中 长流程、跨多服务
本地消息表 最终一致 中 中 低 中小项目首选
MQ 事务消息 最终一致 低 好 中 已用 RocketMQ 的团队
Seata AT 最终一致 极低 中 低 快速落地,通用场景

选型口诀:钱的事用 TCC,流程长用 Saga,图省事用 Seata,不想引框架用本地消息表。


四、消息队列:异步解耦与可靠投递

4.1 三个核心作用

作用 说明 示例
异步解耦 生产者不等消费者处理完 下单后异步发通知
削峰填谷 高速请求入队,消费者按自己节奏处理 秒杀排队
数据同步 系统间数据最终一致 MySQL → ES

4.2 三大 MQ 怎么选

维度 Kafka RocketMQ RabbitMQ
吞吐量 百万级 TPS 十万级 TPS 万级 TPS
延迟 毫秒级 毫秒级 微秒级
顺序消息 分区内有序 分区内有序 不保证
事务消息 有(较弱) 原生支持 不支持
消息回溯 支持(offset) 支持(时间戳) 不支持
适用场景 日志、大数据 电商、金融 低延迟业务

4.3 消息不丢的三道防线

生产端:同步发送 + 检查发送状态:

java 复制代码
public void sendOrderMessage(Order order) {
    Message msg = new Message("order-topic", "order-tag",
        order.getId().toString(),
        JSON.toJSONString(order).getBytes());

    SendResult result = producer.send(msg);   // 同步发送,等 Broker 确认
    if (result.getSendStatus() == SendStatus.SEND_OK) {
        log.info("发送成功: {}", result.getMsgId());
    } else {
        retryOrPersist(msg);   // 失败重试或落库补偿
    }
}

消费端:正常返回才 ACK,异常就不 ACK 让它重试:

java 复制代码
@RocketMQMessageListener(
    topic = "order-topic",
    consumerGroup = "order-consumer-group",
    consumeMode = ConsumeMode.CONCURRENTLY,
    maxReconsumeTimes = 5
)
public class OrderConsumer implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        try {
            Order order = JSON.parseObject(message, Order.class);

            // 幂等检查:已处理过就直接返回
            if (orderMapper.existsById(order.getId())) {
                return;
            }

            processOrder(order);
            // 方法正常返回 → 自动 ACK
        } catch (Exception e) {
            throw new RuntimeException(e);   // 抛异常 → 不 ACK → 自动重试
        }
    }
}

幂等消费:MQ 只能保证 at-least-once,重复消费必须靠业务兜底:

java 复制代码
public class IdempotentConsumer {

    @Autowired
    private StringRedisTemplate redis;

    public void consume(String messageId, Runnable business) {
        // SETNX:key 不存在才设置成功
        Boolean acquired = redis.opsForValue()
            .setIfAbsent("msg:processed:" + messageId, "1", Duration.ofHours(24));

        if (Boolean.TRUE.equals(acquired)) {
            business.run();      // 首次消费,执行业务
        } else {
            log.info("消息已消费,跳过: {}", messageId);
        }
    }
}

4.4 顺序消息

同一业务 ID 的消息必须进同一队列,消费端单线程消费:

java 复制代码
public void sendOrderEvents(Long orderId, String event) {
    Message msg = new Message("order-events", event.getBytes());
    // 用 orderId 做路由,保证同一订单的事件进入同一队列
    producer.send(msg, (mqs, m, arg) -> {
        int index = Math.abs(arg.hashCode()) % mqs.size();
        return mqs.get(index);
    }, orderId.toString());
}

五、并发编程基础:三个必懂的点

5.1 volatile:解决可见性,不解决原子性

JMM 中每个线程有自己的工作内存(CPU 缓存),共享变量修改可能不可见:

java 复制代码
class FlagExample {
    private boolean flag = false;

    void writer() {
        flag = true;        // 线程 A 修改
    }

    void reader() {
        while (!flag) {
            // 线程 B 可能永远读不到最新值,死循环
        }
    }
}

加 volatile 后:强制读主内存、写后立即刷回、禁止指令重排序:

java 复制代码
private volatile boolean flag = false;

注意 :volatile 不保证原子性。flag = true 是原子写,但 count++ 是"读-改-写"三步,多线程下照样丢更新。计数场景用 AtomicInteger。

5.2 synchronized 锁升级:从偏向锁到重量级锁

JDK 6 之后 synchronized 做了大量优化,会根据竞争激烈程度自动升级:

锁状态 适用场景 实现原理 性能
无锁 无竞争 CAS 标记 最快
偏向锁 单线程反复访问 对象头记录线程 ID 极快
轻量级锁 低竞争 CAS 自旋,不阻塞线程 快
重量级锁 高竞争 OS 互斥量,线程阻塞唤醒 慢
java 复制代码
public class LockUpgradeDemo {
    private final Object lock = new Object();

    public void method() {
        synchronized (lock) {
            // 1. 首次访问 → 偏向锁(记录当前线程 ID)
            // 2. 出现竞争 → 升级轻量级锁(CAS 自旋)
            // 3. 自旋失败 → 升级重量级锁(OS 阻塞)
            System.out.println("locked");
        }
    }
}

5.3 CAS 与 ABA 问题

CAS(Compare And Swap)是无锁编程的基石,AtomicInteger 的累加就靠它:

java 复制代码
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
    // 底层循环:do { old = value; } while (!CAS(old, old + 1))
}

ABA 问题:值从 A → B → A,CAS 检查时认为没变过,实际变了两次。典型场景是链表节点被复用导致数据错乱。

java 复制代码
// 用版本号解决 ABA
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);

int oldStamp = ref.getStamp();
Integer oldValue = ref.getReference();
// ... 期间其他线程把值改成 200 又改回 100
ref.compareAndSet(oldValue, 200, oldStamp, oldStamp + 1);
// stamp 不匹配 → CAS 失败,成功识别出 ABA

六、JUC 并发包:核心组件拆解

6.1 AQS:JUC 的地基

AQS(AbstractQueuedSynchronizer)是 ReentrantLock、CountDownLatch、Semaphore 的共同基础。它的核心就两部分:

复制代码
state(int 同步状态,CAS 修改)
  +
CLH 双向队列(存放等待锁的线程)

获取锁流程:
1. CAS 尝试把 state 从 0 改成 1
2. 成功 → 拿到锁,记录持有线程
3. 失败 → 封装成 Node 加入 CLH 队列,park 挂起
4. 前驱节点释放锁 → unpark 唤醒后继
java 复制代码
// AQS 核心骨架(简化)
abstract class AbstractQueuedSynchronizer {
    private volatile int state;
    private transient volatile Node head;
    private transient volatile Node tail;

    protected final boolean acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            Thread.currentThread().interrupt();
    }

    // 留给子类实现:如何 CAS 抢锁
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
}

理解 AQS 的价值 :搞懂它,ReentrantLock、Semaphore、CountDownLatch 的源码就都能读了,因为它们只是 tryAcquire/tryRelease 的不同实现。

6.2 ReentrantLock vs synchronized

特性 synchronized ReentrantLock
实现 JVM 内置 AQS + CAS
可中断 否 lockInterruptibly()
超时获取 否 tryLock(timeout)
公平锁 否 new ReentrantLock(true)
条件变量 wait/notify(1个) newCondition()(多个)
锁释放 自动 必须手动 unlock()
java 复制代码
ReentrantLock lock = new ReentrantLock(true);   // 公平锁

if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        // 业务逻辑
    } finally {
        lock.unlock();    // 必须在 finally 释放
    }
} else {
    // 3 秒拿不到锁,走降级
}

什么时候用 ReentrantLock :需要超时放弃、需要可中断、需要多个条件变量(生产者-消费者分离唤醒)时。其余场景 synchronized 更简洁安全。

6.3 三大并发工具类

CountDownLatch------等 N 个任务都完成:

java 复制代码
CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            Thread.sleep(1000);    // 模拟子任务
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            latch.countDown();     // 计数减 1
        }
    }).start();
}

latch.await();   // 主线程阻塞,直到计数归零
System.out.println("所有任务完成");

CyclicBarrier------N 个线程互相等待,可重复使用:

java 复制代码
CyclicBarrier barrier = new CyclicBarrier(3, () ->
    System.out.println("3 个线程都到达,继续执行"));

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            barrier.await();       // 等齐了才往下走
        } catch (Exception e) {
            Thread.currentThread().interrupt();
        }
    }).start();
}

Semaphore------限制同时访问的线程数:

java 复制代码
Semaphore semaphore = new Semaphore(5);   // 最多 5 个并发

public void access() {
    try {
        semaphore.acquire();      // 拿许可,拿不到就阻塞
        doBusiness();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        semaphore.release();      // 释放许可
    }
}

三者区别:CountDownLatch 是"一个等多个",CyclicBarrier 是"多个互相等",Semaphore 是"限制并发数"。

6.4 ConcurrentHashMap:从分段锁到 CAS + synchronized

JDK 7 用 Segment 分段锁(默认 16 段,锁粒度是段);JDK 8 彻底重写,锁粒度降到单个桶:

java 复制代码
// JDK 8 put 流程
// 1. 计算 hash
// 2. 桶为空 → CAS 插入(无锁)
// 3. 桶非空 → synchronized 锁住头节点 → 链表/红黑树插入
// 4. 链表长度 ≥ 8 → 转红黑树

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key", "value");     // 线程安全,无需外部同步

// 原子操作:key 不存在才计算并放入
map.computeIfAbsent("key", k -> "computed");

6.5 线程池:七个参数和执行流程

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                              // corePoolSize 核心线程数
    20,                              // maximumPoolSize 最大线程数
    60, TimeUnit.SECONDS,           // keepAliveTime 非核心线程空闲存活时间
    new LinkedBlockingQueue<>(100),  // workQueue 任务队列(有界!)
    new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()   // 拒绝策略
);

执行流程(必背):

复制代码
提交任务
  → 核心线程未满 → 创建核心线程执行
  → 核心线程已满 → 进队列排队
  → 队列已满 → 创建非核心线程(直到 maxPoolSize)
  → 线程数达到 max → 执行拒绝策略

四种拒绝策略:

策略 行为
AbortPolicy 抛 RejectedExecutionException(默认)
CallerRunsPolicy 调用者线程自己执行(降级但不丢任务)
DiscardPolicy 静默丢弃新任务
DiscardOldestPolicy 丢队列最旧的,再尝试提交

线程数怎么定:

java 复制代码
// CPU 密集型:核心数 + 1(多一个用于顶替偶发阻塞)
int cpuThreads = Runtime.getRuntime().availableProcessors() + 1;

// IO 密集型:核心数 × 2(大量时间在等 IO)
int ioThreads = cpuThreads * 2;

// 更精确的公式(Little's Law)
// 线程数 = (任务总耗时 / CPU 计算耗时) × CPU 核心数

严重警告 :生产环境禁止用 Executors.newFixedThreadPool() 和 newCachedThreadPool()。前者队列无界(LinkedBlockingQueue 默认容量 Integer.MAX_VALUE),任务堆积会 OOM;后者线程数无界,同样 OOM。必须用 ThreadPoolExecutor 显式指定有界队列。


七、高并发架构:四个关键设计

7.1 缓存策略:Cache Aside 是标准答案

策略 读 写 适用场景
Cache Aside 先读缓存,miss 读 DB 并回填 先更新 DB,再删缓存 通用推荐
Read Through 缓存层代理读 DB --- 读多写少
Write Through --- 同步写缓存 + DB 强一致
Write Behind --- 异步批量写 DB 高吞吐、容忍丢数据
java 复制代码
public Product getProduct(Long id) {
    String key = "product:" + id;

    // 1. 先读缓存
    String cached = redis.opsForValue().get(key);
    if (cached != null) {
        return JSON.parseObject(cached, Product.class);
    }

    // 2. miss 则读 DB
    Product product = productMapper.selectById(id);
    if (product != null) {
        // 3. 回填缓存,必须设过期时间防永久脏数据
        redis.opsForValue().set(key, JSON.toJSONString(product),
            Duration.ofMinutes(30));
    }
    return product;
}

public void updateProduct(Product product) {
    productMapper.updateById(product);        // 1. 先更新 DB
    redis.delete("product:" + product.getId());  // 2. 再删缓存(不是更新!)
}

为什么删缓存而不是更新缓存:并发下"更新缓存"会出现时序错乱------A 先更新 DB、B 后更新 DB,但 B 先写缓存、A 后写缓存,缓存里留下的是 A 的旧值。删除操作天然幂等,下次读自然回填正确值。

7.2 缓存三大问题:穿透、击穿、雪崩

问题 成因 解决方案
穿透 查不存在的 key,每次都打到 DB 空值缓存 + 布隆过滤器
击穿 热点 key 过期,瞬间高并发全打 DB 互斥锁重建 + 逻辑过期
雪崩 大量 key 同时过期 随机过期时间 + 多级缓存

击穿:互斥锁重建

java 复制代码
public Product getProductWithLock(Long id) {
    String key = "product:" + id;
    Product product = getFromCache(key);
    if (product != null) return product;

    RLock lock = redisson.getLock("lock:" + key);
    try {
        // 双重检查:等锁期间可能别人已经重建好了
        product = getFromCache(key);
        if (product != null) return product;

        if (lock.tryLock(3, TimeUnit.SECONDS)) {
            product = productMapper.selectById(id);
            setCache(key, product, Duration.ofMinutes(30));
            return product;
        }
        Thread.sleep(50);
        return getProductWithLock(id);   // 拿锁失败,短暂等待后重试
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return null;
    } finally {
        if (lock.isHeldByCurrentThread()) lock.unlock();
    }
}

穿透:布隆过滤器

java 复制代码
BloomFilter<Long> filter = BloomFilter.create(
    Funnels.longFunnel(), 1000000, 0.01);   // 100 万容量,1% 误判率

public Product getProductWithBloom(Long id) {
    if (!filter.mightContain(id)) {
        return null;      // 布隆说一定不存在 → 直接返回,不查 DB
    }
    return getProduct(id);   // 可能存在(有 1% 误判)→ 走正常流程
}

布隆过滤器的特性:说不存在一定不存在,说存在可能不存在。所以它只能挡穿透,不能替代缓存。

7.3 限流:三种算法

令牌桶(Guava RateLimiter,允许突发):

java 复制代码
RateLimiter limiter = RateLimiter.create(100);   // 100 QPS

public Response handleRequest(Request req) {
    if (!limiter.tryAcquire()) {
        return Response.tooManyRequests();   // 拿不到令牌,拒绝
    }
    return doBusiness(req);
}

滑动窗口(Sentinel 默认,统计更精确):

java 复制代码
@SentinelResource(value = "createOrder", blockHandler = "blockHandler")
public Order createOrder(OrderRequest req) {
    return orderService.create(req);
}

// 被限流时的降级方法
public Order blockHandler(OrderRequest req, BlockException ex) {
    log.warn("订单创建被限流: {}", req);
    throw new ServiceException("系统繁忙,请稍后重试");
}
算法 特点 适用
计数器 简单,有临界问题 粗粒度限流
漏桶 匀速流出,不允许突发 保护下游稳定
令牌桶 允许一定突发 大多数业务场景
滑动窗口 统计精确,无临界问题 精细化限流

7.4 熔断降级:Sentinel 三态机

java 复制代码
DegradeRule rule = new DegradeRule("createOrder")
    .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
    .setCount(500)                  // 慢调用阈值 500ms
    .setSlowRatioThreshold(0.5)     // 慢调用比例超 50%
    .setMinRequests(10)             // 最小请求数(低于此不触发)
    .setStatInterval(10000)         // 统计窗口 10 秒
    .setTimeWindow(5);              // 熔断 5 秒

DegradeRuleManager.loadRules(Collections.singletonList(rule));

熔断器三态转换:

复制代码
CLOSED(正常放行)
   ↓ 错误率超阈值
OPEN(直接拒绝,快速失败)
   ↓ 等待 timeWindow
HALF_OPEN(放行少量试探请求)
   ↓ 成功              ↓ 失败
CLOSED              OPEN

7.5 分库分表:ShardingSphere 配置

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://db0:3306/order
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://db1:3306/order
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..1}.t_order_${0..3}
            database-strategy:          # 按 user_id 分库
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-mod
            table-strategy:             # 按 order_id 分表
              standard:
                sharding-column: order_id
                sharding-algorithm-name: table-mod
        sharding-algorithms:
          db-mod:
            type: MOD
            props:
              sharding-count: 2
          table-mod:
            type: MOD
            props:
              sharding-count: 4
分片策略 说明 适用
取模 hash(key) % N 数据均匀,但扩容要迁移
范围 按 ID / 时间范围 按时间查询多的场景
一致性 Hash 环形空间,扩容只影响相邻 频繁扩缩容

八、实战:秒杀系统完整架构

8.1 分层架构全景

复制代码
┌──────────────────────────────────────────┐
│  用户请求                                  │
└──────────────┬───────────────────────────┘
               ↓
┌──────────────────────────────────────────┐
│  CDN + 静态页面缓存      → 拦截无效流量      │
└──────────────┬───────────────────────────┘
               ↓
┌──────────────────────────────────────────┐
│  Nginx 限流(单机令牌桶)  → 保护应用        │
└──────────────┬───────────────────────────┘
               ↓
┌──────────────────────────────────────────┐
│  Gateway + Sentinel 全局限流 → 保护服务     │
└──────────────┬───────────────────────────┘
               ↓
┌──────────────────────────────────────────┐
│  秒杀接口:Redis 预扣库存   → 内存级抗压     │
└──────────────┬───────────────────────────┘
               ↓ 异步
┌──────────────────────────────────────────┐
│  RocketMQ 消息队列         → 削峰填谷       │
└──────────────┬───────────────────────────┘
               ↓
┌──────────────────────────────────────────┐
│  订单消费者:DB 乐观锁扣库存 → 数据准确       │
└──────────────────────────────────────────┘

8.2 第一层:接口限流 + Redis 预扣

java 复制代码
@RestController
public class SeckillController {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RocketMQTemplate mqTemplate;

    @PostMapping("/seckill")
    @SentinelResource(value = "seckill", blockHandler = "seckillBlocked")
    public Result seckill(@RequestParam Long productId,
                          @RequestParam Long userId) {

        // 1. 布隆过滤器:商品不存在直接拒绝
        if (!productBloomFilter.mightContain(productId)) {
            return Result.fail("商品不存在");
        }

        // 2. Redis 原子扣减库存(Lua 保证原子性)
        RAtomicLong stock = redissonClient.getAtomicLong("seckill:stock:" + productId);
        long remain = stock.decrementAndGet();
        if (remain < 0) {
            stock.incrementAndGet();    // 超卖了,回滚
            return Result.fail("已售罄");
        }

        // 3. 用户维度幂等:防重复下单
        RLock lock = redissonClient.getLock("seckill:user:" + userId + ":" + productId);
        if (!lock.tryLock()) {
            return Result.fail("请勿重复下单");
        }

        try {
            // 4. 发异步消息,立即返回"排队中"
            SeckillMessage msg = new SeckillMessage(productId, userId);
            mqTemplate.asyncSend("seckill-topic",
                MessageBuilder.withPayload(JSON.toJSONString(msg)).build(),
                new SendCallback() {
                    @Override
                    public void onSuccess(SendResult r) {
                        log.info("秒杀消息发送成功");
                    }
                    @Override
                    public void onException(Throwable e) {
                        stock.incrementAndGet();   // 发送失败,回补库存
                        log.error("秒杀消息发送失败", e);
                    }
                });

            return Result.ok("排队中,请稍后查看订单");
        } finally {
            lock.unlock();
        }
    }

    // 限流降级兜底
    public Result seckillBlocked(Long productId, Long userId, BlockException ex) {
        return Result.fail("系统繁忙,请稍后重试");
    }
}

8.3 第二层:MQ 消费者落库

java 复制代码
@RocketMQMessageListener(topic = "seckill-topic", consumerGroup = "seckill-group")
public class SeckillConsumer implements RocketMQListener<String> {

    @Override
    @Transactional
    public void onMessage(String message) {
        SeckillMessage msg = JSON.parseObject(message, SeckillMessage.class);

        // 1. 幂等检查
        String key = "seckill:done:" + msg.getUserId() + ":" + msg.getProductId();
        if (!redis.opsForValue().setIfAbsent(key, "1", Duration.ofDays(1))) {
            return;
        }

        // 2. DB 扣库存(乐观锁)
        int rows = productMapper.deductStock(msg.getProductId());
        if (rows == 0) {
            // DB 库存不足 → Redis 预扣了但没真库存,回补
            redis.opsForValue().increment("seckill:stock:" + msg.getProductId());
            return;
        }

        // 3. 创建订单
        Order order = new Order();
        order.setProductId(msg.getProductId());
        order.setUserId(msg.getUserId());
        order.setStatus("CREATED");
        orderMapper.insert(order);

        // 4. 延迟消息:30 分钟未支付自动取消
        mqTemplate.syncSend("order-cancel-topic",
            MessageBuilder.withPayload(order.getId().toString()).build(),
            1800000, 3);   // 延迟级别 3 = 10s × 3 = 30min
    }
}

8.4 设计要点总结

层级 技术手段 解决什么
前端 CDN 静态化、答题验证码 拦截无效流量
接入层 Nginx 限流 保护应用不被打穿
网关层 Sentinel 全局限流 保护下游服务
应用层 Redis 预扣 + MQ 异步 高并发读写分离
数据层 乐观锁 + 分库分表 数据准确 + 高吞吐
一致性 本地消息表 + 补偿 最终一致

秒杀的本质:把"高并发写"转换成"高并发读 + 异步写"。Redis 承担读和预扣,MQ 承担削峰,DB 只处理最终落库。任何一层单独都扛不住,组合起来才行。


九、常见问题 FAQ

Q1:分布式锁选 Redis 还是 ZooKeeper?

高性能场景选 Redis(Redisson),强一致场景选 ZooKeeper。秒杀、抢券这类容忍极小概率丢锁的场景用 Redis 完全够用;选主、元数据变更这类不允许出错的场景用 ZooKeeper。

Q2:TCC 和最终一致怎么选?

资金类强一致需求用 TCC;非核心链路(发通知、加积分)用本地消息表或 MQ 事务消息做最终一致就够。Seata AT 是最省心的方案,适合大部分业务场景快速落地。

Q3:线程池核心数到底设多少?

CPU 密集型 N+1,IO 密集型 2N。但这是经验值,真实值必须压测。用 Little's Law 算更准:线程数 = (任务总耗时 / CPU 计算耗时) × N。

Q4:缓存和数据库不一致怎么彻底解决?

Cache Aside + 先更新 DB 再删缓存 + 延迟双删(删完等几百毫秒再删一次)。极端要求下用 Canal 监听 binlog 异步刷缓存,做到准实时。彻底强一致只能加分布式锁串行化,但性能代价极大,通常不值得。

Q5:为什么 synchronized 在 JDK 6 之后变快了?

因为引入了锁升级:无竞争时用偏向锁(仅记录线程 ID,无 CAS 开销),低竞争时用轻量级锁(CAS 自旋,不进入内核态),只有高竞争才升级到重量级锁(OS 互斥量)。大部分场景停留在前两阶段,性能接近无锁。


十、下一步怎么学

  1. 啃源码:AQS、ThreadPoolExecutor、ConcurrentHashMap 三份源码,读完 JUC 就通了
  2. 深入中间件:ZooKeeper ZAB 协议、RocketMQ 存储模型、Redis Cluster 槽位迁移
  3. 性能调优:JVM 内存模型、GC 日志分析、Arthas 线上诊断三板斧
  4. 服务网格:Istio + Envoy 在微服务通信中的落地
  5. 云原生:Kubernetes 部署有状态 Java 应用、Operator 模式

总结

回头看这条链路,其实就三句话:

理论定方向------CAP 告诉你必须取舍,BASE 告诉你取舍之后怎么兜底。

工具解问题------分布式锁解决并发写、分布式事务解决跨库一致、MQ 解决异步和削峰。

架构扛流量------缓存挡读、限流挡入口、熔断保下游、分库分表扩容量。

真正的难点不在记住这些名词,而在判断什么场景该用什么方案。同一个"库存扣减",日订单 1000 用数据库乐观锁就够,日订单 100 万就得上 Redis + MQ + 分库分表。技术选型永远服务于业务规模,过度设计和技术缺失一样有害。

如果这篇对你有帮助,欢迎点赞收藏。有问题评论区聊。


标签建议:Java、分布式、高并发、JUC、Redis、分布式事务、消息队列、架构设计

分类专栏:Java 进阶 / 分布式架构

相关推荐
普贤莲花2 小时前
先学Workbuddy还是“人工智能训练师”,或者python基础课?
开发语言·人工智能·python
砚底藏山河2 小时前
python量化实操:回测数据缓存与重放
java·python·金融·1024程序员节
星之擎2 小时前
Task 很优雅的取消方式是什么?C# 里面。
开发语言·c#
学长毕设2 小时前
基于springboot的农资管理系统-附源码
java·spring boot·后端
FRing033 小时前
JavaScript入门到精通二十八(forEach遍历、创建对象的三种方式、构造函数、new实例化执行过程、实例成员和静态成员)
开发语言·前端·javascript
晒太羊的猫3 小时前
hot100子串
java·算法·leetcode
1024奇点3 小时前
【仓颉语言入门 · 第二篇 第 15 课】
开发语言·数据库·ide·开源
开开心心就好3 小时前
本地文件搜索工具,双击Ctrl就能呼出搜索
java·服务器·前端·支持向量机·ffmpeg·音视频·模拟退火算法