1. 全局唯一ID设计与实现
在分布式系统中,生成全局唯一ID是一个常见需求。传统自增ID存在两个明显问题:一是ID规律性太强,容易被猜测和爬取;二是受单表数据量限制,在分库分表场景下难以保证全局唯一性。
1.1 常见全局ID方案对比
目前主流的全局ID生成方案有以下几种:
- UUID:简单但无序,影响数据库索引性能
- 数据库自增:依赖单点数据库,扩展性差
- Snowflake算法:Twitter开源的分布式ID算法
- Redis自增:利用Redis的原子性操作实现
我们重点分析Redis实现方案。Redis的INCR命令具有原子性,可以保证在高并发场景下ID的唯一性。但单纯使用自增数值仍存在问题,我们需要对其进行改造。
1.2 Redis自增ID优化设计
我们采用"时间戳+序列号"的复合结构:
code复制0(符号位)- 时间戳(31bit)- 序列号(32bit)
这种设计有以下优势:
- 时间戳部分保证ID趋势递增
- 序列号保证同一秒内的唯一性
- 64位长整型,兼容各种系统
实现代码如下:
java复制public long nextId(String keyPrefix){
// 1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2. 生成序列号
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
long count = stringRedisTemplate.opsForValue()
.increment("icr:" + keyPrefix + ":" + date);
// 3. 拼接并返回
return timestamp << COUNT_BITS | count;
}
关键点:使用日期作为Redis键的一部分,可以实现每日序列号重置,避免长期运行后序列号溢出问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 优惠券秒杀基础实现
2.1 下单流程设计
基础秒杀下单流程包含以下步骤:
- 校验秒杀是否开始/结束
- 检查库存是否充足
- 扣减库存
- 创建订单
java复制@Transactional
public Result createVoucherOrder(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始!");
}
// 3. 判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束!");
}
// 4. 判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足!");
}
// 5. 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!success) {
return Result.fail("库存不足!");
}
// 6. 创建订单
VoucherOrder order = new VoucherOrder();
// 设置订单信息...
save(order);
return Result.ok(order.getId());
}
2.2 超卖问题解决方案
在高并发场景下,多个线程同时查询库存并扣减,会导致超卖问题。解决方案主要有两种:
2.2.1 悲观锁方案
悲观锁认为并发冲突一定会发生,因此在操作数据前先获取锁。例如使用SELECT...FOR UPDATE:
sql复制SELECT * FROM seckill_voucher WHERE voucher_id = ? FOR UPDATE
缺点:
- 性能影响大
- 容易导致死锁
- 不适用于高并发场景
2.2.2 乐观锁方案
乐观锁认为冲突不常发生,只在更新时检查数据是否被修改过。实现方式:
- 版本号法:每次更新时检查版本号
- CAS法:直接比较数据值
我们采用CAS法实现:
java复制boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0) // CAS操作:库存大于0时才扣减
.update();
注意事项:直接比较库存值可能导致成功率下降(多个线程竞争时只有一个能成功)。实际业务中可以考虑放宽条件,只要库存不为负即可。
3. 一人一单实现与优化
3.1 基础实现方案
为防止用户重复购买,需要在创建订单前检查是否已存在该用户的订单:
java复制// 查询订单
long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId)
.eq("voucher_id", voucherId)
.count();
// 判断是否存在
if (count > 0) {
return Result.fail("该用户已经购买过");
}
3.2 并发安全问题
在高并发场景下,多个线程可能同时通过检查,导致一人多单。解决方案是加锁:
java复制synchronized(userId.toString().intern()) {
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
关键点:使用userId作为锁对象,保证同一用户的操作串行化。intern()方法确保相同用户ID返回相同的字符串对象。
3.3 集群环境下的锁问题
在集群环境下,单机锁失效,因为每个JVM有自己的锁管理。需要引入分布式锁。
4. 分布式锁深度解析
4.1 分布式锁实现方案对比
| 方案 | 实现原理 | 优点 | 缺点 |
|---|---|---|---|
| MySQL | 基于唯一索引或行锁 | 实现简单 | 性能较差 |
| Redis | SETNX命令 | 性能好 | 实现复杂 |
| Zookeeper | 临时有序节点 | 可靠性高 | 性能一般 |
4.2 Redis分布式锁实现
4.2.1 基础实现
获取锁:
redis复制SET lock:order:userId thread1 EX 10 NX
释放锁:
redis复制DEL lock:order:userId
Java实现:
java复制// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent("lock:order:" + userId, "thread1", 10, TimeUnit.SECONDS);
// 释放锁
stringRedisTemplate.delete("lock:order:" + userId);
4.2.2 锁误删问题解决
为防止锁被其他线程误删,需要在释放时验证锁持有者:
java复制// 获取锁标识
String lockValue = stringRedisTemplate.opsForValue().get("lock:order:" + userId);
// 验证是否自己的锁
if ("thread1".equals(lockValue)) {
stringRedisTemplate.delete("lock:order:" + userId);
}
4.2.3 原子性问题解决
使用Lua脚本保证判断和删除的原子性:
lua复制if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
Java调用:
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);
}
// 执行脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList("lock:order:" + userId),
"thread1");
4.3 Redisson高级特性
Redisson提供了更完善的分布式锁实现:
- 可重入锁:支持同一线程多次获取锁
- 锁重试:获取失败后自动重试
- 超时续约:看门狗机制自动延长锁时间
- 红锁:多节点锁,解决主从一致性问题
使用示例:
java复制// 获取锁
RLock lock = redissonClient.getLock("lock:order:" + userId);
try {
// 尝试获取锁,最多等待5秒,锁过期时间10秒
boolean isLock = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (isLock) {
// 执行业务逻辑
}
} finally {
// 释放锁
lock.unlock();
}
5. Redis优化秒杀方案
5.1 异步秒杀设计
将秒杀流程拆分为两个阶段:
-
资格校验阶段(同步)
- 校验秒杀时间
- 校验库存
- 校验一人一单
- 生成订单ID
-
订单创建阶段(异步)
- 扣减库存
- 创建订单
5.2 Redis预校验实现
使用Redis的Set结构存储已参与用户,实现高效校验:
lua复制-- 校验脚本
local voucherId = ARGV[1]
local userId = ARGV[2]
-- 校验库存
local stockKey = "seckill:stock:" .. voucherId
local stock = tonumber(redis.call('get', stockKey))
if stock <= 0 then
return 1
end
-- 校验是否已购买
local orderKey = "seckill:order:" .. voucherId
if redis.call('sismember', orderKey, userId) == 1 then
return 2
end
-- 扣减库存
redis.call('incrby', stockKey, -1)
-- 记录用户
redis.call('sadd', orderKey, userId)
return 0
5.3 消息队列实现
5.3.1 基于List的实现
生产者:
java复制stringRedisTemplate.opsForList().rightPush(
"seckill:queue",
voucherId + ":" + userId + ":" + orderId);
消费者:
java复制while (true) {
String message = stringRedisTemplate.opsForList()
.leftPop("seckill:queue", 30, TimeUnit.SECONDS);
if (message != null) {
// 处理消息
}
}
5.3.2 基于Stream的实现
创建消费者组:
redis复制XGROUP CREATE seckill:stream seckillGroup $ MKSTREAM
生产者:
java复制Map<String, String> message = new HashMap<>();
message.put("voucherId", String.valueOf(voucherId));
message.put("userId", String.valueOf(userId));
message.put("orderId", String.valueOf(orderId));
stringRedisTemplate.opsForStream().add(
"seckill:stream",
message);
消费者:
java复制while (true) {
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream()
.read(Consumer.from("seckillGroup", "consumer1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("seckill:stream", ReadOffset.lastConsumed()));
if (list == null || list.isEmpty()) {
continue;
}
// 处理消息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
// 确认消息
stringRedisTemplate.opsForStream().acknowledge(
"seckill:stream",
"seckillGroup",
record.getId());
}
6. 性能优化与问题排查
6.1 常见性能瓶颈
- Redis连接瓶颈:增加连接池大小
- Lua脚本执行时间过长:优化脚本逻辑
- 消息堆积:增加消费者数量
- 数据库压力:使用缓存减少数据库访问
6.2 监控指标
-
Redis监控:
- 内存使用率
- QPS
- 连接数
- 慢查询
-
业务监控:
- 秒杀成功率
- 平均响应时间
- 订单创建延迟
6.3 常见问题排查
-
库存超卖:
- 检查乐观锁条件
- 验证Lua脚本原子性
- 检查Redis与数据库数据一致性
-
重复下单:
- 验证分布式锁有效性
- 检查Set集合的唯一性
- 验证消息消费的幂等性
-
性能下降:
- 检查Redis慢查询日志
- 监控网络延迟
- 验证连接池配置
在实际项目中,我们通过压测发现当并发超过5000时,Redis连接会成为瓶颈。解决方案是调整Lettuce连接池配置:
properties复制spring.redis.lettuce.pool.max-active=200
spring.redis.lettuce.pool.max-wait=1000
spring.redis.lettuce.pool.max-idle=50
spring.redis.lettuce.pool.min-idle=10
另一个经验是Lua脚本中避免使用KEYS命令,因为它在大数据集上会阻塞Redis。我们改用SCAN命令分批次处理数据。
