UUID

基本实现

@Override
public String genId() {
    return UUID.randomUUID().toString()
            .replaceAll(PunctuationConst.MIDDLE_LINE, PunctuationConst.EMPTY);
}

测试代码

System.out.println(new UUIDId().genId())

日志信息

379a1e5df3534885a94001373467f33e

源码解析

toString()

public String toString() {
    return (digits(mostSigBits >> 32, 8) + "-" +
            digits(mostSigBits >> 16, 4) + "-" +
            digits(mostSigBits, 4) + "-" +
            digits(leastSigBits >> 48, 4) + "-" +
            digits(leastSigBits, 12));
}

digits()

private static String digits(long val, int digits) {
    long hi = 1L << (digits * 4);
    return Long.toHexString(hi | (val & (hi - 1))).substring(1);
}

toHexString()

public static String toHexString(long i) {
        return toUnsignedString(i, 4);
}
  • toUnsignedString
/**
 * Convert the integer to an unsigned number.
 */
private static String toUnsignedString(long i, int shift) {
    char[] buf = new char[64];
    int charPos = 64;
    int radix = 1 << shift;
    long mask = radix - 1;
    do {
        buf[--charPos] = Integer.digits[(int)(i & mask)];
        i >>>= shift;
    } while (i != 0);
    return new String(buf, charPos, (64 - charPos));
}

其中 Integer.digits 是最基本的常量

/**
 * All possible chars for representing a number as a String
 */
final static char[] digits = {
    '0' , '1' , '2' , '3' , '4' , '5' ,
    '6' , '7' , '8' , '9' , 'a' , 'b' ,
    'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
    'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
    'o' , 'p' , 'q' , 'r' , 's' , 't' ,
    'u' , 'v' , 'w' , 'x' , 'y' , 'z'
};

random()

public static UUID randomUUID() {
    SecureRandom ng = Holder.numberGenerator;
    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    randomBytes[6]  &= 0x0f;  /* clear version        */
    randomBytes[6]  |= 0x40;  /* set to version 4     */
    randomBytes[8]  &= 0x3f;  /* clear variant        */
    randomBytes[8]  |= 0x80;  /* set to IETF variant  */
    return new UUID(randomBytes);
}

SecureRandom 单例初始化

其中 Holder.numberGenerator 创建单例 SecureRandom

private static class Holder {
    static final SecureRandom numberGenerator = new SecureRandom();
}

UUID(bytes)

构造器,内容如下

private UUID(byte[] data) {
    long msb = 0;
    long lsb = 0;
    assert data.length == 16 : "data must be 16 bytes in length";
    for (int i=0; i<8; i++)
        msb = (msb << 8) | (data[i] & 0xff);
    for (int i=8; i<16; i++)
        lsb = (lsb << 8) | (data[i] & 0xff);
    this.mostSigBits = msb;
    this.leastSigBits = lsb;
}

UUID 的缺点

1)没有排序,无法保证趋势递增。

2)UUID往往是使用字符串存储,查询的效率比较低。

3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。

4)传输数据量大

5)不可读。

太长

一般为 32 位。

如果用来做唯一主键,占用内容较长,且有无序的问题,影响性能。

无序

  • 无需为什么会影响性能?

因为索引是 B+ Tree

但是,如果我们的插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。

顺序插入的好处

如果我们的ID按递增的顺序来插入,比如陆续插入8,9,10,新的ID都只会插入到最后一个节点当中。

当最后一个节点满了,会裂变出新的节点。

这样的插入是性能比较高的插入,因为这样节点的分裂次数最少,而且充分利用了每一个节点的空间。

ps: 这也正是为什么 mysql 使用自增主键,oracle 建议使用 seq 序列号。

不利于阅读

379a1e5df3534885a94001373467f33e 这种序列号我们无法获取对应的信息。

比如交易订单的 id,交易发生在什么时候?

交易属于哪一个系统?

我们希望看到的是这样的时间戳?

0001 201906121639111 XXX

假设 0001 是一个系统的编号

201906121639111 是交易发生的时间。

下面我们针对这些问题,逐步改进。

改进为更短的展示

原来是 0-9 a-z 可以把大小写加上,位数变得更短。

8 位的 uuid

import com.github.houbb.heaven.annotation.ThreadSafe;
import com.github.houbb.heaven.util.guava.Guavas;
import com.github.houbb.heaven.util.id.Id;

import java.util.List;

/**
 * 在数据量较多的时候,
 *
 * 如果基于 ID 添加索引,字符串越长,则性能越差。
 *
 * (1)uuid 是基于 16 进制的,将 128 位的 bit ,变为 16 进制。
 *  0-F   0123456789ABCDEFG
 *  (2)转换为 62 进制的信息,考虑便于阅读
 *  0-9  52 个字母 共计 62 位
 *
 *  所以 32 位,分成 8 组,每一组 4 位。
 *
 *  16 * 4 = 64 位。
 *
 *  如果不想重复,需要 0xffff 共计 65535 的字符。
 *
 * (3)缺点:会存在重复
 *
 * 重复的概率:
 *
 * 取62模会导致重复率大增 P(A)
 *
 * 短uuid重复概率未知 P(B)
 *
 * uuid重复概率=x
 *
 * P(A|B)=1
 *
 * P(A|B') =y
 *
 * 全概率公式计算 P(A)=P(B)P(A|B)+P(B')P(A|B') =x+y-xy
 *
 * 其中y=((64-62)/64)^8=2^(-40) 看出x远小于y,所以短uuid的重复概率完全取决于y的值
 *
 * (4)建议应用场景
 *
 * 可以用来生成一个 8 位的 token,而不是用来做唯一标识
 *
 * @author binbin.hou
 * @since 0.1.12
 * @see 19 位 uuid https://pittlu.iteye.com/blog/2093880
 */
@ThreadSafe
public class UUID8 implements Id {

    /**
     * 62 进制符号
     */
    private static final char[] CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

    @Override
    public String genId() {
        final List<String> uuidUnits = uuidUnits();
        StringBuilder stringBuilder = new StringBuilder();
        for (String unit : uuidUnits) {
            int x = Integer.parseInt(unit, 16);
            stringBuilder.append(CHARS[x % 62]);
        }
        return stringBuilder.toString();
    }

    /**
     * 获取 uuid 分段后的内容
     * @return 分段后的内容
     */
    private List<String> uuidUnits() {
        final String uuid32 = new UUID32().genId();
        final int size = 8;

        List<String> units = Guavas.newArrayList(size);
        for(int i = 0; i < size; i++) {
            units.add(uuid32.substring(i * 4, i * 4 + 4));
        }
        return units;
    }

}

可读性改进

将 string 转换为 Int

new BigInteger("379a1e5df3534885a94001373467f33e", 16)

结果

73907769400197500546813542834226328382

解决无序性的算法

为了解决UUID无序的问题,NHibernate在其主键生成方式中提供了Comb算法(combined guid/timestamp)。

保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime)。

/// <summary> 
/// Generate a new <see cref="Guid"/> using the comb algorithm. 
/// </summary> 
private Guid GenerateComb()
{
    byte[] guidArray = Guid.NewGuid().ToByteArray();
 
    DateTime baseDate = new DateTime(1900, 1, 1);
    DateTime now = DateTime.Now;
 
    // Get the days and milliseconds which will be used to build    
    //the byte string    
    TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks);
    TimeSpan msecs = now.TimeOfDay;
 
    // Convert to a byte array        
    // Note that SQL Server is accurate to 1/300th of a    
    // millisecond so we divide by 3.333333    
    byte[] daysArray = BitConverter.GetBytes(days.Days);
    byte[] msecsArray = BitConverter.GetBytes((long)
      (msecs.TotalMilliseconds / 3.333333));
 
    // Reverse the bytes to match SQL Servers ordering    
    Array.Reverse(daysArray);
    Array.Reverse(msecsArray);
 
    // Copy the bytes into the guid    
    Array.Copy(daysArray, daysArray.Length - 2, guidArray,
      guidArray.Length - 6, 2);
    Array.Copy(msecsArray, msecsArray.Length - 4, guidArray,
      guidArray.Length - 4, 4);
 
    return new Guid(guidArray);
}

个人收获

uuid

uuid 从实现上看起来很简单,一个方法即可。

但是其中的原理是一样的。

组合

组合是一种非常好的方式。

其实后面的随机序列也是一种组合。

时间戳+随机数的组合

前者保证顺序性,后者保证唯一性。