从PostgreSQL同时获取唯一的序列号

时间:2015-07-23 08:25:28

标签: php sql database postgresql concurrent-programming

我们正在设计订单管理系统,订单ID设计为Postgresql的bigint,地点结构实现如下:

  

以2015072201000010001作为订单ID示例,前八位被认为是20150722的日期,接下来的七个地方被认为是区域代码,这里是0100001,最后四个地方是序列号在上述地区和日期之下。

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

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

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

如果一次只有一个订单生成流程,这是可以的。但是由于数百个并发订单生成请求,自从PostgreSQL的数据库读/写锁机制以来,订单ID会有很多机会发生冲突。

让我们说有两个订单请求A和B. A尝试从数据库中读取最新的订单ID,然后B也读取最新的订单ID,然后A写入数据库,最后B写入由于订单ID主键发生冲突,因此db将失败。

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

3 个答案:

答案 0 :(得分:3)

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

创建序列

您可以在日期和区域后命名序列。所以做一些像:

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。

答案 1 :(得分:2)

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

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

这样的东西
create sequence seq_0100001;

然后你可以使用以下方式获得一个数字:

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

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

所以你最终会得到:

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

或者你可以为每个日/区域组合生成一个序列,但是你需要在第二天开始时删除前几天的序列。

答案 2 :(得分:-1)

尝试使用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

了解详情