场景设计题
2026/1/15大约 7 分钟Java面试系统设计高并发后端
场景设计题
场景设计题是高级面试的重点,考察候选人的系统设计能力和实战经验。
高并发场景
Q1: 如何设计一个秒杀系统?
核心设计要点:
| 层级 | 策略 | 说明 |
|---|---|---|
| 前端 | 按钮置灰、验证码 | 防止重复提交 |
| CDN | 静态资源缓存 | 减少服务器压力 |
| 网关 | 限流、黑名单 | 过滤恶意请求 |
| 应用 | 内存标记、预减库存 | 快速失败 |
| 缓存 | Redis 原子扣减 | 防止超卖 |
| 消息 | 异步下单 | 削峰填谷 |
| 数据库 | 乐观锁 | 最终一致性 |
// Redis Lua 脚本原子扣减库存
String script = """
local stock = redis.call('get', KEYS[1])
if stock and tonumber(stock) > 0 then
redis.call('decr', KEYS[1])
return 1
end
return 0
""";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
List.of("seckill:stock:" + productId)
);
if (result == 1) {
// 发送 MQ 消息异步创建订单
mqTemplate.send("seckill-order", new OrderMessage(userId, productId));
}Q2: 如何防止超卖?
// 方案1:Redis 预减库存 + Lua 脚本(推荐)
// 见上面代码
// 方案2:数据库乐观锁
@Update("UPDATE product SET stock = stock - #{count} " +
"WHERE id = #{id} AND stock >= #{count}")
int deductStock(@Param("id") Long id, @Param("count") int count);
// 方案3:分布式锁
public boolean deductStock(Long productId, int count) {
String lockKey = "lock:stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 扣减库存
return doDeduct(productId, count);
}
} finally {
lock.unlock();
}
return false;
}Q3: 如何设计一个分布式 ID 生成器?
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 简单、无依赖 | 无序、太长、不适合索引 |
| 数据库自增 | 简单、有序 | 性能瓶颈、单点故障 |
| Redis INCR | 性能好 | 依赖 Redis |
| 雪花算法 | 有序、高性能 | 时钟回拨问题 |
| Leaf | 高可用、高性能 | 需要部署服务 |
// 雪花算法实现
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
// 各部分位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095; // 序列号溢出则等待下一毫秒
if (sequence == 0) {
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装 ID:时间戳 | 数据中心 | 机器 | 序列号
return ((timestamp - 1609459200000L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
}缓存场景
Q4: 缓存穿透、击穿、雪崩的解决方案?
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 穿透 | 查询不存在的数据 | 布隆过滤器、缓存空值 |
| 击穿 | 热点 Key 过期 | 互斥锁、永不过期 |
| 雪崩 | 大量 Key 同时过期 | 随机过期时间、多级缓存 |
// 缓存穿透:布隆过滤器
@Autowired
private BloomFilter<Long> userBloomFilter;
public User getUser(Long id) {
// 布隆过滤器判断
if (!userBloomFilter.mightContain(id)) {
return null;
}
// 查缓存
User user = cache.get(id);
if (user != null) return user;
// 查数据库
user = userMapper.selectById(id);
if (user != null) {
cache.put(id, user);
}
return user;
}
// 缓存击穿:互斥锁
public User getUserWithLock(Long id) {
User user = cache.get(id);
if (user != null) return user;
String lockKey = "lock:user:" + id;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
user = cache.get(id);
if (user != null) return user;
user = userMapper.selectById(id);
cache.put(id, user, randomExpire()); // 随机过期时间
}
} finally {
lock.unlock();
}
return user;
}Q5: 如何保证缓存和数据库的一致性?
| 方案 | 说明 | 问题 |
|---|---|---|
| 先更新DB,再删缓存 | 推荐方案 | 删除失败需重试 |
| 先删缓存,再更新DB | 不推荐 | 并发问题 |
| 延迟双删 | 删-更新-延迟删 | 延迟时间难确定 |
| 订阅 Binlog | Canal 监听变更 | 架构复杂 |
// 推荐方案:先更新DB,再删缓存 + 重试机制
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userMapper.updateById(user);
// 2. 删除缓存(失败则发MQ重试)
try {
cache.delete("user:" + user.getId());
} catch (Exception e) {
// 发送到重试队列
mqTemplate.send("cache-delete-retry",
new CacheDeleteMessage("user:" + user.getId()));
}
}
// 延迟双删
public void updateUserWithDoubleDelete(User user) {
// 1. 删除缓存
cache.delete("user:" + user.getId());
// 2. 更新数据库
userMapper.updateById(user);
// 3. 延迟再删一次(异步)
executor.schedule(() -> {
cache.delete("user:" + user.getId());
}, 500, TimeUnit.MILLISECONDS);
}消息队列场景
Q6: 如何保证消息不丢失?
| 环节 | 丢失原因 | 解决方案 |
|---|---|---|
| 生产端 | 网络问题 | 确认机制、重试 |
| Broker | 宕机 | 持久化、集群 |
| 消费端 | 处理失败 | 手动 ACK |
// RocketMQ 生产者确认
SendResult result = producer.send(message);
if (result.getSendStatus() == SendStatus.SEND_OK) {
// 发送成功
}
// 消费者手动 ACK
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "order-consumer"
)
public class OrderConsumer implements RocketMQListener<OrderMessage> {
@Override
public void onMessage(OrderMessage message) {
try {
// 处理消息
processOrder(message);
// 处理成功,自动 ACK
} catch (Exception e) {
// 抛异常会重试
throw new RuntimeException("处理失败", e);
}
}
}Q7: 如何保证消息顺序性?
// RocketMQ 顺序消息
// 生产者:相同订单发到同一队列
SendResult result = producer.send(message, (mqs, msg, arg) -> {
Long orderId = (Long) arg;
int index = (int) (orderId % mqs.size());
return mqs.get(index); // 根据订单ID选择队列
}, orderId);
// 消费者:单线程消费
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "order-consumer",
consumeMode = ConsumeMode.ORDERLY // 顺序消费
)
public class OrderConsumer implements RocketMQListener<OrderMessage> {
// ...
}Q8: 如何处理消息积压?
| 方案 | 说明 |
|---|---|
| 增加消费者 | 扩容消费者实例 |
| 批量消费 | 一次拉取多条消息 |
| 跳过非关键消息 | 丢弃或转存 |
| 临时队列 | 快速消费转存到新队列 |
分布式锁场景
Q9: Redis 分布式锁的实现?
// 基础实现(有问题)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:order:" + orderId, "1", 30, TimeUnit.SECONDS);
// Redisson 实现(推荐)
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 尝试加锁,最多等待3秒,锁自动释放时间30秒
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {
// 业务逻辑
doSomething();
}
} finally {
// 只释放自己的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}Redisson 解决的问题:
- 锁续期:看门狗机制自动续期
- 可重入:同一线程可多次获取
- 防误删:只能释放自己的锁
Q10: RedLock 算法是什么?
RedLock 是 Redis 作者提出的分布式锁算法,使用多个独立的 Redis 实例。
// Redisson RedLock
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 需要在大多数节点上获取锁成功
if (redLock.tryLock(3, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
redLock.unlock();
}数据库场景
Q11: 如何优化慢 SQL?
-- 1. 使用 EXPLAIN 分析
EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 1;
-- 2. 添加合适的索引
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 3. 避免 SELECT *
SELECT id, order_no, amount FROM orders WHERE user_id = 1;
-- 4. 分页优化(避免深分页)
-- 慢:OFFSET 大时性能差
SELECT * FROM orders LIMIT 1000000, 10;
-- 快:使用游标分页
SELECT * FROM orders WHERE id > 1000000 LIMIT 10;
-- 5. 避免在索引列上使用函数
-- 慢
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
-- 快
SELECT * FROM orders WHERE create_time >= '2024-01-01'
AND create_time < '2024-01-02';Q12: 分库分表如何设计?
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 垂直分库 | 按业务拆分 | 业务解耦 |
| 水平分库 | 按数据拆分 | 数据量大 |
| 垂直分表 | 拆分大字段 | 字段多 |
| 水平分表 | 按行拆分 | 单表数据量大 |
# ShardingSphere 配置
spring:
shardingsphere:
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..3}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: orders-inline
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
sharding-algorithms:
orders-inline:
type: INLINE
props:
algorithm-expression: orders_$->{order_id % 4}
db-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}小结
场景设计题考察重点:
- 高并发:秒杀、限流、分布式 ID
- 缓存:穿透/击穿/雪崩、一致性
- 消息:可靠性、顺序性、积压处理
- 分布式锁:Redis 实现、RedLock
- 数据库:SQL 优化、分库分表
面试题预览
高频面试题
- 设计一个秒杀系统,如何防止超卖?
- 缓存和数据库如何保证一致性?
- 如何保证消息不丢失?
- Redis 分布式锁有什么问题?
- 分库分表后如何处理跨库查询?
