Files
dpb/backend/app/utils/number_gen.py
T

62 lines
2.7 KiB
Python
Raw Normal View History

"""统一编号服务 —— 原子取号,替代各处分散扫描式生成(§3.3 编号约定)。
取号基于 bre_sequencename/current_seq)表 + PostgreSQL 事务级 advisory lock
pg_advisory_xact_lock 在事务内串行化同名取号,锁随事务提交/回滚自动释放,
与 CRUD 同事务提交 ⇒ 并发请求不重复、失败自动回滚不留空号。
存量数据迁移:首次使用某序列名时由 seed_fn(服务端传入)从既有记录推算
起始号(原扫描逻辑),此后全部走原子递增,不再扫描。
"""
from typing import Any, Awaitable, Callable
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_bre.sequence.model import SequenceModel
class NumberGenService:
"""原子序号发生器。实例化于 HTTP/服务层,随当前事务取号。"""
def __init__(self, db: AsyncSession) -> None:
self.db = db
@staticmethod
async def _acquire_lock(db: AsyncSession, name: str) -> None:
from app.config.setting import settings
if settings.DATABASE_TYPE != "postgres":
return # sqlite 测试库无 advisory lock,退化为普通自增(单进程无并发竞争)
# hashtextextended 对文本做稳定 64 位哈希,同名序列恒映射同一把锁
await db.execute(text("SELECT pg_advisory_xact_lock(hashtextextended(:n, 0))"), {"n": name})
async def next_seq(
self,
name: str,
*,
start: int = 1,
seed_fn: Callable[[], Awaitable[int | None]] | None = None,
) -> int:
"""原子取号。返回本序列名下一个应使用的序号。
name: 序列名(如 "combination:2026" / "tree:3"),按编号维度天然分片;
start: 新序列的默认起始值;
seed_fn: 仅当序列行尚不存在时调用,从存量数据推算起始号(兼容既有数据迁移)。
"""
await self._acquire_lock(self.db, name)
row = await self.db.scalar(select(SequenceModel).where(SequenceModel.name == name))
if row is None:
init = await seed_fn() if seed_fn is not None else start
if init is None or init < 1:
init = start
self.db.add(SequenceModel(name=name, current_seq=init))
await self.db.flush()
return init
row.current_seq += 1
await self.db.flush()
return int(row.current_seq)
async def exists(self, name: str) -> bool:
"""序列行是否已初始化(供服务端判断是否需要 seed_fn 兜底)。"""
return (await self.db.scalar(select(SequenceModel.id).where(SequenceModel.name == name))) is not None