V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
Phishion
V2EX  ›  Python

请问 Django 并发条件下,生成雪花 ID 为什么会重复?

  •  
  •   Phishion · 58 天前 · 2348 次点击
    这是一个创建于 58 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我用的是别人写好的模块,如下

    https://github.com/tarzanjw/pysnowflake/blob/master/snowflake/server/generator.py

    我首先自己跑单线程测了一下,完全不会有重复,多线程在加线程锁的情况下也完全没有发生重复。

    但是我在实际项目中生成的时候,大概 10 条线提交总共 1600 条数据,每次都会产生大概几十条重复

    我打印过 ID,这个雪花生成器实例并没有被初始化多个,请问如何排查?

    大致代码如下:

    from newsnow import Generator
    logger = logging.getLogger('django-production')
    get_flake_id = Generator(dc=0, worker=0)
    
    def create_product_meta(prepare_product_meta):
        new_product = models.productMeta(
            own_store=prepare_product_meta.get("own_store").upper(),
            product_name=prepare_product_meta.get("product_name"),
        )
        logger.warning(id(get_flake_id))
        new_product.flake_id = get_flake_id.get_next_id()
        return new_product
    
    第 1 条附言  ·  58 天前

    目前问题已经修复,确定是多进程间 “无法共享实例状态” 导致的 SnowFlake ID 重复问题。

    我个人的解决方案是在生成 SnowFlake ID 前,直接动态获取 PID 的后 2 位作为 Worker ID,即如下写法

    flake_worker = int(str(os.getpid())[-2:])

    弄那么“不专业”的理由是,因为我观察到在 Docker 环境中,这个 ID 一般都是“连续生成”,大多是 100~200 范围内,而且我个人也就开了 4 个进程,再者我的程序本身也有重试逻辑,所以就这样简单的修改了。

    另外,本帖中尚未获得其他同学关于映射 PID 的解决方案,我也只是提供一个思路,因为标准的雪花 ID 的 worker 位严格较真的话只有 0~255 可选,直接放 PID 是绝对可能溢出的,所以必然存在一种转换方案,或者想办法找其他类似 PID 的东西作为物理隔离方案。

    希望帮助到同样受到这个问题困扰的人。

    26 条回复    2021-08-01 20:29:07 +08:00
    todd7zhang
        1
    todd7zhang   58 天前   ❤️ 1
    如果生产是多进程跑的话,应该是初始化 Generator(dc=0, worker=0)这个时候,所有进程的 10bit 的 node_id 都是一样的,然后就重复了。可以考虑 worker=os.getpid()
    find456789
        2
    find456789   58 天前
    有考虑过 hashid 吗, 这个支持很多语言,Python 、django 也有对应的 包
    LeeReamond
        3
    LeeReamond   58 天前
    一个个人猜测是,因为 sleep 间隔固定的原因,线程数量一多了之后系统挨个唤醒,可能搞不好获取的时间戳也一样。
    cszchen
        4
    cszchen   58 天前 via iPhone
    你有没有用到多进程
    cszchen
        5
    cszchen   58 天前 via iPhone
    1 楼的方法可以解决
    Phishion
        6
    Phishion   58 天前
    @todd7zhang
    @cszchen
    没有多进程,我就一台机器单跑了一个 Django 服务没有啥特别的设置
    Phishion
        7
    Phishion   58 天前
    @cszchen
    @todd7zhang

    我使用了 uwsgi,请问下面的 processes 算多进程么?

    [uwsgi]
    chdir=/www/project/
    module=project.wsgi
    master=True
    processes=4
    chenqh
        8
    chenqh   58 天前
    这种东西 py 最简单应该是用 redis 写一个把

    ```

    def util_redis_get_next_id_str(redis_client, name="id_sequence", mod_base=1000):
    """
    1 秒并发最多 mod_base 这么多,也就是 10W, 所以我并不怕
    """
    value = redis_client.incr(name)
    value = value % mod_base
    utcnow = datetime.datetime.utcnow()
    now = util_time_utc_to_local(utcnow)
    now = now.strftime("%Y%m%d%H%M%S")
    second = int(time.time() * 1000) % 1000
    return '{}{:03d}{:03d}'.format(now, second, value)
    ```
    chenqh
        9
    chenqh   58 天前
    @Phishion 算的应该
    Phishion
        10
    Phishion   58 天前
    @cszchen
    @todd7zhang
    好像确实是这个问题,我把 processes 配置减少到 1,就没有发现重复了
    LemonK
        11
    LemonK   58 天前
    snowflake 多进程需要不同的 worker 值,我是另外写了个方法分配的。如果是 docker 部署的话 pid 也有可能重复。
    Phishion
        12
    Phishion   58 天前
    @LemonK 请问您有什么方法解决这个问题?
    Phishion
        13
    Phishion   58 天前
    @find456789 我主要就想要一连串数字,实际业务上也需要显示出来
    cszchen
        14
    cszchen   58 天前 via iPhone
    如果多台机器,可以用 redis 的原子特性来分配 workerid
    Phishion
        15
    Phishion   58 天前
    @chenqh

    请问 pid 在整个程序运行周期会不断变化么?
    另外 pid 范围是 几十到上万不等,这个数字是远超出 work id 范围的,是要写一个映射么?
    Phishion
        16
    Phishion   58 天前
    @cszchen 目前没有多台,我不想跑 redis 是因为目前没有其他地方用到这个,不想就为了生成 ID 单跑一个数据库
    chenqh
        17
    chenqh   58 天前
    @Phishion 你没有用 celery 或者 rq 吗?
    est
        18
    est   58 天前
    @Phishion 看源码

    self.node_id = ((dc & 0x03)<< 8) | (worker & 0xff)
    Phishion
        19
    Phishion   58 天前
    @chenqh 有的,这个能获取到 pid 么
    Phishion
        20
    Phishion   58 天前
    @est 这个不是一共支持 10 位,共 1024 个节点么,直接填 pid 号肯定有几率溢出啊,关键这一块儿我觉得也不是填 PID 的地方,只是 PID 确实能解决这个问题,最理想情况下,这个 worker 应该就是从 0 开始,多一个进程就加 1
    todd7zhang
        21
    todd7zhang   58 天前
    差不多啦,uwsgi 启动的子进程的 pid 基本都是连续递增的,这边还有 worker & 0xff, 一台机器支持 256 个 worker 呢。
    实际启动的时候就那么几个进程,没那么巧就重复了吧,哈哈。
    caviar
        22
    caviar   58 天前
    既然是用 uwsgi,直接拿 uwsgi 的 worker id 咯 https://uwsgi-docs.readthedocs.io/en/latest/API.html#uwsgi-worker-id
    Phishion
        23
    Phishion   58 天前
    @caviar 实际上 os.getpid() 打印的就是 uwsgi 的 PID,通过观察我发现这个 ID 一般是连续生成,所以我直接取后 2 位数字,作为 worker ID
    caviar
        24
    caviar   56 天前
    @Phishion os.getpid() 拿的是系统的 pid,个人并不觉得有连续的保证。uwsgi 提供的 worker id 是从 1 开始连续递增的。就像你前面说的,使用 pid 在大部分情况下不会有问题,但是既然有提供更好的 worker id,为什么不用呢。
    Phishion
        25
    Phishion   56 天前
    @caviar 连续不连续实际上无所谓,后面 2 位不重复我觉得就可以了,总不能跳 100 个 PID 再生成第二个,uwsgi 的 worker ID 我还不知道怎么拿,配合重试逻辑,应该已经解决了,所以就没进一步修这个问题。
    ysw
        26
    ysw   56 天前
    可用 redis 的分布式锁
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1001 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 21:59 · PVG 05:59 · LAX 14:59 · JFK 17:59
    ♥ Do have faith in what you're doing.