前言
本來標題是想叫“生成不重複的四位數”的,不過單純數字有點侷限,推廣一下變成不重複 ID 吧~
這個功能是在做下面圖片裡這個小專案時遇到的,有點像微信的面對面建群,生成一個隨機且不重複的密碼,其他人輸入這個密碼就能加入教室。
實現這個功能有不少方法,本文簡單記錄一下。
不依賴第三方庫
首先單純基於 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 生態偷懶一下…