1. 优惠券秒杀的业务挑战与技术选型
在电商大促场景中,优惠券秒杀是最典型的流量洪峰场景之一。去年双十一期间,某头部电商平台单日发放的优惠券数量突破2亿张,其中热门品类的100元无门槛券在300毫秒内被抢空。这种高并发场景下,传统数据库直接扣减库存的方案会导致两个致命问题:
- 超卖问题:当多个请求同时查询剩余库存并执行扣减时,可能造成实际发放数量超过预设库存
- 数据库雪崩:瞬时高并发请求直接冲击数据库,导致连接池耗尽、响应延迟飙升
我们团队在早期实践中曾采用过纯数据库事务的方案,在500QPS的压测下就出现了明显的性能瓶颈。后来通过引入Redis作为缓存层,最终实现了单节点5万QPS的稳定处理能力。这里分享下我们基于Redis的完整解决方案演进过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis分布式锁的进阶实践
2.1 从SETNX到Redisson的升级之路
最初的方案使用Redis的SETNX命令实现简单分布式锁:
java复制// 基础版分布式锁实现
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:coupon:"+couponId, "1");
if(locked) {
try {
// 业务处理
} finally {
redisTemplate.delete("lock:coupon:"+couponId);
}
}
这个方案存在三个明显缺陷:
- 锁没有过期时间,可能因进程崩溃导致死锁
- 非原子性操作,设置锁和设置过期时间不是原子操作
- 不具备可重入性
我们后来切换到Redisson框架,它提供了更完善的分布式锁实现:
java复制RLock lock = redissonClient.getLock("lock:coupon:"+couponId);
try {
if(lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// 业务处理
}
} finally {
lock.unlock();
}
2.2 Redisson锁的底层原理
Redisson的分布式锁实现有几个关键技术点:
- 哈希槽分配:通过CRC16算法将key分配到16384个槽位,保证相同资源的请求总是落到同一Redis节点
- 看门狗机制:后台线程定期(默认10秒)检查并延长锁持有时间,避免业务未完成时锁过期
- Lua脚本原子操作:所有锁操作都通过Lua脚本保证原子性
重要提示:Redisson默认会为每个锁创建独立的连接,在高并发场景下需要注意连接数监控。我们曾遇到过因未正确释放锁导致连接数暴涨的case,可以通过设置connectionPoolSize参数来控制连接池大小。
3. 库存扣减的Lua脚本优化
3.1 为什么需要Lua脚本
考虑以下典型的库存扣减流程:
- 查询当前库存
- 判断是否充足
- 执行扣减
- 更新数据库
在并发环境下,多个客户端可能同时执行到步骤2都判断库存充足,导致最终超卖。我们通过Lua脚本将整个操作原子化:
lua复制local key = KEYS[1]
local change = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or "0")
if current + change >= 0 then
redis.call('SET', key, current + change)
return current + change
else
return -1
end
3.2 Lua脚本的性能优化技巧
- 参数序列化:将复杂参数用JSON序列化后传递,减少网络传输量
- 脚本缓存:使用SCRIPT LOAD预先加载脚本,通过SHA1摘要执行
- 错误处理:通过pcall捕获运行时错误,避免脚本中断影响主流程
我们在生产环境测试发现,使用Lua脚本相比多命令事务方式,吞吐量提升了约40%,平均延迟降低了60%。
4. 分布式环境下的容错设计
4.1 缓存与数据库的一致性
采用"先更新数据库再删除缓存"的策略,配合消息队列实现最终一致性:
java复制// 伪代码示例
@Transactional
public void deductStock(Long couponId) {
// 1. 数据库扣减
couponMapper.deductStock(couponId);
// 2. 发送MQ消息
mqProducer.send(new StockUpdateEvent(couponId));
}
// MQ消费者
public void handleStockUpdate(StockUpdateEvent event) {
redisTemplate.delete("stock:" + event.getCouponId());
}
4.2 热点Key的发现与处理
通过Redis的MONITOR命令或性能监控工具,我们发现秒杀场景下会出现典型的热点Key问题。解决方案包括:
- Key分片:将单个优惠券库存拆分为多个子库存
code复制stock:coupon:1001:shard1 stock:coupon:1001:shard2 - 本地缓存:在应用层使用Guava Cache缓存库存信息,降低Redis访问频率
- 随机过期时间:避免大量Key同时过期导致的缓存雪崩
5. 生产环境中的性能调优
5.1 Redis服务器配置建议
在我们的生产环境中,针对秒杀场景特别调整了以下参数:
code复制# 最大内存限制
maxmemory 16gb
# 内存淘汰策略
maxmemory-policy allkeys-lru
# 连接池配置
timeout 300
tcp-keepalive 60
5.2 Redisson配置优化
通过以下配置解决连接数过多问题:
yaml复制spring:
redis:
redisson:
config: |
singleServerConfig:
connectionPoolSize: 64
idleConnectionTimeout: 10000
connectTimeout: 1000
timeout: 3000
5.3 压力测试指标
我们使用JMeter进行压测,关键指标要求:
- 平均响应时间 < 200ms
- 99线 < 500ms
- 错误率 < 0.1%
- 单节点QPS > 3万
在实际业务中,这个方案成功支撑了单日超2亿次的优惠券领取请求,系统稳定性达到99.99%。对于想深入掌握Redis高并发实践的同学,建议重点理解分布式锁的实现原理和Lua脚本的原子性特性,这是应对秒杀场景的两大核心技术要点。
