- 项目深挖题:5个重点项目STAR法则回答,针对每个项目准备深挖问题 - 场景设计题:秒杀系统、优惠券系统、数据一致性、限流降级等设计题 - 个人发展题:职业规划、学习能力、团队协作、抗压能力、价值观 - 离职原因与动机:离职原因、择公司、职业目标、反问技巧 - 薪资谈判:谈判策略、Web3特有问题(代币激励、远程工作)、DO & DON'T 针对简历特点: - 结合字节跳动、阿里巴巴、ThoughtWorks的项目经验 - 提供STAR法则回答模板 - 强调Web2经验向Web3的转化 - 包含大量代码示例和架构图 - 提供薪资谈判实战策略 适用场景: - 面试前准备:项目深挖、场景设计 - 面试中:个人发展、离职原因 - 面试后:薪资谈判、offer评估
1109 lines
26 KiB
Markdown
1109 lines
26 KiB
Markdown
# 场景设计题
|
||
|
||
## 说明
|
||
|
||
场景设计题是考察系统设计能力和问题解决能力的常见题型。本文档针对你的简历背景,设计相关的场景题。
|
||
|
||
---
|
||
|
||
## 1. 电商促销系统设计
|
||
|
||
### 题目
|
||
|
||
**面试官问**:
|
||
"假设你要设计一个电商秒杀系统,支持百万用户同时抢购,你会如何设计?"
|
||
|
||
---
|
||
|
||
### 参考回答
|
||
|
||
**【需求分析】**
|
||
|
||
```
|
||
核心需求:
|
||
1. 高并发:百万用户同时抢购
|
||
2. 防超卖:库存不能卖超
|
||
3. 高可用:服务不能挂
|
||
4. 公平性:先到先得
|
||
5. 用户体验:快速响应
|
||
|
||
非功能需求:
|
||
- QPS: 100万+
|
||
- 延迟: <100ms
|
||
- 可用性: 99.99%
|
||
- 数据一致性: 强一致
|
||
```
|
||
|
||
---
|
||
|
||
**【架构设计】**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ 客户端层 │
|
||
│ 移动App、Web、H5 │
|
||
└──────────────┬──────────────────────────────┘
|
||
│
|
||
↓
|
||
┌─────────────────────────────────────────────┐
|
||
│ CDN层 │
|
||
│ - 静态资源(图片、CSS、JS) │
|
||
│ - 动态接口(边缘计算) │
|
||
└──────────────┬──────────────────────────────┘
|
||
│
|
||
↓
|
||
┌─────────────────────────────────────────────┐
|
||
│ API网关层 │
|
||
│ - 限流:100万QPS │
|
||
│ - 熔断:保护后端 │
|
||
│ - 负载均衡 │
|
||
└──────────────┬──────────────────────────────┘
|
||
│
|
||
┌────────┴────────┐
|
||
↓ ↓
|
||
┌──────────┐ ┌──────────┐
|
||
│ 缓存层 │ │ 服务层 │
|
||
└──────────┘ └──────────┘
|
||
│ │
|
||
↓ ↓
|
||
┌─────────────────────────────────┐
|
||
│ 数据库层 │
|
||
│ MySQL主从 + 分库分表 │
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
**【详细设计】**
|
||
|
||
### 1. 缓存层设计(关键)
|
||
|
||
```java
|
||
/**
|
||
* Redis预减库存(核心)
|
||
*/
|
||
@Service
|
||
public class SeckillService {
|
||
|
||
@Autowired
|
||
private RedisTemplate<String, Object> redisTemplate;
|
||
|
||
/**
|
||
* 预减库存(原子操作)
|
||
*/
|
||
public boolean decrementStock(Long productId, Long userId) {
|
||
String key = "stock:seckill:" + productId;
|
||
|
||
// 原子减库存
|
||
Long stock = redisTemplate.opsForValue().decrement(key);
|
||
|
||
if (stock < 0) {
|
||
// 库存不足,回滚
|
||
redisTemplate.opsForValue().increment(key);
|
||
return false;
|
||
}
|
||
|
||
// 记录用户抢购记录
|
||
String userKey = "user:seckill:" + productId + ":" + userId;
|
||
redisTemplate.opsForValue().set(userKey, "1", 24, TimeUnit.HOURS);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 异步下单(消息队列)
|
||
*/
|
||
public void asyncOrder(Order order) {
|
||
// 发送到消息队列
|
||
rabbitTemplate.convertAndSend("order.queue", order);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 消息队列消费者
|
||
*/
|
||
@Component
|
||
public class OrderConsumer {
|
||
|
||
@RabbitListener(queues = "order.queue")
|
||
public void createOrder(Order order) {
|
||
// 1. 校验库存(数据库)
|
||
int stock = productMapper.getStock(order.getProductId());
|
||
if (stock <= 0) {
|
||
throw new InsufficientStockException();
|
||
}
|
||
|
||
// 2. 扣减库存(数据库)
|
||
productMapper.decrementStock(order.getProductId());
|
||
|
||
// 3. 创建订单
|
||
orderMapper.insert(order);
|
||
|
||
// 4. 返回成功
|
||
log.info("订单创建成功: {}", order.getId());
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 限流设计
|
||
|
||
```java
|
||
/**
|
||
* 限流策略(多级)
|
||
*/
|
||
@Configuration
|
||
public class RateLimiterConfig {
|
||
|
||
/**
|
||
* 接口级限流(令牌桶)
|
||
*/
|
||
@Bean
|
||
public RateLimiter apiRateLimiter() {
|
||
// 100万QPS
|
||
return RateLimiter.create(1000000.0);
|
||
}
|
||
|
||
/**
|
||
* 用户级限流(防刷)
|
||
*/
|
||
public boolean checkUserRateLimit(Long userId) {
|
||
String key = "rate_limit:user:" + userId;
|
||
Long count = redisTemplate.opsForValue().increment(key);
|
||
|
||
if (count == 1) {
|
||
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
|
||
}
|
||
|
||
// 每分钟最多10次
|
||
return count <= 10;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 限流拦截器
|
||
*/
|
||
@Component
|
||
public class RateLimitInterceptor implements HandlerInterceptor {
|
||
|
||
@Autowired
|
||
private RateLimiter apiRateLimiter;
|
||
|
||
@Override
|
||
public boolean preHandle(HttpServletRequest request,
|
||
HttpServletResponse response,
|
||
Object handler) {
|
||
// 接口级限流
|
||
if (!apiRateLimiter.tryAcquire()) {
|
||
response.setStatus(429); // Too Many Requests
|
||
return false;
|
||
}
|
||
|
||
// 用户级限流
|
||
Long userId = getUserId(request);
|
||
if (!checkUserRateLimit(userId)) {
|
||
response.setStatus(429);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 防刷设计
|
||
|
||
```java
|
||
/**
|
||
* 防刷策略
|
||
*/
|
||
@Service
|
||
public class AntiSpamService {
|
||
|
||
/**
|
||
* 验证码(图形验证码 + 滑动验证)
|
||
*/
|
||
public boolean verifyCaptcha(String token, String answer) {
|
||
// 调用验证码服务
|
||
return captchaService.verify(token, answer);
|
||
}
|
||
|
||
/**
|
||
* 风控规则
|
||
*/
|
||
public RiskLevel checkRisk(Long userId, SeckillRequest request) {
|
||
RiskScore score = new RiskScore();
|
||
|
||
// 1. IP风险检测
|
||
String ip = getIp(request);
|
||
if (ipRiskService.isHighRisk(ip)) {
|
||
score.add(50);
|
||
}
|
||
|
||
// 2. 设备指纹检测
|
||
String deviceId = getDeviceId(request);
|
||
if (deviceRiskService.isEmulator(deviceId)) {
|
||
score.add(80); // 模拟器
|
||
}
|
||
|
||
// 3. 行为检测
|
||
if (userBehaviorService.isAbnormal(userId)) {
|
||
score.add(30);
|
||
}
|
||
|
||
// 4. 频率检测
|
||
int requestCount = getRequestCount(userId, 1, TimeUnit.MINUTES);
|
||
if (requestCount > 100) {
|
||
score.add(40);
|
||
}
|
||
|
||
// 判定风险等级
|
||
if (score.getScore() >= 80) {
|
||
return RiskLevel.HIGH; // 拒绝
|
||
} else if (score.getScore() >= 50) {
|
||
return RiskLevel.MEDIUM; // 需要验证
|
||
} else {
|
||
return RiskLevel.LOW; // 通过
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 降级策略
|
||
|
||
```java
|
||
/**
|
||
* 降级策略
|
||
*/
|
||
@Component
|
||
public class DegradationService {
|
||
|
||
/**
|
||
* 降级开关(配置中心)
|
||
*/
|
||
@Value("${seckill.degradation.enabled:false}")
|
||
private boolean degradationEnabled;
|
||
|
||
/**
|
||
* 降级方案
|
||
*/
|
||
public SeckillResult handleSeckill(SeckillRequest request) {
|
||
if (degradationEnabled) {
|
||
// 降级:返回静态页面
|
||
return SeckillResult.degraded();
|
||
}
|
||
|
||
// 正常流程
|
||
return seckillService.seckill(request);
|
||
}
|
||
|
||
/**
|
||
* 自动降级(基于错误率)
|
||
*/
|
||
@Scheduled(fixedRate = 1000) // 每秒检查
|
||
public void autoDegradation() {
|
||
double errorRate = getErrorRate();
|
||
|
||
if (errorRate > 0.5) { // 错误率超过50%
|
||
// 开启降级
|
||
enableDegradation();
|
||
|
||
// 发送告警
|
||
alertService.send("错误率过高,已自动降级");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 库存回滚(重要)
|
||
|
||
```java
|
||
/**
|
||
* 库存回滚机制
|
||
*/
|
||
@Service
|
||
public class StockRollbackService {
|
||
|
||
/**
|
||
* 定时任务:回滚未支付的订单库存
|
||
*/
|
||
@Scheduled(cron = "0 */5 * * * ?") // 每5分钟
|
||
public void rollbackUnpaidStock() {
|
||
// 查询15分钟前未支付的订单
|
||
List<Order> unpaidOrders = orderMapper.getUnpaidOrders(15);
|
||
|
||
unpaidOrders.forEach(order -> {
|
||
// 回滚库存(Redis)
|
||
String key = "stock:seckill:" + order.getProductId();
|
||
redisTemplate.opsForValue().increment(key);
|
||
|
||
// 回滚库存(数据库)
|
||
productMapper.incrementStock(order.getProductId());
|
||
|
||
// 取消订单
|
||
orderMapper.updateStatus(order.getId(), OrderStatus.CANCELLED);
|
||
|
||
log.info("库存已回滚: orderId={}", order.getId());
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**【容量预估】**
|
||
|
||
```
|
||
1. QPS预估
|
||
- 用户数:100万
|
||
- 峰值QPS:100万/秒
|
||
- 考虑10倍冗余:1000万QPS
|
||
|
||
2. 缓存容量
|
||
- Redis实例:10个
|
||
- 每个实例:10万QPS
|
||
- 总计:100万QPS
|
||
|
||
3. 消息队列
|
||
- Kafka分区:100个
|
||
- 消费者:200个
|
||
- 吞吐量:100万/秒
|
||
|
||
4. 数据库
|
||
- 主库:1个(写入)
|
||
- 从库:10个(读取)
|
||
- 分库分表:100个库,1000个表
|
||
```
|
||
|
||
---
|
||
|
||
**【压测方案】**
|
||
|
||
```java
|
||
/**
|
||
* 压测方案
|
||
*/
|
||
@Test
|
||
public class LoadTest {
|
||
|
||
/**
|
||
* 压测脚本
|
||
*/
|
||
@Test
|
||
public void testSeckill() {
|
||
// 1. 预热
|
||
warmUp();
|
||
|
||
// 2. 施加压力
|
||
int threadCount = 10000; // 1万个线程
|
||
int requestsPerThread = 100; // 每个线程100次请求
|
||
|
||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||
|
||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||
|
||
long startTime = System.currentTimeMillis();
|
||
|
||
for (int i = 0; i < threadCount; i++) {
|
||
executor.submit(() -> {
|
||
try {
|
||
for (int j = 0; j < requestsPerThread; j++) {
|
||
// 发起请求
|
||
seckillService.seckill(request);
|
||
}
|
||
} finally {
|
||
latch.countDown();
|
||
}
|
||
});
|
||
}
|
||
|
||
latch.await();
|
||
|
||
long endTime = System.currentTimeMillis();
|
||
long duration = endTime - startTime;
|
||
|
||
int totalRequests = threadCount * requestsPerThread;
|
||
double qps = (double) totalRequests / (duration / 1000.0);
|
||
|
||
System.out.println("QPS: " + qps);
|
||
System.out.println("平均耗时: " + (duration / totalRequests) + "ms");
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**【监控告警】**
|
||
|
||
```
|
||
监控指标:
|
||
1. QPS
|
||
2. 响应时间(P50、P95、P99)
|
||
3. 错误率
|
||
4. 库存消耗速度
|
||
5. 订单创建速度
|
||
|
||
告警策略:
|
||
- QPS < 100万:警告
|
||
- P99 > 500ms:警告
|
||
- 错误率 > 1%:严重告警
|
||
- 库存耗尽:通知
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 优惠券系统设计
|
||
|
||
### 题目
|
||
|
||
**面试官问**:
|
||
"设计一个优惠券系统,支持满减、折扣、立减等多种类型,支持叠加使用,如何设计?"
|
||
|
||
---
|
||
|
||
### 参考回答
|
||
|
||
**【需求分析】**
|
||
|
||
```
|
||
功能需求:
|
||
1. 优惠券类型
|
||
- 满减券(满100减20)
|
||
- 折扣券(打8折)
|
||
- 立减券(立减10元)
|
||
- 兑换券(兑换商品)
|
||
|
||
2. 使用规则
|
||
- 叠加使用(可以同时使用多张)
|
||
- 互斥使用(不能同时使用)
|
||
- 优先级(先使用优先级高的)
|
||
|
||
3. 限制条件
|
||
- 品类限制(仅限某品类)
|
||
- 商品限制(仅限某商品)
|
||
- 用户限制(新用户专享)
|
||
- 时间限制(有效期)
|
||
|
||
非功能需求:
|
||
- 性能:计算优惠 < 10ms
|
||
- 准确:不能算错
|
||
- 灵活:方便配置新规则
|
||
```
|
||
|
||
---
|
||
|
||
**【数据模型设计】**
|
||
|
||
```sql
|
||
-- 优惠券模板
|
||
CREATE TABLE coupon_template (
|
||
id BIGINT PRIMARY KEY,
|
||
name VARCHAR(128), -- 名称
|
||
type TINYINT, -- 类型:1满减 2折扣 3立减
|
||
discount_value DECIMAL(10,2), -- 优惠值
|
||
min_amount DECIMAL(10,2), -- 最低消费金额
|
||
max_discount DECIMAL(10,2), -- 最大优惠金额
|
||
category_id BIGINT, -- 适用品类
|
||
product_id BIGINT, -- 适用商品
|
||
user_limit INT, -- 每人限领数量
|
||
total_count INT, -- 总发行量
|
||
issued_count INT DEFAULT 0, -- 已发行量
|
||
used_count INT DEFAULT 0, -- 已使用量
|
||
valid_days INT, -- 有效天数
|
||
status TINYINT, -- 状态:1草稿 2进行中 3已结束
|
||
priority INT DEFAULT 0, -- 优先级
|
||
stackable TINYINT DEFAULT 0, -- 是否可叠加
|
||
create_time DATETIME,
|
||
update_time DATETIME
|
||
);
|
||
|
||
-- 用户优惠券
|
||
CREATE TABLE user_coupon (
|
||
id BIGINT PRIMARY KEY,
|
||
user_id BIGINT,
|
||
template_id BIGINT,
|
||
status TINYINT, -- 状态:1未使用 2已使用 3已过期
|
||
order_id BIGINT, -- 关联订单
|
||
receive_time DATETIME,
|
||
use_time DATETIME,
|
||
expire_time DATETIME,
|
||
INDEX idx_user_id_status (user_id, status)
|
||
);
|
||
|
||
-- 优惠券使用记录
|
||
CREATE TABLE coupon_usage_log (
|
||
id BIGINT PRIMARY KEY,
|
||
user_id BIGINT,
|
||
coupon_id BIGINT,
|
||
order_id BIGINT,
|
||
discount_amount DECIMAL(10,2),
|
||
create_time DATETIME
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
**【核心代码设计】**
|
||
|
||
```java
|
||
/**
|
||
* 优惠计算引擎
|
||
*/
|
||
@Service
|
||
public class DiscountCalculator {
|
||
|
||
/**
|
||
* 计算最优优惠
|
||
*/
|
||
public DiscountResult calculateBestDiscount(
|
||
Long userId,
|
||
List<CartItem> items,
|
||
List<Long> availableCouponIds
|
||
) {
|
||
// 1. 获取可用优惠券
|
||
List<UserCoupon> availableCoupons = getAvailableCoupons(userId, availableCouponIds);
|
||
|
||
if (availableCoupons.isEmpty()) {
|
||
return DiscountResult.empty();
|
||
}
|
||
|
||
// 2. 计算订单金额
|
||
BigDecimal originalAmount = calculateOriginalAmount(items);
|
||
|
||
// 3. 尝试所有优惠券组合
|
||
DiscountResult bestResult = null;
|
||
BigDecimal maxDiscount = BigDecimal.ZERO;
|
||
|
||
// 3.1 单张优惠券
|
||
for (UserCoupon coupon : availableCoupons) {
|
||
DiscountResult result = calculateDiscount(coupon, items);
|
||
if (result.getDiscountAmount().compareTo(maxDiscount) > 0) {
|
||
bestResult = result;
|
||
maxDiscount = result.getDiscountAmount();
|
||
}
|
||
}
|
||
|
||
// 3.2 多张优惠券组合(如果可叠加)
|
||
List<UserCoupon> stackableCoupons = availableCoupons.stream()
|
||
.filter(c -> c.getTemplate().isStackable())
|
||
.collect(Collectors.toList());
|
||
|
||
if (stackableCoupons.size() > 1) {
|
||
// 递归计算所有组合
|
||
List<DiscountResult> combinations = calculateCombinations(stackableCoupons, items);
|
||
for (DiscountResult result : combinations) {
|
||
if (result.getDiscountAmount().compareTo(maxDiscount) > 0) {
|
||
bestResult = result;
|
||
maxDiscount = result.getDiscountAmount();
|
||
}
|
||
}
|
||
}
|
||
|
||
return bestResult;
|
||
}
|
||
|
||
/**
|
||
* 计算单张优惠券的优惠
|
||
*/
|
||
private DiscountResult calculateDiscount(UserCoupon coupon, List<CartItem> items) {
|
||
CouponTemplate template = coupon.getTemplate();
|
||
BigDecimal originalAmount = calculateOriginalAmount(items);
|
||
|
||
BigDecimal discountAmount = BigDecimal.ZERO;
|
||
|
||
switch (template.getType()) {
|
||
case 1: // 满减券
|
||
if (originalAmount.compareTo(template.getMinAmount()) >= 0) {
|
||
discountAmount = template.getDiscountValue();
|
||
// 不超过最大优惠
|
||
if (discountAmount.compareTo(template.getMaxDiscount()) > 0) {
|
||
discountAmount = template.getMaxDiscount();
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 2: // 折扣券
|
||
discountAmount = originalAmount.multiply(
|
||
BigDecimal.ONE.subtract(template.getDiscountValue().divide(BigDecimal.valueOf(100)))
|
||
);
|
||
break;
|
||
|
||
case 3: // 立减券
|
||
discountAmount = template.getDiscountValue();
|
||
break;
|
||
}
|
||
|
||
return DiscountResult.builder()
|
||
.couponId(coupon.getId())
|
||
.discountAmount(discountAmount)
|
||
.finalAmount(originalAmount.subtract(discountAmount))
|
||
.build();
|
||
}
|
||
|
||
/**
|
||
* 获取可用优惠券
|
||
*/
|
||
private List<UserCoupon> getAvailableCoupons(Long userId, List<Long> couponIds) {
|
||
// 查询未使用且未过期的优惠券
|
||
return userCouponMapper.selectByUserIdAndStatusAndIds(
|
||
userId,
|
||
UserCouponStatus.UNUSED.getValue(),
|
||
couponIds
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 计算原始金额
|
||
*/
|
||
private BigDecimal calculateOriginalAmount(List<CartItem> items) {
|
||
return items.stream()
|
||
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
|
||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 优惠券使用服务
|
||
*/
|
||
@Service
|
||
public class CouponUsageService {
|
||
|
||
@Autowired
|
||
private DiscountCalculator discountCalculator;
|
||
|
||
@Autowired
|
||
private RedisTemplate<String, Object> redisTemplate;
|
||
|
||
/**
|
||
* 使用优惠券(分布式锁保证幂等)
|
||
*/
|
||
@Transactional
|
||
public void useCoupon(Long userId, Long couponId, Long orderId) {
|
||
String lockKey = "coupon:lock:" + couponId;
|
||
|
||
// 分布式锁
|
||
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
|
||
lockKey,
|
||
"1",
|
||
10,
|
||
TimeUnit.SECONDS
|
||
);
|
||
|
||
if (!locked) {
|
||
throw new BusinessException("系统繁忙,请稍后重试");
|
||
}
|
||
|
||
try {
|
||
// 1. 查询优惠券
|
||
UserCoupon coupon = userCouponMapper.selectById(couponId);
|
||
|
||
// 2. 校验状态
|
||
if (coupon.getStatus() != UserCouponStatus.UNUSED.getValue()) {
|
||
throw new BusinessException("优惠券已使用或已过期");
|
||
}
|
||
|
||
// 3. 校验有效期
|
||
if (coupon.getExpireTime().before(new Date())) {
|
||
throw new BusinessException("优惠券已过期");
|
||
}
|
||
|
||
// 4. 更新状态
|
||
userCouponMapper.updateStatus(couponId, UserCouponStatus.USED.getValue());
|
||
|
||
// 5. 记录使用日志
|
||
couponUsageLogMapper.insert(CouponUsageLog.builder()
|
||
.userId(userId)
|
||
.couponId(couponId)
|
||
.orderId(orderId)
|
||
.discountAmount(coupon.getTemplate().getDiscountValue())
|
||
.build());
|
||
|
||
} finally {
|
||
// 释放锁
|
||
redisTemplate.delete(lockKey);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 数据一致性设计
|
||
|
||
### 题目
|
||
|
||
**面试官问**:
|
||
"在大促活动中,如何保证Redis缓存和数据库的数据一致性?"
|
||
|
||
---
|
||
|
||
### 参考回答
|
||
|
||
**【问题分析】**
|
||
|
||
```
|
||
不一致的场景:
|
||
1. 先更新数据库,后删除缓存
|
||
- A更新数据库
|
||
- B读取缓存(旧数据)
|
||
- A删除缓存
|
||
- B写入缓存(旧数据)
|
||
→ 不一致
|
||
|
||
2. 先删除缓存,后更新数据库
|
||
- A删除缓存
|
||
- B读取数据库(旧数据)
|
||
- B写入缓存(旧数据)
|
||
- A更新数据库
|
||
→ 不一致
|
||
|
||
3. 缓存过期
|
||
- 缓存过期
|
||
- A读取数据库(旧数据)
|
||
- B更新数据库
|
||
- A写入缓存(旧数据)
|
||
→ 不一致
|
||
```
|
||
|
||
---
|
||
|
||
**【解决方案】**
|
||
|
||
### 方案1:延迟双删(推荐)
|
||
|
||
```java
|
||
/**
|
||
* 延迟双删
|
||
*/
|
||
@Service
|
||
public class DataUpdateService {
|
||
|
||
@Autowired
|
||
private RedisTemplate<String, Object> redisTemplate;
|
||
|
||
@Autowired
|
||
private ProductMapper productMapper;
|
||
|
||
/**
|
||
* 更新商品(延迟双删)
|
||
*/
|
||
@Transactional
|
||
public void updateProduct(Product product) {
|
||
// 1. 删除缓存
|
||
String cacheKey = "product:" + product.getId();
|
||
redisTemplate.delete(cacheKey);
|
||
|
||
// 2. 更新数据库
|
||
productMapper.updateById(product);
|
||
|
||
// 3. 延迟删除缓存(异步)
|
||
asyncDeleteCache(cacheKey);
|
||
}
|
||
|
||
/**
|
||
* 异步延迟删除缓存
|
||
*/
|
||
@Async
|
||
public void asyncDeleteCache(String cacheKey) {
|
||
try {
|
||
// 延迟1秒(保证数据库主从同步完成)
|
||
Thread.sleep(1000);
|
||
|
||
// 再次删除缓存
|
||
redisTemplate.delete(cacheKey);
|
||
|
||
log.info("延迟删除缓存成功: {}", cacheKey);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 方案2:订阅Binlog(最终一致)
|
||
|
||
```java
|
||
/**
|
||
* Binlog监听器
|
||
*/
|
||
@Component
|
||
public class BinlogListener {
|
||
|
||
@Autowired
|
||
private RedisTemplate<String, Object> redisTemplate;
|
||
|
||
/**
|
||
* 监听数据库更新
|
||
*/
|
||
@CanalEventListener(
|
||
destination = "product",
|
||
filter = ".*\\..*"
|
||
)
|
||
public void onUpdate(CanalEntry.Entry entry) {
|
||
// 解析Binlog
|
||
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
|
||
|
||
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
|
||
// 获取主键ID
|
||
Long id = rowData.getAfterColumnsList().stream()
|
||
.filter(c -> c.getIsKey())
|
||
.findFirst()
|
||
.map(c -> Long.valueOf(c.getValue()))
|
||
.orElse(null);
|
||
|
||
if (id != null) {
|
||
// 删除缓存
|
||
String cacheKey = "product:" + id;
|
||
redisTemplate.delete(cacheKey);
|
||
|
||
log.info("Binlog监听删除缓存: {}", cacheKey);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 方案3:分布式锁(强一致)
|
||
|
||
```java
|
||
/**
|
||
* 分布式锁保证一致性
|
||
*/
|
||
@Service
|
||
public class ConsistentService {
|
||
|
||
@Autowired
|
||
private RedissonClient redissonClient;
|
||
|
||
/**
|
||
* 更新数据(强一致)
|
||
*/
|
||
public void updateData(String key, Object value) {
|
||
String lockKey = "lock:data:" + key;
|
||
RLock lock = redissonClient.getLock(lockKey);
|
||
|
||
try {
|
||
// 加锁
|
||
lock.lock(10, TimeUnit.SECONDS);
|
||
|
||
// 1. 删除缓存
|
||
redisTemplate.delete(key);
|
||
|
||
// 2. 更新数据库
|
||
databaseMapper.update(key, value);
|
||
|
||
// 3. 查询最新值
|
||
Object newValue = databaseMapper.select(key);
|
||
|
||
// 4. 写入缓存
|
||
redisTemplate.opsForValue().set(key, newValue, 1, TimeUnit.HOURS);
|
||
|
||
} finally {
|
||
// 释放锁
|
||
lock.unlock();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 限流降级设计
|
||
|
||
### 题目
|
||
|
||
**面试官问**:
|
||
"在大促活动中,如何设计限流和降级策略?"
|
||
|
||
---
|
||
|
||
### 参考回答
|
||
|
||
**【限流策略】**
|
||
|
||
```java
|
||
/**
|
||
* 多级限流
|
||
*/
|
||
@Component
|
||
public class MultiLevelRateLimiter {
|
||
|
||
/**
|
||
* Level 1: 接口级限流(保护服务)
|
||
*/
|
||
@Bean
|
||
public RateLimiter interfaceRateLimiter() {
|
||
// 令牌桶:100万QPS
|
||
return RateLimiter.create(1000000.0);
|
||
}
|
||
|
||
/**
|
||
* Level 2: 用户级限流(防刷)
|
||
*/
|
||
public boolean checkUserLimit(Long userId) {
|
||
String key = "limit:user:" + userId;
|
||
Long count = redisTemplate.opsForValue().increment(key);
|
||
|
||
if (count == 1) {
|
||
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
|
||
}
|
||
|
||
// 每分钟10次
|
||
return count <= 10;
|
||
}
|
||
|
||
/**
|
||
* Level 3: IP级限流(防DDoS)
|
||
*/
|
||
public boolean checkIPLimit(String ip) {
|
||
String key = "limit:ip:" + ip;
|
||
Long count = redisTemplate.opsForValue().increment(key);
|
||
|
||
if (count == 1) {
|
||
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
|
||
}
|
||
|
||
// 每分钟100次
|
||
return count <= 100;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**【降级策略】**
|
||
|
||
```java
|
||
/**
|
||
* 降级策略
|
||
*/
|
||
@Service
|
||
public class DegradationService {
|
||
|
||
/**
|
||
* 自动降级(基于错误率)
|
||
*/
|
||
@Scheduled(fixedRate = 5000) // 每5秒检查
|
||
public void autoDegradation() {
|
||
// 1. 统计错误率
|
||
double errorRate = getErrorRate();
|
||
|
||
// 2. 判断是否需要降级
|
||
if (errorRate > 0.5) { // 错误率超过50%
|
||
enableDegradation();
|
||
sendAlert("错误率过高,已自动降级");
|
||
}
|
||
|
||
// 3. 统计RT
|
||
double rt = getAverageResponseTime();
|
||
if (rt > 1000) { // RT超过1秒
|
||
enableDegradation();
|
||
sendAlert("响应时间过长,已自动降级");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 降级执行
|
||
*/
|
||
public Result executeWithDegradation(String serviceName) {
|
||
if (isDegradationEnabled()) {
|
||
// 返回默认值或静态数据
|
||
return Result.degraded();
|
||
}
|
||
|
||
// 正常调用
|
||
return serviceClient.call(serviceName);
|
||
}
|
||
|
||
/**
|
||
* 手动降级(配置中心)
|
||
*/
|
||
@EventListener(condition = "#event.type == 'MANUAL_DEGRADATION'")
|
||
public void onManualDegradation(ConfigChangeEvent event) {
|
||
boolean enabled = Boolean.parseBoolean(event.getValue());
|
||
setDegradationEnabled(enabled);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 💡 设计题答题技巧
|
||
|
||
### 1. 答题框架
|
||
|
||
```
|
||
【需求分析】
|
||
- 功能需求:XX
|
||
- 非功能需求:XX
|
||
- 约束条件:XX
|
||
|
||
【架构设计】
|
||
- 整体架构图
|
||
- 技术选型
|
||
- 数据模型
|
||
|
||
【详细设计】
|
||
- 核心代码
|
||
- 关键流程
|
||
- 边界处理
|
||
|
||
【容量预估】
|
||
- QPS预估
|
||
- 存储预估
|
||
- 带宽预估
|
||
|
||
【压测方案】
|
||
- 压测工具
|
||
- 压测场景
|
||
- 优化方案
|
||
|
||
【监控告警】
|
||
- 监控指标
|
||
- 告警策略
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 常见追问
|
||
|
||
**追问1:如果QPS扩大10倍,怎么办?**
|
||
```
|
||
答:
|
||
1. 垂直扩展:升级服务器配置
|
||
2. 水平扩展:增加服务器数量
|
||
3. 缓存优化:增加缓存命中率
|
||
4. 读写分离:减轻主库压力
|
||
5. 分库分表:分散压力
|
||
6. CDN加速:减轻源站压力
|
||
7. 异步化:消息队列削峰
|
||
```
|
||
|
||
**追问2:如何保证数据一致性?**
|
||
```
|
||
答:
|
||
1. 强一致:分布式锁
|
||
2. 最终一致:延迟双删、订阅Binlog
|
||
3. 补偿机制:定时任务对账
|
||
```
|
||
|
||
**追问3:如何应对突发流量?**
|
||
```
|
||
答:
|
||
1. 弹性扩容:Serverless、K8s HPA
|
||
2. 限流:保护系统
|
||
3. 降级:保证核心功能
|
||
4. 预热:提前加载缓存
|
||
```
|