Appearance
面经整理
虾皮二面后端面试记录
- 1.商品价格需叠加会员折扣、满减、优惠券,如何设计灵活的计算策略?
- 2.每日对账时发现订单金额与支付记录不一致,如何设计核对系统?
- 3.异步处理订单的线程池任务堆积,如何定位和调整参数?
- 4.促期间如何防止系统被流量打挂?实现一个基于 Guava RateLimiter 或 Sentinel 的限流方案
- 5.用户支付成功后,需要更新订单状态、扣减库存、发放优惠券。如何保证这三个操作的事务一致性?
- 单体服务:三个操作在同一个事务里即可
- 分布式服务:CAP -> ACID、BASE
- 6.订单表数据量过大,如何设计分库分表策略?如何解决跨分页查询和订单 ID 全局唯一性问题?
- 7.频繁查询不存在的商品 ID 导致 DB 压力,如何防御?(缓存穿透问题)
- 空值缓存(不存在的 ID 基本相同)
- 布隆过滤器
- 8.用户下单预占库存,15 分钟未支付自动释放,如何实现?
- 9.高并发下发放优惠券如何避免重复发放?请用 Redis 实现一个防重方案
- 10.第三方物流 API 延迟高,如何设计异步回调+本地状态机?
- 回调接口:HMAC-SHA256 签名验证、时间戳防重放
- 定时补偿任务
关键知识点
- 限流
- 熔断
- 降级
- 重试
- 容错
- 兜底策略,防止请求暴涨
- 监控告警机制
- 幂等性控制
- 分库分表
- 分布式事务
- TCC (Try-Confirm-Cancel)
- 最终一致性 + 异步补偿(ACID、BASE:放弃强一致,追求最终一致,出现异常靠补偿机制修复)
- 缓存
- 缓存淘汰策略 LRU
- 缓存击穿 SingleFlight 缓存穿透 缓存雪崩(设置不同 TTL + 限流兜底策略)
- 缓存预热:异步预热、分批预热
- 缓存更新策略
- 链路追踪
- 读写分离
- 案例:MVCC
4.促期间如何防止系统被流量打挂?
- 令牌桶
- 削峰填谷
- 使用消息队列中间件(常规打法)
- 流量控制模型:[用户请求] → [限流判断] → [消息队列] → [消费者服务处理] → [落库存/写数据库]
- 延迟处理 / 排队机制
- 拓展:死信队列
- 使用消息队列中间件(常规打法)
- 异步下单设计 / 分布式锁 / 超卖防御
| 技术 | 解决问题 |
|---|---|
| 消息队列 | 削峰填谷、防止服务雪崩 |
| 分布式锁 | 防止并发冲突、重复下单 |
| Redis 扣库存 | 防止超卖、限量销售 |
下一步安排:
- 库存缓存一致性处理
- 订单状态流转 + 幂等设计
- 分布式唯一订单号生成
- 多服务协作下的事务一致性(TCC / SAGA)
7.频繁查询不存在的商品 ID 导致 DB 压力,如何防御?
布隆过滤器:
- Trade-off: 牺牲“准确率”换“极致性能与内存效率”
- 查询流程:商品 ID -> 中间件/拦截器 -> 布隆过滤器 -> Redis 缓存(缓存配合)-> DB
- 通过定时 reload+全量替换,而不是实时 add来更新商品 ID
- 因为大多数中大型系统都走的是:读写分离/服务解耦 + 无状态/分布式多副本部署 的架构模型
go
import (
"github.com/bits-and-blooms/bloom/v3"
"sync"
)
type ProductBloomFilter struct {
filter *bloom.BloomFilter
lock sync.RWMutex
}
func NewProductBloomFilter(expectedItems uint, falsePositiveRate float64) *ProductBloomFilter {
bf := bloom.NewWithEstimates(expectedItems, falsePositiveRate)
return &ProductBloomFilter{filter: bf}
}
// 加载商品 ID
func (p *ProductBloomFilter) Load(ids []string) {
p.lock.Lock()
defer p.lock.Unlock()
for _, id := range ids {
p.filter.AddString(id)
}
}
// 判断商品 ID 是否可能存在
func (p *ProductBloomFilter) Exists(id string) bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.filter.TestString(id)
}9.高并发下发放优惠券如何避免重复发放?
幂等性控制:不管多少请求并发打过来,同一个用户(或同一业务唯一标识)只能成功一次。
普通 Redis 防重方案
用 Redis 来搞,思路很直接:利用 Redis 的原子操作 + 过期控制,实现一个“先占坑,后发券”的防重机制
go
key := fmt.Sprintf("coupon:%d:user:%d", couponId, userId)
// NX 保证只有第一次能成功(不存在才创建)
ok, err := rdb.SetNX(ctx, key, "1", 5*time.Minute).Result()
if err != nil {
panic(err)
}
if !ok {
return fmt.Errorf("重复请求,已发过")
}
// 发券逻辑:只有占坑成功的人才进入发券流程
issueCoupon(userId, couponId)Lua 脚本原子防重方案
业务背景 高并发优惠券发放,要求:
- 防重复(同一用户+券只发一次)
- 发券成功后:
- 写发券记录(日志)
- 同步库存(Redis 扣减)
- 推送发券消息(Redis List 进队列)
- 更新用户的“已领取券列表”缓存
这就是典型的多步 Redis 操作,如果用多条命令执行,会有并发空隙和数据不一致风险
为什么必须 Lua
- 防重 + 多步业务(占坑、扣库存、写日志、推消息、更新缓存)全在一条命令里
- 消除并发空隙,不怕插队
- 无网络多次往返,性能更稳
go
script := `
-- KEYS[1] = 防重Key
-- KEYS[2] = 库存Key
-- KEYS[3] = 日志List
-- KEYS[4] = 消息队列Key
-- KEYS[5] = 用户已领券Set
-- ARGV[1] = 锁值
-- ARGV[2] = 锁过期秒数
-- ARGV[3] = 用户ID
-- ARGV[4] = 券ID
-- 1. 防重检查 + 占坑
if redis.call("setnx", KEYS[1], ARGV[1]) == 0 then
return 0 -- 已经发过
end
redis.call("expire", KEYS[1], ARGV[2])
-- 2. 扣减库存
if redis.call("decr", KEYS[2]) < 0 then
return -1 -- 库存不足
end
-- 3. 写日志
redis.call("rpush", KEYS[3], ARGV[3] .. ":" .. ARGV[4])
-- 4. 推送消息到队列
redis.call("rpush", KEYS[4], ARGV[3] .. ":" .. ARGV[4])
-- 5. 更新用户已领取集合
redis.call("sadd", KEYS[5], ARGV[4])
return 1 -- 成功
`
keys := []string{
fmt.Sprintf("coupon:lock:%d:%d", couponId, userId),
fmt.Sprintf("coupon:stock:%d", couponId),
"coupon:log",
"coupon:queue",
fmt.Sprintf("user:%d:coupons", userId),
}
args := []interface{}{"1", "300", userId, couponId}
res, err := rdb.Eval(ctx, script, keys, args...).Result()
if err != nil {
panic(err)
}
switch res.(int64) {
case 0:
fmt.Println("重复请求")
case -1:
fmt.Println("库存不足")
case 1:
fmt.Println("发券成功")
}