文章目录
-
- 写在前面
- 一、分布式理论基础
-
- [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 互斥量)。大部分场景停留在前两阶段,性能接近无锁。
十、下一步怎么学
- 啃源码:AQS、ThreadPoolExecutor、ConcurrentHashMap 三份源码,读完 JUC 就通了
- 深入中间件:ZooKeeper ZAB 协议、RocketMQ 存储模型、Redis Cluster 槽位迁移
- 性能调优:JVM 内存模型、GC 日志分析、Arthas 线上诊断三板斧
- 服务网格:Istio + Envoy 在微服务通信中的落地
- 云原生:Kubernetes 部署有状态 Java 应用、Operator 模式
总结
回头看这条链路,其实就三句话:
理论定方向------CAP 告诉你必须取舍,BASE 告诉你取舍之后怎么兜底。
工具解问题------分布式锁解决并发写、分布式事务解决跨库一致、MQ 解决异步和削峰。
架构扛流量------缓存挡读、限流挡入口、熔断保下游、分库分表扩容量。
真正的难点不在记住这些名词,而在判断什么场景该用什么方案。同一个"库存扣减",日订单 1000 用数据库乐观锁就够,日订单 100 万就得上 Redis + MQ + 分库分表。技术选型永远服务于业务规模,过度设计和技术缺失一样有害。
如果这篇对你有帮助,欢迎点赞收藏。有问题评论区聊。
标签建议:Java、分布式、高并发、JUC、Redis、分布式事务、消息队列、架构设计
分类专栏:Java 进阶 / 分布式架构