从 PostgreSQL 中并发获取一个唯一的序列序号

Obtain an unique sequence order number concurrently from PostgreSQL

我们在设计一个订单管理系统,订单id用Postgre设计成bigintsql,place结构实现如下:

Take 2015072201000010001 as an order id example, the first eight places are considered as the date which is 20150722 here, the next seven places are considered as the region code which is 0100001 here, and the last four places are for the sequence number under the aforementioned region and date.

因此每次创建新订单时,php 逻辑应用层将使用以下 like sql 语句查询 PostgreSQL:

select id from orders where id between 2015072201000010000 and 2015072201000019999 order by id desc limit 1 offset 0

然后增加新订单的id,然后将订单插入PostgreSQL数据库。

如果一次只有一个订单生成过程,这是可以的。但由于 PostgreSQL 的数据库 read/write 锁机制,有数百个并发订单生成请求,订单 ID 发生冲突的机会非常多。

假设有两个订单请求A和B。A尝试从数据库中读取最新的订单id,然后B也读取最新的订单id,然后A写入数据库,最后B写入由于订单 ID 主键冲突,数据库将失败。

关于如何使此订单生成操作同时可行有任何想法吗?

尝试使用 UUIDv1 类型,它是时间戳和 MAC 地址的组合。如果插入顺序对您很重要,您可以在服务器端自动生成它。否则,可以在插入之前从您的任何客户端生成 ID(您可能需要同步他们的时钟)。请注意,使用 UUIDv1 时,您可以公开生成 UUID 的主机的 MAC 地址。在这种情况下,您可能想要欺骗 MAC 地址。

对于你的情况,你可以这样做

CREATE TABLE orders (
    id uuid PRIMARY KEY DEFAULT uuid_generate_v1(),
    created_at timestamp NOT NULL DEFAULT now(),
    region_code text NOT NULL REFERENCES...
    ...
);

http://www.postgresql.org/docs/9.4/static/uuid-ossp.html

阅读更多内容

在 Postgres 中避免锁定 id 的常用方法是通过序列。

您可以为每个区域使用 Postgresql 序列。像

create sequence seq_0100001;

然后你可以从中得到一个数字:

select nextval('seq_'||regioncode) % 10000 as order_seq

这确实意味着订单号不会每天重置为 0001,但您的订单号确实具有相同的 0000 -> 9999 范围。它会环绕。

所以你最终可能会得到:

2015072201000010001 -> 2015072201000017500 
2015072301000017501 -> 2015072301000019983
2015072401000019984 -> 2015072401000010293

或者,您可以只为每个 day/region 组合生成一个序列,但您需要在第二天开始时删除前几天的序列。

在许多并发操作的情况下,您唯一的选择是使用序列。在这种情况下,您需要为每个日期和地区创建一个序列。这听起来工作量很大,但其中大部分都可以自动化。

创建序列

您可以根据日期和地区命名您的序列。所以做这样的事情:

CREATE SEQUENCE seq_201507220100001;

您应该为每个日期和区域的组合创建一个序列。在函数中执行此操作以避免重复。 运行 这个功能每天一次。您可以提前执行此操作,或者 - 甚至更好 - 在每天的预定工作中执行此操作以创建明天的序列。假设您不需要将订单回溯到前几天,您可以在同一函数中删除昨天的序列。

CREATE FUNCTION make_and_drop_sequences() RETURNS void AS $$
DECLARE
  region    text;
  tomorrow  text;
  yesterday text;
BEGIN
  tomorrow  := to_char((CURRENT_DATE + 1)::date, 'YYYYMMDD');
  yesterday := to_char((CURRENT_DATE - 1)::date, 'YYYYMMDD');
  FOREACH region IN 
    SELECT DISTINCT region FROM table_with_regions
  LOOP
    EXECUTE format('CREATE SEQUENCE %I', 'seq_' || tomorrow || region);
    EXECUTE format('DROP SEQUENCE %I', 'seq_' || yesterday|| region);
  END LOOP;
  RETURN;
END;
$$ LANGUAGE plpgsql;

使用序列

在您的 PHP 代码中,您显然知道需要为其输入新订单 ID 的日期和区域。创建另一个函数,根据日期和地区从正确的序列生成新值:

CREATE FUNCTION new_date_region_id (region text) RETURN bigint AS $$
DECLARE
  dt_reg  text;
  new_id  bigint;
BEGIN
  dt_reg := tochar(CURRENT_DATE, 'YYYYMMDD') || region;
  SELECT dt_reg::bigint * 10000 + nextval(quote_literal(dt_reg)) INTO new_id;
  RETURN new_id;
END;
$$ LANGUAGE plpgsql STRICT;

在 PHP 你然后调用:

SELECT new_date_region_id('0100001');

这将为指定区域提供今天的下一个可用 ID。