关于全局唯一id的一些思考

       在实际工作中,经常会涉及到生成全局唯一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, 仅仅需要在某用户名下, 保持唯一就可以了, 全局冲突也不影响。

      不多说。

猜你喜欢

转载自blog.csdn.net/stpeace/article/details/81260953