0%

001-mysql-主键设计

摘要:MySQL InnoDB数据存储是以B+树索引方式存储
存储单元、mysql InnoDB-B+树组织数据、查询数据、mysql InnoDB-B+树存储数据量、实际操作查看

mysql知识储备

参看:http://blog.bjlhx.top/categories/db-mysql-core/

  1. InnoDB为聚集主键类型的引擎,数据会按照主键进行排序
  2. mysql 的索引是B+tree,聚集索引使用主键,没有主键默认生成一个隐式主键rowId,辅助索引保存的是主键
  3. innodb 存储单元是页,每页 16k,数据类型过大,会跨页,溢出等
  4. 每张表都会有主键,如没有显式设置主键,则
    (1)、表中的非空唯一索引作为主键
    (2)、自动创建一个6字节大小的指针

主键定义

表中经常有一个列或多列的组合,其值能唯一地标识表中的每一行。这样的一列或多列称为表的主键,通过它可强制表的实体完整性。

主键设计原则

总原则:根据数据库表的具体使用范围来决定采用不同的表主键定义。

数据库设计的三大范式

  • 第一范式(1NF):指在关系模型中,对于添加的一个规范要求,所有的域都应该是原子性的,即数据库表的每一列都是不可分割的原子数据项,而不能是集合,数组,记录等非原子数据项。
    即实体中的某个属性有多个值时,必须拆分为不同的属性。在符合第一范式(1NF)表中的每个域值只能是实体的一个属性或一个属性的一部分。简而言之,第一范式就是无重复的域。

  • 第二范式(2NF):首先要满足它是1NF,另外还需要包含两部分内容:一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。简而言之,第二范式就是非主属性非部分依赖于主关键字

  • 第三范式(3NF):首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。

反范式主键的设计原则

  • 主键应当是对用户没有意义的。业务上的‘主键’可以通过唯一键(Unique Key)或唯一索引(Unique Index)和其它约束条件实现
  • 主键应该是单列的,以便提高连接和筛选操作的效率
  • 不要更新主键。实际上,因为主键除了惟一地标识一行之外再没有其他的用途了,所以也就没有理由去对它更新。另外,主键的值通常不重用,意味着记录被删除后,该主键值不再使用
  • 主键不应包含动态变化的数据,如时间戳、创建时间列、修改时间列等
  • 主键应当由计算机自动生成。

反范式跟范式所要求的正好相反,在反范式的设计模式,可以允许适当的数据的冗余,用这个冗余去取操作数据时间的缩短。
本质上就是用空间来换取时间,把数据冗余在多个表中,当查询时可以减少或者是避免表之间的关联(手册中也有禁止三表以上 JOIN 的条款)。

但需要注意的是,采用代理主键是为了避免业务逻辑变化导致主键变更,以及提高 JOIN 效率等。但在实际查询中,数据查询还是需要通过业务上的唯一键进行匹配的,
而不应该将代理主键作为查询条件,尤其不能将代理主键作为查询条件输入项提供给用户。

总体来说,实际应用中应当具体问题具体分析,结合范式和反范式两种设计思想。
对数据一致性和完整性较高、而对查询效率要求并不严格的地方,应当更倾向于遵从范式;而类似分布式、高并发集群的场景,则应当更考虑反范式的设计方案。

确保主键的无意义性

在开发过程中,有意义的字段例如“用户登录信息表”将“登录名”(英文名)作为主键,“订单表”中将“订单编号”作为主键,如此设计主键一般都是没什么问题,因为将这些主键基本不具有“意义更改”的可能性。

但是,也有一些例外的情况,例如“订单表”需要支持需求“订单可以作废,并重新生成订单,而且订单号要保持原订单号一致”,那将“订单编号”作为主键就满足不了要求了。

因此在使用具有实际意义的字段作为主键时,需要考虑是否存在这种可能性。

要用代理主键,不要使用业务主键。任何一张表,强烈建议不要使用有业务含义的字段充当主键。我们通常都是在表中单独添加一个整型的编号充当主键字段。

采用整型主键

主键通常都是整数,不建议使用字符串当主键。(如果主键是用于集群式服务,可以采用字符串类型)

减少主键的变动

主键的值通常都不允许修改,除非本记录被删除。

避免重复使用主键

主键的值通常不重用,意味着记录被删除后,该主键值不再使用。

主键字段定义区分

主键不要直接定义成【id】,而要加上前缀,定义成【表名id】或者【表名_id】

主键设计的常用方案

自增ID

  • 方案:数据库设置:AUTO_INCREMENT

  • 使用场景:不拆分库的情况下,推荐使用

  • 优点:

  1. 数据库自动编号,速度快,而且是增量增长,聚集型主键按顺序存放,对于检索非常有利。
  2. 数字型,占用空间小,易排序,在程序中传递方便。
    同样数据量的情况下,自增id主键的数据量是字符串主键的1/2,对于考虑成本的公司来说无疑是一件好事,并且数据量小对备份还原数据都有大大的好处。
  3. Mysql会按照键值的大小进行顺序存放,如果设置自增id为主键,这个时候主键是按照一种紧凑的接近顺序写入的方式进行存储数据。
    如果用其他字段作为主键的话,此时Mysql不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,
    此时又要从磁盘上读回来,这增加了很多额外的开销,同时频繁的移动、分页操作造成了大量的碎片。
  • 缺点:
  1. 不支持水平分片架构,水平分片的设计当中,这种方法显然不能保证全局唯一。
  2. 表锁
    在MySQL5.1.22之前,InnoDB自增值是通过其本身的自增长计数器来获取值,该实现方式是通过表锁机制来完成的(AUTO-INC LOCKING)。
    锁不是在每次事务完成后释放,而是在完成对自增长值插入的SQL语句后释放,要等待其释放才能进行后续操作。
    比如说当表里有一个auto_increment字段的时候,innoDB会在内存里保存一个计数器用来记录auto_increment的值,当插入一个新行数据时,
    就会用一个表锁来锁住这个计数器,直到插入结束。如果大量的并发插入,表锁会引起SQL堵塞。
    在5.1.22之后,InnoDB为了解决自增主键锁表的问题,引入了参数innodb_autoinc_lock_mode:
    0:通过表锁的方式进行,也就是所有类型的insert都用AUTO-inc locking(表锁机制)。
    1:默认值,对于simple insert 自增长值的产生使用互斥量对内存中的计数器进行累加操作,对于bulk insert 则还是使用表锁的方式进行。
    2:对所有的insert-like 自增长值的产生使用互斥量机制完成,性能最高,并发插入可能导致自增值不连续,可能会导致Statement 的 Replication 出现不一致,使用该模式,需要用 Row Replication的模式。
  3. 可能不连续
  4. 当系统与其他系统集成时,需要数据导入时,很难保证原系统的ID不发生主键冲突。
  5. 如果其他系统主键不是数字型,会导致修改主键数据类型,导致其他相关表的修改。

UUID

UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。在UUID的算法中,可能会用到诸如网卡MAC地址,IP,主机名,进程ID等信息以保证其独立性。

  • 方案:mysql uuid()函数,应用程序UUID函数

  • 使用场景:不推荐使用,针对mysql缺点过于严重

  • 优点:

  1. 全局唯一性、安全性、可移植性。
  2. 能够保证独立性,程序可以在不同的数据库间迁移,效果不受影响。
  3. 保证生成的ID不仅是表独立的,而且是库独立的,在你切分数据库的时候尤为重要
  • 缺点:
  1. 针对InnoDB引擎会徒增IO压力,InnoDB为聚集主键类型的引擎,数据会按照主键进行排序,由于UUID的无序性,InnoDB会产生巨大的IO压力。InnoDB主键索引和数据存储位置相关(簇类索引),uuid 主键可能会引起数据位置频繁变动,严重影响性能。
  2. UUID长度过长,一个UUID占用128个比特(16个字节)。主键索引KeyLength长度过大,而影响能够基于内存的索引记录数量,进而影响基于内存的索引命中率,而基于硬盘进行索引查询性能很差。严重影响数据库服务器整体的性能表现。

自建的id生成器

当数据量比较大,又是分布式架构的时候,可能需要考虑各种分库分表方案了。
自建id生成器,可以保证全局唯一,可以参考snowflake的算法(18位)方案,具体实施也可以根据自身业务进行调整算法。其次需要考虑的就是服务的高可用。

业务编号做主键

  • 优点
    此方法就是采用实际业务中的唯一字段的“编号”作为主键设计,这在小型的项目中是推荐这样做的,因为这可以使项目比较简单化,

  • 缺点:
    但在使用中却可能带来一些麻烦,比如要进行编号修改”时,可能要涉及到很多相关联的其他表,后果很严重”;或“业务要求允许编号重复时”

max加1

  • 优点:
    由于自动编号存在那些问题,采用自己生成,同样是数字型的,只是把自动增长去掉了,采用在Insert时,读取Max值后加一,这种方法可 以避免自动编号的问题,

  • 缺点:
    但也存在一个效率问题,如果记录非常大的话,那么Max()也会影响效率的;更严重的是并发性问题,如果同时有两人读到相同的Max后,加一后插入的ID值会重复。

自制加一

  • 优点:
    考虑Max加一的效率后,采用自制加一,也就是建一个特别的表,字段为:表名,当前序列值。这样在往表中插入值时,先从此表中找到相应表的最大值后加 一,进行插入,
    也可能会存在并发处理,这个并发处理,我们可以采用lock线程的方式来避免,在生成此值的时,先Lock,取到值以后,再unLock出来,这样不会有两人同时生成了。
    这比Max加一的速度要快多了。

  • 缺点:
    但同样存在一个问题:在与其他系统集成时,脱离了系统中的生成方法后,很麻 烦保证自制表中的最大值与导入后的保持一致,而且数字型都存在老数据的导入问题。
    因此在“自制加一”中可以把主键设为字符型的。字符型的 自制加一我倒是蛮推荐的,应该字符型主键可以应付很多我们意想不到的情况。

主键的选取建议

此处的分布式主要指 分库分表。非应用分布式

  1. 非分布式架构直接套用自增id做主键

  2. 小规模分布式架构用uuid或者自增id+步长做主键

  3. 大规模分布式架构用自建的id生成器做主键,参考twitter的[snowflake算法][2]

(1)单实例或者单节点组:
经过500W、1000W的单机表测试,自增ID相对UUID来说,自增ID主键性能高于UUID,磁盘存储费用比UUID节省一半的钱。所以在单实例上或者单节点组上,使用自增ID作为首选主键。

(2)分布式架构场景:
20个节点组下的小型规模的分布式场景,为了快速实现部署,可以采用多花存储费用、牺牲部分性能而使用UUID主键快速部署;

20到200个节点组的中等规模的分布式场景,可以采用自增ID+步长的较快速方案。

200以上节点组的大数据下的分布式场景,可以借鉴类似twitter雪花算法构造的全局自增ID作为主键。

一分也是爱,两分情更浓【还没有人赞赏,支持一下呗】