Orta5 dakikalık okuma

DynamoDB Condition Expression'ları

Bir condition expression, DynamoDB'nin yazmanı işlemeden önce mevcut öğe üzerinde değerlendirdiği bir yüklemdir (predicate). Yüklem yanlışsa, yazma reddedilir ve hiçbir şey değişmez. DynamoDB'nin bir yazma üzerindeki bir WHERE cümlesine en yakın şeyidir — ve bir değişmezi (invariant) zorlamanın tek güvenli yoludur.

DynamoDB condition expression'ları nasıl çalışır?

Bir condition expression, DynamoDB'nin bir yazmayı işlemeden önce mevcut öğeye karşı sunucu tarafında değerlendirdiği bir yüklemdir (predicate). Doğruysa yazma devam eder; yanlışsa yazma ConditionalCheckFailedException ile reddedilir ve hiçbir şey değişmez. Kontrolü ve mutasyonu tek bir atomik işleme katlar, böylece eşzamanlı çağıranlar eski bir okuma üzerinden yarışamaz.

  • Bu bir koruyucudur, bir filtre değil. ConditionExpression, mevcut öğe üzerinde sunucu tarafında çalışır; yanlış bir sonuç yazmayı ConditionalCheckFailedException ile başarısız kılar.
  • Oku-sonra-yaz'ın yerini alır. Bir SELECT sonra UPDATE turu yok — kontrol ve mutasyon tek bir atomik işlemdir, bu yüzden iki çağıran (caller) yarışamaz.
  • Reddetmesi bedava, çalışması değil. Başarısız bir koşullu yazma yine de yazma kapasitesi tüketir. Garanti, engellediği yazmayla aynıya mal olur.

SQL'den gelince, satırı okur, onu uygulama kodunda kontrol eder, sonra güncellerdin. DynamoDB'de okuma ile yazma arasındaki o boşluk, eşzamanlı bir çağıranı bekleyen bir veri-bozulması hatasıdır. Condition expression boşluğu kapatır.

Nerede geçerlidir

Bir ConditionExpressionPutItem, UpdateItem, DeleteItem ve TransactWriteItems içindeki her eyleme eklersin. Query veya Scan'in bir parçası değildir — bunlar okuma yolunda farklı bir şey olan FilterExpression'ı kullanır.

O ayrım insanları yanıltır, bu yüzden net ol:

ConditionExpressionFilterExpression
YolYazmalar (Put/Update/Delete)Okumalar (Query/Scan)
Başarısızlıkta etkisiTüm yazmayı reddederÖğeyi sonuçlardan düşürür
GördüğüMevcut öğe, yazma-öncesiHer aday öğe, okuma-sonrası
MaliyetBaşarısız yazma yine faturalanırFiltrelenen öğeler okuma için yine faturalanır

İkisi de sunucu tarafında çalışır. Fark, "yanlış"ın ne yaptığıdır: bir koşul bir mutasyonu iptal eder; bir filtre, okumak için zaten ödediğin bir satırı sadece gizler. (AWS: Condition Expressions)

Gerçekten kullanacağın fonksiyonlar

Koşul dili küçüktür. İş atları:

  • attribute_exists(path) / attribute_not_exists(path) — bu attribute öğede var mı? "Yalnızca yoksa oluştur" / "yalnızca varsa güncelle" için klasik deyim.
  • Karşılaştırıcılar — =, <>, <, <=, >, >= — bir değere veya başka bir attribute'a karşı.
  • attribute_type, begins_with, contains, size — tür ve string/set kontrolleri.
  • BETWEEN … AND …, IN (…) — aralık ve üyelik.
  • AND, OR, NOT, parantezler — yukarıdakileri birleştirmek için.

Partition key üzerindeki attribute_not_exists, PutItem'ın mevcut bir öğeyi ezmeyen bir insert gibi davranmasını sağlamanın kanonik yoludur — DynamoDB'nin ayrı bir "insert" işlemi yoktur, bu yüzden koşul zaten insert semantiğidir. (AWS: Comparison Operator and Function Reference)

İşlenmiş bir örnek: bir defteri borca düşmeye karşı koruma

Bir bankacılık defteri (ledger) al. Her hesap tek bir öğedir:

PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents    = 0

Değişmez: bir borçlandırma (debit) kullanılabilir bakiyeyi asla sıfırın altına itmemeli ve var olmayan bir hesabı asla borçlandırmamalısın. İki kural, ikisi de yazmanın içinde zorlanabilir.

Yanlış yol (ayak tuzağı)

GetItem ACCT#a7f3 / BALANCE     → clearedCents = 50000
if (50000 >= 30000) ...         ← uygulama tarafı kontrol
UpdateItem  SET clearedCents = 20000

GetItem ile UpdateItem arasında, ikinci bir borçlandırma aynı 50000'i okuyabilir, kendi kontrolünü geçebilir ve o da yazabilir. İkisi de başarılı olur; hesap negatife düşer. Bu bir oku-değiştir-yaz yarışıdır ve hiçbir miktar uygulama tarafı doğrulama bunu düzeltmez — kontrol ve yazma ayrı işlemlerdir.

Doğru yol

Kontrolü yazmaya katla. Hesabın var olmasına ve yeterli tutmasına koşullu olarak 30000 cent borçlandır:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = clearedCents - :amt
  ConditionExpression:
    attribute_exists(PK) AND clearedCents >= :amt

:amt = 30000 ile. Bakiye çok düşükse ya da öğe hiç oluşturulmamışsa, DynamoDB yazmayı ConditionalCheckFailedException ile reddeder ve bakiye dokunulmamış kalır. Eşzamanlı borçlandırma ya orijinal bakiyeyi görür ve ona karşı kontrol edilir ya da güncellenmiş olanı görür — asla üzerine işlem yaptığı eski bir okumayı görmez.

Tam ifadeyi — adlar, değerler, hepsi — ExpressionAttributeValues eşlemesini elle birleştirmek yerine DynamoDB expression builder ile oluşturup kopyalayabilirsin.

Koruyucuyu DynoTable'da inceleme

Bir koşullu yazma başarısız olduğunda, öğenin gerçek durumunu tahmin etmek yerine görmek istersin. Hesap öğesini çek ve clearedCents'i doğrudan oku.

DynoTable'da defter koleksiyonu — BALANCE öğesi, hesabın işlem öğelerinin üzerinde clearedCents'i gösteriyor.
DynoTable'da defter koleksiyonu — BALANCE öğesi, hesabın işlem öğelerinin üzerinde clearedCents'i gösteriyor.

Reddi oku, körlemesine yeniden deneme

ConditionalCheckFailedException geçici bir hata değildir — aynı yazmayı yeniden denemek hiçbir şeyi değiştirmez. Bir iş kuralının ateşlendiği anlamına gelir: yetersiz bakiye, çift oluşturma, eski versiyon. Onu bir altyapı arızası olarak değil, bir alan sonucu olarak yüzeye çıkar.

İki şey başarısızlıkları ayıklanabilir yapar:

  • ReturnValuesOnConditionCheckFailure: ALL_OLD — DynamoDB başarısızlığın yanında mevcut öğeyi döndürür, böylece ikinci bir okuma olmadan "bakiye 20000'di, 30000 istedin" gösterebilirsin. (AWS: Working with Items)
  • İki başarısızlık nedenini ayırt etme. attribute_exists(PK) AND clearedCents >= :amt, "hesap yok" ve "para yok"u tek bir istisnaya çökertir. Çağıranların onları ayırt etmesi gerekiyorsa, iki yazmaya böl veya döndürülen öğeyi incele.

İyimser kilitleme (optimistic locking) aynı püf noktasıdır

Versiyon-numarası deseni, farklı bir şapka takan bir condition expression'dan başka bir şey değildir. Bir version attribute'u sakla; her yazma okuduğun versiyonu öne sürer ve onu artırır:

UpdateItem  ACCT#a7f3 / BALANCE
  SET clearedCents = :new, version = :next
  ConditionExpression: version = :seen

Başka bir yazıcı önce hareket ettiyse, version = :seen yanlıştır, yazma reddedilir ve yeniden okur ve yeniden denersin. DynamoDB eşzamanlılık denetimini kilitler olmadan böyle yapar — gördüğünü öne sür, hareket ettiyse başarısız ol. (AWS: Optimistic Locking with Version Number)

Tuzaklar ve sonraki adımlar

  • Rezerve kelimelerle çakışan adlar. status, size, name ve ~570 tane daha rezervedir. Onları ExpressionAttributeNames ile takma ad ver (#s = status) ya da ifaden sessizce ayrıştırılamaz.
  • Bir koşul başka bir öğeye referans veremez. Yalnızca yazılan öğeyi görür. Öğeler arası değişmezler, eylem başına bir ConditionExpression ile TransactWriteItems ya da bir sentinel öğeye karşı bir ConditionCheck gerektirir.
  • Başarısız yazmalar yine WCU'ya mal olur. Zamanın %90'ında reddeden bir koruyucu yine de o redler için fatura keser. Ucuz sigorta, ama bedava değil.

Bu koruyucuların üzerinde çalıştığı anahtarları modellemek için, bkz. single-table tasarım ve Query ve Scan. Gerçek veriye karşı koşullu yazmalar yapmaya hazır olduğunda, DynoTable'ı indir ve onları kendi tablolarına karşı çalıştır.

Güncellendi