İleri5 dakikalık okuma

DynamoDB'de birden fazla nitelikte benzersizliği uygulama

DynamoDB tam olarak tek bir şey için benzersizlik garanti eder: birincil anahtar. UNIQUE (email) kısıtı yok, UNIQUE (username) yok ve iki niteliği kapsayan hiçbir şey yok. SQL'den geliyorsanız, bu yokluk ilk sürprizdir — ve insanların sessizce bir yarış durumu yayınladığı ilk yerdir.

DynamoDB'de birden fazla nitelik üzerinde benzersizlik kısıtı nasıl uygulanır?

DynamoDB'nin dışında UNIQUE kısıtı yoktur; bu yüzden benzersizliği kendiniz uygularsınız: korunan her değeri, anahtarı o değer olan kendi işaretçi (marker) öğesi olarak modelleyin, ardından kaydı ve tüm işaretçileri tek bir TransactWriteItems içinde, her put'u attribute_not_exists ile koruyarak yazın. Motorun zaten uyguladığı çakışma sizin kısıtınız olur.

  • Benzersizlik kısıtı yoktur — yalnızca birincil anahtar motor tarafından benzersiz uygulanır. Diğer her "benzersiz olmalı" nitelik sizin işinizdir.
  • Her benzersizlik kuralını kendi öğesi olarak modelleyin. Anahtarı korumakta olduğunuz değer olan özel bir işaretçi (marker) öğesi, "bu e-posta alınmış mı?" sorusunu motorun zaten uyguladığı bir anahtar çakışmasına çevirir.
  • Onları TransactWriteItems ile atomik yazın. Tek bir işlem, her put attribute_not_exists ile korunmuş, böylece tüm işaretçiler ve gerçek kayıt ya birlikte işlenir ya da hiçbiri.
  • Kontrol-et-sonra-yaz yapmayın. Ekleme-öncesi okuma, ders kitabı bir yarıştır; iki eşzamanlı kayıt, ikisi de "boş" okur ve ikisi de yazar.

Bariz yaklaşım neden yanlış

İçgüdü, e-posta için Query (ya da daha kötüsü Scan) yapmak, hiçbir şey görmemek, sonra yeni hesabı PutItem etmektir. Bu bir kontrol-et-sonra-davran yarışıdır.

İki kişi aynı milisaniyede ada@lovelace.io'yu kaydeder. İki okuma da boş döner. İki yazma da başarılı olur. Artık bir e-postada iki hesabınız var — ve tabloda bunu işaretleyen hiçbir şey yok.

email üzerinde bir GSI de sizi kurtarmaz. GSI'ler nihai tutarlıdır, bu yüzden yazmanızı kapılayan (gate) okuma, tasarımı gereği bayat olabilir. Çözüm daha hızlı bir kontrol değil; yazmanın kendisini alınmış bir değere inmeyi reddettirmektir.

Her kısıtı bir işaretçi öğesi olarak modelleyin

Motor zaten bir benzersizlik kuralını bedava uygular: aynı anahtarla iki öğe yazamazsınız. Bu yüzden her benzersizlik kuralını bir anahtar olarak kodlayın.

Gerçek hesap öğesinin yanında, korunan nitelik başına bir işaretçi öğesi yazın. İşaretçinin bölüm anahtarı, ad alanlandırılmış değerin kendisidir. Değer alınmışsa, anahtar vardır ve korumalı bir put onun üzerine yazamaz.

Hem email hem username'i benzersiz tutması gereken bir kayıt için, üç öğe birlikte hareket eder — tek tablo düzeninde anahtarlanmış (bkz. tek tablo tasarımı):

ÖğePKSKAmaç
Hesap kaydıACCT#a1f9c3PROFILEGerçek hesap
E-posta kilidiUNIQ#EMAIL#ada@lovelace.ioLOCKE-postayı rezerve eder
Kullanıcı adı kilidiUNIQ#HANDLE#adaLOCKKullanıcı adını rezerve eder

Hesabın kendi PK'si üretilmiş bir kimliktir (ACCT#a1f9c3) — asla e-posta değil — böylece kullanıcı birincil anahtarı yeniden yazmadan e-postasını sonradan değiştirebilir. Kilit öğeleri profil verisi taşımaz; yalnızca anahtarları dolu olsun diye vardırlar.

Üçünü de atomik yazın

TransactWriteItems en fazla 100 yazmayı tek bir hep-ya-hiç birimi olarak uygular. Her put'u attribute_not_exists(PK) ile koruyun, böylece o anahtar zaten varsa başarısız olsun.

Herhangi bir koşul başarısız olursa — e-posta kilidi, kullanıcı adı kilidi ya da hesabın kendisi — DynamoDB tüm işlemi geri alır ve TransactionCanceledException fırlatır. Kısmi kayıt yok, öksüz kilit yok.

{
  "TransactItems": [
    {
      "Put": {
        "TableName": "accounts",
        "Item": {
          "PK": {"S": "ACCT#a1f9c3"},
          "SK": {"S": "PROFILE"},
          "email": {"S": "ada@lovelace.io"},
          "username": {"S": "ada"}
        },
        "ConditionExpression": "attribute_not_exists(PK)"
      }
    },
    {
      "Put": {
        "TableName": "accounts",
        "Item": {
          "PK": {"S": "UNIQ#EMAIL#ada@lovelace.io"},
          "SK": {"S": "LOCK"}
        },
        "ConditionExpression": "attribute_not_exists(PK)"
      }
    },
    {
      "Put": {
        "TableName": "accounts",
        "Item": {
          "PK": {"S": "UNIQ#HANDLE#ada"},
          "SK": {"S": "LOCK"}
        },
        "ConditionExpression": "attribute_not_exists(PK)"
      }
    }
  ]
}

Koşul, tüm mekanizmadır. attribute_not_exists olmadan, aynı e-postayla ikinci bir kayıt sessizce ilk kilidin üzerine yazar. Onunla, put reddeder, işlem iptal olur ve uygulamanız "e-posta zaten kullanımda" yüzeye çıkarır.

ConditionExpression'ı ve değer map'ini elle oluşturmak, yazım hatalarının sızdığı yerdir. DynamoDB Expression Builder, her put için koşulu ve türlü Item'ı üretir, böylece doğru bir işlemi doğrudan SDK çağrınıza yapıştırabilirsiniz.

Başarısızlığı okuyun, tahmin etmeyin

İşlem iptal edildiğinde, DynamoDB bir CancellationReasons dizisini konumsal olarak döndürür — istek sırasında, öğe başına bir giriş. Slot 1'deki bir ConditionalCheckFailed, e-postanın alındığı anlamına gelir; slot 2, kullanıcı adının. Genel bir "kayıt başarısız" yerine slotu kesin, alan düzeyinde bir hataya geri eşleyin.

Kilitleri DynoTable'da inceleyin

İşaretçi öğeleri uygulamanızın arayüzünde görünmezdir — onlar tesisattır. Bir kayıt gizemli biçimde başarısız olduğunda, kilidin gerçekten var olup olmadığını görmeniz gerekir.

Tabloyu DynoTable'da açın ve UNIQ# önekini Query edin. Hesap ve onun iki kilit öğesi birlikte oturur, böylece sıkışmış bir kayıt (bozuk bir silme tarafından geride bırakılan bir kilit) bir bakışta belli olur.

DynoTable tabloyu tararken — hesap öğeleri, UNIQ#EMAIL ve UNIQ#HANDLE kilit öğeleriyle iç içe geçmiş şekilde.
DynoTable tabloyu tararken — hesap öğeleri, UNIQ#EMAIL ve UNIQ#HANDLE kilit öğeleriyle iç içe geçmiş şekilde.

Değişiklik ve silmede kilitleri dürüst tutun

Kilitler bir kez yaz değildir. Canlı değeri yansıtırlar, bu yüzden yaşam döngüsü onları senkronda tutmalıdır — korunan bir niteliğe dokunan her işlem de bir işlemdir.

  • E-postayı değiştirme. Tek işlem: yeni UNIQ#EMAIL#… kilidini attribute_not_exists ile put edin, eski kilidi silin, hesabı güncelleyin. Aynı hep-ya-hiç garantisi.
  • Hesabı silme. Hesap öğesini ve her iki kilit öğesini tek bir işlemde silin, yoksa değeri sonsuza dek engelleyen bir kilidi öksüz bırakırsınız.
  • Güvenli yeniden deneme. Bir ClientRequestToken geçirin, böylece yeniden gönderilen bir işlem (bir ağ kesintisinden sonra) bir çift yazma değil, idempotent olsun.

Tuzak, kiliti gönder-ve-unut olarak ele almaktır. Kayıtta oluşturulan ama hesap kaldırılırken hiç silinmeyen bir kilit, kimsenin asla yeniden kullanamayacağı bir değerdir — ve gerçek bir kullanıcı kendi eski kullanıcı adını talep edemeyene kadar ortaya çıkmaz.

Sonraki adımlar

Benzersizlik işaretçileri bir tek tablo desenidir, bu yüzden diğer öğelerinizin yanında doğal olarak otururlar — anahtar düzeni için tek tablo tasarımı'nı ve bir kiliti kontrol etmek için asla bir Scan'e uzanmamanız için Query vs Scan'i okuyun. Desen ilk olarak AWS'nin re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions oturumunda adım adım anlatıldı.

Koşul korumalı put'ları DynamoDB Expression Builder ile taslaklayın, ardından kilit öğelerini kendi tablonuza karşı incelemek için DynoTable'ı deneyin.

Güncellendi