Menengah5 menit baca

Reference Count DynamoDB

Sebuah reference count adalah angka yang Anda simpan pada Item induk yang melacak berapa banyak Item anak yang menunjuk padanya — like pada sebuah post, anggota dalam sebuah workspace, balasan pada sebuah comment. Anda menyimpannya karena menghitung anak-anak pada setiap pembacaan terlalu mahal.

Bagaimana cara memelihara hitungan di DynamoDB?

Simpan total berjalan sebagai angka pada Item induk dan perbarui dalam tulisan yang sama yang membuat anak. Sebuah memastikan keduanya mendarat atau tidak sama sekali, dan sebuah condition pada tulisan anak mencegah retry dari double-counting — sehingga satu GetItem mengembalikan hitungan yang akurat.

  • Jangan hitung anak-anak saat pembacaan. Sebuah Query untuk menghitung like membayar setiap Item like yang dipindainya. Simpan totalnya pada post dan baca satu Item saja.
  • Pelihara hitungannya di tempat anak ditulis, bukan setelahnya. Naikkan dalam operasi yang sama yang membuat anak sehingga keduanya tidak pernah menyimpang.
  • Gunakan transaction saat tulisan dan kenaikan menyentuh Item berbeda. Sebuah like adalah satu Item, hitungannya ada di Item lain — TransactWriteItems membuat keduanya mendarat atau tidak sama sekali.
  • Footgun-nya adalah double-counting. Sebuah like yang dicoba-ulang atau diduplikasi yang menjalankan-ulang increment menggelembungkan angkanya. Jaga tulisan anak dengan sebuah condition.

Mengapa menghitung sama sekali

Datang dari SQL, Anda tidak akan pernah menyimpan jumlah-like — Anda akan SELECT COUNT(*) FROM likes WHERE post_id = ? dan membiarkan sebuah index membuatnya murah. DynamoDB tidak punya COUNT(*) yang melewatkan pembacaan Item.

Sebuah Query atas like sebuah post membaca — dan menagih — setiap Item like di partition itu, bahkan jika Anda hanya menginginkan angkanya. Pada sebuah post yang viral itu ribuan RCU untuk menjawab "berapa banyak like?" Itulah footgun pembacaan yang menjadi alasan keberadaan reference count.

Jadi Anda denormalisasi: simpan total berjalan pada post itu sendiri. Membaca hitungannya menjadi satu GetItem. Biayanya adalah kini Anda yang memiliki tugas menjaganya akurat.

Modelkan Item-nya

Dua tipe Item berbagi sebuah partition sehingga post dan like-nya berada dalam satu item collection. Key yang dikarang:

Post item
PKSKattributes
POST#a91fMETAlikeTally (Number), body, authorId, createdAt
Like item
PKSKattributes
POST#a91fLIKE#USER#7c20likedAt

Atribut likeTally pada Item META adalah reference count. Setiap Item LIKE# adalah anak. Menempatkan keduanya di bawah PK = "POST#a91f" berarti satu Query bisa mengambil post dan para peng-like-nya sekaligus saat Anda memang menginginkan daftarnya.

Naikkan hitungannya secara atomik

DynamoDB meng-increment sebuah angka dengan update expression ADD (atau SET x = x + :n) — ini adalah atomic counter: DynamoDB menerapkan delta di sisi-server tanpa Anda membaca nilai saat ini terlebih dahulu, sehingga increment bersamaan tidak saling menabrak. (AWS: atomic counters)

Masalahnya: menyukai sebuah post adalah dua tulisan ke dua Item — membuat Item LIKE#, dan menambahkan 1 ke likeTally pada META. Jika like mendarat tapi kenaikan gagal, tally salah selamanya. Anda butuh keduanya atau tidak sama sekali.

Itulah yang dijamin TransactWriteItems — all-or-nothing di banyak Item, dan ia membatalkan seluruh transaction jika ada Item yang dimodifikasi secara bersamaan (AWS: pessimistic locking dengan transaction):

{
  "TransactItems": [
    {
      "Put": {
        "TableName": "Social",
        "Item": {
          "PK": {"S": "POST#a91f"},
          "SK": {"S": "LIKE#USER#7c20"},
          "likedAt": {"N": "1750636800"}
        },
        "ConditionExpression": "attribute_not_exists(SK)"
      }
    },
    {
      "Update": {
        "TableName": "Social",
        "Key": {
          "PK": {"S": "POST#a91f"},
          "SK": {"S": "META"}
        },
        "UpdateExpression": "ADD likeTally :one",
        "ExpressionAttributeValues": {":one": {"N": "1"}}
      }
    }
  ]
}

Put dan Update di-commit bersama. Jika salah satunya gagal, DynamoDB me-rollback keduanya dan mengembalikan sebuah TransactionCanceledException.

Jaga terhadap double-counting

Bug sebenarnya bukanlah like yang separuh-tertulis — transaction mencegah itu. Ia adalah pengguna yang sama menyukai dua kali, atau sebuah retry klien yang memutar-ulang request. Setiap pemutaran-ulang menambah 1 lagi, dan likeTally diam-diam menyimpang di atas hitungan sebenarnya.

ConditionExpression: attribute_not_exists(SK) pada Put adalah penjaganya. Jika Item LIKE# pengguna itu sudah ada, condition Put gagal, seluruh transaction dibatalkan, dan — yang krusial — ADD tidak pernah berjalan. Satu like per pengguna, ditegakkan oleh key.

Bangun dan salin update dan condition expression ini — dengan ExpressionAttributeValues yang tepat dan penjaga attribute_not_exists — di DynamoDB Expression Builder alih-alih merakit JSON dengan tangan.

Unlike, dan biayanya

Menghapus sebuah like adalah bayangan cerminnya: Delete Item LIKE# dengan ConditionExpression: attribute_exists(SK), dan ADD likeTally :minusOne dalam transaction yang sama. Condition menghentikan double-unlike dari mendorong tally menjadi negatif.

Ketahui harganya. Sebuah tulisan transaksional berbiaya 2 WCU per Item untuk Item hingga 1 KB — satu untuk menyiapkan, satu untuk meng-commit — versus 1 WCU untuk tulisan biasa. Sebuah like adalah dua Item, jadi setiap like sekitar empat WCU. Murah per aksi, tapi layak diketahui sebelum sebuah post selebritas terkena badai-like.

Lihat di DynoTable

Saat Anda mencurigai sebuah tally telah menyimpang, Anda ingin membandingkan likeTally yang tersimpan dengan jumlah aktual anak LIKE# — tanpa menjalankan query penghitungan di prod.

Item META post di samping anak LIKE#-nya dalam satu item collection, sehingga Anda bisa melihat tally yang tersimpan terhadap jumlah anak yang sebenarnya.
Item META post di samping anak LIKE#-nya dalam satu item collection, sehingga Anda bisa melihat tally yang tersimpan terhadap jumlah anak yang sebenarnya.

Untuk rekonsiliasi sejati di seluruh himpunan post yang terbatas — "tally mana yang tidak cocok dengan jumlah anaknya?" — SQL Workbench DynoTable menjalankan GROUP BY dan join di sisi-klien atas baris yang telah Anda muat, yang tidak bisa diekspresikan oleh PartiQL biasa.

Jebakan dan langkah berikutnya

  • Jangan pelihara hitungannya di luar-jalur (sebuah Lambda yang menghitung-ulang tiap malam). Itu plester di atas jalur tulisan yang seharusnya transaksional sejak awal.
  • Awasi hot partition. Sebuah post tunggal yang amat populer mengonsentrasikan setiap like — dan setiap kenaikan tally — pada satu partition key. Hitungannya benar; partition mungkin tetap di-throttle.
  • Rekonsiliasi jarang, perbaiki secara bedah. Penyimpangan seharusnya mendekati nol jika setiap mutasi dikondisikan. Perlakukan ketidakcocokan sebagai bug untuk ditemukan, bukan angka untuk ditimpa.

Bacaan terkait: single-table design untuk alasan post dan like berbagi sebuah partition, dan Query vs Scan untuk alasan menghitung anak saat pembacaan adalah pola yang Anda hindari.

Lalu unduh DynoTable untuk memeriksa item collection ini dan memverifikasi tally Anda terhadap tabel Anda sendiri.

Diperbarui