深度思考
我将根据你的项目代码和学习内容,为你整理三大模块的高频面试题。让我先查看你的相关代码实现。
已检索仓库 23 个结果
已检索仓库 voucher seckill order inventory oversold一人一单 21 个结果
现在让我查看用户登录和商户查询的相关代码实现:
已检索仓库 user login sms verification code session redis interceptor 14 个结果
完美!现在我已经掌握了你的所有代码实现。让我为你整理三大模块的高频面试题,并带你深入分析。
📚 三大模块高频面试题精讲
🔐 模块一:短信登录功能
Q1: 为什么要用 Redis 代替 Session?Session 共享问题是什么?
问题分析:
传统 Session 的问题:
// ❌ Session 方案(单机没问题)
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
// 用户信息存在 session 中
session.setAttribute("user", userDTO);
return Result.ok(token);
}
// 问题场景:集群部署
用户请求 → 服务器 A (session 在 A 上)
↓
用户刷新 → 服务器 B (session 在 B 上没有!)
↓
结果:用户需要重新登录!
图解 Session 共享问题:
用户登录
↓
┌──────┴──────┐
│ 服务器 A │ ← Session 存在这里
│ (有用户信息) │
└──────┬──────┘
↓
用户下次请求
↓
┌────────────┐
│ 服务器 B │ ← Session 没有!❌
│ (无用户信息) │
└─────────────┘
Redis 解决方案:
// ✅ Redis 方案(集群通用)
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm) {
// 1. 生成随机 token
String token = UUID.randomUUID().toString(true);
// 2. 将用户信息存到 Redis
String tokenKey = "login:token:" + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES);
// 3. 返回 token 给前端
return Result.ok(token);
}
// 下次请求:任何服务器都能从 Redis 获取用户信息
用户请求 + token → 服务器 B → Redis 查询 → 找到用户信息 ✅
Redis vs Session 对比:
| 存储位置 | 服务器内存 | 独立 Redis 服务器 |
| 集群共享 | ❌ 需要额外配置 | ✅ 天然支持 |
| 容量限制 | 受服务器内存限制 | 可扩展,支持大容量 |
| 过期策略 | 依赖容器管理 | 灵活控制 |
| 性能 | 本地访问快 | 网络访问,但更快 |
面试加分项:
在我的项目中,使用 Redis 存储用户登录信息,key 是 login:token:随机 UUID,value 是用户的 HashMap(包含 id、昵称、头像)。这样无论请求打到哪台服务器,都能通过 token 从 Redis 获取用户信息,完美解决了 Session 共享问题。
Q2: 短信登录的完整流程是什么?
完整流程:
// 步骤 1: 发送验证码
@PostMapping("/code")
public Result sendCode(@RequestParam String phone) {
// 1. 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// 2. 生成随机验证码(4 位或 6 位)
String code = RandomUtil.randomNumbers(6);
// 3. 存入 Redis,2 分钟过期
stringRedisTemplate.opsForValue().set(
"login:code:" + phone,
code,
2,
TimeUnit.MINUTES
);
// 4. 发送短信(实际项目中调用短信平台 API)
log.debug("发送短信验证码成功:code={}", code);
return Result.ok();
}
// 步骤 2: 登录验证
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1. 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// 2. 从 Redis 获取验证码
String cacheCode = stringRedisTemplate.opsForValue()
.get("login:code:" + phone);
// 3. 校验验证码
if (code == null || !code.equals(cacheCode)) {
return Result.fail("验证码错误");
}
// 4. 查询或创建用户
User user = query().eq("phone", phone).one();
if (user == null) {
user = createUserWithPhone(phone);
}
// 5. 生成 token,保存用户到 Redis
String token = UUID.randomUUID().toString(true);
String tokenKey = "login:token:" + token;
// User 对象 → UserDTO → Map<String, Object>
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(
userDTO,
new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) ->
fieldValue.toString()
)
);
// 存入 Redis 并设置过期时间
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES);
// 6. 返回 token
return Result.ok(token);
}
流程图:
┌─────────┐ ┌───────── ┌─────────┐ ┌─────────┐
│ 用户 │ │ 前端 │ │ 后端 │ │ Redis │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
│ ①输入手机号 │ │ │
│ ──────────────►│ │ │
│ │ ②发送验证码请求│ │
│ │ ──────────────►│ │
│ │ │ ③生成验证码 │
│ │ │ ──────────────►│
│ │ │ │
│ ④收到验证码短信│ │ │
│ ◄──────────────│ │ │
│ │ │ │
│ ⑤输入验证码 │ │ │
│ ──────────────►│ │ │
│ │ ⑥登录请求 │ │
│ │ (phone+code) │ │
│ │ ──────────────►│ │
│ │ │ ⑦从 Redis 获取验证码│
│ │ │ ──────────────►│
│ │ │ │
│ │ │ ⑧校验验证码 │
│ │ │ ◄──────────────│
│ │ │ │
│ │ │ ⑨查询/创建用户│
│ │ │ ───── │
│ │ │ │ │
│ │ │ ◄────┘ │
│ │ │ │
│ │ │ ⑩保存用户到 Redis│
│ │ │ ──────────────►│
│ │ │ │
│ │ ⑪返回 token │ │
│ │ ◄──────────────│ │
│ ⑫登录成功 │ │ │
│ ◄──────────────│ │ │
│ │ │ │
Q3: 登录拦截器是如何工作的?ThreadLocal 有什么用?
拦截器工作流程:
// 配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code", // 发送验证码
"/user/login", // 登录
"/blog/hot", // 热门博客
"/shop/**", // 查询店铺
"/shop-type/**", // 查询店铺类型
"/upload/**", // 上传
"/voucher/**" // 优惠券
)
.order(1);
// Token 刷新拦截器
registry.addInterceptor(new ReFreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**")
.order(0);
}
}
登录拦截器实现:
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 从 ThreadLocal 中获取用户
if (UserHolder.getUser() == null) {
// 没有用户,返回 401 未授权
response.setStatus(401);
return false;
}
// 有用户,放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// 清理 ThreadLocal,防止内存泄漏
UserHolder.removeUser();
}
}
Token 刷新拦截器:
@Slf4j
public class ReFreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public ReFreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 获取请求头中的 token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true; // 没有 token,放行(让登录拦截器处理)
}
// 2. 从 Redis 查询用户
String key = "login:token:" + token;
Map<Object, Object> userMap = stringRedisTemplate
.opsForHash()
.entries(key);
// 3. 判断用户是否存在
if (userMap.isEmpty()) {
return true; // 用户不存在,放行
}
// 4. 用户存在,转换为 UserDTO 并存入 ThreadLocal
UserDTO userDTO = BeanUtil.fillBeanWithMap(
userMap,
new UserDTO(),
false
);
UserHolder.saveUser(userDTO);
// 5. 刷新 token 过期时间(续期)
stringRedisTemplate.expire(key, 30, TimeUnit.MINUTES);
return true; // 放行
}
}
ThreadLocal 的作用:
// ThreadLocal 工具类
public class UserHolder {
// 线程本地变量,每个线程独立存储
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
// 保存用户
public static void saveUser(UserDTO user) {
tl.set(user);
}
// 获取用户
public static UserDTO getUser() {
return tl.get();
}
// 清理用户(重要!防止内存泄漏)
public static void removeUser() {
tl.remove();
}
}
// 使用场景
// 请求 1 → 线程 A → UserHolder.saveUser(user1)
// 请求 2 → 线程 B → UserHolder.saveUser(user2)
//
// 在线程 A 中:UserHolder.getUser() → user1
// 在线程 B 中:UserHolder.getUser() → user2
// 互不干扰!
为什么用 ThreadLocal?
传统方式的问题:
Controller → Service → Mapper
↓ ↓ ↓
都要传递 user 参数,太麻烦!
使用 ThreadLocal:
拦截器保存用户 → ThreadLocal
↓
Controller 直接从 ThreadLocal 获取
↓
Service 直接从 ThreadLocal 获取
↓
整个请求链路都能访问用户信息,无需参数传递
🏪 模块二:商户查询缓存
Q4: 什么是缓存穿透?如何解决?
什么是缓存穿透:
正常查询流程:
用户查询 id=100 → Redis 命中 → 返回数据 ✅
用户查询 id=200 → Redis 未命中 → 查询数据库 → 存入 Redis → 返回 ✅
缓存穿透场景:
恶意用户查询 id=-1 → Redis 未命中 → 查询数据库(不存在)→ 返回 null
恶意用户继续查询 id=-1 → Redis 未命中 → 查询数据库 → 返回 null
恶意用户继续查询 id=-1 → Redis 未命中 → 查询数据库 → 返回 null
↓
数据库压力山大!💥
解决方案 1:缓存空值
public Shop queryWithPassThrough(Long id) {
String key = "cache:shop:" + id;
// 1. 从 Redis 查询
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 如果存在,直接返回
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, Shop.class);
}
// 3. 判断是否为空值标记
if (shopJson != null) {
// 是空值标记,返回 null
return null;
}
// 4. Redis 不存在,查询数据库
Shop shop = getById(id);
// 5. 数据库也不存在,写入空值到 Redis(防止再次查询)
if (shop == null) {
stringRedisTemplate.opsForValue().set(
key,
"", // 空字符串作为标记
2, // 短时间过期
TimeUnit.MINUTES
);
return null;
}
// 6. 数据库存在,写入 Redis
stringRedisTemplate.opsForValue().set(
key,
JSONUtil.toJsonStr(shop),
30,
TimeUnit.MINUTES
);
return shop;
}
空值缓存的工作流程:
第一次查询 id=-1:
Redis 未命中 → 查询数据库(不存在)→ 写入空值 "" → 返回 null
第二次查询 id=-1:
Redis 命中空值 "" → 直接返回 null ✅(不再查数据库)
2 分钟后:
空值过期,允许再次查询(防止正常数据永远无法写入)
解决方案 2:布隆过滤器(了解)
使用布隆过滤器判断 ID 是否存在:
用户查询 id=-1 → 布隆过滤器判断不存在 → 直接返回 ✅
用户查询 id=100 → 布隆过滤器判断可能存在 → 继续查询
Q5: 什么是缓存击穿?你的项目中是如何解决的?
什么是缓存击穿:
热点 key 过期瞬间:
1000 个并发请求同时查询 id=1 的店铺
↓
Redis 中 key 刚好过期
↓
1000 个请求全部打到数据库
↓
数据库瞬间崩溃 💥
解决方案 1:互斥锁(你的项目实现)
public Shop queryWithMutex(Long id) {
String key = "cache:shop:" + id;
String lockKey = "lock:shop:" + id;
Shop shop = null;
try {
// 1. 从 Redis 查询
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 命中直接返回
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, Shop.class);
}
// 3. 未命中,尝试获取互斥锁
boolean isLock = tryLock(lockKey);
if (!isLock) {
// 获取锁失败,休眠后重试
Thread.sleep(50);
return queryWithMutex(id); // 递归重试
}
// 4. 获取锁成功,再次检查缓存(双检锁)
shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, Shop.class);
}
// 5. 查询数据库
shop = getById(id);
// 6. 数据库不存在,写入空值
if (shop == null) {
stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
return null;
}
// 7. 写入 Redis
stringRedisTemplate.opsForValue().set(
key,
JSONUtil.toJsonStr(shop),
30,
TimeUnit.MINUTES
);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 8. 释放锁
unlock(lockKey);
}
return shop;
}
// 获取锁
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
互斥锁工作流程:
1000 个并发请求查询 id=1(缓存已过期):
请求 1: 获取锁成功 → 查询数据库 → 写入 Redis → 释放锁
请求 2: 获取锁失败 → 休眠 50ms → 重试 → Redis 命中 → 返回 ✅
请求 3: 获取锁失败 → 休眠 50ms → 重试 → Redis 命中 → 返回 ✅
…
请求 1000: 获取锁失败 → 休眠 50ms → 重试 → Redis 命中 → 返回 ✅
结果:只有 1 个请求查数据库,其他 999 个都从 Redis 获取 ✅
解决方案 2:逻辑过期(你的项目实现)
public Shop queryWithLogicalExpire(Long id) {
String key = "cache:shop:" + id;
// 1. 从 Redis 查询
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json)) {
return null;
}
// 2. 反序列化
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 3. 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return shop;
}
// 4. 已过期,获取互斥锁
String lockKey = "lock:shop:" + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
// 获取锁成功,开启独立线程重建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlock(lockKey);
}
});
}
// 5. 返回旧数据(不阻塞用户)
return shop;
}
逻辑过期的数据结构:
@Data
class RedisData {
private LocalDateTime expireTime; // 逻辑过期时间
private Object data; // 实际数据
}
// 写入 Redis
public void saveShop2Redis(Long id, Long expireSeconds) {
Shop shop = getById(id);
// 设置逻辑过期时间(当前时间 + 过期秒数)
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireSeconds);
// 封装为 RedisData
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(expireTime);
// 写入 Redis(不设置 TTL,永不过期)
stringRedisTemplate.opsForValue().set(
"cache:shop:" + id,
JSONUtil.toJsonStr(redisData)
);
}
两种方案对比:
| 一致性 | 强一致 | 弱一致(返回旧数据) |
| 性能 | 阻塞等待 | 不阻塞,性能好 |
| 实现复杂度 | 简单 | 复杂 |
| 适用场景 | 一般场景 | 热点数据,允许短暂不一致 |
Q6: 什么是缓存雪崩?如何解决?
什么是缓存雪崩:
场景 1:大量 key 同时过期
双 11 零点,100 万个商品缓存同时过期
↓
所有请求打到数据库
↓
数据库崩溃 💥
场景 2:Redis 宕机
Redis 服务器故障
↓
所有缓存失效
↓
数据库崩溃 💥
解决方案:
// 方案 1:随机过期时间
public void saveShopToRedis(Shop shop) {
// 基础过期时间 + 随机值(1-5 分钟)
long randomTime = 30 + RandomUtil.randomInt(1, 300);
stringRedisTemplate.opsForValue().set(
"cache:shop:" + shop.getId(),
JSONUtil.toJsonStr(shop),
randomTime,
TimeUnit.MINUTES
);
}
// 方案 2:多级缓存
本地缓存 (Caffeine) → Redis → 数据库
// 方案 3:Redis 集群
主从复制 + 哨兵模式 + Cluster 分片
🎫 模块三:优惠券秒杀
Q7: 秒杀下单的核心问题是什么?如何解决超卖?
超卖问题:
// ❌ 错误示范:直接减库存
@Transactional
public void seckillVoucher(Long voucherId) {
// 1. 查询库存
SeckillVoucher voucher = getById(voucherId);
if (voucher.getStock() < 1) {
throw new RuntimeException("库存不足");
}
// 2. 扣减库存
voucher.setStock(voucher.getStock() – 1);
updateById(voucher);
// 3. 创建订单
VoucherOrder order = new VoucherOrder();
order.setVoucherId(voucherId);
order.setUserId(UserHolder.getUser().getId());
save(order);
}
// 并发场景:
线程 A: 查询库存=10 → 减 1 → 更新为 9
线程 B: 查询库存=10 → 减 1 → 更新为 9 ❌(覆盖更新)
结果:2 个线程卖了 1 张券,库存只减了 1!
解决方案 1:悲观锁(synchronized)
// 同步锁解决超卖
public void seckillVoucher(Long voucherId) {
synchronized (voucherId.toString().intern()) {
// 1. 查询库存
SeckillVoucher voucher = getById(voucherId);
if (voucher.getStock() < 1) {
throw new RuntimeException("库存不足");
}
// 2. 扣减库存
voucher.setStock(voucher.getStock() – 1);
updateById(voucher);
// 3. 创建订单
VoucherOrder order = new VoucherOrder();
order.setVoucherId(voucherId);
order.setUserId(UserHolder.getUser().getId());
save(order);
}
}
// 问题:
// 1. 锁粒度太大,性能差
// 2. 集群环境下失效(不同服务器的锁不共享)
解决方案 2:乐观锁(CAS)
// 乐观锁实现
@Transactional
public void seckillVoucher(Long voucherId) {
// 1. 扣减库存(使用 SQL 更新)
boolean success = update()
.setSql("stock = stock – 1")
.eq("voucher_id", voucherId)
.eq("stock", 10) // 乐观锁条件
.update();
if (!success) {
throw new RuntimeException("库存不足或更新失败");
}
// 2. 创建订单
VoucherOrder order = new VoucherOrder();
order.setVoucherId(voucherId);
order.setUserId(UserHolder.getUser().getId());
save(order);
}
// SQL 执行:
UPDATE tb_seckill_voucher
SET stock = stock – 1
WHERE voucher_id = ? AND stock = 10
// 优点:性能好,支持集群
// 缺点:ABA 问题,需要重试机制
解决方案 3:Redis 预减库存(你的项目实现)
// 1. 秒杀开始前,将库存预热到 Redis
stringRedisTemplate.opsForValue().set(
"seckill:stock:" + voucherId,
String.valueOf(stock)
);
// 2. 秒杀时,使用 Redis 原子操作扣减库存
public Result seckillVoucher(@PathVariable Long voucherId) {
// 使用 Lua 脚本保证原子性
String luaScript = """
local key = KEYS[1]
local stock = tonumber(redis.call('GET', key))
if stock <= 0 then
return -1
end
redis.call('DECR', key)
return 1
""";
RedisScript<Long> redisScript = RedisScript.of(luaScript, Long.class);
Long result = stringRedisTemplate.execute(
redisScript,
Collections.singletonList("seckill:stock:" + voucherId)
);
if (result == null || result <= 0) {
return Result.fail("库存不足");
}
// 3. 异步创建订单
voucherOrderService.createVoucherOrder(voucherId);
return Result.ok("秒杀成功");
}
Q8: 如何实现一人一单?
问题分析:
一个用户购买多张券:
用户 A → 线程 1 → 购买成功
用户 A → 线程 2 → 购买成功 ❌(违反一人一单)
解决方案:
@Transactional
public void createVoucherOrder(Long voucherId) {
// 获取当前用户
Long userId = UserHolder.getUser().getId();
// 1. 一人一单控制
int count = query().eq("user_id", userId)
.eq("voucher_id", voucherId)
.count();
if (count > 0) {
throw new RuntimeException("每人只能购买一单!");
}
// 2. 扣减库存
boolean success = update()
.setSql("stock = stock – 1")
.eq("voucher_id", voucherId)
.eq("stock", stock)
.update();
if (!success) {
throw new RuntimeException("库存不足");
}
// 3. 创建订单
VoucherOrder order = new VoucherOrder();
order.setVoucherId(voucherId);
order.setUserId(userId);
order.setId(redisIdWorker.nextId("order"));
save(order);
}
// 问题:并发场景下,两次查询可能都返回 0
// 解决:加锁
public synchronized void createVoucherOrder(Long voucherId) {
// 获取当前用户
Long userId = UserHolder.getUser().getId();
// 一人一单检查
int count = query().eq("user_id", userId)
.eq("voucher_id", voucherId)
.count();
if (count > 0) {
throw new RuntimeException("每人只能购买一单!");
}
// 创建订单…
}
// 集群环境:使用 Redis 分布式锁
String lockKey = "lock:order:" + userId;
boolean isLock = tryLock(lockKey);
if (isLock) {
try {
createVoucherOrder(voucherId);
} finally {
unlock(lockKey);
}
}
Q9: 全局唯一 ID 在秒杀中有什么作用?
作用:
// 订单 ID 生成
public long nextId(String keyPrefix) {
// 1. 生成时间戳(相对时间)
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond – BEGIN_TIMESTAMP;
// 2. 生成序列号(按天递增)
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long count = stringRedisTemplate.opsForValue()
.increment("icr:" + keyPrefix + ":" + date);
// 3. 拼接返回
return timestamp << COUNT_BITS | count;
}
// 订单表设计
CREATE TABLE tb_voucher_order (
id bigint(20) PRIMARY KEY, — 雪花算法生成
user_id bigint(20),
voucher_id bigint(20),
status int,
create_time timestamp
);
// 优点:
// 1. 趋势递增,InnoDB 主键索引性能好
// 2. 不依赖数据库自增,支持分库分表
// 3. 包含时间信息,便于问题排查
🎯 综合实战题
Q10: 如果让你设计一个完整的秒杀系统,你会考虑哪些方面?
完整设计方案:
┌─────────────────────────────────────────────┐
│ 秒杀系统架构 │
└─────────────────────────────────────────────┘
1. 前端层
– 秒杀按钮防重复点击
– 倒计时同步
– 静态资源 CDN 加速
2. 网关层
– 限流(令牌桶、漏桶)
– 黑名单
– 请求校验
3. 服务层
– Redis 预减库存
– 消息队列异步下单
– 分布式锁保证一人一单
4. 数据层
– 数据库乐观锁
– 分库分表
– 主从复制
5. 监控层
– QPS 监控
– 库存监控
– 异常告警
核心代码:
// 完整秒杀流程
@PostMapping("/seckill/{id}")
public Result seckillVoucher(@PathVariable Long id) {
// 1. 校验秒杀资格(黑名单、限购等)
checkSeckillQualify(id);
// 2. Redis 预减库存
Long result = redisTemplate.execute(
luaScript,
Collections.singletonList("seckill:stock:" + id)
);
if (result <= 0) {
return Result.fail("库存不足");
}
// 3. 发送消息到队列
SeckillMessage message = new SeckillMessage();
message.setVoucherId(id);
message.setUserId(UserHolder.getUser().getId());
rabbitTemplate.convertAndSend("seckill.queue", message);
return Result.ok("排队中,请稍后…");
}
// 异步消费消息
@RabbitListener(queues = "seckill.queue")
public void handleSeckill(SeckillMessage message) {
// 1. 一人一单检查(分布式锁)
String lockKey = "lock:order:" + message.getUserId();
RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock()) {
try {
// 2. 检查是否已购买
int count = orderService.count()
.eq("user_id", message.getUserId())
.eq("voucher_id", message.getVoucherId())
.count();
if (count > 0) {
return;
}
// 3. 扣减库存(乐观锁)
boolean success = seckillVoucherService.update()
.setSql("stock = stock – 1")
.eq("voucher_id", message.getVoucherId())
.eq("stock", message.getStock())
.update();
if (!success) {
throw new RuntimeException("库存不足");
}
// 4. 创建订单
VoucherOrder order = new VoucherOrder();
order.setVoucherId(message.getVoucherId());
order.setUserId(message.getUserId());
order.setId(redisIdWorker.nextId("order"));
orderService.save(order);
} finally {
lock.unlock();
}
}
}
📝 面试技巧总结
回答策略:
加分项:
✅ 能说出多种方案并对比优劣
✅ 有实际的代码实现经验
✅ 考虑过并发、性能、扩展性
✅ 了解方案的适用场景
✅ 能画出架构图说明
这些题目涵盖了你这三个模块的核心知识点,理解透彻后,面试中遇到相关问题都能应对自如!有任何不懂的地方随时问我!🚀

