编程

数据库 Auto Increment 的实际问题

776 2023-05-25 17:21:00

本文中,我将展示我以后将在 Postgres 和 MySQL 中避免使用 auto-increment 自增长字段的两个原因。我更愿意使用 UUID 字段除非有非常好的不使用的理由。

› MySQL < 8.0 auto-increment ID 重用

如果您运行的是旧版本的 MySQL,那么自动递增的 ID 可能会被重复使用。让我们看看这个动作。

$ docker volume create mysql-data
$ docker run --platform linux/amd64 -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 -v mysql-data:/var/lib/mysql mysql:5.7

上面为我们提供了一个运行 MySQL 5.7 的 Docker 容器,该容器连接到一个数据卷上,该卷将在该容器的运行期间持久保存数据。接下来,让我们获得一个可以使用的简单 schema :

$ docker run -it --rm --network host --platform linux/amd64 mysql:5.7 mysql -h 127.0.0.1 -P 3306 -u root -p
mysql> CREATE DATABASE my_database;
Query OK, 1 row affected (0.01 sec)

mysql> USE my_database;
Database changed
mysql> CREATE TABLE my_table (
    -> ID INT AUTO_INCREMENT PRIMARY KEY
    -> );
Query OK, 0 rows affected (0.02 sec)

接下来插入一些行:

mysql> INSERT INTO my_table () VALUES ();
Query OK, 1 row affected (0.03 sec)

mysql> INSERT INTO my_table () VALUES ();
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO my_table () VALUES ();
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM my_table;
+----+
| ID |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.01 sec)

到现在为止,一直都还不错。我们可以重新启动 MySQL 服务器,再次运行相同的 SELECT 语句,得到相同的结果。

让我们删除一行。

mysql> DELETE FROM my_table WHERE ID=3;
Query OK, 1 row affected (0.03 sec)

mysql> SELECT * FROM my_table;
+----+
| ID |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

让我们插入一个新行,以确保 ID 3 不会被重用。

mysql> INSERT INTO my_table () VALUES ();
Query OK, 1 row affected (0.02 sec)

mysql> SELECT * FROM my_table;
+----+
| ID |
+----+
|  1 |
|  2 |
|  4 |
+----+
3 rows in set (0.00 sec)

完美。让我们删除最新添加的行,重启服务并插入新行。

mysql> DELETE FROM my_table WHERE ID=4;
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM my_table;
ERROR 2013 (HY000): Lost connection to MySQL server during query

$ docker run -it --rm --network host --platform linux/amd64 mysql:5.7 mysql -h 127.0.0.1 -P 3306 -u root -p

mysql> USE my_database;
Database changed

mysql> SELECT * FROM my_table;
+----+
| ID |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

mysql> INSERT INTO my_table () VALUES ();
Query OK, 1 row affected (0.03 sec)

mysql> SELECT * FROM my_table;
+----+
| ID |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

嗯。MySQL 重新使用了 ID 3。这是因为 InnoDB 中 auto-increment 的工作方式是,在服务器重新启动时,它将通过有效地运行此查询来确定下一个要使用的 ID:

SELECT MAX(ID) FROM my_table;

如果您在重新启动之前从表中删除了最新的记录,那么当服务器恢复运行时,已使用的 ID 将被重新使用。

理论上,这不应该给你带来麻烦。最佳实践表明,除非是某个外键字段,否则不应该使用该表之外的数据库表中的 ID,当然也不会将该 ID 泄露到系统之外,对吧?

在实践中,这种情况会发生,并可能导致毁灭性的微妙错误。MySQL 8.0 通过将自动增量值存储在磁盘上的方式改变了这种行为,这种方式可以跨越重启将其持久化。

› Postgres 序列值不会被复制

像 MySQL 8.0 一样,Postgres 在磁盘上储存 auto-increment 值。它通过一个称为 “sequence” 的 schema 对象中实现。当你在 Postgres 中创建一个 auto-increment 自动递增字段时,在后台会创建一个序列来支持该字段,并持久地跟踪下一个值应该是什么。

让我们在实践中看看这一点。

$ docker volume create postgres-14-data
$ docker run --network host -e POSTGRES_PASSWORD=my-secret-pw -v postgres-14-data:/var/lib/postgresql/data -p postgres:14

随着 Postgres 的启动和运行,让我们继续创建我们的表:

$ docker run -it --rm --network host postgres:14 psql -h 127.0.0.1 -U postgres
postgres=# CREATE TABLE my_table (id SERIAL PRIMARY KEY);
CREATE TABLE

插入几行:

postgres=# INSERT INTO my_table DEFAULT VALUES;
INSERT 0 1
postgres=# INSERT INTO my_table DEFAULT VALUES;
INSERT 0 1
postgres=# INSERT INTO my_table DEFAULT VALUES;
INSERT 0 1
postgres=# SELECT * FROM my_table;
 id
----
  1
  2
  3
(3 rows)

目前一切都好。我们看一下该表:

postgres=# \d my_table
                            Table "public.my_table"
 Column |  Type   | Collation | Nullable |               Default
--------+---------+-----------+----------+--------------------------------------
 id     | integer |           | not null | nextval('my_table_id_seq'::regclass)
Indexes:
    "my_table_pkey" PRIMARY KEY, btree (id)

这个输出告诉我们 id 字段的默认值是 my_table_id_seq 的 nextval。让我们看看 my_table_id_seq:

postgres=# \d my_table_id_seq
                  Sequence "public.my_table_id_seq"
  Type   | Start | Minimum |  Maximum   | Increment | Cycles? | Cache
---------+-------+---------+------------+-----------+---------+-------
 integer |     1 |       1 | 2147483647 |         1 | no      |     1
Owned by: public.my_table.id

postgres=# SELECT currval('my_table_id_seq');
 currval
---------
       3
(1 row)

不错,我们在 Postgres 中有一个真正的对象,它跟踪自动递增的 ID 值。如果我们重复我们在 MySQL 中所做的操作,删除一些行并重新启动,我们就不会有同样的问题了。my_table_id_seq 被保存到磁盘,并且不会丢失它的位置。

真是这样吗?

如果你想将 Postgres 更新到一个新的主版本,通常的方法是在你想升级的版本上创建一个新 Postgres 实例,从旧实例逻辑复制到新实例,然后切换你的应用程序与新实例对话。

首先,我们需要用一些新的配置重新启动 Postgres 14,以允许逻辑复制:

$ docker run --network host -e POSTGRES_PASSWORD=my-secret-pw -v postgres-14-data:/var/lib/postgresql/data -p postgres:14 -c wal_level=logical

现在让我们启动 Postgres 15 并运行

$ docker volume create postgres-15-data
$ docker run --network host -e POSTGRES_PASSWORD=my-secret-pw -v postgres-15-data:/var/lib/postgresql/data postgres:15 postgres:14 -c wal_level=logical -p 5431

接下来,我们在 Postgres 14 实例中创建一个 “publication”:

postgres=# CREATE PUBLICATION my_publication FOR ALL TABLES;
CREATE PUBLICATION

然后,我们在 Postgres 15 实例上创建 “my_table” 表和一个订阅 “subscription”:

postgres=# CREATE TABLE my_table (id SERIAL PRIMARY KEY);
CREATE TABLE
postgres=# CREATE SUBSCRIPTION my_subscription CONNECTION 'host=127.0.0.1 port=5432 dbname=postgres user=postgres password=my-secret-pw' PUBLICATION my_publication;
NOTICE:  created replication slot "my_subscription" on publisher
CREATE SUBSCRIPTION

然后,我们可以看到数据在两个实例之间的同步:

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5432 -c "SELECT * FROM my_table"
 id
----
  1
  2
  3
(3 rows)

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c "SELECT * FROM my_table"
 id
----
  1
  2
  3
(3 rows)

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5432 -c "INSERT INTO my_table DEFAULT VALUES"
INSERT 0 1

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c "SELECT * FROM my_table"
 id
----
  1
  2
  3
  4
(4 rows)

那么问题是什么呢?

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5432 -c "SELECT nextval('my_table_id_seq')"
 nextval
---------
       5
(1 row)

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c "SELECT nextval('my_table_id_seq')"
 nextval
---------
       1
(1 row)

序列值没有被复制。如果我们在 Postgres 15 中插入一行,会得到:

$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c "INSERT INTO my_table DEFAULT VALUES"
ERROR:  duplicate key value violates unique constraint "my_table_pkey"
DETAIL:  Key (id)=(2) already exists.

注意:它试图在这里插入 id=2,因为当我们之前调用 nextval 时,它修改了序列。

如果严重依赖自动递增 ID 字段,这可能会使 Postgres 主版本更新变得非常棘手。你需要手动将序列值修改为你知道的值,因为在升级过程中无法获得该值,然后你可能需要根据你的工作负载在升级期间禁用写入。

› 结语

通过使用 UUID 字段而不是自动递增 interger,可以避免上述所有痛苦。它的好处是不可预测,并且如果您最终在表之外使用它们(不应该这样做),则不会泄露有关底层表基数的信息。

多亏了 Incident.io 上那些了不起的人的这篇文章,我现在意识到了德国坦克的问题。阅读该链接文章和维基百科相关页面很值得,因此有更多的理由不使用自动递增 ID 字段。