Lanjutan5 menit baca

Keunikan pada Beberapa Atribut di DynamoDB

DynamoDB menjamin keunikan untuk tepat satu hal: . Tidak ada constraint UNIQUE (email), tidak ada UNIQUE (username), dan tidak ada apa pun yang membentang dua atribut. Datang dari SQL, ketiadaan itu adalah kejutan pertama — dan tempat pertama orang diam-diam mengirim sebuah race condition.

Bagaimana Anda menerapkan sebuah constraint unik pada beberapa atribut di DynamoDB?

DynamoDB tidak punya constraint UNIQUE di luar , jadi Anda menerapkan keunikan sendiri: modelkan setiap nilai yang dilindungi sebagai marker item-nya sendiri yang key-nya adalah nilai itu, lalu tulis record dan setiap marker bersama-sama dalam satu TransactWriteItems, masing-masing put ber-guard attribute_not_exists. Tabrakan yang sudah diterapkan engine menjadi constraint Anda.

  • Tidak ada constraint unik — hanya primary key yang diterapkan unik oleh engine. Setiap atribut "harus unik" lainnya adalah tugas Anda.
  • Modelkan setiap aturan keunikan sebagai item-nya sendiri. Sebuah marker item khusus yang key-nya adalah nilai yang Anda lindungi mengubah "apakah email ini sudah dipakai?" menjadi tabrakan key yang sudah diterapkan engine.
  • Tulis secara atomik dengan TransactWriteItems. Satu , setiap put ber-guard attribute_not_exists, sehingga semua marker dan record sungguhan commit bersama atau tidak sama sekali.
  • Jangan check-then-write. Sebuah read-before-insert adalah race klasik; dua signup bersamaan sama-sama membaca "kosong" dan sama-sama menulis.

Mengapa pendekatan yang jelas itu salah

Nalurinya adalah men-Query (atau lebih buruk, men-Scan) untuk email, tidak melihat apa pun, lalu men-PutItem akun baru. Itu adalah race check-then-act.

Dua orang mendaftarkan ada@lovelace.io pada milidetik yang sama. Kedua pembacaan mengembalikan kosong. Kedua penulisan berhasil. Kini Anda punya dua akun pada satu email — dan tidak ada apa pun di tabel yang menandainya.

Sebuah pada email juga tidak menyelamatkan Anda. GSI bersifat eventually consistent, jadi pembacaan yang menggerbang penulisan Anda bisa basi secara desain. Perbaikannya bukan pemeriksaan yang lebih cepat; melainkan membuat penulisan itu sendiri menolak mendarat pada nilai yang sudah dipakai.

Modelkan setiap constraint sebagai marker item

Engine sudah menerapkan satu aturan keunikan secara gratis: Anda tidak bisa menulis dua item dengan key yang sama. Jadi enkode setiap aturan keunikan sebagai sebuah key.

Di samping item akun sungguhan, tulis satu marker item per atribut yang dilindungi. Partition key marker adalah nilai bernamespace itu. Jika nilainya sudah dipakai, key-nya ada, dan sebuah put ber-guard tidak bisa menimpanya.

Untuk sebuah signup yang harus menjaga email dan username sama-sama unik, tiga item bergerak bersama — ber-key dalam tata letak single-table (lihat single-table design):

ItemPKSKTujuan
Record akunACCT#a1f9c3PROFILEAkun sungguhan
Lock emailUNIQ#EMAIL#ada@lovelace.ioLOCKMereservasi email
Lock usernameUNIQ#HANDLE#adaLOCKMereservasi username

PK akun itu sendiri adalah id yang dihasilkan (ACCT#a1f9c3) — jangan pernah email — sehingga pengguna bisa mengubah email-nya nanti tanpa menulis ulang primary key. Item lock tidak membawa data profil; mereka ada hanya agar key-nya terisi.

Tulis ketiganya secara atomik

TransactWriteItems menerapkan hingga 100 penulisan sebagai satu unit all-or-nothing. Beri guard setiap put dengan attribute_not_exists(PK) sehingga gagal jika key itu sudah ada.

Jika salah satu kondisi gagal — lock email, lock handle, atau akun itu sendiri — DynamoDB menggulung balik seluruh transaksi dan melempar TransactionCanceledException. Tanpa signup parsial, tanpa lock yatim.

{
  "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)"
      }
    }
  ]
}

Kondisi itulah seluruh mekanismenya. Tanpa attribute_not_exists, sebuah signup kedua dengan email yang sama diam-diam menimpa lock pertama. Dengannya, put menolak, transaksi dibatalkan, dan aplikasi Anda memunculkan "email sudah dipakai."

Membangun ConditionExpression dan value map dengan tangan adalah tempat typo menyelinap masuk. DynamoDB Expression Builder memancarkan kondisi dan Item bertipe untuk setiap put sehingga Anda bisa menempelkan transaksi yang benar langsung ke pemanggilan SDK Anda.

Baca kegagalannya, jangan menebak-nebak

Ketika transaksi dibatalkan, DynamoDB mengembalikan sebuah array CancellationReasons secara posisional — satu entri per item, dalam urutan permintaan. Sebuah ConditionalCheckFailed di slot 1 berarti email sudah dipakai; slot 2 berarti username. Petakan slot kembali ke error tingkat-field yang presisi alih-alih "signup gagal" yang generik.

Inspeksi lock di DynoTable

Marker item tak terlihat di UI aplikasi Anda — mereka adalah pipa. Ketika sebuah signup gagal misterius, Anda perlu melihat apakah lock benar-benar ada.

Buka tabel di DynoTable dan Query prefix UNIQ#. Akun dan kedua item lock-nya duduk bersama, sehingga sebuah signup macet (sebuah lock yang tertinggal oleh delete yang gagal) jelas terlihat sekilas.

DynoTable memindai tabel — item akun berselang-seling dengan item lock UNIQ#EMAIL dan UNIQ#HANDLE-nya.
DynoTable memindai tabel — item akun berselang-seling dengan item lock UNIQ#EMAIL dan UNIQ#HANDLE-nya.

Jaga lock tetap jujur saat perubahan dan penghapusan

Lock bukan write-once. Mereka mencerminkan nilai yang hidup, jadi siklus hidupnya harus menjaga mereka tetap sinkron — setiap operasi yang menyentuh atribut yang dilindungi adalah sebuah transaksi juga.

  • Ubah email. Satu transaksi: put lock UNIQ#EMAIL#… baru dengan attribute_not_exists, hapus lock lama, update akun. Jaminan all-or-nothing yang sama.
  • Hapus akun. Hapus item akun dan kedua item lock dalam satu transaksi, atau Anda akan menelantarkan sebuah lock yang memblokir nilai itu selamanya.
  • Retry dengan aman. Sertakan sebuah ClientRequestToken sehingga transaksi yang dikirim ulang (setelah gangguan jaringan) bersifat idempoten alih-alih penulisan ganda.

Jebakannya adalah memperlakukan lock sebagai fire-and-forget. Sebuah lock yang dibuat saat signup tetapi tidak pernah dihapus saat penghapusan akun adalah nilai yang tak seorang pun bisa gunakan ulang selamanya — dan itu tidak akan muncul sampai seorang pengguna sungguhan tidak bisa mengklaim handle lamanya sendiri.

Langkah selanjutnya

Marker keunikan adalah pola single-table, jadi mereka duduk secara alami di samping item Anda yang lain — baca single-table design untuk tata letak key, dan Query vs Scan agar Anda tidak pernah meraih sebuah Scan untuk memeriksa sebuah lock. Pola ini pertama kali ditelusuri dalam sesi AWS re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.

Draf put ber-guard kondisi dengan DynamoDB Expression Builder, lalu coba DynoTable untuk menginspeksi item lock terhadap tabel Anda sendiri.

Diperbarui