Redis 秒杀优化实战
秒杀场景的挑战
秒杀活动是高并发场景的典型代表,主要面临以下挑战:
- 高并发访问:短时间内大量用户涌入
- 库存超卖:库存扣减的原子性问题
- 系统性能:数据库压力过大
- 公平性:防止机器人抢购
Redis 优化方案
1. 页面静态化 + CDN 加速
java
// 使用 Thymeleaf 或其他模板引擎提前生成静态页面
@GetMapping("/seckill/item/{id}")
public String getSeckillItem(@PathVariable Long id, Model model) {
// 从Redis获取商品信息
SeckillItem item = redisService.get("seckill:item:" + id);
model.addAttribute("item", item);
return "seckill_item_static";
}
2. Redis 预减库存
java
// 活动开始前将库存加载到Redis
public void loadSeckillItemToRedis(Long itemId, Integer stock) {
String key = "seckill:stock:" + itemId;
redisTemplate.opsForValue().set(key, stock);
}
3. 内存标记 + Redis 原子操作
java
// 使用本地内存标记减少Redis访问
private Map<Long, Boolean> localOverMap = new ConcurrentHashMap<>();
public boolean seckill(Long userId, Long itemId) {
// 1. 检查内存标记
if (localOverMap.get(itemId) != null) {
return false;
}
// 2. Redis预减库存
Long stock = redisTemplate.opsForValue().decrement("seckill:stock:" + itemId);
if (stock == null || stock < 0) {
localOverMap.put(itemId, true);
return false;
}
// 3. 进入下单队列
SeckillMessage message = new SeckillMessage(userId, itemId);
rabbitTemplate.convertAndSend("seckill.exchange", "seckill.routingKey", message);
return true;
}
4. Lua 脚本保证原子性
lua
-- seckill.lua
local userId = KEYS[1]
local itemId = KEYS[2]
local stockKey = "seckill:stock:" .. itemId
local orderKey = "seckill:order:" .. itemId
-- 检查库存
local stock = tonumber(redis.call('get', stockKey))
if stock <= 0 then
return 0
end
-- 检查是否已购买
if redis.call('sismember', orderKey, userId) == 1 then
return 1
end
-- 扣减库存并记录订单
redis.call('decr', stockKey)
redis.call('sadd', orderKey, userId)
return 2
Java 调用 Lua 脚本:
java
public class SeckillScript {
private static final String SECKILL_SCRIPT =
"local userId = KEYS[1]\n" +
"local itemId = KEYS[2]\n" +
// ... 上面Lua脚本内容
public static final RedisScript<Long> script =
new DefaultRedisScript<>(SECKILL_SCRIPT, Long.class);
}
// 使用脚本
Long result = redisTemplate.execute(
SeckillScript.script,
Arrays.asList(userId.toString(), itemId.toString())
);
5. 消息队列异步处理
java
@RabbitListener(queues = "seckill.queue")
public void processSeckillOrder(SeckillMessage message) {
Long userId = message.getUserId();
Long itemId = message.getItemId();
// 1. 数据库校验库存
SeckillItem item = seckillItemMapper.selectById(itemId);
if (item.getStock() <= 0) {
return;
}
// 2. 检查是否重复购买
SeckillOrder exist = seckillOrderMapper.findByUserIdAndItemId(userId, itemId);
if (exist != null) {
return;
}
// 3. 创建订单
createOrder(userId, item);
}
6. 分布式锁防止重复提交
java
public boolean trySeckillLock(Long userId, Long itemId) {
String lockKey = "seckill:lock:" + userId + ":" + itemId;
String value = UUID.randomUUID().toString();
Boolean success = redisTemplate.opsForValue().setIfAbsent(
lockKey, value, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
try {
// 执行业务逻辑
return true;
} finally {
// 释放锁时要验证value,防止误删其他线程的锁
if (value.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
return false;
}
完整优化流程
- 系统初始化:将秒杀商品库存加载到Redis
- 请求入口 :
- 先查内存标记 → 快速失败
- 执行Lua脚本 → 原子性操作
- 异步下单 :
- 通过消息队列削峰
- 后台服务处理订单
- 结果返回 :
- 轮询查询订单结果
- 或使用WebSocket推送结果
监控与降级
- Redis监控:监控QPS、内存使用、连接数等
- 限流措施 :
- 接口限流(如Guava RateLimiter)
- Nginx层限流
- 降级方案 :
- 活动过于火爆时展示友好提示
- 关闭非核心服务保证秒杀进行
通过以上优化,可以极大提升秒杀系统的性能和可靠性,轻松应对高并发场景。