系统设计面试题集
大约 20 分钟约 6054 字
系统设计面试题集
简介
系统设计面试是高级工程师面试的核心环节,考察候选人在面对开放性问题时,如何从需求分析、容量估算、架构设计到技术选型,给出结构化的解决方案。本文涵盖 URL 短链、限流器、新闻 Feed、分布式缓存、消息队列、搜索系统、通知系统、文件存储等经典系统设计题,每题包含需求分析、容量估算、高层设计、详细设计、权衡取舍和追问。
特点
题目一:URL 短链系统
需求分析
功能需求:
- 给定长 URL,生成唯一的短 URL
- 用户访问短 URL 时,重定向到原始长 URL
- 短 URL 应该是不可预测的(安全性)
非功能需求:
- 高可用:系统不可用意味着所有短链失效
- 低延迟:重定向应在 10ms 内完成
- 高并发读:读远多于写(100:1 读写比)
容量估算:
- 假设每月 1 亿新 URL 生成
- 读写比 100:1 → 每月 100 亿次读取
- 每秒写入:1亿 / (30 * 86400) ≈ 40 URL/s
- 每秒读取:4000 URL/s
- 存储:每个 URL 约 500 字节
- 5 年 = 1亿 * 12 * 5 = 60 亿条
- 60亿 * 500B = 3TB高层设计
客户端 → API 网关 → 短链服务 → ID 生成器
↓
数据库/缓存
↓
重定向服务详细设计
/// <summary>
/// URL 短链服务核心实现
/// </summary>
public class UrlShortenerService
{
private readonly IUrlRepository _repository;
private readonly IIdGenerator _idGenerator;
private readonly ICacheService _cache;
private readonly IBaseUrlValidator _validator;
public UrlShortenerService(
IUrlRepository repository,
IIdGenerator idGenerator,
ICacheService cache,
IBaseUrlValidator validator)
{
_repository = repository;
_idGenerator = idGenerator;
_cache = cache;
_validator = validator;
}
/// <summary>
/// 创建短链
/// </summary>
public async Task<ShortenResult> ShortenAsync(ShortenRequest request)
{
// 1. 验证原始 URL
if (!_validator.IsValid(request.OriginalUrl))
throw new ArgumentException("无效的 URL");
// 2. 检查是否已有短链(幂等性)
string? existing = await _repository.FindShortUrlAsync(request.OriginalUrl);
if (existing != null)
return new ShortenResult { ShortUrl = existing, IsNew = false };
// 3. 生成唯一 ID
long id = await _idGenerator.NextIdAsync();
// 4. ID 转短码(Base62 编码)
string shortCode = Base62Encoder.Encode(id);
// 5. 存储
var mapping = new UrlMapping
{
Id = id,
ShortCode = shortCode,
OriginalUrl = request.OriginalUrl,
CreatedAt = DateTime.UtcNow,
ExpiresAt = request.ExpirationDays.HasValue
? DateTime.UtcNow.AddDays(request.ExpirationDays.Value)
: null
};
await _repository.SaveAsync(mapping);
// 6. 写入缓存
await _cache.SetAsync($"url:{shortCode}", request.OriginalUrl, TimeSpan.FromHours(24));
return new ShortenResult
{
ShortUrl = $"https://short.link/{shortCode}",
IsNew = true
};
}
/// <summary>
/// 重定向:获取原始 URL
/// </summary>
public async Task<RedirectResult?> GetOriginalUrlAsync(string shortCode)
{
// 1. 先查缓存
string? cached = await _cache.GetAsync($"url:{shortCode}");
if (cached != null)
return new RedirectResult { OriginalUrl = cached, FromCache = true };
// 2. 查数据库
UrlMapping? mapping = await _repository.GetByShortCodeAsync(shortCode);
if (mapping == null) return null;
// 3. 检查过期
if (mapping.ExpiresAt.HasValue && mapping.ExpiresAt < DateTime.UtcNow)
return null;
// 4. 回填缓存
await _cache.SetAsync($"url:{shortCode}", mapping.OriginalUrl, TimeSpan.FromHours(24));
return new RedirectResult { OriginalUrl = mapping.OriginalUrl, FromCache = false };
}
}
/// <summary>
/// Base62 编码器(0-9, a-z, A-Z = 62 个字符)
/// </summary>
public static class Base62Encoder
{
private const string Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static readonly int Base = Alphabet.Length;
public static string Encode(long id)
{
if (id == 0) return Alphabet[0].ToString();
var sb = new StringBuilder();
while (id > 0)
{
sb.Insert(0, Alphabet[(int)(id % Base)]);
id /= Base;
}
return sb.ToString();
}
public static long Decode(string shortCode)
{
long id = 0;
foreach (char c in shortCode)
{
id = id * Base + Alphabet.IndexOf(c);
}
return id;
}
}
/// <summary>
/// 分布式 ID 生成器(雪花算法简化版)
/// </summary>
public class SnowflakeIdGenerator : IIdGenerator
{
private readonly long _machineId;
private long _sequence;
private long _lastTimestamp = -1;
private readonly object _lock = new();
public SnowflakeIdGenerator(long machineId)
{
_machineId = machineId & 0x3FF; // 10 位机器ID
}
public Task<long> NextIdAsync()
{
lock (_lock)
{
long timestamp = GetCurrentTimestamp();
if (timestamp == _lastTimestamp)
{
_sequence = (_sequence + 1) & 0xFFF; // 12 位序列号
if (_sequence == 0)
timestamp = WaitNextMillis(_lastTimestamp);
}
else
{
_sequence = 0;
}
_lastTimestamp = timestamp;
long id = (timestamp << 22) | (_machineId << 12) | _sequence;
return Task.FromResult(id);
}
}
private static long GetCurrentTimestamp()
{
return (long)(DateTime.UtcNow - new DateTime(2024, 1, 1)).TotalMilliseconds;
}
private static long WaitNextMillis(long lastTimestamp)
{
long timestamp = GetCurrentTimestamp();
while (timestamp <= lastTimestamp)
timestamp = GetCurrentTimestamp();
return timestamp;
}
}
// 数据模型和接口
public record ShortenRequest(string OriginalUrl, int? ExpirationDays);
public record ShortenResult { public string ShortUrl { get; init; } = ""; public bool IsNew { get; init; } }
public record RedirectResult { public string OriginalUrl { get; init; } = ""; public bool FromCache { get; init; } }
public record UrlMapping { public long Id { get; init; } public string ShortCode { get; init; } = ""; public string OriginalUrl { get; init; } = ""; public DateTime CreatedAt { get; init; } public DateTime? ExpiresAt { get; init; } }
public interface IIdGenerator { Task<long> NextIdAsync(); }
public interface IUrlRepository { Task SaveAsync(UrlMapping mapping); Task<UrlMapping?> GetByShortCodeAsync(string shortCode); Task<string?> FindShortUrlAsync(string originalUrl); }
public interface ICacheService { Task<string?> GetAsync(string key); Task SetAsync(string key, string value, TimeSpan expiration); }
public interface IBaseUrlValidator { bool IsValid(string url); }权衡取舍
| 决策点 | 方案 A | 方案 B | 选择 |
|---|---|---|---|
| ID 生成 | UUID(无需协调) | 雪花算法(有序、可解码) | 雪花算法 — 更短、有序 |
| 存储 | SQL(强一致) | NoSQL(高吞吐) | SQL+缓存组合 |
| 编码 | Base62 | 自定义短码 | Base62 — 标准化 |
| 缓存策略 | Cache-Aside | Write-Through | Cache-Aside — 简单高效 |
追问
- Q: 如何处理热点短链? A: 多级缓存 + 本地缓存
- Q: 如何支持自定义短码? A: 预留 ID 段 + 唯一性校验
- Q: 数据量增长后如何分片? A: 按 shortCode 哈希分片
题目二:限流器设计
需求分析
功能需求:
- 对 API 请求按用户/IP 进行限流
- 支持多种限流策略(固定窗口、滑动窗口、令牌桶)
- 被限流时返回 429 和 Retry-After 头
非功能需求:
- 低延迟:限流检查 < 1ms
- 分布式:多个服务节点共享限流状态
- 准确性:允许少量误差(最终一致)详细设计
/// <summary>
/// 分布式限流器实现
/// </summary>
public interface IRateLimiter
{
Task<RateLimitResult> CheckAsync(string key, int limit, TimeSpan window);
}
/// <summary>
/// 滑动窗口限流(基于 Redis Sorted Set)
/// </summary>
public class SlidingWindowRateLimiter : IRateLimiter
{
private readonly IDatabase _redis;
public SlidingWindowRateLimiter(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}
public async Task<RateLimitResult> CheckAsync(string key, int limit, TimeSpan window)
{
string redisKey = $"ratelimit:{key}";
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long windowStart = now - (long)window.TotalMilliseconds;
// Lua 脚本保证原子性
var script = @"
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local window_ms = tonumber(ARGV[4])
-- 移除过期的请求记录
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 未超限,添加当前请求
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('PEXPIRE', key, window_ms)
return {1, limit - count - 1, limit}
else
-- 超限
return {0, 0, limit}
end
";
var result = (RedisValue[])await _redis.ScriptEvaluateAsync(
script,
new RedisKey[] { redisKey },
new RedisValue[] { now, windowStart, limit, window.TotalMilliseconds }
);
bool allowed = result[0] == 1;
int remaining = (int)result[1];
int limitValue = (int)result[2];
return new RateLimitResult
{
Allowed = allowed,
Remaining = remaining,
Limit = limitValue,
RetryAfter = allowed ? 0 : (int)window.TotalSeconds
};
}
}
/// <summary>
/// 令牌桶限流器(适用于突发流量)
/// </summary>
public class TokenBucketRateLimiter : IRateLimiter
{
private readonly IDatabase _redis;
public TokenBucketRateLimiter(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}
public async Task<RateLimitResult> CheckAsync(
string key, int capacity, TimeSpan window)
{
string redisKey = $"bucket:{key}";
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// 令牌桶 Lua 脚本
var script = @"
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local now = tonumber(ARGV[2])
local refill_rate = tonumber(ARGV[3]) -- tokens per millisecond
local refill_ms = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- 计算应补充的令牌数
local elapsed = now - last_refill
local refill = math.floor(elapsed * refill_rate)
tokens = math.min(capacity, tokens + refill)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('PEXPIRE', key, refill_ms)
return {allowed, math.floor(tokens), capacity}
";
double refillRate = (double)capacity / window.TotalMilliseconds;
var result = (RedisValue[])await _redis.ScriptEvaluateAsync(
script,
new RedisKey[] { redisKey },
new RedisValue[] { capacity, now, refillRate, window.TotalMilliseconds }
);
return new RateLimitResult
{
Allowed = result[0] == 1,
Remaining = (int)result[1],
Limit = (int)result[2],
RetryAfter = result[0] == 1 ? 0 : (int)(1.0 / refillRate / 1000)
};
}
}
/// <summary>
/// ASP.NET Core 限流中间件
/// </summary>
public class RateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly IRateLimiter _rateLimiter;
private readonly ILogger<RateLimitMiddleware> _logger;
public RateLimitMiddleware(
RequestDelegate next,
IRateLimiter rateLimiter,
ILogger<RateLimitMiddleware> logger)
{
_next = next;
_rateLimiter = rateLimiter;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
string key = GetClientKey(context);
var result = await _rateLimiter.CheckAsync(
key, limit: 100, TimeSpan.FromMinutes(1));
// 设置响应头
context.Response.Headers["X-RateLimit-Limit"] = result.Limit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = result.Remaining.ToString();
if (!result.Allowed)
{
context.Response.StatusCode = 429;
context.Response.Headers["Retry-After"] = result.RetryAfter.ToString();
await context.Response.WriteAsJsonAsync(new
{
Error = "Too Many Requests",
RetryAfter = result.RetryAfter
});
_logger.LogWarning("限流触发: {Key}", key);
return;
}
await _next(context);
}
private static string GetClientKey(HttpContext context)
{
// 优先使用认证用户 ID,其次使用 IP
string? userId = context.User?.FindFirst("sub")?.Value;
return userId ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
}
}
public record RateLimitResult
{
public bool Allowed { get; init; }
public int Remaining { get; init; }
public int Limit { get; init; }
public int RetryAfter { get; init; }
}
public interface IDatabase
{
Task<RedisResult> ScriptEvaluateAsync(string script, RedisKey[] keys, RedisValue[] values);
}
public interface IConnectionMultiplexer { IDatabase GetDatabase(); }
public struct RedisKey { public static implicit operator RedisKey(string s) => default; }
public struct RedisValue { public static implicit operator RedisValue(string s) => default; public static implicit operator RedisValue(int i) => default; public static implicit operator RedisValue(long l) => default; public static implicit operator RedisValue(double d) => default; }
public struct RedisResult { public static explicit operator RedisValue[](RedisResult r) => Array.Empty<RedisValue>(); }追问
- Q: 如何处理 Redis 故障? A: 本地降级 + Circuit Breaker
- Q: 分布式限流的一致性如何保证? A: 最终一致,允许少量超限
- Q: 如何支持多级限流(IP + 用户 + 接口)? A: 组合 key + 多次检查
题目三:新闻 Feed 系统
需求分析
功能需求:
- 用户可以发布帖子
- 用户可以关注其他用户
- 首页展示关注用户的帖子(Feed 流)
- 支持按时间排序
容量估算:
- 1 亿用户,日活 1000 万
- 平均每人关注 100 人
- 平均每人每天发 2 条帖子
- Feed 读取是写入的 100 倍详细设计
/// <summary>
/// Feed 系统核心 — 两种推拉模式
/// </summary>
public class FeedService
{
private readonly IPostRepository _postRepo;
private readonly IFeedRepository _feedRepo;
private readonly IFollowRepository _followRepo;
private readonly IMessageBus _messageBus;
/// <summary>
/// 发布帖子(写扩散 — Fan-out on write)
/// 适用于:普通用户(粉丝少)
/// </summary>
public async Task PublishPost_FanOutOnWriteAsync(Post post)
{
// 1. 保存帖子
await _postRepo.SaveAsync(post);
// 2. 获取所有粉丝
var followers = await _followRepo.GetFollowersAsync(post.UserId);
// 3. 将帖子推入每个粉丝的 Feed 收件箱
var tasks = followers.Select(followerId =>
_feedRepo.AddToFeedAsync(followerId, post));
await Task.WhenAll(tasks);
}
/// <summary>
/// 读取 Feed(拉模式 — Fan-out on read)
/// 适用于:大V(粉丝多)
/// </summary>
public async Task<List<Post>> GetFeed_FanOutOnReadAsync(
string userId, int pageSize, string? cursor)
{
// 1. 获取关注列表
var following = await _followRepo.GetFollowingAsync(userId);
// 2. 从每个关注者的帖子中拉取,合并排序
var posts = new List<Post>();
foreach (var followeeId in following)
{
var userPosts = await _postRepo.GetRecentPostsAsync(
followeeId, limit: pageSize / following.Count + 1);
posts.AddRange(userPosts);
}
// 3. 合并排序 + 分页
return posts
.OrderByDescending(p => p.CreatedAt)
.Take(pageSize)
.ToList();
}
/// <summary>
/// 混合模式(推荐)
/// - 普通用户:写扩散
/// - 大V用户:读扩散
/// </summary>
public async Task<List<Post>> GetFeed_HybridAsync(
string userId, int pageSize)
{
// 从自己的 Feed 收件箱读取(包含写扩散的帖子)
var feedPosts = await _feedRepo.GetFeedAsync(userId, pageSize * 2);
// 获取关注的大V列表
var celebreties = await _followRepo.GetFollowingCelebritiesAsync(userId);
// 实时拉取大V的最新帖子
var celebrityPosts = new List<Post>();
foreach (var celebId in celebreties)
{
var posts = await _postRepo.GetRecentPostsAsync(celebId, limit: 10);
celebrityPosts.AddRange(posts);
}
// 合并、去重、排序
var allPosts = feedPosts.Concat(celebrityPosts)
.DistinctBy(p => p.Id)
.OrderByDescending(p => p.CreatedAt)
.Take(pageSize)
.ToList();
return allPosts;
}
}
// 数据模型
public record Post
{
public string Id { get; init; } = Guid.NewGuid().ToString();
public string UserId { get; init; } = "";
public string Content { get; init; } = "";
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}
public interface IPostRepository
{
Task SaveAsync(Post post);
Task<List<Post>> GetRecentPostsAsync(string userId, int limit);
}
public interface IFeedRepository
{
Task AddToFeedAsync(string userId, Post post);
Task<List<Post>> GetFeedAsync(string userId, int limit);
}
public interface IFollowRepository
{
Task<List<string>> GetFollowersAsync(string userId);
Task<List<string>> GetFollowingAsync(string userId);
Task<List<string>> GetFollowingCelebritiesAsync(string userId);
}
public interface IMessageBus { Task PublishAsync(string topic, object message); }追问
- Q: 写扩散的延迟如何优化? A: 异步写入 + 消息队列
- Q: 如何处理大V发布时的写扩散风暴? A: 切换到拉模式 + 预计算
- Q: Feed 如何支持个性化排序? A: 增加权重分 + 机器学习排序
题目四:分布式缓存
需求分析
场景:电商网站的商品详情页缓存
- QPS: 10 万次/秒
- 缓存命中率目标: > 99%
- 数据量: 1 亿商品
- 每件商品信息: ~10KB
- 总缓存数据量: ~1TB详细设计
/// <summary>
/// 多级缓存实现
/// </summary>
public class MultiLevelCache : ICacheService
{
private readonly IMemoryCache _localCache; // L1: 进程内缓存
private readonly IDistributedCache _redis; // L2: Redis 缓存
private readonly ICacheBackfill _backfill; // 数据源
private readonly ILogger _logger;
private readonly TimeSpan _localTtl = TimeSpan.FromSeconds(30);
private readonly TimeSpan _redisTtl = TimeSpan.FromMinutes(30);
public async Task<T?> GetOrSetAsync<T>(
string key, Func<Task<T>> factory, CancellationToken ct = default)
{
// L1: 本地缓存
if (_localCache.TryGetValue(key, out T? localValue) && localValue != null)
{
return localValue;
}
// L2: Redis 缓存
byte[]? redisValue = await _redis.GetAsync(key, ct);
if (redisValue != null)
{
T? result = Deserialize<T>(redisValue);
if (result != null)
{
// 回填 L1
_localCache.Set(key, result, _localTtl);
return result;
}
}
// L3: 数据源
T value = await factory();
if (value != null)
{
// 写入 L1 和 L2
_localCache.Set(key, value, _localTtl);
await _redis.SetAsync(key, Serialize(value), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _redisTtl
});
}
return value;
}
/// <summary>
/// 缓存穿透防护:布隆过滤器
/// </summary>
public async Task<T?> GetWithBloomFilterAsync<T>(
string key, Func<Task<T?>> factory)
{
// 布隆过滤器判断 key 是否可能存在
if (!await _backfill.MayExistAsync(key))
{
// 一定不存在,直接返回
return default;
}
return await GetOrSetAsync(key, async () =>
{
var result = await factory();
if (result == null)
{
// 缓存空值防止穿透
await _redis.SetAsync(
$"null:{key}", Array.Empty<byte>(),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
}
return result!;
});
}
/// <summary>
/// 缓存雪崩防护:随机过期时间
/// </summary>
public async Task SetWithJitterAsync<T>(string key, T value, TimeSpan baseTtl)
{
// 基础过期时间 + 随机偏移(±10%)
var jitter = TimeSpan.FromTicks(
(long)(baseTtl.Ticks * (0.9 + Random.Shared.NextDouble() * 0.2)));
await _redis.SetAsync(key, Serialize(value), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = jitter
});
}
/// <summary>
/// 缓存击穿防护:互斥锁
/// </summary>
public async Task<T?> GetWithLockAsync<T>(
string key, Func<Task<T>> factory, TimeSpan lockTimeout)
{
string lockKey = $"lock:{key}";
// 尝试获取分布式锁
bool locked = await TryAcquireLockAsync(lockKey, lockTimeout);
if (!locked)
{
// 等待其他实例重建缓存
await Task.Delay(100);
return await GetOrSetAsync(key, factory);
}
try
{
// 获取锁后再次检查缓存
byte[]? value = await _redis.GetAsync(key);
if (value != null)
return Deserialize<T>(value);
// 重建缓存
T result = await factory();
await SetWithJitterAsync(key, result, TimeSpan.FromMinutes(30));
return result;
}
finally
{
await ReleaseLockAsync(lockKey);
}
}
private async Task<bool> TryAcquireLockAsync(string key, TimeSpan timeout) => true;
private Task ReleaseLockAsync(string key) => Task.CompletedTask;
private byte[] Serialize<T>(T value) => System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value);
private T? Deserialize<T>(byte[] data) => System.Text.Json.JsonSerializer.Deserialize<T>(data);
}
public interface ICacheBackfill { Task<bool> MayExistAsync(string key); }
public interface IMemoryCache { bool TryGetValue<T>(object key, out T? value); void Set<T>(object key, T value, TimeSpan ttl); }
public interface IDistributedCache { Task<byte[]?> GetAsync(string key, CancellationToken ct = default); Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken ct = default); }
public class DistributedCacheEntryOptions { public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } }追问
- Q: 如何保证缓存与数据库的一致性? A: Cache-Aside + 延迟双删
- Q: 本地缓存如何失效? A: Pub/Sub 广播失效消息
- Q: 热点 key 如何处理? A: 本地缓存 + key 分片
题目五:消息队列
需求分析
场景:订单系统的异步处理
- 解耦:订单服务与库存/物流/通知服务解耦
- 削峰:促销期间订单量暴涨
- 可靠:消息不丢失
- 有序:同一订单的消息需要顺序处理详细设计
/// <summary>
/// 基于消息队列的订单处理系统
/// </summary>
public class OrderMessageProcessor
{
private readonly IMessageConsumer _consumer;
private readonly IInventoryService _inventory;
private readonly INotificationService _notification;
private readonly ILogger _logger;
/// <summary>
/// 消费订单消息 — 至少一次语义 + 幂等处理
/// </summary>
public async Task ProcessOrderMessageAsync(
OrderMessage message, CancellationToken ct)
{
// 1. 幂等检查:是否已处理过此消息
if (await IsProcessedAsync(message.MessageId))
{
_logger.LogInformation("消息已处理,跳过: {Id}", message.MessageId);
return;
}
// 2. 根据消息类型处理
switch (message.Type)
{
case "OrderCreated":
await HandleOrderCreatedAsync(message);
break;
case "OrderPaid":
await HandleOrderPaidAsync(message);
break;
case "OrderCancelled":
await HandleOrderCancelledAsync(message);
break;
}
// 3. 标记消息已处理
await MarkProcessedAsync(message.MessageId);
}
private async Task HandleOrderCreatedAsync(OrderMessage message)
{
// 扣减库存
bool success = await _inventory.DeductAsync(
message.OrderId, message.Items);
if (!success)
{
// 库存不足,发布取消事件
throw new InsufficientInventoryException(message.OrderId);
}
// 发送确认通知
await _notification.SendOrderConfirmAsync(message.UserId, message.OrderId);
}
private async Task HandleOrderPaidAsync(OrderMessage message)
{
// 触发物流
await _notification.SendShippingNotificationAsync(
message.UserId, message.OrderId);
}
private async Task HandleOrderCancelledAsync(OrderMessage message)
{
// 释放库存
await _inventory.ReleaseAsync(message.OrderId, message.Items);
await _notification.SendCancelNotificationAsync(message.UserId, message.OrderId);
}
private Task<bool> IsProcessedAsync(string messageId) => Task.FromResult(false);
private Task MarkProcessedAsync(string messageId) => Task.CompletedTask;
}
/// <summary>
/// 延迟消息处理(延迟队列)
/// </summary>
public class DelayedMessageService
{
private readonly IMessageProducer _producer;
/// <summary>
/// 场景:下单后 30 分钟未支付自动取消
/// </summary>
public async Task ScheduleAutoCancelAsync(string orderId, TimeSpan delay)
{
var message = new DelayedMessage
{
Type = "AutoCancel",
OrderId = orderId,
ProcessAt = DateTime.UtcNow.Add(delay)
};
await _producer.PublishDelayedAsync(
"order-delayed", message, delay);
}
/// <summary>
/// 消费延迟消息
/// </summary>
public async Task HandleAutoCancelAsync(DelayedMessage message)
{
// 检查订单状态
var order = await GetOrderAsync(message.OrderId);
if (order?.Status == "Pending")
{
// 超时未支付,自动取消
await CancelOrderAsync(message.OrderId, reason: "支付超时");
}
}
private Task<Order?> GetOrderAsync(string orderId) => Task.FromResult<Order?>(null);
private Task CancelOrderAsync(string orderId, string reason) => Task.CompletedTask;
}
// 模型定义
public record OrderMessage
{
public string MessageId { get; init; } = Guid.NewGuid().ToString();
public string Type { get; init; } = "";
public string OrderId { get; init; } = "";
public string UserId { get; init; } = "";
public List<OrderItem> Items { get; init; } = new();
}
public record OrderItem { public string ProductId { get; init; } = ""; public int Quantity { get; init; } }
public record DelayedMessage { public string Type { get; init; } = ""; public string OrderId { get; init; } = ""; public DateTime ProcessAt { get; init; } }
public record Order { public string Id { get; init; } = ""; public string Status { get; init; } = ""; }
public interface IMessageConsumer { }
public interface IMessageProducer { Task PublishDelayedAsync(string topic, object message, TimeSpan delay); }
public interface IInventoryService { Task<bool> DeductAsync(string orderId, List<OrderItem> items); Task ReleaseAsync(string orderId, List<OrderItem> items); }
public interface INotificationService { Task SendOrderConfirmAsync(string userId, string orderId); Task SendShippingNotificationAsync(string userId, string orderId); Task SendCancelNotificationAsync(string userId, string orderId); }
public class InsufficientInventoryException : Exception { public InsufficientInventoryException(string orderId) : base($"库存不足: {orderId}") { } }追问
- Q: 消息积压怎么办? A: 增加消费者 + 临时扩容
- Q: 如何保证消息顺序? A: 相同 key 路由到同一队列
- Q: 消息丢失如何处理? A: 生产者确认 + 持久化 + 消费者手动 ACK
题目六:搜索系统
详细设计
/// <summary>
/// 简化版搜索引擎实现
/// </summary>
public class SearchService
{
private readonly ISearchIndex _index;
private readonly ITokenizer _tokenizer;
/// <summary>
/// 倒排索引搜索
/// </summary>
public async Task<SearchResult> SearchAsync(SearchQuery query)
{
// 1. 分词
var tokens = _tokenizer.Tokenize(query.Keyword);
// 2. 从倒排索引查找文档
var docIds = await _index.SearchAsync(tokens, query.Filters);
// 3. 相关性排序(简化版 TF-IDF)
var ranked = await RankDocumentsAsync(docIds, tokens);
// 4. 分页
var paged = ranked
.Skip((query.Page - 1) * query.PageSize)
.Take(query.PageSize)
.ToList();
return new SearchResult
{
Total = ranked.Count,
Items = paged,
Page = query.Page
};
}
/// <summary>
/// 相关性排序
/// </summary>
private async Task<List<SearchItem>> RankDocumentsAsync(
List<string> docIds, string[] tokens)
{
var results = new List<SearchItem>();
foreach (var docId in docIds)
{
double score = 0;
foreach (var token in tokens)
{
// TF(词频)* IDF(逆文档频率)
double tf = await _index.GetTermFrequencyAsync(docId, token);
double idf = await _index.GetInverseDocumentFrequencyAsync(token);
score += tf * idf;
}
results.Add(new SearchItem { DocId = docId, Score = score });
}
return results.OrderByDescending(r => r.Score).ToList();
}
}
public record SearchQuery { public string Keyword { get; init; } = ""; public int Page { get; init; } = 1; public int PageSize { get; init; } = 20; public Dictionary<string, string>? Filters { get; init; } }
public record SearchResult { public int Total { get; init; } public List<SearchItem> Items { get; init; } = new(); public int Page { get; init; } }
public record SearchItem { public string DocId { get; init; } = ""; public double Score { get; init; } }
public interface ISearchIndex { Task<List<string>> SearchAsync(string[] tokens, Dictionary<string, string>? filters); Task<double> GetTermFrequencyAsync(string docId, string token); Task<double> GetInverseDocumentFrequencyAsync(string token); }
public interface ITokenizer { string[] Tokenize(string text); }追问
- Q: 如何支持中文分词? A: IK/Jieba 分词器
- Q: 如何实现搜索建议(自动补全)? A: 前缀树/有限状态机
- Q: 如何处理同义词搜索? A: 同义词词典 + 查询扩展
题目七:通知系统
详细设计
/// <summary>
/// 多渠道通知系统
/// </summary>
public class NotificationService
{
private readonly IEnumerable<INotificationChannel> _channels;
private readonly INotificationRepository _repo;
private readonly ILogger _logger;
public async Task<NotificationResult> SendAsync(NotificationRequest request)
{
// 1. 持久化通知记录
var notification = new Notification
{
Id = Guid.NewGuid().ToString(),
UserId = request.UserId,
Type = request.Type,
Title = request.Title,
Content = request.Content,
Channels = request.Channels,
Status = "Pending",
CreatedAt = DateTime.UtcNow
};
await _repo.SaveAsync(notification);
// 2. 获取用户偏好
var preferences = await _repo.GetUserPreferencesAsync(request.UserId);
// 3. 根据渠道并发发送
var tasks = request.Channels
.Where(ch => preferences.IsChannelEnabled(ch))
.Select(channel => SendToChannelAsync(channel, notification));
var results = await Task.WhenAll(tasks);
// 4. 更新状态
var status = results.All(r => r.Success) ? "Sent"
: results.Any(r => r.Success) ? "Partial"
: "Failed";
await _repo.UpdateStatusAsync(notification.Id, status);
return new NotificationResult
{
NotificationId = notification.Id,
Status = status,
ChannelResults = results
};
}
private async Task<ChannelResult> SendToChannelAsync(
string channel, Notification notification)
{
try
{
var handler = _channels.FirstOrDefault(c => c.ChannelName == channel);
if (handler == null) return new ChannelResult { Channel = channel, Success = false };
await handler.SendAsync(notification);
return new ChannelResult { Channel = channel, Success = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "发送通知失败: {Channel}", channel);
return new ChannelResult { Channel = channel, Success = false, Error = ex.Message };
}
}
}
// 通知渠道接口
public interface INotificationChannel
{
string ChannelName { get; }
Task SendAsync(Notification notification);
}
public class EmailChannel : INotificationChannel
{
public string ChannelName => "email";
public async Task SendAsync(Notification notification) { /* SMTP 发送 */ }
}
public class SmsChannel : INotificationChannel
{
public string ChannelName => "sms";
public async Task SendAsync(Notification notification) { /* 短信网关 */ }
}
public class PushChannel : INotificationChannel
{
public string ChannelName => "push";
public async Task SendAsync(Notification notification) { /* APNs/FCM */ }
}
// 数据模型
public record NotificationRequest { public string UserId { get; init; } = ""; public string Type { get; init; } = ""; public string Title { get; init; } = ""; public string Content { get; init; } = ""; public List<string> Channels { get; init; } = new(); }
public record Notification { public string Id { get; init; } = ""; public string UserId { get; init; } = ""; public string Type { get; init; } = ""; public string Title { get; init; } = ""; public string Content { get; init; } = ""; public List<string> Channels { get; init; } = new(); public string Status { get; init; } = ""; public DateTime CreatedAt { get; init; } }
public record NotificationResult { public string NotificationId { get; init; } = ""; public string Status { get; init; } = ""; public ChannelResult[] ChannelResults { get; init; } = Array.Empty<ChannelResult>(); }
public record ChannelResult { public string Channel { get; init; } = ""; public bool Success { get; init; } public string? Error { get; init; } }
public interface INotificationRepository { Task SaveAsync(Notification notification); Task UpdateStatusAsync(string id, string status); Task<UserPreferences> GetUserPreferencesAsync(string userId); }
public record UserPreferences { public bool IsChannelEnabled(string channel) => true; }追问
- Q: 如何防止通知轰炸? A: 频率限制 + 用户静默时段
- Q: 如何保证通知不丢失? A: 持久化 + 重试 + 死信队列
- Q: 如何做 A/B 测试? A: 模板分组 + 效果追踪
题目八:文件存储系统
详细设计
/// <summary>
/// 分布式文件存储服务
/// </summary>
public class FileStorageService
{
private readonly IFileMetadataRepo _metadataRepo;
private readonly IChunkStorage _chunkStorage;
private readonly ILogger _logger;
/// <summary>
/// 分块上传(大文件)
/// </summary>
public async Task<UploadSession> InitiateUploadAsync(UploadRequest request)
{
// 1. 验证文件类型和大小
ValidateFile(request.FileName, request.FileSize);
// 2. 创建上传会话
var session = new UploadSession
{
Id = Guid.NewGuid().ToString(),
FileName = request.FileName,
FileSize = request.FileSize,
ChunkSize = CalculateChunkSize(request.FileSize),
TotalChunks = (int)Math.Ceiling(
(double)request.FileSize / CalculateChunkSize(request.FileSize)),
Status = "Pending"
};
await _metadataRepo.SaveSessionAsync(session);
return session;
}
/// <summary>
/// 上传单个分块
/// </summary>
public async Task<ChunkUploadResult> UploadChunkAsync(
string sessionId, int chunkIndex, Stream chunkData)
{
var session = await _metadataRepo.GetSessionAsync(sessionId);
if (session == null) throw new FileNotFoundException("会话不存在");
// 存储分块
string chunkKey = $"{session.Id}/{chunkIndex}";
await _chunkStorage.StoreAsync(chunkKey, chunkData);
// 更新进度
await _metadataRepo.MarkChunkUploadedAsync(sessionId, chunkIndex);
// 检查是否全部上传完成
bool allUploaded = await _metadataRepo.AreAllChunksUploadedAsync(sessionId);
if (allUploaded)
{
await CompleteUploadAsync(session);
return new ChunkUploadResult { ChunkIndex = chunkIndex, Complete = true };
}
return new ChunkUploadResult { ChunkIndex = chunkIndex, Complete = false };
}
/// <summary>
/// 合并分块
/// </summary>
private async Task CompleteUploadAsync(UploadSession session)
{
string fileKey = GenerateFileKey(session.FileName);
// 合并所有分块
await _chunkStorage.MergeChunksAsync(
session.Id, fileKey, session.TotalChunks);
// 计算哈希(完整性校验)
string hash = await _chunkStorage.ComputeHashAsync(fileKey);
// 保存元数据
var metadata = new FileMetadata
{
Id = Guid.NewGuid().ToString(),
FileName = session.FileName,
FileKey = fileKey,
FileSize = session.FileSize,
Hash = hash,
UploadedAt = DateTime.UtcNow
};
await _metadataRepo.SaveFileMetadataAsync(metadata);
// 清理临时分块
await _chunkStorage.CleanupChunksAsync(session.Id);
}
private void ValidateFile(string fileName, long fileSize) { }
private int CalculateChunkSize(long fileSize) => fileSize > 100 * 1024 * 1024 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;
private string GenerateFileKey(string fileName) => $"{DateTime.UtcNow:yyyy/MM/dd}/{Guid.NewGuid()}{Path.GetExtension(fileName)}";
}
// 模型定义
public record UploadRequest { public string FileName { get; init; } = ""; public long FileSize { get; init; } }
public record UploadSession { public string Id { get; init; } = ""; public string FileName { get; init; } = ""; public long FileSize { get; init; } public int ChunkSize { get; init; } public int TotalChunks { get; init; } public string Status { get; init; } = ""; }
public record ChunkUploadResult { public int ChunkIndex { get; init; } public bool Complete { get; init; } }
public record FileMetadata { public string Id { get; init; } = ""; public string FileName { get; init; } = ""; public string FileKey { get; init; } = ""; public long FileSize { get; init; } public string Hash { get; init; } = ""; public DateTime UploadedAt { get; init; } }
public interface IFileMetadataRepo { Task SaveSessionAsync(UploadSession session); Task<UploadSession?> GetSessionAsync(string id); Task MarkChunkUploadedAsync(string sessionId, int chunkIndex); Task<bool> AreAllChunksUploadedAsync(string sessionId); Task SaveFileMetadataAsync(FileMetadata metadata); }
public interface IChunkStorage { Task StoreAsync(string key, Stream data); Task MergeChunksAsync(string sessionId, string targetKey, int totalChunks); Task<string> ComputeHashAsync(string key); Task CleanupChunksAsync(string sessionId); }追问
- Q: 如何支持秒传? A: 文件哈希比对 + 去重
- Q: 如何保证数据安全? A: 服务端加密 + 签名 URL
- Q: 如何处理并发上传同一文件? A: 参考计数 + 写时复制
通用系统设计框架
面试答题模板
1. 澄清需求(3-5 分钟)
- 功能需求:核心功能是什么?
- 非功能需求:QPS、延迟、可用性、一致性
- 约束条件:团队规模、技术栈、预算
2. 容量估算(3-5 分钟)
- 用户量估算
- QPS 估算(读写比)
- 存储估算
- 带宽估算
3. 高层设计(5-10 分钟)
- 画出系统架构图
- 标注核心组件
- 说明数据流向
4. 详细设计(10-15 分钟)
- 深入每个核心组件
- 数据库选型与表设计
- 缓存策略
- 关键算法
5. 权衡取舍(5 分钟)
- 列出主要决策点
- 对比方案优劣
- 说明选择理由
6. 扩展讨论
- 瓶颈在哪?
- 如何扩展?
- 单点故障如何处理?关键知识点
- 容量估算是系统设计的起点,从数字推导架构
- 读写比决定了缓存和存储策略
- 一致性 vs 可用性是分布式系统的核心权衡
- 幂等性是分布式系统可靠性的基石
- 推拉模式各有适用场景,混合模式通常是最佳选择
- 缓存三大问题(穿透、雪崩、击穿)各有对应方案
常见误区
| 误区 | 正确理解 |
|---|---|
| 系统设计需要完美的解决方案 | 面试考察的是思维过程,不是标准答案 |
| 直接跳到高级架构 | 应从简单方案开始,逐步迭代 |
| 忽略非功能需求 | 可用性、一致性等往往比功能更重要 |
| 过度设计 | 应根据实际规模选择合适的复杂度 |
| 只关注技术选型 | 数据模型和接口设计同样重要 |
进阶路线
- 入门阶段:掌握系统设计框架、常见模式(缓存、队列、分片)
- 进阶阶段:理解 CAP 定理、一致性模型、分布式事务
- 高级阶段:能设计高可用、可扩展的分布式系统
- 专家阶段:结合业务场景做技术决策,考虑成本和团队因素
适用场景
- 高级/资深工程师面试
- 架构师面试
- 技术方案评审参考
- 系统架构设计学习
落地建议
- 每道题至少练习 2-3 次,形成自己的答题节奏
- 画图辅助说明,白板/平板是必备工具
- 练习时用真实数字做容量估算,培养直觉
- 准备 3-5 个自己参与过的系统设计案例
排错清单
复盘问题
- 你的项目中最复杂的系统设计是什么?如何用这套框架描述?
- 系统设计中哪些决策点最容易出错?
- 如何在有限时间内展示深度而非广度?
