在实际工作中,经常会涉及到生成全局唯一id的问题, 比如用户id,比如某个分享动作的id, 当然, 还有其他更多的情形。本文简单聊一下。
方案一: 利用数据库自增序列
这种方案我在实际开发中用过好几次, 在mysql中,可以设置一个自增字段, 每次insert一条后, 都会生成一个自增的id.
优点: 寄托于数据库, 简单, 而且id自然自增。
缺点: 数据迁移和扩展比较蛋疼, 而且,如果分库分表了, 那么在每个表中,id唯一, 但全局来看,id就不唯一了。那是不是说, 如果分库分表, 就真的没法全局唯一呢? 不是的! 可以这么考虑, 假设有4个表, 要做到全局唯一, 可以这么搞: 表1自增方式为: 1 5 9, 表2自增方式为2 6 10, 依次类推。
方案二: 随机生成(比如利用linux的random设备, 我博客介绍过了)
这种方案实际上就是经典的UUID方案, 随机生成一个16字节的串, 作为id, 总共128bit, 总结果数是2的128次方中, 这绝对是个天文数字, 大得惊人, 利用linux的random设备生成, 数学上可能不唯一, 但在实际中, 在世界末日之前, 可以认为是唯一的。
这里有个问题: 这个16字节的串,不一定是可见的, 在开发程序和后续维护过程中, 很不方便, 怎么办呢? 可以转成可见的, 常见的转换方式是buf2hex, 这也是老生常谈了, 用md5其实也可以。 这样, 就是32字节了。我之前在开发中, 经常用这种方式生成全局唯一id.
优点:简单方便, 性能好, 满足搞并发场景, 迁移起来也很爽。意思是, 只顾生成, 别管重复(实际不会重复)。
缺点:太随机, 没有排序, 当然, 这个问题也好解决, 可以考虑截取部分字节, 另外的字节用时间戳来填充。 另外,在某些场景下并不好, 比如我最近遇到的一个需求: 房间号是8位数字, 用户在操作过程中,需要输入房间号, 现要生成全局唯一的房间号(总房间号预计需要100万个)。 我对方法二进行了改进,实现了, 目前跑起来很顺利。
方案三: 用Redis来生成唯一id
Redis可能是集群多机器的, 可以采取方案一中类似的思路来做。 在实际中, 我几乎没玩过Redis, 所以就不多扯。
优点: 性能会比数据库好。
缺点: 比数据库方案复杂。
补充一下: 之前看过用zookeeper生成唯一id的介绍, 但确实麻烦, 也没有必要, 故不介绍zookper生成方法。
方案四: twitter的snowflake算法
该方案产生一个64位的long型数据, 包括了机器特征、时间毫秒特征和随机特征对维度的数据, 实际上就是旧瓶装新酒,而已。
优点: 跟方案二类似
缺点: 单机递增, 但全局时钟可能不完全同步, 不一定完全自增。 不过几乎没啥大影响。
其实, 生成全局唯一id, 无非就是两个思路, 也就是方案一和方案二, 其余的, 都是变种。 最后, 来看看snowflake算法的源码:
/// From: https://github.com/twitter/snowflake
/// An object that generates IDs.
/// This is broken into a separate class in case
/// we ever want to support multiple worker threads
/// per process
/// </summary>
public class IdWorker
{
private long workerId;
private long datacenterId;
private long sequence = 0L;
private static long twepoch = 1288834974657L;
private static long workerIdBits = 5L;
private static long datacenterIdBits = 5L;
private static long maxWorkerId = -1L ^ (-1L << (int)workerIdBits);
private static long maxDatacenterId = -1L ^ (-1L << (int)datacenterIdBits);
private static long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << (int)sequenceBits);
private long lastTimestamp = -1L;
private static object syncRoot = new object();
public IdWorker(long workerId, long datacenterId)
{
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0)
{
throw new ArgumentException(string.Format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0)
{
throw new ArgumentException(string.Format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public long nextId()
{
lock (syncRoot)
{
long timestamp = timeGen();
if (timestamp < lastTimestamp)
{
throw new ApplicationException(string.Format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp)
{
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0)
{
timestamp = tilNextMillis(lastTimestamp);
}
}
else
{
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << (int)timestampLeftShift) | (datacenterId << (int)datacenterIdShift) | (workerId << (int)workerIdShift) | sequence;
}
}
protected long tilNextMillis(long lastTimestamp)
{
long timestamp = timeGen();
while (timestamp <= lastTimestamp)
{
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen()
{
return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
}
}
注意一下那个最关键的 sequence = (sequence + 1) & sequenceMask;
具体采用哪种方式, 要看场景和需求。 有时候, 需要采用更为灵活的变种方式。 另外, 补充说明一下, 有时候, 并不需要全局唯一id, 仅仅是局部唯一id就可以满足条件, 比如csdn的博文id, 仅仅需要在某用户名下, 保持唯一就可以了, 全局冲突也不影响。
不多说。