精华内容
下载资源
问答
  • 2022-03-31 21:12:01

    在 MySQL 数据表的设计中,官方推荐我们不要使用 UUID 或者其他不连续不重复的 id,而是推荐使用连续自增的主键 id(auto_increment)。

    随着现在许多项目都涉及到了分布式或者微服务,后续或多或少都会针对具体的服务需求对数据库进行拆分(分库分表),这里就会产生一个问题,拆分后的 id 该如何妥善处理?

    例如,在之前的业务中,所有的数据内容都是存放在同一张数据表中的,主键 id 都是自增的,这当然没有任何问题。但是当单表的数据量上来之后我们就需要进行水平分表操作(将一张数据表的数据分成多张表),如果这时我们还是按照之前的自增形式来做主键 id,就有可能会出现 id 重复的问题。

    对于高并发的环境,InnoDB 按主键进行插入时会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入操作都发生在这里,并发插入会导致间隙锁竞争。auto_increment 锁机制会造成自增锁的争抢并带来一定的性能消耗,如果需要改善我们可以配置 innodb_autoinc_lock_mode 参数。

    分布式 id 的基本规则

    • 全局唯一性:不能出现重复的 id
    • 递增性:MySQL 的 InnoDB 使用的是聚簇索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,因此在主键的选择上我们还是应该尽可能地使用有序的主键来保证写入性能,我们保证下一个 id 一定大于上一个 id,以此来满足事务版本号、IM 增量消息或者排序的特殊需求
    • 安全性:如果 id 是连续的,那么我们在知道一些基本规则的情况下就能很轻松地推测出下一份数据,这在一些机密性较高的业务场景是很危险的。所以我们有时会希望 id 是无规则的,最好还能包含有时间戳,这样就能够在开发中快速了解这个分布式 id 的生成时间
    • 高性能高可用性:确保在任何时候都能正确地生成 id,并且在高并发的环境下也能表现良好

    分布式 id 的解决方案

    • 数据表:我们可以在某个数据库中专门地去维护一张数据表,然后每次无论是哪张数据表需要自增 id,都需要去查这张表的记录,然后再利用 for update 锁表,并将取到的值加一,再把新值记录到数据表中。显然因为每次我们都需要锁表,所以这仅对于并发量小的项目而言是可以接受的

      • 优点:简单粗暴
      • 缺点:严重依赖数据库
    • Redis:因为 Redis 是单线程的,所以我们可以在 Redis 中维护一个键值对,之后无论是哪张数据表需要自增 id,都需要直接先去 Redis 中取值并加一。显然这种方式和上面单独维护一张数据表的方式是一样的,对高并发的支持都有所不足

      • 优点:灵活、不依赖数据库
      • 缺点:性能不太好
    • UUID:我们可以使用 UUID 来作为不重复的主键 id,但是 UUID 是无序的字符串,所以主键索引就会失效

      • 优点:简单、方便、性能好、全球唯一
      • 缺点:无序性、存储的是字符串、查询效率低、传输数据量大
    • 雪花算法:雪花算法是 Twitter 推出的针对分布式环境下的 id 生成算法,其结果是一个 Long 型的 64bit id。具体实现上使用 41bit 作为毫秒数,10bit 作为机器的 id(5bit 是数据中心,5bit 是机器 id),12bit 作为毫秒内的流水号(这意味着每个节点在每毫秒内可以产生 4096 个 id),最后还有一个符号位永远是 0

      • 优点:不依赖数据库、完全在内存中生成 id、高性能高可用、容量大、每秒可生成数百万个 id、id 递增、后续插入数据库的索引时性能较高
      • 缺点:严重依赖系统时钟,如果某台机器的系统时钟发生回拨,就有可能会造成 id 冲突甚至 id 乱序

    数据表实验对比

    新建多张字段完全相同的数据表,主键 id 类型分别使用自增 id 和 UUID,测试大批量数据读写。

    结果:UUID 在数据量较大的情况下,其效率直线下滑。

    索引结构对比

    自增 id

    自增的主键由于是顺序的,所以 InnoDB 会把每一条记录都存储在前一条记录的后面,当达到页的最大填充因子时(InnoDB 默认的最大填充因子是页大小的 15/16,它会预留出 1/16 的空间用作以后的数据修改),下一条记录就会写入到新的页中。

    一旦数据按照这种顺序的方式进行加载,那么主键页就会近乎于顺序地填满,这将大大提高页的最大填充率,从而不会造成页的浪费。

    此外,新插入的行一定会在原有的最大数据行的下一行,这对 MySQL 的定位和寻址很有帮助,MySQL 不必为计算出新行的位置而做出额外的消耗,并且能减少页分裂以及碎片的产生。

    UUID

    因为 UUID 是无序的,所以新行的值并不一定会比之前的主键值大,所以 InnoDB 无法做到总是把新行插入到索引的最后,而是需要为新行寻找到合适的位置,从而来分配新的空间(这个过程会需要做很多额外的工作,数据的毫无顺序会导致数据分布散乱)。

    同时,写入的目标页很可能已经刷新到磁盘上并且已经从缓存中移除,甚至可能是还没有被加载到缓存中,以至于 InnoDB 在插入前不得不先在磁盘中读取目标页到内存中(这将伴随着大量的随机 I/O)。

    又因为写入是乱序的,InnoDB 不得不频繁地做页分裂的操作,以便为新行分配空间。页分裂会导致需要移动大量的数据,一次插入操作最少需要修改三个页以上,而频繁地页分裂会导致页变得稀疏并且被不规则地填充,最终造成数据碎片。

    更多相关内容
  • Twitter的分布式自增ID雪花算法snowflake (Java版)
  • random,雪花id,想对比自增id在mysql中要用哪个呢,本文可以指引一个方向给大家,直接上数据. 可以看出在数据量100W左右的时候,uuid的插入效率垫底,并且在后序增加了130W的数据,uudi的时间又直线下降。时间占用量...

    random,雪花id,想对比自增id在mysql中要用哪个呢,本文可以指引一个方向给大家,直接上数据.

    可以看出在数据量100W左右的时候,uuid的插入效率垫底,并且在后序增加了130W的数据,uudi的时间又直线下降。时间占用量总体可以打出的效率排名为:auto_key>random_key>uuid,uuid的效率最低,在数据量较大的情况下,效率直线下滑。

    对比一下mysql关于两者索引的使用情况.

    自增的主键的值是顺序的,所以Innodb把每一条记录都存储在一条记录的后面。当达到页面的最大填充因子时候(innodb默认的最大填充因子是页大小的15/16,会留出1/16的空间留作以后的修改):

    ①下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费

    ②新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗

    ③减少了页分裂和碎片的产生

    因为uuid相对顺序的自增id来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以innodb无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:

    ①:写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,innodb在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机IO

    ②:因为写入是乱序的,innodb不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,一次插入最少需要修改三个页以上

    ③:由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片

    在把随机值(uuid和雪花id)载入到聚簇索引(innodb默认的索引类型)以后,有时候会需要做一次OPTIMEIZE TABLE来重建表并优化页的填充,这将又需要一定的时间消耗。

     

    展开全文
  • id自增器(雪花算法) @Author caijiu @Date 2019年3月26日 上午10:31:48 @Version 1.0 */ public class SnowFlake { private final static long twepoch = 12888349746579L; // 机器标识位数 private final ...
    /**
     * id自增器(雪花算法)
     *	@Author caijiu
     *	@Date 2019年3月26日 上午10:31:48
     *	@Version 1.0
     */
    public class SnowFlake {
    
        private final static long twepoch = 12888349746579L;
        // 机器标识位数
        private final static long workerIdBits = 5L;
        // 数据中心标识位数
        private final static long datacenterIdBits = 5L;
    
        // 毫秒内自增位数
        private final static long sequenceBits = 12L;
        // 机器ID偏左移12位
        private final static long workerIdShift = sequenceBits;
        // 数据中心ID左移17位
        private final static long datacenterIdShift = sequenceBits + workerIdBits;
        // 时间毫秒左移22位
        private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        //sequence掩码,确保sequnce不会超出上限
        private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
        //上次时间戳
        private static long lastTimestamp = -1L;
        //序列
        private long sequence = 0L;
        //服务器ID
        private long workerId = 1L;
        private static long workerMask = -1L ^ (-1L << workerIdBits);
        //进程编码
        private long processId = 1L;
        private static long processMask = -1L ^ (-1L << datacenterIdBits);
    
        private static SnowFlake snowFlake = null;
    
        static{
            snowFlake = new SnowFlake();
        }
        public static synchronized long nextId(){
            return snowFlake.getNextId();
        }
    
        /**
         * 获取机器编码
         * @return
         */
        private long getMachineNum(){
            long machinePiece;
            StringBuilder sb = new StringBuilder();
            Enumeration<NetworkInterface> e = null;
            try {
                e = NetworkInterface.getNetworkInterfaces();
            } catch (SocketException e1) {
                e1.printStackTrace();
            }
            while (e.hasMoreElements()) {
                NetworkInterface ni = e.nextElement();
                sb.append(ni.toString());
            }
            machinePiece = sb.toString().hashCode();
            return machinePiece;
        }
    
        private SnowFlake() {
    
            //获取机器编码
            this.workerId=this.getMachineNum();
            //获取进程编码
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();
    
            //避免编码超出最大值
            this.workerId=workerId & workerMask;
            this.processId=processId & processMask;
        }
    
    
        public synchronized long getNextId() {
            //获取时间戳
            long timestamp = timeGen();
            //如果时间戳小于上次时间戳则报错
            if (timestamp < lastTimestamp) {
                try {
                    throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //如果时间戳与上次时间戳相同
            if (lastTimestamp == timestamp) {
                // 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    // 当前毫秒内计数满了,则等待下一秒
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
            lastTimestamp = timestamp;
            // ID偏移组合生成最终的ID,并返回ID
            long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
            return nextId;
        }
    
        /**
         * 获取时间戳
         */
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
    
        /**
         * 再次获取时间戳直到获取的时间戳与现有的不同
         * @param lastTimestamp
         * @return 下一个时间戳
         */
        private long tilNextMillis(final long lastTimestamp) {
            long timestamp = this.timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = this.timeGen();
            }
            return timestamp;
        }
        
        public static void main(String[] args) {
            System.out.println(SnowFlake.nextId());
            System.out.println(SnowFlake.nextId());
            System.out.println(SnowFlake.nextId());
            System.out.println(SnowFlake.nextId());
            System.out.println(SnowFlake.nextId());
        }
    }
    
    展开全文
  • 自增ID,UUID,雪花ID的使用

    千次阅读 2021-04-07 11:36:27
    雪花ID 数据库的自增主键: 从大学做实验开始就一直用自增主键,以前也没考虑过用自增主键有啥问题,直到工作后… (1)首先自增主键不适用于分库分表情况,在做数据集合的时候会出现主键重复问题,做多个系统的数据...

    主键ID主要有:
    数据库自增主键
    uuid
    雪花ID

    数据库的自增主键:

    从大学做实验开始就一直用自增主键,以前也没考虑过用自增主键有啥问题,直到工作后…
    (1)首先自增主键不适用于分库分表情况,在做数据集合的时候会出现主键重复问题,做多个系统的数据汇总也会有冲突
    (2)其次,详情…删除…查询…这些接口如果都是依赖着自增主键做操作,很容易有安全性问题(这是测试同学做渗透测试的时候告诉我的),同时很容易造成越权(掌握你自增主键的规律然后一定程度上越过某些校验获取数据)
    所以以后再给我好好设计数据库的话,我都不会再用自增做主键了……

    uuid:

    uuid用过一段时间,它能解决自增id的安全性和数据汇总时候的问题,但是效率不高,后来优化的时候取消了…
    (1)uuid首先会比较长,为保证唯一性长度肯定比自增id长很多,批量提交,批量删除,批量查询的时候,list会非常大,传输数据这么大,对性能和带宽都会造成一定影响,get请求拼接在url上的话,过长甚至会被浏览器截断,url看上去也会很丑
    (2)最最重要的是,它是完全乱序的,新增一条新数据可能会打乱整棵索引树结构,它做主键索引也比自增ID做的主键索引占更大的空间

    雪花ID:

    它解决了以上两者的问题,首先它长度可以自定义限制,通过时间戳+机器号或者业务表单id+自增ID这样可以保证有序性

    怎么用?

    自己写规则生成也行,只不过我们项目用的mybatis plus ,已经自带了,所以直接在你自己的实体类中调用一下setId方法:
    在这里插入图片描述
    同时在实体类对应的id上加注解
    在这里插入图片描述
    结果:
    在这里插入图片描述

    IdWorker.getId() 方法跟进去简单看一下源码:

    在这里插入图片描述

    public synchronized long nextId() {
        long timestamp = this.timeGen();
        if (timestamp < this.lastTimestamp) {
            long offset = this.lastTimestamp - timestamp;
            if (offset > 5L) {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }
    
            try {
                this.wait(offset << 1);
                timestamp = this.timeGen();
    // 判断当前时间戳,防止服务器系统时间被手动调整(这个不会影响插入,但是会影响排序,因为依赖着该时间戳进行部分的索引排序)
                if (timestamp < this.lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception var6) {
                throw new RuntimeException(var6);
            }
        }
    // 如果时间戳相等,进行毫秒级别的序列,阻塞一毫秒,获得下一毫秒的新的时间戳
        if (this.lastTimestamp == timestamp) {
            this.sequence = this.sequence + 1L & 4095L;
            if (this.sequence == 0L) {
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
        } else {
            this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L);
        }
    
        this.lastTimestamp = timestamp;
    // 进行位与运算,看不懂,反正移一下就返回雪花ID
        return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
    }
    

    加了synchronized,保证了生成ID时候的线程安全,同时会判断当前时间戳,防止服务器系统时间被手动调整(这个不会影响插入,但是会影响排序,因为依赖着该时间戳进行部分的索引排序),同时如果时间戳相等,进行毫秒级别的序列,阻塞一毫秒,获得下一毫秒的新的时间戳拼在id里面

    展开全文
  • 自增ID算法snowflake(雪花)

    千次阅读 2020-12-20 03:47:05
    在数据库主键设计上,比较常见的方法是采用自增ID(1开始,每次加1)和生成GUID。生成GUID的方式虽然简单,但是由于采用的是无意义的字符串,推测会在数据量增大时造成访问过慢,在基础互联网的系统设计中都不推荐采用...
  • 摘自一篇文章开头: ... B站讲解 B树,B+树的视频,还有mysql...https://www.bilibili.com/video/BV1qE411273M?spm_id_from=333.999 https://www.bilibili.com/video/BV1e5411T77z?spm_id_from=333.999.0.0 https://www.
  • 文章目录前言UUID雪花ID雪花ID生成类测试自动递增重新计数测试自增Id的好处自增Id的坏处总结 前言 那些个公众号天天推来推去就几篇剩饭文章,直接实操测试一下。 单元测试:SpringBootTest /* * 使用STS创建的...
  • * 描述: Twitter的分布式自增ID雪花算法snowflake (Java版) * * @create 2018-03-13 12:37 **/ public class SnowFlake { public static Long mac ; public static Long ip ; /** *...
  • package ... /** /** * Biz-Boot, All rights reserved * 版权:企业之家网 -- 企业建站管理系统<br/> ...br/>... * 描述: Twitter的分布式自增ID雪花算法snowflake (Java版) * * @au...
  • 自增id.雪花算法.redis.zookeeper uuid 使用程序生成 优: 简单,方便 id生成性能好 缺: 无序,不能保证趋势递增 uuid往往使用字符串,查询效率较低 存储空间较大 自增 优: 简单,有序,可分页 缺: 数据库不同,在...
  • 分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止ID冲突可以使用36位的通用唯一识别码/UUID(Universally Unique Identifier),但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的...
  • 前言:在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究竟有什么坏处?...
  • PostgreSQL 主键弃用自增ID使用雪花算法
  • 主要介绍了Java实现Twitter的分布式自增ID算法snowflake,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • 1 雪花算法:分布式ID生成器 雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。 1.1 核心思想: 长度共64bit(一个long型)。 首先是一个符号位,1bit...
  • Twitter 的分布式雪花算法 Snowflake,经测试 Snowflake 每秒能够产生 26 万个自增可排序的ID twitter 的 Snowflake 生 成ID能够按照时间有序生成 Snowflake 算法生成 id 的结果是一 个 64bit 大小的整数, 为一个 ...
  • Uuid、数据库自增雪花算法、基于redis自研等数据库唯一ID生成策略对比 使用环境 分布式、高并发下全局唯一,趋势递增,效率高,控制并发 先直接上个对比图吧,下面大量干货警告 一、Uuid(java1.5后自带生成工具...
  • 自增ID的问题在于以下两点: 当数据量太大,比如客户信息一张表的数据超过上千万条时,我们就面临着要分表存储的问题,在多张表的情况下,如何划分自增ID是一个很麻烦的问题。 安全问题,客户端可以根据自增ID很...
  • 一般情况,实现全局唯一ID,有三种方案,分别是通过中间件方式、UUID、雪花算法。  方案一,通过中间件方式,可以是把数据库或者redis缓存作为媒介,从中间件获取ID。这种呢,优点是可以体现全局的递增趋势(优点...
  • * 描述: Twitter的分布式自增ID雪花算法snowflake (Java版) * https://github.com/souyunku/SnowFlake * * @author yanpenglei * @create 2018-03-13 12:37 **/ public class SnowFlake { /** * 起...
  • 自增ID算法snowflake及时间回拔的解决
  • 直接上代码 /** * Twitter_Snowflake<br> * SnowFlake的结构如下(每... * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> * 41
  • 1、静态内部类实现的单例模式 2、初始化时通过获取系统的mac和ip作为机器id和数据标识id,不需要额外指定
  • /** * Twitter 的分布式自增 ID 雪花算法 snowflake (Java版) * * @author bood * @since 2020/10/16 */ public class SnowFlake { /** * 起始的时间戳 */ private final static long START_STMP = 1480166465631L;...
  • twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。 snowflake的结构如下(每部分用-分开): 0 - ...
  • 雪花算法(SnowFlake) 1. 原理 SnowFlake算法生成id的结果是一个64bit大小的整数: 1bit符号位 + 41bit时间戳 + 10bit机器ID + 12bit序列号 由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,630
精华内容 2,252
关键字:

自增id雪花id