如何優雅地在Django專案裡生成不重複的ID?

程序设计实验室發表於2024-12-08

前言

本來標題是想叫“生成不重複的四位數”的,不過單純數字有點侷限,推廣一下變成不重複 ID 吧~

這個功能是在做下面圖片裡這個小專案時遇到的,有點像微信的面對面建群,生成一個隨機且不重複的密碼,其他人輸入這個密碼就能加入教室。

LearnMates

實現這個功能有不少方法,本文簡單記錄一下。

不依賴第三方庫

首先單純基於 Django ORM 來實現這個功能

先定義一個模型

from django.db import models

class MyModel(models.Model):
    unique_code = models.CharField(max_length=4, unique=True)

單任務

最簡單粗暴的方法,寫一個死迴圈

import random

def generate_unique_code():
    while True:
        code = str(random.randint(1000, 9999))
        if not MyModel.objects.filter(unique_code=code).exists():
            return code

這個實現在單執行緒測試環境下肯定是沒問題的,不過這個操作並不是原子化的,併發環境下可能會生成重複的數字。

考慮併發

高併發情況下,可以使用資料庫事務或樂觀鎖。

from django.db import transaction, IntegrityError

def create_instance():
	  # 嘗試次數
  	retry = 10
    for _ in range(retry):
        code = generate_unique_code()
        try:
            with transaction.atomic():
                instance = MyModel(unique_code=code)
                instance.save()
            return instance
        except IntegrityError:
            # 如果出現唯一性衝突,重新嘗試
            continue
    raise Exception("無法生成唯一的四位數")

預先生成

前面兩種方法都要頻繁讀取資料庫,效能比較差。

還可以用空間換時間的方式,因為只是四位數,0000-9999 這個範圍的數字也不多,預先把這一萬行存入資料庫,加個 available 欄位

當需要生成唯一 ID 的時候,就先篩選 available == True 的資料,然後隨機抽取一個;並且把這個欄位設定為 False

大概思路就是這樣

使用第三方庫

在這個專案裡,我搭配使用了這三個庫(這也是我寫這篇文章的主要目的,記錄一下這幾個庫)

  • shortuuid
  • hashids
  • django-autoslug

shortuuid

shortuuid 是一個輕量級的庫,可以生成比較短的 UUID

使用這個庫來實現這個功能的話很簡單

import shortuuid
shortuuid.ShortUUID(alphabet="0123456789").random(length=4)

不過這個專案中,我並沒有使用這個庫來做這個

事實上,這個庫顧名思義有個 uuid,自然是用來做與 python 內建 UUID 有關工作

我用這個庫把 Client 模型的 ID 簡化到 7 位

class Client(ModelExt):
    client_id = models.UUIDField(default=uuid.uuid4, editable=False)
    client_key = models.CharField(max_length=100, default=uuid.uuid4)
    user = models.OneToOneField(
        User, on_delete=models.SET_NULL, db_constraint=False, null=True, blank=True, unique=True,
    )
    consumer_name = models.CharField(max_length=100, null=True, blank=True)
    is_online = models.BooleanField(default=False)

    def short_client_id(self):
        short = shortuuid.encode(self.client_id)
        return short[:7]

使用 shortuuid.encode 方法可以把 32 位的 UUID 變成 22 位,並且還能使用 decode 方法復原

hashids

這個是將數字轉為短字串的庫

雖然名字裡帶個 hash ,但這個庫的編碼是可逆的

import hashids
h = hashids.Hashids()
h.encode(123, 456)
# Out[15]: 'X68fkp'
h.decode('X68fkp')
# Out[16]: (123, 456)

我用來根據時間戳生成教室名稱

def get_timestamp_hashid():
    hashids = Hashids(salt='hahaha salt lala')
    t = timezone.now().timestamp()
    result = tuple(map(int, str(t).split('.')))
    return hashids.encode(*result)

因為 encode 方法接收的是數字(也可以是包含數字的 tuple),所以這裡把時間戳的整數部分和小數部分轉換為 tuple 然後傳入 hashids

django-autoslug

這個庫用於生成基於欄位的唯一 slug,同時可以自定義生成邏輯。

我就是用這個庫來實現生成唯一的教室密碼功能(但並不是很推薦這種方式)

from autoslug import AutoSlugField


def populate_classroom_number(instance):
    return str(random.randint(1000, 9999))
  
class ClassroomIdiom(ModelExt):
    name = models.CharField(max_length=100)
    number = AutoSlugField(populate_from=populate_classroom_number, unique=True)

這個庫的原理很簡單,根據使用者定義的規則生成 slug,然後檢查資料庫是否重複,遇到重複的話就在後面追加數字,這樣有可能導致生成出來的數字超過 4 位數

最好的還是我前面說的 不依賴第三方庫 的第三種方式。

這裡使用 django-autoslug 單純是為了偷懶,把複雜的判斷邏輯交給第三方庫,畢竟這只是個玩具專案。

小結

在生成唯一 ID 這件事上,Django 和其他後端框架沒啥不同的,思路都是類似的,只不過可以藉助 Python 生態偷懶一下…

相關文章