设置唯一性ID的方法

设置MySQL数据库唯一性ID的方法

1.使用uuid来作为ID

使用方法如下:

System.out.println(UUID.randomUUID());
System.out.println(UUID.randomUUID().toString().length());

输出结果如下:
由此可知,uuid获取的值是一串长度为36的字符

此种方法的优缺点如下:

优点

1.使用起来很简单
2.不影响数据库的拓展,比如分表分库

缺点

1.表结构中ID类型是字符串且长度在36,空间占用大
2.mysql数据库在索引上使用的是B+树算法,有序的查找效率更高,且整型的查找效率比字符串类型要高,所以使用无序字符串作为ID类型,索引效率低。
3.当服务器集群的时候,并发量大时可能会出现ID重复的情况

2.使用数据库ID的自增方法

使用方法如下:
在设置表结构的时候,将ID设置为整型且自动增长。
因为这种方式在数据库集群的时候,会出现大量ID重复的情况,所以在集群环境下,可以对不同的库设置不同的起始值和自增量
MySQL下的修改配置如下:
1.show VARIABLES like ‘auto_inc%’ 先查看当前配置
2.auto_increment_offset 参数代表起始值,
修改该参数配置的语法为 set @@auto_increment_offset =n(根据情况自行设置)
3.auto_increment_increment 参数代表自增量,
修改该参数配置的语法为 set @@auto_increment_increment = n(根据情况自行设置)
例如:当有100以内的数据库集群的时候,可以将自增量设置为100,而给每个数据库设置从1-99的起始值,这样自增的ID就不会出现重复的情况。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

优缺点如下:

优点:

1.使用起来也很简单,只需在建表的时候修改几个参数配置
2.ID为有序的整型,索引效率高
3.性能也比较好

缺点:

1.不适合水平分表,因为在不同表下新增ID时会出现重复的ID
2.前期建表的时候就要先规划好配置的值,不方便后期拓展
3.依赖数据库的自增锁,在高并发情况下会影响性能

3.使用雪花算法

该算法设计如图
由图可知该算法分为4个部分

1.第一位

占用1bit,其值始终是0,没有实际作用。

2.时间戳

占用41bit,精确到毫秒,总共可以容纳约69 年的时间。

3.工作机器id

占用10bit,其中高位5bit是数据中心ID(datacenterId),低位5bit是工作节点ID(workerId),最多可以容纳1024个节点。

4.序列号

占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095。

所以这种算法在同一毫秒内可以产生的ID数为1024*4096=4194304个,对于绝大多数场景下的高并发已经足够使用了

该算法的实现代码如下:
public class SnowFlake {
    //2019-01-01的毫秒数
    private static final long Initial_time_stamp =1548950400000L;
    //工作节点id所占的位数
    private static final long Worker_ID_bits = 5L;
    //数据中心id所占的位数
    private static final long Data_center_ID_bits = 5L;
    //序列号所占的位数
    private static final long Serial_number_bits=12L;
    //工作节点的最大数(根据二进制数算出最多十进制数)
    private static final long Max_Worker_ID = -1L ^ (-1L<<Worker_ID_bits);
    //数据中心的最大数
    private static final long Max_Data_center_ID = -1L ^ (-1L<<Data_center_ID_bits);
    //序列号的最大数
    private static final long Max_Serial_number = -1L ^ (-1L<<Serial_number_bits);
    //数据中心ID
    private long datacenterId;
    //工作节点ID
    private long workerId;
    //上次时间戳毫秒值
    private static long lastTimestamp;
    //序列号
    private static long serialNumber=0L;
    //时间戳偏移位数
    private static final long timeStamp_Offset=Data_center_ID_bits+Worker_ID_bits+Serial_number_bits;
    //数据中心偏移位数
    private static final long datacenter_Offset=Worker_ID_bits+Serial_number_bits;
    //工作节点偏移位数
    private static final long worker_Offset=Serial_number_bits;
    /**
     * 无参构造函数默认取数据中心ID和工作节点ID为最大值
     */
    public SnowFlake() {
        this.datacenterId = Max_Data_center_ID;
        this.workerId=Max_Worker_ID;
    }

    /**
     * @param datacenterId  数据中心ID
     * @param workerId  工作节点ID
     */
    public SnowFlake(long datacenterId, long workerId) {
        if(datacenterId<0||datacenterId>Max_Data_center_ID){
            throw new IllegalArgumentException(String.format("数据中心ID不能小于0或大于%d",Max_Data_center_ID));
        }
        if(workerId<0||workerId>Max_Worker_ID){
            throw new IllegalArgumentException(String.format("工作节点ID不能小于0或大于%d",Max_Worker_ID));
        }
        this.datacenterId = datacenterId;
        this.workerId = workerId;
    }

    public synchronized long getNextID(){
        long timeStamp = System.currentTimeMillis();
        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if(timeStamp<lastTimestamp){
            //服务器时钟被调整了,ID生成器停止服务.
            throw new RuntimeException(String.format("当前时间戳小于上次添加的时间.在小于之前时间%d毫秒的情况下禁止生成ID", lastTimestamp - timeStamp));
        }
        //当毫秒值相同时,序列号自增
        if(timeStamp==lastTimestamp){
            serialNumber=(serialNumber+1)&Max_Serial_number;
            //当前毫秒内序列已经满了,需要等待下一毫秒
            if(serialNumber==0){
                timeStamp = getNextMills(lastTimestamp);
            }
        }else {
            serialNumber=0L;
        }
        //将当前时间戳赋值给上次时间戳
        lastTimestamp=timeStamp;
        return ((timeStamp-Initial_time_stamp)<<timeStamp_Offset)|(datacenterId<<datacenter_Offset)|(workerId<<worker_Offset)|serialNumber;
    }

    /**
     * 再次获取一次时间戳,与上次进行比较
     * @param lastTimestamp
     * @return
     */
    public long getNextMills(long lastTimestamp){
        long currentTimeMillis = System.currentTimeMillis();
        while (currentTimeMillis<=lastTimestamp){
            currentTimeMillis=System.currentTimeMillis();
        }
        return currentTimeMillis;
    }

}

该种方法的优缺点

优点:

1.性能较好,速度快
2.可以根据情况拓展该算法,比较灵活
3.无需其他依赖,使用也比较简单

缺点:

1.依赖机器时间,当数据库服务器时间回调时,可能会出现ID重复的情况

4.使用Redis的自增方法

使用redis的incr自增的方法,这种方法利用了Redis的特性:单线程原子操作、自增计数API、数据有效期机制。
可以自行设定规则,例如 时间+自增数值 来制成一个唯一性ID

实现代码如下:

public String getID(){
        long id = redisTemplate.opsForValue().increment("Market_Order_ID");
        SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
        String prefix="";
        //获取当前时间的年月日作为ID的前缀
        try {
            prefix = format.format(new Date());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //自动补0的语法,当整型长度没有超过5时自动在前面补0
        String format_id = String.format("%1$05d", id);
        //根据时间和ID拼接
        return prefix+format_id;
    }

这种方法的优缺点如下:

优点:

1.拓展性强,可以结合业务场景进行处理
2.利用Redis原子性的特性,在高并发情况下ID不会重复
3.性能好

缺点

1.依赖Redis的使用
2.增加了网络开销
3.需要对Redis实现高可用

5.高并发场景测试

这几种方法均可用CountDownLatch类在高并发场景下测试
以下是使用Redis自增的方法进行高并发测试

public class ServiceApplicationTest {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private final static int count=500;//线程并发数
    private static CountDownLatch countDownLatch =new CountDownLatch(count);
    public void testRedisId(){
        System.out.println(getID(););
    }
    @Test
    public void testMain(){
        System.out.println("-------------start-------------");
        for (int i=0;i<count;i++){
            new Thread(new ThreadDemo()).start();
            //当countDownLatch中的数量减至0时,线程全部被唤醒
            countDownLatch.countDown();
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------------end-------------");
    }
    public String getID(){
        long id = redisTemplate.opsForValue().increment("Market_Order_ID");
        SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
        String prefix="";
        //获取当前时间的年月日作为ID的前缀
        try {
            prefix = format.format(new Date());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //自动补0的语法,当整型长度没有超过5时自动在前面补0
        String format_id = String.format("%1$05d", id);
        //根据时间和ID拼接
        return prefix+format_id;
    }
    class ThreadDemo implements Runnable {
        @Override
        public void run() {
            try {
                //线程在这里进行等待
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            testRedisId();
        }
    }
}

总结

方法优点缺点
UUID实现简单、不占用网络无序、占空间、索引不友好
数据库自增无代码调整、递增数据库单点故障、扩展性有瓶颈
雪花算法性能优、不占用网络、趋势递增依赖服务器时间
Redis自增无单点故障、性能优于DB、递增、拓展性好占用网络连接、需要维护Redis集群
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值