数据库可以通过主从复制将数据复制多份实现读写分离,读走从库,写走主库,应对量并发读的能力,同时提⾼数据安全性。
但是对于单个表,还存在很多问题,比如:
- 单表记录过多,字段加上索引,索引的占用空间也会越来越大,影响查询。
- 不同的数据,用户,商品等都存放在⼀个库中,甚至一张表中,⼀旦崩溃,全部模块都受到影响。
因此可以做分库分表,将问题隔离在某一张表或者某一个库中,降低单个库的压力。基本思想是依照某⼀种策略将数据尽量平均地分配到多个数据库节点或者多个表中。分库分表后,不同的节点值保存部分数据,所有库表加到一块就是所有的数据。拆分分为垂直拆分和水平拆分。
一、垂直拆分
原则⼀般是专库专用,注重业务相关性,将数据库的表分离到不同的库中,比如用户关系放在用户库,内容放在内容库。
但是水平拆分无法解决单张表记录量增长的问题,⽐如视频或者直播业务中,用户会在视频播放过程中发送大量的弹幕,会有数据膨胀的问题。因此需要做单张表拆分。
二、水平拆分
水平拆分更注重数据的特点,将单表内具有一定相关性的记录拆分到不同的表或者库中。但是要考虑拆分规则,也就是如何根据单条记录定位到库。
可选方案有:
- 根据字段进行hash值计算,比如根据用户ID做hash,相当于把ID打散,然后对数据库的个数取余,得到的值就是记录对应的库。
- 用常规字段做区间划分,比如根据记录的创建时间,⼀条记录是1⽉创建,插⼊和查找的时候⽤创建时间这个字段先去找到对应的1号库。但是这种方式有明显的热点现象。⽐如6月11月是购物的热点事件,对应的两个表或者库的QPS就会更多。
三、分库分表引入的问题
主要是水平分库
1、区分键的问题
分区键问题:具体的表现是,如果把记录存放在哪个库是依据主键ID进行hash取余计算得来的,也就是分表的时候是根据主键定位到具体的库。那么后续如果根据非主键,比如根据用户名去查找记录,现在只根据用户名去定位记录,就要先找到主键ID,否则就要把所有库扫描一遍。
解决办法之⼀就是:建立⼀个额外的映射表。这个表只有两个字段用户名和主键ID,查询记录时候先根据用户名去找ID,然后再根据ID去定位到库表,虽然这个映射表会有⼀定的存储消耗,但是毕竟只有两个字段,相对来说可以接受。
2、数据库特性难以实现
聚合操作:在未分库分表之前查询记录总数时只需要在SQL中执⾏count()聚合函数即可,现在数据被分散到多个库表中,我们可能要考虑其他的⽅案,比方说将计数的数据单独在⼀张表中或者记录在Redis⾥⾯单独记录。
3、全局ID问题
水平分表之后引入的另外一个比较严重的问题就是如何选择全局唯一ID作为主键。
四、如何选择全局ID
1、业务字段作为主键
选择业务字段作为主键,⽐如⾝份证,邮箱或者⼿机号等,但是并不通⽤
- 比如⾝份证,对于一些匿名登陆产生的记录,则⽆法使用身份证证作为主键
- 比如邮箱手机号,一般邮箱和手机号还存在变更的可能
2、UUID作为全局
主键要保证全局唯⼀性,保证每个机器上生成的记录都不会冲突,UUID类似的随机生成可以保证唯一,但是无法保证一定递增。
为什么要保证递增?
- 因为可能会根据ID排序,⽐如弹幕系统,会按照倒序看最新发送的弹幕。如果ID不能保证递增,只能额外⽤时间字段记录时间戳,对所有的记录再排序。
- 另外B+树的数据页是叶内记录按照ID递增,如果ID非递增,则可能导致记录非尾部插入最终数据页分裂,降低插⼊的性能。
3、雪花算法计算全局ID
Snowflake的核心思想是将64bit的⼆进制数字分成若干部分,每⼀部分都存储有特定含义的数据,比如说时间戳、机器ID、序列号等等,最终⽣成全局唯⼀的有序ID。
比如当前机器上,第2台机器,时间戳第32毫秒,有12个序列号位,当前时间戳下可以生成2^12个序号ID,基本可以满足要求。
雪花算法缺点:
- 比较依赖系统时间戳,重启服务器就会机器编号,时间不准可能导致生成重复ID,当⽣成重复ID,可以暂停发号,进行时钟校准。
- ⽐如服务的QPS并不⾼,时间间隔是毫秒为单位,导致当前毫秒下可以生成2^12个序号ID,序列号只了1-10的前几个ID,可以序列号的起始号随机或者扩⼤时间间隔为s。