☰
Redis分布式ID与秒杀系统高并发实战
2026/11/3 8:59:07 网站建设 项目流程

1. Redis全局唯一ID实现方案解析

在分布式系统中,全局唯一ID(Distributed ID)是构建高并发业务的基础设施。我们先来看一个典型的业务场景:某电商平台在618大促期间,每秒需要处理超过10万笔订单,每笔订单都需要一个绝对不会重复的ID标识。

1.1 为什么不能用数据库自增ID?

传统单机系统中,我们习惯使用数据库的自增ID(AUTO_INCREMENT)作为主键。但在分布式环境下,这种方式存在三个致命缺陷:

  1. 分库分表时ID冲突:当订单表被水平拆分到多个数据库实例时,各实例的自增ID会出现重复
  2. 业务数据暴露风险:连续的ID值会暴露业务规模(如从订单ID推测每日订单量)
  3. 性能瓶颈:ID生成完全依赖数据库写入,高并发时可能成为系统瓶颈

1.2 Redis生成ID的核心设计

我们采用的方案是:时间戳 + 每日序列号的64位长整型组合。具体实现如下:

public class RedisIdWorker { // 基准时间戳(可设置为项目上线日期) private static final long BEGIN_TIMESTAMP = 1735689600L; // 序列号位数 private static final int COUNT_BITS = 32; private StringRedisTemplate stringRedisTemplate; public long nextId(String keyPrefix) { // 1.生成时间戳(当前秒数-基准秒数) long timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP; // 2.生成序列号(按天自增) String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); long count = stringRedisTemplate.opsForValue() .increment("icr:" + keyPrefix + ":" + date); // 3.拼接返回(时间戳左移32位后与序列号或运算) return timestamp << COUNT_BITS | count; } }

这个方案的巧妙之处在于:

  • 时间戳部分(31位):可支持约68年的时间范围(2^31秒≈68年)
  • 序列号部分(32位):每天每个业务线可生成42亿个ID(2^32)
  • 每日Key自动过期:通过Redis的过期机制自动清理历史数据

生产环境建议:对于时间戳部分,建议选择项目上线日期作为基准时间,而不是默认的1970年,这样可以延长ID的有效使用期限。

2. 优惠券秒杀核心逻辑实现

2.1 基础秒杀流程

我们先实现最基础的秒杀功能,核心流程包括:

  1. 校验秒杀时间
  2. 校验库存
  3. 扣减库存
  4. 创建订单
@Transactional public Result seckillVoucher(Long voucherId) { // 1.查询优惠券 SeckillVoucher voucher = seckillVoucherService.getById(voucherId); // 2.校验秒杀时间 if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀尚未开始"); } if (voucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已经结束"); } // 3.校验库存 if (voucher.getStock() < 1) { return Result.fail("库存不足"); } // 4.扣减库存(CAS乐观锁) boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0) // 核心:库存大于0时才扣减 .update(); if (!success) { return Result.fail("库存不足"); } // 5.创建订单 VoucherOrder order = new VoucherOrder(); order.setId(redisIdWorker.nextId("order")); order.setUserId(UserHolder.getUser().getId()); order.setVoucherId(voucherId); save(order); return Result.ok(order.getId()); }

2.2 超卖问题解决方案

当多个线程同时执行"查询库存→扣减库存"时,会出现超卖问题。我们采用两种解决方案:

方案一:悲观锁
// 在方法上添加synchronized(不推荐用于生产环境) public synchronized Result seckillVoucher(Long voucherId) { // ...原有逻辑 }

缺点:性能差,所有请求串行处理

方案二:乐观锁(推荐)
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) //.eq("stock", voucher.getStock()) // 严格版本(成功率低) .gt("stock", 0) // 宽松版本(推荐) .update();

优化技巧:使用gt("stock", 0)代替精确值匹配,可以提高并发成功率

3. 一人一单的并发控制

为了防止用户重复抢购,我们需要实现"同一用户只能购买一次"的限制。

3.1 单机版解决方案

public Result seckillVoucher(Long voucherId) { // ...前置校验 Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()) { // 获取Spring事务代理对象 IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder(Long voucherId) { // 查询订单 Long count = query().eq("user_id", userId) .eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用户已经购买过"); } // ...扣减库存和创建订单 }

关键点:

  1. 使用userId.toString().intern()作为锁对象,确保同一个用户的请求串行执行
  2. 通过AopContext.currentProxy()获取代理对象,保证事务生效
  3. 锁要加在事务方法外部,避免事务未提交时锁已释放

3.2 集群环境下的分布式锁

单机锁在集群环境下会失效,我们需要引入Redis分布式锁:

public Result seckillVoucher(Long voucherId) { // ...前置校验 // 获取锁对象 SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId); if (!lock.tryLock(10, TimeUnit.SECONDS)) { return Result.fail("不允许重复下单"); } try { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { lock.unlock(); } }

4. Redis分布式锁的进阶优化

4.1 防止锁误删

基础分布式锁存在锁误删问题,我们通过添加线程标识来解决:

public class SimpleRedisLock implements Lock { private static final String ID_PREFIX = UUID.randomUUID().toString(); public boolean tryLock(long timeoutSec) { String threadId = ID_PREFIX + Thread.currentThread().getId(); return stringRedisTemplate.opsForValue() .setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS); } public void unlock() { String threadId = ID_PREFIX + Thread.currentThread().getId(); String id = stringRedisTemplate.opsForValue().get(key); if (threadId.equals(id)) { stringRedisTemplate.delete(key); } } }

4.2 保证原子性操作

使用Lua脚本保证"判断锁归属+删除锁"的原子性:

-- unlock.lua if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

Java调用代码:

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); UNLOCK_SCRIPT.setResultType(Long.class); } public void unlock() { stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(key), ID_PREFIX + Thread.currentThread().getId()); }

5. 使用Redisson生产级分布式锁

手动实现的分布式锁仍存在不足,推荐使用Redisson:

5.1 配置Redisson客户端

@Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); return Redisson.create(config); } }

5.2 使用可重入锁

public Result seckillVoucher(Long voucherId) { // ...前置校验 RLock lock = redissonClient.getLock("lock:order:" + userId); if (!lock.tryLock()) { return Result.fail("不允许重复下单"); } try { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } finally { lock.unlock(); } }

Redisson锁的优势:

  1. 可重入设计:同一线程可以多次获取锁
  2. WatchDog机制:自动续期,防止业务未完成锁已过期
  3. 多种锁类型:支持公平锁、联锁、红锁等复杂场景

6. 性能优化:异步秒杀设计

6.1 Redis消息队列实现方案

方案优点缺点
List结构实现简单不支持消费者组
Pub/Sub实时性高消息不持久化
Stream功能完善实现复杂

6.2 基于Stream的实现代码

// 1. 创建消息队列 @Bean public StreamMessageListenerContainer<String, ObjectRecord<String, String>> streamContainer() { StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> options = StreamMessageListenerContainer.StreamMessageListenerContainerOptions .builder() .pollTimeout(Duration.ofSeconds(3)) .build(); StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create(redisConnectionFactory, options); container.receive(Consumer.from("group1", "consumer1"), StreamOffset.create("stream.orders", ReadOffset.lastConsumed()), message -> { // 处理消息 handleVoucherOrder(message.getValue()); }); return container; } // 2. 修改秒杀Lua脚本 local voucherId = ARGV[1] local userId = ARGV[2] local orderId = ARGV[3] if (tonumber(redis.call('get', 'seckill:stock:' .. voucherId)) <= 0) then return 1 end if (redis.call('sismember', 'seckill:users:' .. voucherId, userId) == 1) then return 2 end redis.call('xadd', 'stream.orders', '*', 'voucherId', voucherId, 'userId', userId, 'id', orderId) return 0

7. 总结与最佳实践

在实现高并发秒杀系统时,建议采用以下技术组合:

  1. ID生成:Redis自增+时间戳方案
  2. 库存扣减:Redis原子计数+Lua脚本
  3. 并发控制:Redisson分布式锁
  4. 性能优化:Redis Stream异步处理

避坑指南:

  1. 分布式锁一定要设置过期时间,且要比业务执行时间稍长
  2. Lua脚本中不要编写复杂逻辑,保持简洁
  3. 生产环境建议使用Redis集群模式,提高可用性
  4. 对于超高并发场景,可以考虑库存预热+本地缓存

实测数据显示,经过上述优化后,单机Redis可以支撑约5万QPS的秒杀请求,完全满足大多数电商场景的需求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询