【问题标题】:Obtain an unique sequence order number concurrently from PostgreSQL从PostgreSQL并发获取唯一的序列号
【发布时间】:2015-10-13 10:32:53
【问题描述】:

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

以2015072201000010001为例,这里前八位为日期,这里为20150722,后七位为区域码,这里为0100001,后四位为序号在上述地区和日期下。

所以每次创建新订单,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的数据库读/写锁机制,在数百个并发订单生成请求的情况下,订单id发生冲突的可能性很大。

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

关于如何使这个订单生成操作同时可行有什么想法吗?

【问题讨论】:

    标签: php sql database postgresql concurrent-programming


    【解决方案1】:

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

    创建序列

    您可以在日期和地区之后命名您的序列。因此,请执行以下操作:

    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。

    【讨论】:

    • 对不起,我在 PostgreSQL 上是无辜的,但事务会做吗?我的意思是由于事务是原子的,我们可以将读写结合为一个事务。
    • 是的,这是一个选项,但您仍然需要为每个(日、地区)组合设置一个序列。否则,您将不得不查找每个订单的最高分配 ID(就像您在问题中显示的那样),这在计算上非常昂贵,并且会阻止所有其他等待新订单 ID 的交易。序列是原子的,因此不需要锁定其他事务。
    • 另见更新的 id 检索函数:值始终为今天,因此无需指定日期。
    【解决方案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
    

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

    【讨论】:

    • 这将在 20150724 上产生超出时间顺序的订单 ID,如果这与 OP 有任何关系的话。
    • 这似乎是一个简单轻量级的解决方案。
    【解决方案3】:

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

    【讨论】:

    • 这种考虑日期和区域的id设计的好处是,只需一个过滤条件即可轻松有效地对记录进行排序和过滤。所以保留原始设计对我们有好处。
    猜你喜欢
    • 1970-01-01
    • 2020-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-28
    相关资源
    最近更新 更多