7.1 Mengapakah perlu

Kami telah membincangkan secara terperinci semua sifat ACID, tujuan dan kes penggunaannya. Seperti yang anda lihat, tidak semua pangkalan data menawarkan jaminan ACID, mengorbankannya untuk prestasi yang lebih baik. Oleh itu, mungkin berlaku bahawa pangkalan data yang tidak menawarkan ACID dipilih dalam projek anda, dan anda mungkin perlu melaksanakan beberapa fungsi ACID yang diperlukan pada bahagian aplikasi. Dan jika sistem anda direka bentuk sebagai perkhidmatan mikro, atau aplikasi teragih jenis lain, transaksi tempatan biasa dalam satu perkhidmatan kini akan menjadi transaksi teragih - dan, sudah tentu, akan kehilangan sifat ACIDnya, walaupun pangkalan data setiap perkhidmatan mikro individu akan menjadi ACID.

Saya tidak mahu memberikan anda panduan lengkap tentang cara membuat pengurus transaksi, semata-mata kerana ia terlalu besar dan rumit, dan saya hanya ingin merangkumi beberapa teknik asas. Jika kita tidak bercakap tentang aplikasi yang diedarkan, maka saya tidak melihat sebab untuk cuba melaksanakan sepenuhnya ACID di sisi aplikasi jika anda memerlukan jaminan ACID - lagipun, ia akan menjadi lebih mudah dan lebih murah dalam setiap segi untuk mengambil penyelesaian siap pakai ( iaitu pangkalan data dengan ACID).

Tetapi saya ingin menunjukkan kepada anda beberapa teknik yang akan membantu anda dalam membuat transaksi di bahagian aplikasi. Lagipun, mengetahui teknik ini boleh membantu anda dalam pelbagai senario, malah senario yang tidak semestinya melibatkan transaksi, dan menjadikan anda pembangun yang lebih baik (saya harap begitu).

7.2 Alat asas untuk pencinta transaksi

Penyekatan optimis dan pesimis. Ini adalah dua jenis kunci pada beberapa data yang boleh diakses pada masa yang sama.

Optimismenganggap bahawa kebarangkalian akses serentak tidak begitu besar, dan oleh itu ia melakukan perkara berikut: membaca baris yang dikehendaki, mengingati nombor versinya (atau cap waktu, atau checksum / hash - jika anda tidak boleh menukar skema data dan menambah lajur untuk versi atau cap waktu), dan sebelum menulis perubahan pada pangkalan data untuk data ini, ia menyemak sama ada versi data ini telah berubah. Jika versi telah berubah, maka entah bagaimana anda perlu menyelesaikan konflik yang dibuat dan mengemas kini data ("komit"), atau melancarkan semula transaksi ("mengembalikan"). Kelemahan kaedah ini ialah ia mewujudkan keadaan yang menggalakkan untuk pepijat dengan nama panjang "masa-semak kepada masa-penggunaan", disingkatkan sebagai TOCTOU: keadaan mungkin berubah dalam tempoh masa antara semak dan tulis. Saya tidak mempunyai pengalaman dengan penguncian optimistik,

Sebagai contoh, saya menemui satu teknologi daripada kehidupan harian pembangun yang menggunakan sesuatu seperti penguncian optimistik - ini ialah protokol HTTP. Respons kepada permintaan HTTP GET awal MUNGKIN termasuk pengepala ETag untuk permintaan PUT berikutnya daripada klien, yang MUNGKIN digunakan oleh klien dalam pengepala If-Match. Untuk kaedah GET dan HEAD, pelayan akan menghantar semula sumber yang diminta hanya jika ia sepadan dengan salah satu ETag yang diketahuinya. Untuk PUT dan kaedah tidak selamat yang lain, ia hanya akan memuatkan sumber dalam kes ini juga. Jika anda tidak tahu cara ETag berfungsi, berikut ialah contoh yang baik menggunakan pustaka "feedparser" (yang membantu menghuraikan RSS dan suapan lain).


>>> 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!' 

Orang yang pesimis, sebaliknya, berasal dari fakta bahawa urus niaga selalunya akan "bertemu" pada data yang sama, dan untuk memudahkan hidupnya dan mengelakkan keadaan perlumbaan yang tidak perlu, dia hanya menyekat data yang dia perlukan. Untuk melaksanakan mekanisme penguncian, anda sama ada perlu mengekalkan sambungan pangkalan data untuk sesi anda (daripada menarik sambungan dari kolam - dalam hal ini anda berkemungkinan besar perlu bekerja dengan penguncian optimistik), atau menggunakan ID untuk transaksi , yang boleh digunakan tanpa mengira sambungan. Kelemahan penguncian pesimis ialah penggunaannya melambatkan pemprosesan transaksi secara umum, tetapi anda boleh bertenang tentang data dan mendapatkan pengasingan sebenar.

Bahaya tambahan, bagaimanapun, mengintai dalam kemungkinan kebuntuan, di mana beberapa proses menunggu sumber dikunci oleh satu sama lain. Sebagai contoh, transaksi memerlukan sumber A dan B. Proses 1 telah menduduki sumber A, dan proses 2 telah menduduki sumber B. Kedua-dua proses itu tidak boleh meneruskan pelaksanaan. Terdapat pelbagai cara untuk menyelesaikan isu ini - Saya tidak mahu pergi ke butiran sekarang, jadi baca Wikipedia dahulu, tetapi secara ringkasnya, terdapat kemungkinan untuk mencipta hierarki kunci. Jika anda ingin mengenali konsep ini dengan lebih terperinci, maka anda dijemput untuk memerah otak anda mengenai "Masalah Ahli Falsafah Makan" ("masalah ahli falsafah makan").

Berikut ialah contoh yang baik tentang cara kedua-dua kunci akan berkelakuan dalam senario yang sama.

Mengenai pelaksanaan kunci. Saya tidak mahu pergi ke butiran, tetapi terdapat pengurus kunci untuk sistem yang diedarkan, contohnya: ZooKeeper, Redis, etcd, Consul.

7.3 Idempotensi operasi

Kod idempoten secara amnya merupakan amalan yang baik, dan ini betul-betul berlaku apabila pembangun adalah baik untuk dapat melakukan ini, tidak kira sama ada dia menggunakan transaksi atau tidak. Idempotensi ialah sifat operasi untuk menghasilkan hasil yang sama apabila operasi itu digunakan pada objek sekali lagi. Fungsi itu dipanggil - memberikan hasilnya. Dipanggil semula selepas satu atau lima saat - memberikan hasil yang sama. Sudah tentu, jika data dalam pangkalan data telah berubah, hasilnya akan berbeza. Data dalam sistem ketiga mungkin tidak bergantung pada fungsi, tetapi apa-apa yang dilakukan mesti boleh diramal.

Terdapat beberapa manifestasi mati pucuk. Salah satunya hanyalah cadangan tentang cara menulis kod anda. Adakah anda ingat bahawa fungsi terbaik adalah yang melakukan satu perkara? Dan apakah perkara yang baik untuk menulis ujian unit untuk fungsi ini? Jika anda mematuhi dua peraturan ini, maka anda sudah meningkatkan peluang bahawa fungsi anda akan menjadi idempoten. Untuk mengelakkan kekeliruan, saya akan menjelaskan bahawa fungsi idempoten tidak semestinya "tulen" (dalam erti kata "fungsi kesucian"). Fungsi tulen ialah fungsi yang beroperasi hanya pada data yang mereka terima pada input, tanpa mengubahnya dalam apa jua cara dan mengembalikan hasil yang diproses. Ini ialah fungsi yang membolehkan anda menskalakan aplikasi anda menggunakan teknik pengaturcaraan berfungsi. Oleh kerana kita bercakap tentang beberapa data umum dan pangkalan data, fungsi kita tidak mungkin tulen,

Ini adalah fungsi tulen:


def square(num: int) -> int: 
	return num * num 

Tetapi fungsi ini tidak tulen, tetapi idempoten (sila jangan membuat kesimpulan tentang cara saya menulis kod daripada kepingan ini):


def insert_data(insert_query: str, db_connection: DbConnectionType) -> int: 
  db_connection.execute(insert_query) 
  return True 

Daripada banyak perkataan, saya hanya boleh bercakap tentang bagaimana saya terpaksa belajar cara menulis program idempotent. Saya melakukan banyak kerja dengan AWS, seperti yang anda lihat sekarang, dan terdapat perkhidmatan yang dipanggil AWS Lambda. Lambda membenarkan anda untuk tidak menjaga pelayan, tetapi hanya memuatkan kod yang akan dijalankan sebagai tindak balas kepada beberapa acara atau mengikut jadual. Acara boleh menjadi mesej yang dihantar oleh broker mesej. Dalam AWS, broker ini ialah AWS SNS. Saya fikir ini harus jelas walaupun bagi mereka yang tidak bekerja dengan AWS: kami mempunyai broker yang menghantar mesej melalui saluran ("topik"), dan perkhidmatan mikro yang melanggan saluran ini menerima mesej dan entah bagaimana mereka bertindak balas.

Masalahnya ialah SNS menghantar mesej "sekurang-kurangnya sekali" ("sekurang-kurangnya-sekali penghantaran"). Apakah maksudnya? Lambat laun kod Lambda anda akan dipanggil dua kali. Dan ia benar-benar berlaku. Terdapat beberapa senario di mana fungsi anda perlu idempoten: sebagai contoh, apabila wang dikeluarkan daripada akaun, kita boleh mengharapkan seseorang mengeluarkan jumlah yang sama dua kali, tetapi kita perlu memastikan bahawa ini benar-benar 2 masa bebas - dalam erti kata lain, ini adalah 2 transaksi berbeza, dan bukan pengulangan satu.

Untuk perubahan, saya akan memberikan contoh lain - mengehadkan kekerapan permintaan kepada API (“penghad kadar”). Lambda kami menerima acara dengan user_id tertentu yang mana semakan harus dibuat untuk melihat sama ada pengguna dengan ID tersebut telah menghabiskan bilangan permintaan yang mungkin kepada beberapa API kami. Kami boleh menyimpan dalam DynamoDB daripada AWS nilai panggilan yang dibuat dan meningkatkannya dengan setiap panggilan ke fungsi kami sebanyak 1.

Tetapi bagaimana jika fungsi Lambda ini dipanggil oleh acara yang sama dua kali? Dengan cara ini, adakah anda memberi perhatian kepada hujah fungsi lambda_handler(). Argumen kedua, konteks dalam AWS Lambda diberikan secara lalai dan mengandungi pelbagai metadata, termasuk request_id yang dijana untuk setiap panggilan unik. Ini bermakna kini, daripada menyimpan bilangan panggilan yang dibuat dalam jadual, kami boleh menyimpan senarai request_id dan pada setiap panggilan Lambda kami akan menyemak sama ada 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"
    	})
	}

Oleh kerana contoh saya sebenarnya diambil dari Internet, saya akan meninggalkan pautan ke sumber asal, terutamanya kerana ia memberikan sedikit lagi maklumat.

Ingat bagaimana saya menyebut sebelum ini bahawa sesuatu seperti ID transaksi unik boleh digunakan untuk mengunci data kongsi? Kami kini telah mengetahui bahawa ia juga boleh digunakan untuk membuat operasi idempoten. Mari ketahui cara anda boleh menjana ID sedemikian sendiri.