共计 2346 个字符,预计需要花费 6 分钟才能阅读完成。
背景痛点
在电商促销活动中,Skill 脚本的高并发执行常常面临三大核心问题:

-
库存超卖:当多个请求同时扣减库存时,传统的事务锁可能导致数据库连接耗尽。例如:
BEGIN; SELECT stock FROM items WHERE id=1001 FOR UPDATE; -- 行锁竞争 UPDATE items SET stock=stock-1 WHERE id=1001; COMMIT;在 1000QPS 下,MySQL 的 TPS 通常不超过 500,导致大量请求堆积。
-
重复执行:由于网络抖动或重试机制,用户可能多次触发同一秒杀动作。某案例显示,未做幂等处理时 17% 的请求存在重复提交。
-
服务雪崩:某次大促中,因未做熔断保护,一个 SKU 的脚本异常导致整个订单服务响应时间从 50ms 飙升到 2s。
技术选型
分布式锁方案对比
| 特性 | Redis | ZooKeeper |
|---|---|---|
| 性能 | 10w+ QPS | 1w- QPS |
| 锁释放保障 | 依赖 TTL | 会话断开自动释放 |
| 实现复杂度 | 低(SETNX) | 高(临时顺序节点) |
选择 Redis 因其更高的吞吐量,配合 Lua 脚本可实现原子操作:
-- KEYS[1]:lock_key, ARGV[1]:request_id, ARGV[2]:expire_time
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
return redis.call('expire', KEYS[1], ARGV[2])
else
return 0
end
消息队列削峰
采用 Kafka 分区消费模式,将库存扣减操作异步化:
# 生产者示例
producer.send('skill_event',
key=user_id,
value=json.dumps({'sku': 1001, 'ts': int(time.time())}))
# 消费者组确保单分区顺序消费
consumer = KafkaConsumer('skill_event',
group_id='stock_group',
value_deserializer=lambda m: json.loads(m.decode('utf-8')))
核心实现
Go 语言分布式锁实现
type RedisLock struct {
conn redis.Conn
key string
requestID string
ttl time.Duration
}
// 加锁(时间复杂度 O(1))func (l *RedisLock) Acquire() (bool, error) {reply, err := redis.String(l.conn.Do("SET", l.key, l.requestID, "NX", "PX", l.ttl.Milliseconds()))
return reply == "OK", err
}
// 释放锁(CAS 操作)func (l *RedisLock) Release() error {
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end`
_, err := redis.Int(l.conn.Do("EVAL", script, 1, l.key, l.requestID))
return err
}
库存扣减 Lua 脚本
-- KEYS[1]: 库存 key, KEYS[2]: 订单去重 key, ARGV[1]: 购买数量, ARGV[2]: 用户 ID
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return {-1, "库存不足"} -- 第一个返回值作为状态码
end
-- 检查是否已下单(幂等控制)if redis.call('exists', KEYS[2]) == 1 then
return {-2, "请勿重复提交"}
end
-- 原子操作
redis.call('decrby', KEYS[1], ARGV[1])
redis.call('set', KEYS[2], 1, 'EX', 3600)
return {1, "下单成功"}
性能优化
压测数据对比(JMeter 500 并发)
| 指标 | 纯 DB 方案 | Redis+Lua 方案 |
|---|---|---|
| 平均响应时间 | 1200ms | 35ms |
| 错误率 | 23% | 0.1% |
| 吞吐量 | 82/sec | 4800/sec |
Sentinel 熔断规则配置
// 当慢调用比例 >50% 且持续 5 秒时熔断
FlowRule rule = new FlowRule();
rule.setResource("skillScript");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(10000);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
rule.setWarmUpPeriodSec(10);
避坑指南
-
Redis 集群模式 :Lua 脚本必须使用
hash tag确保所有操作在相同 slot{sku1001}.stock -- 使用大括号包裹关键 hash 部分 -
消息幂等:结合 Redis SETNX 实现去重
def is_duplicate(msg): key = f"dedup:{msg['sku']}:{msg['user_id']}" return not redis.setnx(key, 1, ex=86400) -
灰度发布:采用双队列方案逐步迁移
v1_script_queue --> 10% 流量 v2_script_queue --> 90% 流量
开放性问题
- 当业务需要支持多地域(如华东、华北集群)时,如何设计库存同步方案?
- 在 Redis 持久化策略(AOF vs RDB)的选择上,如何平衡性能和数据安全性?
- 如何利用 Wireshark 分析 Redis 协议层面的性能瓶颈?
正文完
