Files
fastapi-best-architecture/backend/utils/snowflake.py
T
Dylan 16bcaf963d Add distributed deployment support for snowflake ID (#927)
* feat: Add support distributed deployment for Snowflake

* Update the algorithm implementation

* Remove duplicate codes and update error messages
2025-11-19 12:48:24 +08:00

238 lines
9.9 KiB
Python

import asyncio
import datetime
import os
import threading
import time
from dataclasses import dataclass
from backend.common.dataclasses import SnowflakeInfo
from backend.common.exception import errors
from backend.common.log import log
from backend.core.conf import settings
from backend.database.redis import redis_client
from backend.utils.timezone import timezone
@dataclass(frozen=True)
class SnowflakeConfig:
"""雪花算法配置类,采用 Twitter 原版 Snowflake 64 位 ID 位分配配置(通用标准)"""
# 位分配
WORKER_ID_BITS: int = 5
DATACENTER_ID_BITS: int = 5
SEQUENCE_BITS: int = 12
# 最大值
MAX_WORKER_ID: int = (1 << WORKER_ID_BITS) - 1 # 31
MAX_DATACENTER_ID: int = (1 << DATACENTER_ID_BITS) - 1 # 31
SEQUENCE_MASK: int = (1 << SEQUENCE_BITS) - 1 # 4095
# 位移偏移
WORKER_ID_SHIFT: int = SEQUENCE_BITS
DATACENTER_ID_SHIFT: int = SEQUENCE_BITS + WORKER_ID_BITS
TIMESTAMP_LEFT_SHIFT: int = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS
# 元年时间戳
EPOCH: int = 1262275200000
# 时钟回拨容忍阈值,应对 NTP 自动同步引起的正常回跳(非标准)
CLOCK_BACKWARD_TOLERANCE_MS: int = 10_000
class SnowflakeNodeManager:
"""雪花算法节点管理器,负责从 Redis 分配和管理节点 ID"""
def __init__(self) -> None:
"""初始化节点管理器"""
self.datacenter_id: int | None = None
self.worker_id: int | None = None
self.node_redis_prefix: str = f'{settings.SNOWFLAKE_REDIS_PREFIX}:nodes'
self._heartbeat_task: asyncio.Task | None = None
async def acquire_node_id(self) -> tuple[int, int]:
"""从 Redis 获取可用的 datacenter_id 和 worker_id"""
occupied_nodes = set()
async for key in redis_client.scan_iter(match=f'{self.node_redis_prefix}:*'):
parts = key.split(':')
if len(parts) >= 5:
try:
datacenter_id = int(parts[-2])
worker_id = int(parts[-1])
occupied_nodes.add((datacenter_id, worker_id))
except ValueError:
continue
# 顺序查找第一个可用的 ID 组合
for datacenter_id in range(SnowflakeConfig.MAX_DATACENTER_ID + 1):
for worker_id in range(SnowflakeConfig.MAX_WORKER_ID + 1):
if (datacenter_id, worker_id) not in occupied_nodes and await self._register(datacenter_id, worker_id):
return datacenter_id, worker_id
raise errors.ServerError(msg='无可用的雪花算法节点,节点已耗尽')
async def _register(self, datacenter_id: int, worker_id: int) -> bool:
key = f'{self.node_redis_prefix}:{datacenter_id}:{worker_id}'
value = f'pid:{os.getpid()}-ts:{timezone.now().timestamp()}'
return await redis_client.set(key, value, nx=True, ex=settings.SNOWFLAKE_NODE_TTL_SECONDS)
async def start_heartbeat(self, datacenter_id: int, worker_id: int) -> None:
"""启动节点心跳"""
self.datacenter_id = datacenter_id
self.worker_id = worker_id
async def heartbeat() -> None:
key = f'{self.node_redis_prefix}:{datacenter_id}:{worker_id}'
while True:
await asyncio.sleep(settings.SNOWFLAKE_HEARTBEAT_INTERVAL_SECONDS)
try:
await redis_client.expire(key, settings.SNOWFLAKE_NODE_TTL_SECONDS)
log.debug(f'雪花算法节点心跳任务开始:datacenter_id={datacenter_id}, worker_id={worker_id}')
except Exception as e:
log.error(f'雪花算法节点心跳任务失败:{e}')
self._heartbeat_task = asyncio.create_task(heartbeat())
async def release(self) -> None:
"""释放节点"""
if self._heartbeat_task:
self._heartbeat_task.cancel()
try:
await self._heartbeat_task
except asyncio.CancelledError:
log.debug(f'雪花算法节点心跳任务释放:datacenter_id={self.datacenter_id}, worker_id={self.worker_id}')
if self.datacenter_id is not None and self.worker_id is not None:
key = f'{self.node_redis_prefix}:{self.datacenter_id}:{self.worker_id}'
await redis_client.delete(key)
class Snowflake:
"""雪花算法类"""
def __init__(self) -> None:
"""初始化雪花算法"""
self.datacenter_id: int | None = None
self.worker_id: int | None = None
self.sequence: int = 0
self.last_timestamp: int = -1
self._lock = threading.Lock()
self._initialized = False
self._node_manager: SnowflakeNodeManager | None = None
self._auto_allocated = False # 标记是否由 Redis 自动分配 ID
async def init(self) -> None:
"""初始化雪花算法"""
if self._initialized:
return
with self._lock:
# 环境变量固定分配
if settings.SNOWFLAKE_DATACENTER_ID is not None and settings.SNOWFLAKE_WORKER_ID is not None:
self.datacenter_id = settings.SNOWFLAKE_DATACENTER_ID
self.worker_id = settings.SNOWFLAKE_WORKER_ID
log.debug(
f'雪花算法使用环境变量固定节点:datacenter_id={self.datacenter_id}, worker_id={self.worker_id}'
)
elif (settings.SNOWFLAKE_DATACENTER_ID is not None and settings.SNOWFLAKE_WORKER_ID is None) or (
settings.SNOWFLAKE_DATACENTER_ID is None and settings.SNOWFLAKE_WORKER_ID is not None
):
log.error('雪花算法 datacenter_id 和 worker_id 配置错误,只允许同时非 None 或同时为 None')
raise errors.ServerError(msg='雪花算法配置失败,请联系系统管理员')
else:
# Redis 动态分配
self._node_manager = SnowflakeNodeManager()
self.datacenter_id, self.worker_id = await self._node_manager.acquire_node_id()
self._auto_allocated = True
await self._node_manager.start_heartbeat(self.datacenter_id, self.worker_id)
log.debug(
f'雪花算法使用 Redis 动态分配节点:datacenter_id={self.datacenter_id}, worker_id={self.worker_id}'
)
# 严格校验范围
if not (0 <= self.datacenter_id <= SnowflakeConfig.MAX_DATACENTER_ID):
log.error(f'雪花算法 datacenter_id 配置失败,必须在 0~{SnowflakeConfig.MAX_DATACENTER_ID} 之间')
raise errors.ServerError(msg='雪花算法数据中心配置失败,请联系系统管理员')
if not (0 <= self.worker_id <= SnowflakeConfig.MAX_WORKER_ID):
log.error(f'雪花算法 worker_id 配置失败,必须在 0~{SnowflakeConfig.MAX_WORKER_ID} 之间')
raise errors.ServerError(msg='雪花算法工作机器配置失败,请联系系统管理员')
self._initialized = True
async def shutdown(self) -> None:
"""释放 Redis 节点"""
if self._node_manager and self._auto_allocated:
await self._node_manager.release()
@staticmethod
def _current_ms() -> int:
return int(timezone.now().timestamp() * 1000)
def _till_next_ms(self, last_timestamp: int) -> int:
"""等待直到下一毫秒"""
ts = self._current_ms()
while ts <= last_timestamp:
time.sleep(0.0001)
ts = self._current_ms()
return ts
def generate(self) -> int:
"""生成雪花 ID"""
if not self._initialized:
raise errors.ServerError(msg='雪花 ID 生成失败,雪花算法未初始化')
with self._lock:
timestamp = self._current_ms()
# 时钟回拨处理
if timestamp < self.last_timestamp:
back_ms = self.last_timestamp - timestamp
if back_ms <= SnowflakeConfig.CLOCK_BACKWARD_TOLERANCE_MS:
log.warning(f'检测到时钟回拨 {back_ms} ms,等待恢复...')
timestamp = self._till_next_ms(self.last_timestamp)
else:
raise errors.ServerError(msg=f'雪花 ID 生成失败,时钟回拨超过 {back_ms} ms,请立即联系系统管理员')
# 同毫秒内序列号递增
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & SnowflakeConfig.SEQUENCE_MASK
if self.sequence == 0:
timestamp = self._till_next_ms(self.last_timestamp)
else:
self.sequence = 0
self.last_timestamp = timestamp
# 组合 64 位 ID
return (
((timestamp - SnowflakeConfig.EPOCH) << SnowflakeConfig.TIMESTAMP_LEFT_SHIFT)
| (self.datacenter_id << SnowflakeConfig.DATACENTER_ID_SHIFT)
| (self.worker_id << SnowflakeConfig.WORKER_ID_SHIFT)
| self.sequence
)
@staticmethod
def parse(snowflake_id: int) -> SnowflakeInfo:
"""
解析雪花 ID,获取其包含的详细信息
:param snowflake_id: 雪花ID
:return:
"""
timestamp = (snowflake_id >> SnowflakeConfig.TIMESTAMP_LEFT_SHIFT) + SnowflakeConfig.EPOCH
datacenter_id = (snowflake_id >> SnowflakeConfig.DATACENTER_ID_SHIFT) & SnowflakeConfig.MAX_DATACENTER_ID
worker_id = (snowflake_id >> SnowflakeConfig.WORKER_ID_SHIFT) & SnowflakeConfig.MAX_WORKER_ID
sequence = snowflake_id & SnowflakeConfig.SEQUENCE_MASK
return SnowflakeInfo(
timestamp=timestamp,
datetime=timezone.to_str(datetime.datetime.fromtimestamp(timestamp / 1000, timezone.tz_info)),
datacenter_id=datacenter_id,
worker_id=worker_id,
sequence=sequence,
)
snowflake = Snowflake()