Skip to content

面经整理

虾皮二面后端面试记录

  • 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.促期间如何防止系统被流量打挂?

  • 令牌桶
  • 削峰填谷
    1. 使用消息队列中间件(常规打法)
      • 流量控制模型:[用户请求] → [限流判断] → [消息队列] → [消费者服务处理] → [落库存/写数据库]
    2. 延迟处理 / 排队机制
      • 拓展:死信队列
  • 异步下单设计 / 分布式锁 / 超卖防御
技术解决问题
消息队列削峰填谷、防止服务雪崩
分布式锁防止并发冲突、重复下单
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 脚本原子防重方案

业务背景 高并发优惠券发放,要求:

  1. 防重复(同一用户+券只发一次)
  2. 发券成功后:
    • 写发券记录(日志)
    • 同步库存(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("发券成功")
}