大型系统设计核心技术(第一篇)---分布式订单号生成策略

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m0_37774696/article/details/85012554

一、使用UUID生成

1.定义

UUID是一个128位长的数字,一般用16进制表示。算法的核心思想是结合机器的网卡、当地时间、一个随即数来生成GUID。从理论上讲,如果一台机器每秒产生10000000个GUID,则可以保证(概率意义上)3240年不重复。

2.用法
在java使用UUID非常简单,在jdk的工具类中已经有生成UUID的类可以直接使用,如下:
代码实例:
在这里插入图片描述
运行结果:
在这里插入图片描述
可以看出在单机环境下,生成的uuid不会重复

3.优缺点

优点 缺点
使用简单 数据库索引效率低(这里针对mysql的b+树索引而言)
不依赖其他组件,直接是使用jdk工具类 太长无意义,用户不友好
不影响数据库的拓展 空间占用大
在应用集群环境,机器多的时候,重复几率很大!!!

二、使用数据库自增序列生成

1.定义
利用数据库,全数据库唯一。
2.用法
设置mysql数据库自增长起始值以及步长,多个不同数据库应设置不同起始值,而步长设置一致,则每个数据库生成的自增序列码可以避免相同。
以一个数据库中设置为例:
代码:

SHOW VARIABLES like 'auto_inc%';//查看自增相关变量值
set auto_increment_increment=100;//设置步长
set auto_increment_offset=1;//设置起始值
INSERT into `user`VALUES(null);//模拟插入数据

结果运行图:
在这里插入图片描述在这里插入图片描述
3.优缺点

优点 缺点
开发人员无需编码 大表不能做水平拓展,否则插入或删除容易出现问题
性能过得去 依赖前期的规划,拓展麻烦
索引友好(针对mysql中b+树) 依赖mysql内部维护的“自增锁”,高并发下插入数据影响性能
在业务操作父子关联表(ER表)插入时,必须先父后子

三、推特的雪花算法(snowflake)
1.定义
推特公司使用的一款通过划分命名空间并行生成的算法,来解决全局唯一ID的需求,类似的还有MongoDB的object_id。雪花算法,是64位二进制,转换十进制,不超过20位。第一位是符号位,一般是不变的0,第二阶梯的是41位的毫秒,第三阶梯是10位的机器ID,第四阶梯是12位的序列号,雪花算法能保证一毫秒内,支持1024*4096个并发,400多W了,对付绝大多数场景,都适用了。业界使用一般根据雪花算法做拓展。
2.用法
snowflake 使用java实现源码:

package com.jd.qztest;

/**
* twitter的snowflake算法 -- java实现
*
* @author rock
* @date 2016/11/26
*/
public class SnowFlake {
  
    /**
    * 起始的时间戳
    */
    private final static long START_STMP = 1480166465631L;
  
    /**
    * 每一部分占用的位数
    */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;  //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数
  
    /**
    * 每一部分的最大值
    */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
  
    /**
    * 每一部分向左的位移
    */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
  
    private long datacenterId;  //数据中心
    private long machineId;    //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳
  
    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }
  
    /**
    * 产生下一个ID
    *
    * @return
    */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
  
        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }
  
        lastStmp = currStmp;
  
        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT      //数据中心部分
                | machineId << MACHINE_LEFT            //机器标识部分
                | sequence;                            //序列号部分
    }
    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }
  
    private long getNewstmp() {
        return System.currentTimeMillis();
    }
  
}

使用java模拟一百个线程调用雪花算法生成方法:

package com.jd.qztest;

import java.util.concurrent.CountDownLatch;

public class Main1  { 
	private static int threadNum=100;
	private static CountDownLatch countDownLatch=new CountDownLatch(threadNum);
public static void main(String [] args) { 
	SnowFlake flake=new SnowFlake(2,3);
  for (int i=0;i<threadNum;i++){
	  new IdThread(flake).start();
	  countDownLatch.countDown();
  }
} 
static class IdThread extends Thread{
	private SnowFlake flake;
	public IdThread(SnowFlake flake){
		this.flake=flake;
	}
	@Override
	public void run(){
		try {
			countDownLatch.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(flake.nextId());
	}
	
}
}

运行结果:
在这里插入图片描述
3.优缺点

优点 缺点
性能较好,速度快 依赖机器时间,如果发生时间会把,则可能产生id重复
无需第三方依赖,实现比较简单
可以根据实际情况调整和拓展雪花是短发,方便灵活

四、基于redis的自增

1.定义
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
在实际公司开发项目中,我们一般将redis的命名为:系统名:模块名:功能名
2.用法
可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
这个,负载到哪个机要先确定好,未来很难做修改。但是3-5台服务器基本能够满足器上,都可以获得不同的ID。但是步长和初始值一定需要事先需要了。使用Redis集群也可以方式单点故障的问题。
另外,比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

单机使用redis自增特性运行截图:
在这里插入图片描述
3.优缺点

优点 缺点
拓展性强,可以方便地结合业务进行处理 增加一次网络开销
利用redis的单纤成操作的原子性,保证高并发下不会重复 引入redis第三方依赖库
需要做reids高并发支持

五、说在最后

技术选型并无绝对,针对以上的分布式订单号生成常用的四种策略,我们应该根据自己业务场景去选择合适的策略。

猜你喜欢

转载自blog.csdn.net/m0_37774696/article/details/85012554