为什么在负载均衡环境下生成唯一标识是个问题
公司上了云之后,服务通常会部署在多个服务器上,前端请求通过负载均衡器自动分发到不同的实例。这时候如果每个服务器都用自己的方式生成订单号、用户ID或者日志追踪码,很容易出现重复。比如财务系统里两个订单编号一样,对账就乱套了。
传统用时间戳加随机数的方式,在单机还能凑合,一旦并发上来,特别是在秒杀场景下,几台机器同时在1毫秒内生成ID,冲突概率直线上升。
数据库自增主键不够用了
很多人第一反应是让数据库来生成唯一ID,比如用MySQL的自增字段。这确实能保证全局唯一,但问题也很明显——每次发请求都要先写一条记录拿ID,性能扛不住。而且数据库成了瓶颈,万一主库延迟,整个系统都卡住。
更别说现在很多系统用的是读写分离或多主架构,自增ID还可能撞车。所以这条路只适合小流量场景。
雪花算法:分布式ID的常见解法
目前用得最多的是Snowflake算法,也就是“雪花算法”。它把一个64位的长整型拆成几段:最高位固定为0,接着是时间戳(毫秒级),然后是机器ID,再往后是数据中心ID,最后是序列号。
比如某次生成的ID是 1234567890123456789,前边一部分表示时间,中间标示这是第几台服务器,最后几位是同一毫秒内的递增序号。这样即使多台机器同时生成,只要机器ID不同,结果就不会重复。
public class SnowflakeIdGenerator {
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & 0xFFF;
if (sequence == 0) {
timestamp = waitForNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - 1288834974657L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
}上面这段Java代码就是一个简单的实现。关键点在于机器ID不能重复,否则还是可能冲突。你可以通过配置文件、ZooKeeper或启动参数来分配不同的workerId。
用Redis生成递增ID也是一种选择
如果你已经在用Redis做缓存,可以顺便让它帮忙生成唯一ID。利用Redis的INCR命令,原子性地递增一个键,返回最新值。这个值天然不会重复,而且性能不错。
Long id = redisTemplate.opsForValue().increment("global_id_seq");不过要注意,Redis要是挂了,ID生成就停了。所以得做好高可用,比如用Redis集群或者哨兵模式。另外递增ID太有规律,有些业务场景下不希望暴露数据增长趋势,比如订单量被竞争对手猜出来。
UUID是不是万能解?
UUID生成简单,Java里一行UUID.randomUUID()搞定,完全去中心化,不需要协调。但它有几个硬伤:一是长度太长,36个字符,存数据库占空间;二是无序,作为数据库主键会导致B+树频繁分裂,写入性能差;三是看起来像一串乱码,排查问题时不如数字直观。
所以UUID更适合用在非核心链路,比如临时会话ID、日志追踪标记这类不要求排序也不频繁查询的场景。
结合业务特点选方案
没有哪种方案通吃所有情况。电商订单可以用雪花算法,内部日志追踪可以用带服务名前缀的UUID,而支付流水号可能还得走数据库序列确保严格有序。
关键是别等到上线才发现ID撞了。在设计阶段就想清楚:要不要有序?能不能容忍短暂延迟?有没有跨区域部署需求?把这些理清了,选型就清晰多了。