1. 为什么需要全局唯一ID?
在优惠券秒杀系统中,生成全局唯一ID是一个看似简单实则关键的技术挑战。想象一下双十一零点,数百万用户同时点击"立即抢购"按钮的场景——每个请求都需要生成一个绝不重复的订单号或优惠券码。传统数据库自增ID在这种高并发场景下会立即暴露出致命缺陷。
我经历过一个真实案例:某电商平台促销活动期间,使用MySQL自增ID导致主键冲突,最终引发级联锁表现象。数据库连接池被耗尽,整个系统雪崩。事后分析发现,当QPS超过5000时,自增ID的获取速度完全跟不上请求量,多个事务互相阻塞等待获取下一个ID值。
Redis的INCR命令完美解决了这个问题。它通过单线程原子操作特性,确保即使百万级并发也不会产生重复值。其核心优势在于:
- 原子性:一个INCR操作从读取到写入是不可分割的
- 高性能:单节点可达10万+ QPS
- 持久化:通过RDB/AOF保证ID不丢失
- 分布式扩展:可通过分片进一步提升容量
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis生成ID的完整方案设计
2.1 基础INCR实现
最简实现只需要一行命令:
bash复制INCR global:id
但实际生产环境需要考虑更多维度。以下是经过多个项目验证的健壮方案:
java复制public class RedisIdGenerator {
private static final String ID_KEY = "coupon:id";
private static final String DATE_PATTERN = "yyyyMMdd";
private JedisPool jedisPool;
public long nextId() {
try (Jedis jedis = jedisPool.getResource()) {
String date = new SimpleDateFormat(DATE_PATTERN).format(new Date());
String key = ID_KEY + ":" + date;
long sequence = jedis.incr(key);
return Long.parseLong(date) * 1000000 + sequence;
}
}
}
这个实现包含几个关键设计:
- 按日期分键:每天自动重置序列,避免单个key过大
- 组合ID:日期+序列号的组合既保证唯一又带时间信息
- 连接池管理:避免频繁创建销毁连接
2.2 高可用改造
单节点Redis存在单点故障风险。我们通过以下措施提升可靠性:
- 主从架构:配置哨兵监控,故障自动切换
bash复制# redis-sentinel.conf
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
- 集群模式:当单机性能不足时,采用分片集群
java复制public class ClusterIdGenerator {
private static final String CLUSTER_NAME = "id-cluster";
private static final String SCRIPT =
"local prefix = ARGV[1]\n" +
"local key = 'id:'..prefix\n" +
"return redis.call('INCR', key)";
private JedisCluster jedisCluster;
public long nextId(String bizType) {
return jedisCluster.eval(SCRIPT, 1, CLUSTER_NAME, bizType);
}
}
- 本地缓存缓冲:预先获取一批ID缓存在应用本地,减少Redis访问
java复制public class BatchIdGenerator {
private AtomicLong current = new AtomicLong(0);
private long batchSize = 1000;
private long max = 0;
public synchronized long nextId() {
if(current.get() >= max) {
refreshBatch();
}
return current.getAndIncrement();
}
private void refreshBatch() {
long start = redis.incrBy("global:id", batchSize);
current.set(start - batchSize + 1);
max = start;
}
}
3. 性能优化实战技巧
3.1 管道化(Pipeline)提升吞吐量
当需要批量生成ID时,常规的循环INCR会导致网络往返耗时。通过Pipeline可将多个请求合并:
java复制public List<Long> batchGenerate(int count) {
try (Jedis jedis = jedisPool.getResource()) {
Pipeline p = jedis.pipelined();
for(int i=0; i<count; i++) {
p.incr("batch:id");
}
List<Object> results = p.syncAndReturnAll();
return results.stream()
.map(o -> (Long)o)
.collect(Collectors.toList());
}
}
实测对比:
- 单次INCR:1000次操作耗时≈1200ms
- Pipeline批量:1000次操作耗时≈80ms
3.2 Lua脚本原子操作
对于需要先判断再生成的场景,使用Lua脚本保证原子性:
lua复制-- 检查key是否存在,不存在则初始化
local exists = redis.call('EXISTS', KEYS[1])
if exists == 0 then
redis.call('SET', KEYS[1], ARGV[1])
end
return redis.call('INCR', KEYS[1])
Java调用示例:
java复制String script = "local e=redis.call('EXISTS',KEYS[1])\n" +
"if e==0 then\n" +
" redis.call('SET',KEYS[1],ARGV[1])\n" +
"end\n" +
"return redis.call('INCR',KEYS[1])";
Object result = jedis.eval(script, 1, "counter:id", "1000");
3.3 内存优化策略
长期运行的ID生成服务需要注意内存增长问题:
- 设置过期时间:
EXPIRE coupon:id 86400 - 定期归档旧key:通过脚本扫描并删除三天前的key
- 监控内存使用:
INFO memory命令获取关键指标
4. 生产环境避坑指南
4.1 时钟回拨问题
使用时间戳作为ID组成部分时,服务器时钟回拨会导致ID重复。解决方案:
- 采用NTP服务同步时间
- 检测到时钟回拨时告警并暂停服务
java复制long lastTimestamp = 0;
public synchronized long nextId() {
long current = System.currentTimeMillis();
if(current < lastTimestamp) {
throw new IllegalStateException("Clock moved backwards");
}
lastTimestamp = current;
// ...生成ID逻辑
}
4.2 热点key处理
当单个key的QPS过高时可能造成Redis CPU负载不均。应对方法:
- 分片策略:
INCR coupon:id:{hash(userId)} - 本地缓冲:如前文介绍的批量预取方案
- 读写分离:从库处理读请求
4.3 持久化配置
错误的持久化配置可能导致ID丢失或服务阻塞:
- RDB配置:
save 300 100表示5分钟内有100次修改则快照 - AOF配置:
appendfsync everysec平衡性能与安全
建议同时开启RDB和AOF,并定期备份到异地。
5. 扩展应用场景
5.1 分布式锁实现
基于相同的原子操作原理,可以实现分布式锁:
java复制public boolean tryLock(String lockKey, long expireSec) {
String result = jedis.set(lockKey, "1", "NX", "EX", expireSec);
return "OK".equals(result);
}
public void unlock(String lockKey) {
jedis.del(lockKey);
}
5.2 限流器设计
INCR配合EXPIRE可以实现简单限流:
lua复制-- 每秒不超过100次调用
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], 1)
end
return current <= 100
5.3 订单号生成最佳实践
电商系统订单号通常需要包含:
- 业务标识(2位)
- 时间信息(6位年月日)
- 序列号(6位)
- 随机防重码(2位)
示例实现:
java复制public String generateOrderNo() {
String date = new SimpleDateFormat("yyMMdd").format(new Date());
long seq = redis.incr("order:seq:" + date) % 1000000;
int random = ThreadLocalRandom.current().nextInt(100);
return String.format("10%s%06d%02d", date, seq, random);
}
