7.1 Mengapa perlu
Kami telah membahas secara rinci semua properti ACID, tujuan dan kasus penggunaannya. Seperti yang Anda lihat, tidak semua database menawarkan jaminan ACID, mengorbankannya untuk kinerja yang lebih baik. Oleh karena itu, mungkin saja database yang tidak menawarkan ACID dipilih dalam proyek Anda, dan Anda mungkin perlu mengimplementasikan beberapa fungsi ACID yang diperlukan di sisi aplikasi. Dan jika sistem Anda dirancang sebagai layanan mikro, atau jenis aplikasi terdistribusi lainnya, apa yang akan menjadi transaksi lokal normal dalam satu layanan sekarang akan menjadi transaksi terdistribusi - dan, tentu saja, akan kehilangan sifat ACID-nya, bahkan jika database dari setiap layanan mikro individu akan menjadi ASAM.
Saya tidak ingin memberi Anda panduan lengkap tentang cara membuat pengelola transaksi, hanya karena terlalu besar dan rumit, dan saya hanya ingin membahas beberapa teknik dasar. Jika kita tidak berbicara tentang aplikasi terdistribusi, maka saya tidak melihat alasan untuk mencoba mengimplementasikan ACID sepenuhnya di sisi aplikasi jika Anda memerlukan jaminan ACID - lagipula, akan lebih mudah dan lebih murah dalam segala hal untuk mengambil solusi yang sudah jadi ( yaitu, database dengan ACID).
Namun saya ingin menunjukkan kepada Anda beberapa teknik yang akan membantu Anda dalam melakukan transaksi di sisi aplikasi. Lagi pula, mengetahui teknik-teknik ini dapat membantu Anda dalam berbagai skenario, bahkan yang tidak melibatkan transaksi, dan menjadikan Anda pengembang yang lebih baik (saya harap begitu).
7.2 Alat dasar untuk pencinta transaksi
Pemblokiran optimis dan pesimis. Ini adalah dua jenis kunci pada beberapa data yang dapat diakses secara bersamaan.
Optimismengasumsikan bahwa kemungkinan akses bersamaan tidak begitu besar, dan oleh karena itu ia melakukan hal berikut: membaca baris yang diinginkan, mengingat nomor versinya (atau stempel waktu, atau checksum / hash - jika Anda tidak dapat mengubah skema data dan menambahkan kolom untuk versi atau stempel waktu), dan sebelum menulis perubahan ke database untuk data ini, ia memeriksa apakah versi data ini telah berubah. Jika versinya telah berubah, maka Anda perlu menyelesaikan konflik yang dibuat dan memperbarui data ("commit"), atau memutar kembali transaksi ("rollback"). Kerugian dari metode ini adalah menciptakan kondisi yang menguntungkan untuk bug dengan nama panjang "time-of-check to time-of-use", disingkat TOCTOU: status dapat berubah dalam periode waktu antara check dan write. Saya tidak punya pengalaman dengan penguncian optimis,
Sebagai contoh, saya menemukan satu teknologi dari kehidupan sehari-hari pengembang yang menggunakan sesuatu seperti penguncian optimis - ini adalah protokol HTTP. Tanggapan terhadap permintaan GET HTTP awal MUNGKIN menyertakan header ETag untuk permintaan PUT berikutnya dari klien, yang MUNGKIN digunakan klien di header If-Match. Untuk metode GET dan HEAD, server akan mengirimkan kembali sumber daya yang diminta hanya jika cocok dengan salah satu ETag yang diketahuinya. Untuk PUT dan metode tidak aman lainnya, itu hanya akan memuat sumber daya dalam kasus ini juga. Jika Anda tidak tahu cara kerja ETag, inilah contoh yang bagus menggunakan pustaka "feedparser" (yang membantu mengurai RSS dan umpan lainnya).
>>> import feedparser
>>> d = feedparser.parse('http://feedparser.org/docs/examples/atom10.xml')
>>> d.etag
'"6c132-941-ad7e3080"'
>>> d2 = feedparser.parse('http://feedparser.org/docs/examples/atom10.xml', etag=d.etag)
>>> d2.feed
{}
>>> d2.debug_message
'The feed has not changed since you last checked, so the server sent no data. This is a feature, not a bug!'
Sebaliknya, orang pesimis berasal dari fakta bahwa transaksi akan sering "bertemu" pada data yang sama, dan untuk menyederhanakan hidupnya dan menghindari kondisi balapan yang tidak perlu, dia hanya memblokir data yang dia butuhkan . Untuk menerapkan mekanisme penguncian, Anda perlu mempertahankan koneksi database untuk sesi Anda (alih-alih menarik koneksi dari kumpulan - dalam hal ini Anda kemungkinan besar harus bekerja dengan penguncian optimis), atau menggunakan ID untuk transaksi , yang dapat digunakan terlepas dari koneksinya. Kerugian dari penguncian pesimistis adalah penggunaannya memperlambat pemrosesan transaksi secara umum, tetapi Anda dapat tenang tentang data dan mendapatkan isolasi yang nyata.
Namun, bahaya tambahan mengintai di kebuntuan yang mungkin terjadi, di mana beberapa proses menunggu sumber daya terkunci satu sama lain. Misalnya, sebuah transaksi membutuhkan sumber daya A dan B. Proses 1 telah menempati sumber daya A, dan proses 2 telah menempati sumber daya B. Tak satu pun dari kedua proses tersebut dapat melanjutkan eksekusi. Ada berbagai cara untuk mengatasi masalah ini - saya tidak ingin membahas detailnya sekarang, jadi baca Wikipedia terlebih dahulu, tetapi singkatnya, ada kemungkinan untuk membuat hierarki kunci. Jika Anda ingin mengenal konsep ini lebih detail, maka Anda diajak untuk memutar otak atas “Dinning Philosophers Problem” (“masalah filsuf makan”).
Berikut adalah contoh yang bagus tentang bagaimana kedua kunci akan berperilaku dalam skenario yang sama.
Mengenai implementasi kunci. Saya tidak ingin merinci, tetapi ada manajer kunci untuk sistem terdistribusi, misalnya: ZooKeeper, Redis, etcd, Consul.
7.3 Idempotensi operasi
Kode idempoten umumnya merupakan praktik yang baik, dan ini adalah kasus yang tepat ketika pengembang dapat melakukannya dengan baik, terlepas dari apakah dia menggunakan transaksi atau tidak. Idempotensi adalah properti dari suatu operasi untuk menghasilkan hasil yang sama ketika operasi itu diterapkan ke objek lagi. Fungsi itu dipanggil - memberikan hasilnya. Dipanggil lagi setelah satu atau lima detik - memberikan hasil yang sama. Tentu saja, jika data di database berubah, hasilnya akan berbeda. Data dalam sistem ketiga mungkin tidak bergantung pada suatu fungsi, tetapi apa pun yang terjadi harus dapat diprediksi.
Mungkin ada beberapa manifestasi dari idempotensi. Salah satunya hanyalah rekomendasi tentang cara menulis kode Anda. Apakah Anda ingat bahwa fungsi terbaik adalah yang melakukan satu hal? Dan apa yang baik untuk menulis tes unit untuk fungsi ini? Jika Anda mematuhi kedua aturan ini, Anda sudah meningkatkan kemungkinan fungsi Anda menjadi idempoten. Untuk menghindari kebingungan, saya akan mengklarifikasi bahwa fungsi idempoten belum tentu "murni" (dalam arti "kemurnian fungsi"). Fungsi murni adalah fungsi yang hanya beroperasi pada data yang mereka terima saat input, tanpa mengubahnya dengan cara apa pun dan mengembalikan hasil yang diproses. Ini adalah fungsi yang memungkinkan Anda menskalakan aplikasi menggunakan teknik pemrograman fungsional. Karena kita berbicara tentang beberapa data umum dan database, fungsi kita kemungkinan besar tidak murni,
Ini adalah fungsi murni:
def square(num: int) -> int:
return num * num
Tapi fungsi ini tidak murni, tapi idempoten (tolong jangan menarik kesimpulan tentang bagaimana saya menulis kode dari potongan-potongan ini):
def insert_data(insert_query: str, db_connection: DbConnectionType) -> int:
db_connection.execute(insert_query)
return True
Alih-alih banyak kata, saya hanya bisa berbicara tentang bagaimana saya dipaksa untuk belajar menulis program idempoten. Saya melakukan banyak pekerjaan dengan AWS, seperti yang Anda lihat sekarang, dan ada layanan bernama AWS Lambda. Lambda memungkinkan Anda untuk tidak mengurus server, tetapi cukup memuat kode yang akan dijalankan sebagai respons terhadap beberapa peristiwa atau sesuai jadwal. Suatu peristiwa dapat berupa pesan yang disampaikan oleh perantara pesan. Di AWS, broker ini adalah AWS SNS. Saya pikir ini harus jelas bahkan bagi mereka yang tidak bekerja dengan AWS: kami memiliki broker yang mengirim pesan melalui saluran ("topik"), dan layanan mikro yang berlangganan saluran ini menerima pesan dan entah bagaimana bereaksi terhadapnya.
Masalahnya adalah SNS mengirimkan pesan "setidaknya sekali" ("pengiriman setidaknya sekali"). Apa artinya? Bahwa cepat atau lambat kode Lambda Anda akan dipanggil dua kali. Dan itu benar-benar terjadi. Ada sejumlah skenario di mana fungsi Anda harus idempoten: misalnya, ketika uang ditarik dari akun, kami dapat mengharapkan seseorang untuk menarik jumlah yang sama dua kali, tetapi kami perlu memastikan bahwa ini benar-benar 2 waktu independen - dengan kata lain, ini adalah 2 transaksi yang berbeda, dan bukan pengulangan dari satu transaksi.
Untuk perubahan, saya akan memberikan contoh lain - membatasi frekuensi permintaan ke API ("pembatasan tarif"). Lambda kami menerima peristiwa dengan user_id tertentu yang pemeriksaannya harus dilakukan untuk melihat apakah pengguna dengan ID tersebut telah kehabisan jumlah kemungkinan permintaannya ke beberapa API kami. Kita dapat menyimpan di DynamoDB dari AWS nilai panggilan yang dilakukan, dan meningkatkannya dengan setiap panggilan ke fungsi kita sebesar 1.
Tetapi bagaimana jika fungsi Lambda ini dipanggil oleh peristiwa yang sama dua kali? Omong-omong, apakah Anda memperhatikan argumen fungsi lambda_handler() . Argumen kedua, konteks di AWS Lambda diberikan secara default dan berisi berbagai metadata, termasuk request_id yang dibuat untuk setiap panggilan unik. Ini berarti bahwa sekarang, alih-alih menyimpan jumlah panggilan yang dilakukan di tabel, kami dapat menyimpan daftar request_id dan pada setiap panggilan, Lambda kami akan memeriksa apakah permintaan yang diberikan telah diproses:
import json
import os
from typing import Any, Dict
from aws_lambda_powertools.utilities.typing import LambdaContext # needed only for argument type annotation
import boto3
limit = os.getenv('LIMIT')
def handler_name(event: Dict[str: Any], context: LambdaContext):
request_id = context.aws_request_id
# We find user_id in incoming event
user_id = event["user_id"]
# Our table for DynamoDB
table = boto3.resource('dynamodb').Table('my_table')
# Doing update
table.update_item(
Key={'pkey': user_id},
UpdateExpression='ADD requests :request_id',
ConditionExpression='attribute_not_exists (requests) OR (size(requests) < :limit AND NOT contains(requests, :request_id))',
ExpressionAttributeValues={
':request_id': {'S': request_id},
':requests': {'SS': [request_id]},
':limit': {'N': limit}
}
)
# TODO: write further logic
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": json.dumps({
"status ": "success"
})
}
Karena contoh saya sebenarnya diambil dari Internet, saya akan meninggalkan tautan ke sumber aslinya, terutama karena memberikan lebih banyak informasi.
Ingat bagaimana saya menyebutkan sebelumnya bahwa sesuatu seperti ID transaksi unik dapat digunakan untuk mengunci data bersama? Kami sekarang telah belajar bahwa itu juga dapat digunakan untuk membuat operasi idempoten. Mari cari tahu dengan cara apa Anda dapat membuat sendiri ID tersebut.
Karena contoh saya sebenarnya diambil dari Internet, saya akan meninggalkan tautan ke sumber aslinya, terutama karena memberikan lebih banyak informasi.
Ingat bagaimana saya menyebutkan sebelumnya bahwa sesuatu seperti ID transaksi unik dapat digunakan untuk mengunci data bersama? Kami sekarang telah belajar bahwa itu juga dapat digunakan untuk membuat operasi idempoten. Mari cari tahu dengan cara apa Anda dapat membuat sendiri ID tersebut.
GO TO FULL VERSION