You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

并发场景下的数据存储:避免重复ID与单据编号问题

解决发票存储项目并发ID/单据号重复的最优方案

嘿,这个并发场景下的竞态条件问题太常见了!我给你整理几个最优解决方案,从最省心的数据库原生方案到适配复杂场景的处理方式,你可以根据自己的技术栈和业务需求挑:

1. 数据库原生自增主键/序列(最推荐,省心又可靠)

别自己手动计算last value + 1了,把这个活儿交给数据库!几乎所有主流数据库都支持原子性的自增ID生成,从根源上避免并发冲突。

针对ID字段

直接用数据库的自增主键特性:

  • MySQL 用 AUTO_INCREMENT
  • PostgreSQL 用 SERIAL 或 SEQUENCE
  • SQL Server 用 IDENTITY

示例(MySQL):

CREATE TABLE INVOICE (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    -- 其他字段:比如客户ID、开票日期等
    ...
);

CREATE TABLE INVOICE_PRODUCTS (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    INVOICE_ID INT NOT NULL,
    -- 其他商品字段:名称、单价、数量等
    ...
    FOREIGN KEY (INVOICE_ID) REFERENCES INVOICE(ID)
);

针对DOCUMENT_NUMBER字段

如果单据号是单纯的自增数字,直接给它加UNIQUE约束的自增字段就行;如果需要带前缀(比如INV-2024-0001),可以结合序列+触发器实现:
示例(PostgreSQL):

-- 创建一个专门生成单据号的序列
CREATE SEQUENCE invoice_doc_seq START 1;

CREATE TABLE INVOICE (
    ID SERIAL PRIMARY KEY,
    DOCUMENT_NUMBER VARCHAR(20) UNIQUE NOT NULL,
    -- 其他字段
    ...
);

-- 触发器函数:插入前自动生成带前缀的单据号
CREATE OR REPLACE FUNCTION generate_invoice_doc_number()
RETURNS TRIGGER AS $$
BEGIN
    NEW.DOCUMENT_NUMBER = 'INV-' || TO_CHAR(CURRENT_DATE, 'YYYY') || '-' || LPAD(nextval('invoice_doc_seq')::TEXT, 4, '0');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 绑定触发器到INVOICE表
CREATE TRIGGER trigger_gen_invoice_doc
BEFORE INSERT ON INVOICE
FOR EACH ROW EXECUTE FUNCTION generate_invoice_doc_number();

数据库的自增/序列是原子性操作,同一时间只会给一个请求分配唯一值,完全不用担心重复问题,而且性能开销极低,是首选方案。

2. 乐观锁+重试机制(适合复杂单据号规则)

如果你的单据号有特殊规则(比如和客户ID关联、包含特定编码),没法直接用数据库自增,可以试试乐观锁思路:

  1. 查询当前最大单据号时,同时获取一个“版本标识”(比如表的最后更新时间、或者专门的version字段)
  2. 计算出新的单据号后,执行插入/更新操作时,带上这个版本标识做校验
  3. 如果操作失败(返回影响行数为0),说明有其他用户抢先修改了,重试几次

伪代码示例(Python + SQL):

import psycopg2

def create_invoice():
    conn = psycopg2.connect("dbname=your_db user=your_user")
    cursor = conn.cursor()
    retry_count = 3
    
    while retry_count > 0:
        # 1. 查询当前最大单据号和版本标识(这里用最新发票的ID当版本)
        cursor.execute("SELECT MAX(DOCUMENT_NUMBER), MAX(ID) FROM INVOICE")
        max_doc_num, latest_id = cursor.fetchone()
        
        # 2. 计算新单据号(假设规则是数字+1)
        new_doc_num = int(max_doc_num) + 1 if max_doc_num else 1
        
        # 3. 插入时校验版本:只有最新ID还是刚才查询的那个,才允许插入
        cursor.execute("""
            INSERT INTO INVOICE (DOCUMENT_NUMBER, ...)
            VALUES (%s, ...)
            WHERE (SELECT MAX(ID) FROM INVOICE) = %s
        """, (new_doc_num, latest_id))
        
        if cursor.rowcount > 0:
            conn.commit()
            print("发票创建成功!")
            break
        else:
            retry_count -= 1
            print(f"并发冲突,重试第{3 - retry_count}次...")
    
    if retry_count == 0:
        print("多次重试失败,请稍后再试")
    conn.close()

这个方案不需要加锁,对并发性能影响小,但需要业务代码处理重试逻辑,适合单据号规则复杂的场景。

3. 分布式ID生成器(分布式系统场景)

如果你的系统是多数据库节点的分布式架构,数据库自增就没法跨节点保证唯一性了,这时候可以用:

  • Redis的INCR命令:Redis的自增操作是原子性的,专门用来生成全局唯一的序列值
  • Snowflake算法:生成包含时间戳、机器ID、序列号的64位唯一ID,适合高并发分布式场景

示例(Redis生成单据号):

import redis

r = redis.Redis(host='your_redis_host', port=6379, db=0)

# 原子性生成自增单据号
new_doc_num = r.incr('invoice_doc_counter')
# 如果需要前缀,拼接一下
formatted_doc_num = f"INV-{new_doc_num:06d}"

# 然后把这个号插入数据库

这个方案适合分布式系统,但需要引入额外组件(Redis或者自定义Snowflake服务),增加了系统复杂度。

4. 事务+排他锁(并发量低的场景)

如果你的系统并发量不高,可以用数据库事务加排他锁的方式,强制同一时间只有一个用户能获取并更新最大单据号:

BEGIN TRANSACTION;

-- 加排他锁,阻塞其他查询这个表的请求
SELECT MAX(DOCUMENT_NUMBER) FROM INVOICE FOR UPDATE;

-- 计算新单据号
SET @new_doc_num = (SELECT MAX(DOCUMENT_NUMBER) FROM INVOICE) + 1;

-- 插入新发票
INSERT INTO INVOICE (DOCUMENT_NUMBER, ...) VALUES (@new_doc_num, ...);

COMMIT;

这个方案简单直接,但会阻塞其他并发请求,降低系统吞吐量,只适合并发量低的小型系统。


总结

优先选数据库原生自增/序列方案,它最可靠、性能最好、代码复杂度最低;如果单据号规则复杂,再考虑乐观锁+重试;分布式场景用Redis/Snowflake;并发量极低的情况可以用事务+排他锁。

内容的提问来源于stack exchange,提问作者Ricardo Sousa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:53:06