mysql设置主键

发布时间: 2023-11-21 12:01 阅读: 文章来源:1MUMB2741PS
前言

主键,可以唯一标识表中的某一行(记录)。合理地设置主键,可以帮助我们准确、快速地找到所需要的数据记录。但是设置出正确的主键似乎并没有那么简单,请思考如下几个问题:

表中的业务字段可以用来做主键吗?单机系统下使用自增字段做主键,那在分布式系统下可以吗?在分布式系统下如何保证主键的唯一性呢?

如果对以上问题感到困惑,那么我相信这篇文章可以帮助到你。

我坚信,友好的交流会让彼此快速进步!文章难免有疏漏之处,欢迎大家在评论区中批评指正。

为什么一定要给表定义主键?提高查询效率

前言中提到,主键可以唯一标识表中的某一行(记录),合理地设置主键,可以帮助我们准确、快速找到所需要的数据记录。为什么呢?这是因为给表定义了主键,就相当于给表加了一个主键索引。索引可以帮助提高数据查询的效率,就像书的目录一样。

安全地更新或删除特定行

如果没有不为表添加主键,不仅查询效率会变低,更会导致更新或删除表中的特定行很困难,因为没有安全的方法保证只更新或删除相关的行。

如果你使用的图形化管理工具是 Workbench(MySQL 官方图形化管理工具)的话,在执行 update 或 delete 操作时,必须包含 WHERE 条件。而且,WHERE 条件中,必须用到主键约束或唯一性约束的字段,否则会报如下错误。

Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column.To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.实际案例

现在我们有一个商品信息表,表中字段和样例数据如下所示。

创建商品信息表 sku_info# 创建商品信息表 sku_infoCREATE TABLE sku_info(pk_idBIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,sku_nameVARCHAR(255) COMMENT ‘商品名‘,priceDECIMAL(10, 2) COMMENT ‘商品价格‘,gmt_create DATETIME COMMENT ‘创建时间‘,gmt_modified DATETIME COMMENT ‘更新时间‘);######################### 查看表结构DESCRIBE sku_info;+--------------+-----------------+------+-----+---------+----------------+| Field| Type| Null | Key | Default | Extra|+--------------+-----------------+------+-----+---------+----------------+| pk_id| bigint unsigned | NO| PRI | NULL| auto_increment || sku_name | varchar(255)| YES| | NULL||| price| decimal(10,2)| YES| | NULL||| gmt_create| datetime| YES| | NULL||| gmt_modified | datetime| YES| | NULL||+--------------+-----------------+------+-----+---------+----------------+5 rows in set (0.04 sec)

《阿里巴巴 Java 开发手册 1.4.0》的部分建表规约如下:

【强制】 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。

【强制】 主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名则为idx_字段名。

【强制】 小数类型为 decimal,禁止使用 float 和 double。

【强制】 表必备三字段:id,create_time,update_time。其中 id 必为主键,类型为 bigint unsigned、单表自增、步长为 1。

根据上面这个强制要求,我们也知道了数据表中一定要定义主键。

想要了解更多,请参见《阿里巴巴 Java 开发手册 1.4.0》。

插入样例数据# 插入数据insert INTO sku_info(sku_name, price, gmt_create, gmt_modified) VALUES(‘书本‘, 10, ‘2023-06-01‘, ‘2023-06-01‘),(‘鼠标‘, 199, ‘2023-06-01‘, ‘2023-06-01‘);Query OK, 2 rows affected (0.02 sec)Records: 2Duplicates: 0Warnings: 0######################### 查看表数据select * FROM sku_info;+-------+----------+----------+---------------------+---------------------+| pk_id | sku_name | price| gmt_create| gmt_modified|+-------+----------+----------+---------------------+---------------------+| 1 | 书本 |10.00 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 || 2 | 鼠标 | 199.00 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 |+-------+----------+----------+---------------------+---------------------+2 rows in set (0.00 sec)

谨慎使用通配符

除非你明确需要表中所有的列,否则最好别使用 * 通配符。虽然通配符挺省事的,不用明确列出所需要的列,但检索不需要的列通常会降低检索和应用程序的性能。

当然,使用通配符有一个大优点。由于不明确指定列名(通配符 * 检索每个列),所以能检索出名字未知的列。

安全模式下特定删除语句报错

在 Workbench 中执行下面任意一条命令都会报错:

# 没有添加 WHERE 条件delete FROM sku_info; # WHERE 条件中的字段没有主键约束或唯一性约束delete FROM sku_info WHERE price = 10;

报错情况如下:

具体报错信息如下:

Error Code: 1175. You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column.To disable safe mode, toggle the option in Preferences -> SQL Editor and reconnect.

第一条语句会删除 sku_info 表中的所有数据记录,第二条语句会删除 sku_info 表中所有价格为 10 的数据记录。这两种操作都会删除大量数据,可能错误地包括不相关的数据,因此被 MySQL 的安全模式予以禁止。

当然,如果你在非安全模式下进行上述两种删除操作是没问题的,比如你直接使用终端去执行删除操作是可以通过的,但是请务必确保不要删除了不相关的数据。

使用主键要注意哪些事情?主键值必须是的唯一的

表中每行的主键值必须唯一(主键列不允许 NULL 值)。如果主键使用单个列,值必须唯一。如果使用多个列,则这些列的组合值必须唯一。

不要修改主键列中的值

主键值是一条数据记录的唯一标识,如果修改了主键的值,就可能破坏数据的完整性。

如果你需要去修改主键的值,那么很有可能是你的主键设置得不合理。

为什么尽量不要用业务字段做主键?

使用业务字段做主键,由于无法预测项目在整个生命周期中,哪个业务字段会因为业务需求而有重复,或者重用之类的情况,此时需要变更主键,这往往是不能被接受的。

我们举一个实际案例,来看看使用业务字段做主键,当需求发生变更时将会发生什么样的事情。

用案例说明使用业务字段「会员卡号」做主键

现在有一家零售店,需要进行会员营销,会员信息表字段和样例数据如下所示。

在这里,我们将业务字段「会员卡号」设置成了主键,「会员卡号」本身不能为空,而且具有唯一性,可以用来唯一标识一名会员。

下面我们创建出来这张会员信息表,并把上面的数据插入表中。

创建会员信息表 member_infoCREATE TABLE member_info(pk_card_no char(8) PRIMARY KEY COMMENT ‘会员卡号‘, # 会员卡号为主键member_name VARCHAR(20) COMMENT ‘姓名‘,gender char(1) COMMENT ‘性别‘,pid char(18) COMMENT ‘身份证号‘,gmt_create DATETIME COMMENT ‘注册时间‘,gmt_modified DATETIME COMMENT ‘更新时间‘);Query OK, 0 rows affected (0.86 sec)插入样例数据# 插入样例数据insert INTO member_info(pk_card_no, member_name, gender, pid, gmt_create, gmt_modified)VALUES(‘11000001‘, ‘柿子先生‘, ‘男‘, ‘123456199902107891‘, ‘2023-06-01‘, ‘2023-06-01‘),(‘11000002‘, ‘可可小姐‘, ‘女‘, ‘123456199809077891‘, ‘2023-06-01‘, ‘2023-06-01‘);Query OK, 2 rows affected (0.00 sec)Records: 2Duplicates: 0Warnings: 0######################### 查看表中数据select * FROM member_info;+------------+--------------+--------+---------------------+---------------------+| pk_card_no | member_name| gender | gmt_create| gmt_modified|+------------+--------------+--------+---------------------+---------------------+| 11000001| 柿子先生| 男| 2023-06-01 00:00:00 | 2023-06-01 00:00:00 || 11000002| 可可小姐| 女| 2023-06-01 00:00:00 | 2023-06-01 00:00:00 |+------------+--------------+--------+---------------------+---------------------+2 rows in set (0.00 sec)

可以看到,「会员卡号」pk_card_no 可以唯一地标识某个会员,系统可以正常运行。

但是,系统上线后发生了一件事,导致「会员卡号」无法再唯一识别某个会员了。

退卡引发的危机

现在柿子先生搬家了,不再前往该商家消费,于是他退还了会员卡。商家没有扔掉这张会员卡(毕竟有制卡成本),而是把这张卡号为 11000001 的会员卡发给了下一个办理会员卡的用户,六一。

如果只单看这一张表,的确是没有什么影响的,我们只要把会员信息表中卡号为 11000001 的会员信息修改一下不就可以啦,但是从整个系统的业务层面来看,将会出现大问题!

下面,我们来一起看看这个问题是如何发生的?

现在我们有一张订单信息表,上面记录了所有的订单信息。2023-06-02,柿子先生购买了一本书,消费了 10 元,系统会记录该订单信息,如下所示:

创建订单信息表 order_info

订单信息表 order_info 引入了之前的商品信息表 sku_info 的商品 id,以及会员信息表 member_info 的会员卡号。

我们先把订单信息表创建出来,并插入上面的数据。

# 创建订单信息表CREATE TABLE order_info(pk_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,card_no char(8) COMMENT ‘会员卡号‘, # 与会员信息表建立联系sku_id BIGINT UNSIGNED COMMENT ‘商品 id‘, # 与商品信息表建立联系sku_price DECIMAL(10,2) COMMENT ‘商品价格‘,sale_quantity INT UNSIGNED COMMENT ‘销售数量‘,sale_amount DECIMAL(10,2) COMMENT ‘销售金额‘,gmt_create DATETIME COMMENT ‘交易时间‘,gmt_modified DATETIME COMMENT ‘更新时间‘);Query OK, 0 rows affected (0.06 sec)插入样例数据# 插入样例数据insert INTO order_info(card_no, sku_id, sku_price, sale_quantity, sale_amount, gmt_create, gmt_modified)VALUES(‘11000001‘, 1, 10, 1, 10, ‘2023-06-02‘, ‘2023-06-02‘);Query OK, 1 row affected (0.00 sec)######################### 查看表中的数据select * FROM order_info;+-------+----------+--------+-----------+---------------+-------------+---------------------+---------------------+| pk_id | card_no| sku_id | sku_price | sale_quantity | sale_amount | gmt_create| gmt_modified|+-------+----------+--------+-----------+---------------+-------------+---------------------+---------------------+| 1 | 11000001 |1 | 10.00 | 1 |10.00 | 2023-06-02 00:00:00 | 2023-06-02 00:00:00 |+-------+----------+--------+-----------+---------------+-------------+---------------------+---------------------+1 row in set (0.01 sec)查询会员销售记录

现在,我们要完成这样的需求,根据商品信息表 sku_info、会员信息表 member_info、订单信息表 order_info 查询出 2023-06-02 当天的会员销售记录,具体形式如下:

为了实现这个查询,我们需要使用关联查询,具体执行语句如下:

select m.member_name, s.sku_name, o.sale_quantity, o.sale_amount, o.gmt_create AS trade_timeFROM order_info AS oJOIN sku_info AS s JOIN member_info AS mON (o.card_no = m.pk_card_no AND o.sku_id = s.pk_id);+--------------+----------+---------------+-------------+---------------------+| member_name| sku_name | sale_quantity | sale_amount | trade_time|+--------------+----------+---------------+-------------+---------------------+| 柿子先生| 书本| 1 |10.00 | 2023-06-02 00:00:00 |+--------------+----------+---------------+-------------+---------------------+1 row in set (0.02 sec)

我们查询得到的结果是柿子先生在 2023-06-02 这一天,买了一本书,消费了 10 元。

退卡

然后,2023-06-03,柿子先生退还了会员卡,商家又把这张卡发给了六一。那么,我们是不是要修改会员信息表 member_info 中的会员信息。

update member_infoSET member_name = ‘六一‘,gender = ‘男‘,pid = ‘123456202203017891‘,gmt_modified = ‘2023-06-03‘WHERE pk_card_no = ‘11000001‘;Query OK, 1 row affected (0.01 sec)Rows matched: 1Changed: 1Warnings: 0查询会员销售记录出现异常

现在我们再查询一下 2023-06-02 当天的会员销售记录:

select m.member_name, s.sku_name, o.sale_quantity, o.sale_amount, o.gmt_create AS trade_timeFROM order_info AS oJOIN sku_info AS s JOIN member_info AS mON (o.card_no = m.pk_card_no AND o.sku_id = s.pk_id);+-------------+----------+---------------+-------------+---------------------+| member_name | sku_name | sale_quantity | sale_amount | trade_time|+-------------+----------+---------------+-------------+---------------------+| 六一| 书本 | 1 |10.00 | 2023-06-02 00:00:00 |+-------------+----------+---------------+-------------+---------------------+1 row in set (0.01 sec)

出现大问题了!查询得到的结果是 2023-06-02,六一买了一本书,消费 10 元。可是,六一 2023-06-03 才拿到会员卡。

很明显,因为退卡重发,柿子先生的消费行为转移到了六一身上,这肯定是无法接受的。

发生这个问题的原因就是,我们把会员卡号是 11000001 的会员信息修改了,而会员卡号是主键,会员消费查询通过会员卡号关联到会员信息,得到了完全错误的结果。

所以,我们千万不可把会员卡号这种业务字段当做主键。

身份证号可以当会员信息表的主键吗

那可不可以使用身份证号来做主键呢?身份证号绝不会重复,而且可以与一个人一一对应起来,看起来很适合做主键呢。

但实际上,这种选择是不合适的。一方面,身份证号属于个人隐私,客户不一定会把身份证号给你,所以我们在设计会员信息表时,会允许身份证号这个字段为空。另一方面,身份证号来做主键实在是太长了,过长的主键不仅会占用过多的空间,还会导致查询性能降低,我们会在后面展开来说。

小结

在建表的时候尽量不要用业务字段做主键。毕竟,作为项目设计的技术人员,我们谁都无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。

那么,应该使用什么来做主键呢?如果你有仔细观察我定义的商品信息表 sku_info 和订单信息表 order_info的话,你会发现我在这两张表中都定义了自增主键 pk_id,在单机系统中,推荐使用自增字段做主键。

单机系统推荐使用自增字段做主键

不仅是《阿里巴巴 Java 开发手册》中提到建表语句里一定要使用自增主键,在很多建表规范中都有提及。下面,我们就来看看给表加上自增字段是如何解决上一节中出现的问题的。

修改表结构

对于上一小节中出现的问题,我们只要在会员信息表 member_info中添加一个自增字段 pk_id 来做主键就可以解决了。

修改会员信息表 member_info

首先,我们要修改一下会员信息表的结构,添加自增字段 pk_id 做主键。

第一步,先删除会员信息表的主键约束(删除主键约束,并不会删除字段)。

ALTER TABLE member_infoDROP PRIMARY KEY;Query OK, 2 rows affected (0.13 sec)Records: 2Duplicates: 0Warnings: 0

第二步,重新命名会员卡号,取消 pk_ 前缀。

ALTER TABLE member_infoCHANGE pk_card_no card_no char(8) NOT NULL COMMENT ‘会员卡号‘;Query OK, 0 rows affected (0.04 sec)Records: 0Duplicates: 0Warnings: 0

第三步,添加自增字段 pk_id 为主键。

ALTER TABLE member_infoADD pk_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT;Query OK, 0 rows affected (0.11 sec)Records: 0Duplicates: 0Warnings: 0

将自增字段定义为无符号,不仅可以避免误存负数,还扩大了表示范围。

现在我们来查看一下会员信息表的表结构。

DESCRIBE member_info;+--------------+-----------------+------+-----+---------+----------------+| Field| Type| Null | Key | Default | Extra|+--------------+-----------------+------+-----+---------+----------------+| card_no| char(8) | NO| | NULL||| member_name| varchar(20) | YES| | NULL||| gender| char(1) | YES| | NULL||| pid| char(18)| YES| | NULL||| gmt_create| datetime| YES| | NULL||| gmt_modified | datetime| YES| | NULL||| pk_id| bigint unsigned | NO| PRI | NULL| auto_increment |+--------------+-----------------+------+-----+---------+----------------+7 rows in set (0.00 sec)修改订单信息表 order_info

接下来,我们要给订单信息表 order_info,添加一个新的字段 member_id,用于对应会员信息表 member_info 中的主键。

ALTER TABLE order_infoADD member_id BIGINT UNSIGNED;Query OK, 0 rows affected (0.15 sec)Records: 0Duplicates: 0Warnings: 0######################### 查看修改后的表结构DESCRIBE order_info;+---------------+-----------------+------+-----+---------+----------------+| Field | Type| Null | Key | Default | Extra|+---------------+-----------------+------+-----+---------+----------------+| pk_id | bigint unsigned | NO| PRI | NULL| auto_increment || card_no| char(8) | YES| | NULL||| sku_id| bigint unsigned | YES| | NULL||| sku_price | decimal(10,2)| YES| | NULL||| sale_quantity | int unsigned| YES| | NULL||| sale_amount| decimal(10,2)| YES| | NULL||| gmt_create| datetime| YES| | NULL||| gmt_modified| datetime| YES| | NULL||| member_id | bigint unsigned | YES| | NULL||+---------------+-----------------+------+-----+---------+----------------+9 rows in set (0.06 sec)

最后,我们还要再对订单信息表 order_info 进行修改,给新添加的 member_id 字段赋值,让它指向对应的会员信息。

# 给新添加的 member_id 字段赋值,让它指向对应的会员信息update order_info AS o, member_info AS mSET o.member_id = m.pk_idWHERE o.pk_id > 0AND o.card_no = m.card_no;Query OK, 1 row affected (0.05 sec)Rows matched: 1Changed: 1Warnings: 0######### 查看更新后的订单信息表 `order_info` 内的数据select * FROM order_info;+-------+----------+--------+-----------+---------------+-------------+---------------------+---------------------+-----------+| pk_id | card_no| sku_id | sku_price | sale_quantity | sale_amount | gmt_create| gmt_modified| member_id |+-------+----------+--------+-----------+---------------+-------------+---------------------+---------------------+-----------+| 1 | 11000001 |1 | 10.00 | 1 |10.00 | 2023-06-02 00:00:00 | 2023-06-02 00:00:00 | 1 |+-------+----------+--------+-----------+---------------+-------------+---------------------+---------------------+-----------+1 row in set (0.01 sec)

看到这里,可能有读者会问,可以先删除订单信息表 order_info,重建表,再插入数据吗?不建议这样做,虽然这样可以达到同样的目的。

考虑这样的情况,order_info 表中存储了大量重要的数据,此时是不能去删除表的。最好的方法就是用 WHERE 条件去更新指定的记录。

解决退卡危机恢复会员信息表 member_info 数据

为了复现这种情况,我们先恢复会员信息表中会员卡号 11000001 为柿子先生的个人信息。

# 恢复会员信息表中会员卡号 11000001 为柿子先生的个人信息update member_infoSET member_name = ‘柿子先生‘,gender = ‘男‘,pid = ‘123456199902107891‘,gmt_create = ‘2023-06-01‘,gmt_modified = ‘2023-06-01‘WHERE card_no = ‘11000001‘;Query OK, 1 row affected (0.04 sec)Rows matched: 1Changed: 1Warnings: 0######################### 查看恢复后的 member_info 表中的数据select * FROM member_info;+----------+--------------+--------+--------------------+---------------------+---------------------+-------+| card_no| member_name| gender | pid| gmt_create| gmt_modified| pk_id |+----------+--------------+--------+--------------------+---------------------+---------------------+-------+| 11000001 | 柿子先生 | 男 | 123456199902107891 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 | 1 || 11000002 | 可可小姐 | 女 | 123456199809077891 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 | 2 |+----------+--------------+--------+--------------------+---------------------+---------------------+-------+2 rows in set (0.00 sec)再一次退卡

这次,柿子先生退换会员卡 11000001,商家再把这张卡发给六一,我们只要在会员信息表 member_info 中添加一条记录就可以了。

# 在会员信息表 member_info 中添加一条记录insert INTO member_info(card_no, member_name, gender, pid, gmt_create, gmt_modified)VALUES(‘11000001‘, ‘六一‘, ‘男‘, ‘123456202203017891‘, ‘2023-06-01‘, ‘2023-06-01‘);Query OK, 1 row affected (0.03 sec)######################### 查看会员信息表中的信息select * FROM member_info;+----------+--------------+--------+--------------------+---------------------+---------------------+-------+| card_no| member_name| gender | pid| gmt_create| gmt_modified| pk_id |+----------+--------------+--------+--------------------+---------------------+---------------------+-------+| 11000001 | 柿子先生 | 男 | 123456199902107891 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 | 1 || 11000002 | 可可小姐 | 女 | 123456199809077891 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 | 2 || 11000001 | 六一 | 男 | 123456202203017891 | 2023-06-01 00:00:00 | 2023-06-01 00:00:00 | 3 |+----------+--------------+--------+--------------------+---------------------+---------------------+-------+3 rows in set (0.04 sec)

因为会员卡号 card_no 不再是主键了,可以允许重复,因此,我们就可以在保留会员「柿子先生」信息的同时,添加使用同一会员卡号的「六一」的信息。

查询会员销售记录正常

现在,我们再来查询会员的销售记录,就会发现一切正常啦。

select m.member_name, s.sku_name, o.sale_quantity, o.sale_amount, o.gmt_create AS trade_timeFROM order_info AS oJOIN member_info AS mJOIN sku_info AS sON (o.member_id = m.pk_id AND o.sku_id = s.pk_id);+--------------+----------+---------------+-------------+---------------------+| member_name| sku_name | sale_quantity | sale_amount | trade_time|+--------------+----------+---------------+-------------+---------------------+| 柿子先生 | 书本 | 1 |10.00 | 2023-06-02 00:00:00 |+--------------+----------+---------------+-------------+---------------------+1 row in set (0.01 sec)

可以看到,查询结果是 2023-06-02 柿子先生买了一本书,消费 10 元,是正确的。

新的挑战:更多的连锁店

我们的超市经过一段时间发展变得越来越好了,接下来我们要开几家连锁店。

发展新会员的过程一般是在门店进行的,人们总是习惯在购物结账时申请会员。因此,连锁店的信息系统需要添加新会员的功能,把新会员信息先存放到本地 MySQL 数据库中,再上传到总部进行汇总。

可是问题来了,我们的会员信息表的主键都是自增的,那么各个门店新加的会员就会出现「id」冲突的可能。

比如,A 店和 B 店的会员信息表最大的 pk_id 都是 100,各自新增了一个会员,pk_id 都变成了 101。然后 A 店和 B 店把新会员信息都上传到了总部,此时麻烦出现了,两个 pk_id 都是 101,但却是不同的会员。要如何处理这个问题呢?

分布式系统应使用全局唯一 ID

上一节的最后提出的问题,其实就是在分布式系统中如何保证数据记录具有唯一标识,这就不得不提到分布式 ID 了。分布式 ID 可以为不同数据节点的数据记录生成全局唯一标识。

本小节会先介绍一种基于数据库主键自增的分布式 ID 生成方案,更多的分布式 ID 解决方案的介绍,我将在另一篇文章详细展开。

总部数据库自增生成分布式 ID

我们可以取消会员信息表 member_info 的主键 pk_id 的自增属性,改成在添加新会员时对 pk_id 赋值。

然后,在总部数据库系统中,专门准备一张表 sequence_id_generator 用于生成全局唯一 ID。当门店需要添加会员的时候,要先到总部的这张表中,更新 pk_id 的值为最大值 + 1,并将新的值作为新会员的 pk_id 值。

如此一来,各个门店添加会员的时候,都从同一个总部的 sequence_id_generator 表中获取新会员的 pk_id 值,解决了各门店添加会员时会员编号冲突的问题,同时也避免了使用业务字段导致数据错误的问题。

创建生成分布式 ID 的表 sequence_id_generator# 创建表CREATE TABLE `sequence_id_generator`(`pk_id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,`cert` VARCHAR(255) NOT NULL UNIQUE KEY DEFAULT ‘‘) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;######################### 查看表结构DESCRIBE sequence_id_generator;+-------+-----------------+------+-----+---------+----------------+| Field | Type| Null | Key | Default | Extra|+-------+-----------------+------+-----+---------+----------------+| pk_id | bigint unsigned | NO| PRI | NULL| auto_increment || cert| varchar(255)| NO| UNI | ||+-------+-----------------+------+-----+---------+----------------+2 rows in set (0.01 sec)

pk_id 是自增主键,用于指明当前最大的会员编号。cert是业务凭证字段,创建了唯一索引,保证其唯一性,用于不同业务插入或修改数据。

获取新会员的 id 值

当我们想要获取新会员的 id 值时,需要执行显式事务(事务中的语句必须全部执行,一条失败则全部回滚)来获取,具体如下所示:

BEGIN;replace INTO sequence_id_generator (cert) VALUES (‘member_info‘);select LAST_INSERT_ID(); # 获取表中最大的自增值COMMIT;

使用 replace INTO 插入数据的流程如下:

第一步:尝试将数据插入到表中;

第二步:如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。

模拟获取新会员的 id 值

下面的动图是使用该解决方案模拟两家门店添加会员时,从总部获取新会员 id 值。

起初,sequence_id_generator 表中 pk_id 的最大值为 5,表明现在所有门店中最大的会员 id 值为 5。

现在 A 门店(左边)要获取新会员的 id 值,开始执行事务 A,与此同时 B 门店(右边)也要获取新会员的 id 值,开始执行事务 B。

由于事务 A 先执行了 replace INTO 的插入操作,事务 B 只能阻塞等待。

事务 A 执行 select LAST_INSERT_ID(); 成功获取到了新会员的 id 值为 6,然后执行 COMMIT; 提交事务。

然后事务 B 开始执行 replace INTO 操作与 select LAST_INSERT_ID(); 成功获取到了新会员的 id 值为 7,然后执行 COMMIT; 提交事务。

如此一来,A 门店和 B 门店新添加的会员 id 值都是唯一的,成功解决了会员 id 值冲突的问题。

方案的优缺点

优点

实现简单,会员的 id 值是有序递增的,占用的存储空间少。

缺点

一个非常明显的缺点,并发能力很差。一个事务执行时,另一个事务会被阻塞。

存在安全问题,试想一下如果这个 id 唯一标注的是订单呢?根据 id 的递增规律就可以推算出每天的订单量,会泄露商业机密!

每次获取 id 都要访问一次数据库,增加了对数据库的压力,获取速度也慢。

存在数据库单点宕机风险。所有门店添加新会员都要访问总部的数据库,当门店数量很多时,总部数据库很容易就宕机了。

后期可以采用水平扩展的数据库集群,并通过规定 id 的起始值和自增步长的方式来解决数据库单点压力问题。虽然这种解决方案解决了单点问题,但仍然存在缺陷,不利于后续扩容,直接使用数据库抗流量,无法满足高并发场景。

我们已经通过一个简单的解决方案初步了解了分布式 ID,下面开始正式介绍它。

总结

今天,和大家一起探讨了如何给数据表设置出正确的主键,介绍了给表定义主键的好处,使用主键要注意的事情。

重点强调了尽量不要使用业务字段做主键,因为无法预测未来会不会因为业务需要,而出现业务字段重复或者重用的情况。

在单机系统中,推荐使用自增字段做主键。但是,如果有多台服务器(分布式系统),各自录入数据,那就无法适用了。因为如果需要合并每台服务器录入的数据,可能出现主键重复的问题。

分布式 ID 可以很好地解决这个问题,我们介绍了一种简单的方案,在总部的数据库中专门有一个负责生成全局唯一 ID 的表,所有门店想要添加新会员的时候要从总部的这张表中获取 ID 值,这样就可以保证所有门店新添加的会员都有全局唯一的 ID 了。

•••展开全文