聊一个实际问题,淘宝的数据库的主键是如何设计的呢?
某些错误的离谱的答案还在网上流传着。其中一个明显的错误就是关于mysql的主键设计。
大部分人的回答如此自信就是:用8个自己的bigint做主键,而不是用int。
这样的回答只是站在数据库这一层而没有从业务的角度思考。主键就是一个自增的ID吗?
目录
自增ID的问题
1.可靠性不高
2.安全性不高
3.性能差
4.交互多
5.局部唯一性
业务字段做主键
选择卡号
选择会员电话或身份证号
某宝的主键设计
推荐的主键设计
UUID的特点
认识UUID
改造UUID
有序uuid性能测试
自增id作为主键,简单易懂,几乎所有的数据库都支持自增类型,只是实现上各自有所不同。自增id除了简单,其他都是缺点,总体来看存在以下几方面的问题
存在自增id的回溯问题,这个问题直到mysql8.0才修复。
对外暴露的接口可以非常容易猜到对应的信息。比如/user/1/这样的接口,可以非常容易猜测用户id的值为多少,总用户数量有多少,也可以非常容易的通过接口进行数据的爬取。
自增id的性能较差,需要在数据库服务器端生成
业务还需要额外执行一次类似last_insert_id()的函数才能直到刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条sql就多一次性能上的开销。
最重要的一点自增id是局部唯一性,只在当前数据库中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直是噩梦。
为了能够唯一地标识一个会员的信息,需要为会员信息表设置一个主键。那么怎么为这个表设置主键才能达到我们的目的呢?这里我们考虑业务字段做主键。
比如会员表中的字段有 cardno卡号 、membername名称、phone电话、memberpid身份证号、address地址、sex性别、birthday生日。在表中哪个字段比较合适呢?
会员卡号cardno看起来比较合适,因为会员卡号不能为空且唯一标识一条会员记录。但实际情况是,会员卡可能存在重复使用的情况。比如张三因为工作变动办理了原来的地址,不再到商家消费了退了会员卡,于是张三就不再是会员了。但是,商家不想让这个会员卡空着,就把这个卡发给了王五。
从系统角度看这个变化只是修该了会员信息表中的这个卡号的这个会员信息,并不影响到数据的一致性。也就是说,修改会员信息,系统的各个模块都会获取到修改后的会员信息。因此,从信息系统层面上看是没有问题的。
但是从使用系统的业务层面来看就有很大问题了,会对商家造成影响。
比如,我们有一个销售流水表,记录了所有销售流水明细。张三在商家买了一本书,那么系统中就有了张三的流水记录。比如
如果会员卡又发给了王五,我们会更改会员信息表。导致查询时候会显示王五买了书。这显然是错误的,结论是不能将会员卡作为主键。
会员电话可以做主键吗?不可以。在实际操作中,手机号也存在被运营商收回,重新发给别人使用的情况。
那么身份证号呢?好像可以,因为其不会重复且与人一一对应。可问题是身份证属于个人隐私,顾客不一定愿意提供给你。要是强制要求登记身份证号会把顾客赶跑。其实电话也一样。这就是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。
所以,建议尽量不要用跟业务有关的字段做主键。毕竟作为项目设计的人员我们谁也无法预测在项目的整个生命周期中哪个业务字段会因为项目的业务需求而重复或者重用之类的情况出现。
经验:刚开始使用mysql时,很多人都容易犯的错误就是喜欢用业务字段做主键,想当然的认为了解业务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。
在某宝的电商业务中订单服务是一个核心业务。那么它的主键是怎么设计的呢?
从上图可以看出,订单号不是自增id。订单号是19位长度且后6位都是一样的,都是08113.前14位部分是单调递增的。大胆猜测其订单号应该是
订单ID=时间+去重字段+用户ID后6位
这样的设计能做到全局唯一且分布式系统查询极其友好。
非核心业务:对应表的主键自增ID,如告警,日志,监控等信息。
核心业务:主键设计至少应该是全局唯一且单调递增。全局唯一保证在各个系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。
这里推荐最简单的一种设计UUID
全部唯一,占用36字节,数据无序,插入性能差
①为什么uuid是全局唯一
②为什么uuid占用36个字节
③为什么uuid是无序的
mysql数据库的uuid组成如下图所示
为什么uuid是全局唯一的?
在uuid中间部分占用60位,存储的类似timestamp的时间戳。但实际上其时间精度比时间戳要高。
时间序列是为了避免时钟被回拨导致产生时间重复的可能性。Mac地址用于全局唯一。
为什么占用36个字节?
uuid根据字符串进行存储,设计时还有无用的“-”字符串,因此总共需要36个字节。
为什么uuid是随机无序的?
因为uuid的设计中,将时间地位放在最前面,而这部分的数据是一直变化的,且是无序的。
若将时间高低位互换,则时间就是单调递增的了,也就变得单调递增了。mysql8.0可以更换时间地位和高位的存储方式,这样uuid就是有序的uuid了。
mysql8.0还解决了uuid存在的占用空间的问题,去除了uuid字符串中无用的“-”字符串,并且将字符串用二进制类型保存,这样存储空间降低为了16字节。
可以通过mysql8.0提供的uuid_to_bin函数实现上述功能,同样的,mysql也提供了bin_to_uuid函数进行转化:
通过函数uuid_to_bin(@uuid,true)将uuid转化为有序uuid了。全局唯一+单调递增。
下面截图是我在别人测试的地方截取的
在当今的互联网环境中,非常不推荐自增id作为数据库主键。更推荐类似有序uuid全局唯一的实现。
另外在真实的业务系统中,主键还可以进入业务和系统属性,如用户的尾号,机房的信息等。这样的主键设计就更为考研架构师的水平了。