个人随笔
目录
论数据库设计选择一个有序的ID的方法和重要性
2020-03-19 23:29:58

一、数据库自增

我们做数据库建表的时候,往往会考虑ID主键怎么定义,之前通常我们都用数据库自增即可,这个不失为一个好办法。但是也会有问题,比如
大型分布式系统,订单分库,那么就比较难保证ID唯一了,并且太过依赖于数据库,ID比较容易被猜测到,所以对于大型系统,一般不建议自增。

二、UUID

有些人会想,那么如果要保证唯一的话,直接用UUID它不香么,全球唯一,但是这里也有很大的弊端,首先UUID长度过长,用来做ID主键会浪费存储空间,当做索引页会浪费太大的空间,
其次,UUID会暴露MAC地址,并且最主要的是UUID无序,如果要做排序,那么只能靠别的字段,因此这里也不建议UUID

三、雪花算法

雪花算法是有序的,并且机器ID不同的话单线程唯一。这里强烈推荐,我们可以对雪花算法进行一些修改,对其加上redis分布式锁,那么就可以保证
绝对唯一啦。完美。雪花算法+Redis分布式锁思路如下:

  • 1、对雪花算法进行改造,取ID的时候进行方法加同步锁:synchronized ,这个的好处是可以在同一个进程中,所有线程取的ID不同。

  • 2、第一步只是控制了同一个进程的唯一性,那么假如是多个进程呢,此时就需要继续对雪花算法进行改造,获取ID的方法里面使用Redis分布式锁来处理。

  • 3、不同进程的线程进入取ID的方法,先对一个key进行setNx设置值,超时时间为三分钟,如果设置成功,则表明获取到锁,可以进入获取一个id,获取成功后移除key。若是设置失败,也就是返回0,则进行休眠Sleep(30)毫秒,这个可以具体情况调整,再次调用获取的方法(相当于递归循环),此时若锁被释放,则将设置成功。

大概思路如上,当然最简单的还是不同应用启动的时候,设置不同的工作机器ID(0~31)和数据中心ID(0~31),若是有上万台服务器,那么
就避免不了,机器ID相同情况下,那么我们再相同的机器ID那一批应用,就可以结合雪花算法+Redis分布式锁来完美解决。

下面给出Java版本一个雪花算法例子:不包括Redis分布式锁
  1. public class SnowflakeIdUtil {
  2. // ==============================Fields===========================================
  3. /** 开始时间截 (2015-01-01) */
  4. private final long twepoch = 1420041600000L;
  5. /** 机器id所占的位数 */
  6. private final long workerIdBits = 5L;
  7. /** 数据标识id所占的位数 */
  8. private final long datacenterIdBits = 5L;
  9. /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
  10. private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
  11. /** 支持的最大数据标识id,结果是31 */
  12. private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
  13. /** 序列在id中占的位数 */
  14. private final long sequenceBits = 12L;
  15. /** 机器ID向左移12位 */
  16. private final long workerIdShift = sequenceBits;
  17. /** 数据标识id向左移17位(12+5) */
  18. private final long datacenterIdShift = sequenceBits + workerIdBits;
  19. /** 时间截向左移22位(5+5+12) */
  20. private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
  21. /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
  22. private final long sequenceMask = -1L ^ (-1L << sequenceBits);
  23. /** 工作机器ID(0~31) */
  24. private long workerId;
  25. /** 数据中心ID(0~31) */
  26. private long datacenterId;
  27. /** 毫秒内序列(0~4095) */
  28. private long sequence = 0L;
  29. /** 上次生成ID的时间截 */
  30. private long lastTimestamp = -1L;
  31. private static SnowflakeIdUtil idWorker;
  32. /**
  33. * 以SnowFlake算法,获取唯一有序id
  34. * @return
  35. */
  36. public static long getSnowflakeId() {
  37. if(idWorker == null) {
  38. synchronized (SnowflakeIdUtil.class) {
  39. if(idWorker == null) {
  40. idWorker = new SnowflakeIdUtil(0, 0);
  41. }
  42. }
  43. }
  44. return idWorker.nextId();
  45. }
  46. // ==============================Methods==========================================
  47. private SnowflakeIdUtil() {
  48. }
  49. //==============================Constructors=====================================
  50. /**
  51. * 构造函数
  52. * @param workerId 工作ID (0~31)
  53. * @param datacenterId 数据中心ID (0~31)
  54. */
  55. private SnowflakeIdUtil(long workerId, long datacenterId) {
  56. if (workerId > maxWorkerId || workerId < 0) {
  57. throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
  58. }
  59. if (datacenterId > maxDatacenterId || datacenterId < 0) {
  60. throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
  61. }
  62. this.workerId = workerId;
  63. this.datacenterId = datacenterId;
  64. }
  65. /**
  66. * 阻塞到下一个毫秒,直到获得新的时间戳
  67. * @param lastTimestamp 上次生成ID的时间截
  68. * @return 当前时间戳
  69. */
  70. protected long tilNextMillis(long lastTimestamp) {
  71. long timestamp = timeGen();
  72. while (timestamp <= lastTimestamp) {
  73. timestamp = timeGen();
  74. }
  75. return timestamp;
  76. }
  77. /**
  78. * 返回以毫秒为单位的当前时间
  79. * @return 当前时间(毫秒)
  80. */
  81. protected long timeGen() {
  82. return System.currentTimeMillis();
  83. }
  84. /**
  85. * 获得下一个ID (该方法是线程安全的)
  86. * @return SnowflakeId
  87. */
  88. protected synchronized long nextId() {
  89. long timestamp = timeGen();
  90. //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
  91. if (timestamp < lastTimestamp) {
  92. throw new RuntimeException(
  93. String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
  94. }
  95. //如果是同一时间生成的,则进行毫秒内序列
  96. if (lastTimestamp == timestamp) {
  97. sequence = (sequence + 1) & sequenceMask;
  98. //毫秒内序列溢出
  99. if (sequence == 0) {
  100. //阻塞到下一个毫秒,获得新的时间戳
  101. timestamp = tilNextMillis(lastTimestamp);
  102. }
  103. }
  104. //时间戳改变,毫秒内序列重置
  105. else {
  106. sequence = 0L;
  107. }
  108. //上次生成ID的时间截
  109. lastTimestamp = timestamp;
  110. //移位并通过或运算拼到一起组成64位的ID
  111. return ((timestamp - twepoch) << timestampLeftShift) //
  112. | (datacenterId << datacenterIdShift) //
  113. | (workerId << workerIdShift) //
  114. | sequence;
  115. }
  116. public static void main(String[] args) {
  117. long snowflakeId = getSnowflakeId();
  118. System.out.println(snowflakeId);
  119. }
  120. }

总结:单体应用采用雪花算法完美解决ID问题,大一点十几二十个节点的采用雪花算法+Redis锁完美解决,上万的服务器应用建议采用机器ID,操作ID+雪花算法+Redis分布式锁。有顺序的ID
可以在排序的时候完全根据ID来升序或降序,不建议用时间日期字段。

纯属个人观点,有什么误区或者不足之处麻烦各位同行指教指教,或者有什么更好的建议分享出来学习学习。

 834

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2