欢迎光临
我们一直在努力

短信登录+查询缓存+优惠卷秒杀

深度思考

我将根据你的项目代码和学习内容,为你整理三大模块的高频面试题。让我先查看你的相关代码实现。

已检索仓库 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 → 服务器 BRedis 查询 → 找到用户信息 ✅

Redis vs Session 对比:

维度SessionRedis
存储位置 服务器内存 独立 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();
}
}
}


📝 面试技巧总结

回答策略:

  • 先说问题 – 什么是 XX 问题
  • 再讲原理 – 为什么会出现
  • 给出方案 – 多种解决方案对比
  • 结合实际 – 你项目中的实现
  • 展示代码 – 关键代码片段
  • 加分项:

    ✅ 能说出多种方案并对比优劣
    ✅ 有实际的代码实现经验
    ✅ 考虑过并发、性能、扩展性
    ✅ 了解方案的适用场景
    ✅ 能画出架构图说明


    这些题目涵盖了你这三个模块的核心知识点,理解透彻后,面试中遇到相关问题都能应对自如!有任何不懂的地方随时问我!🚀

    赞(0)
    未经允许不得转载:171主机测评 » 短信登录+查询缓存+优惠卷秒杀
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址