7.1 เหตุใดจึงจำเป็น

เราได้กล่าวถึงรายละเอียดคุณสมบัติทั้งหมดของกรด วัตถุประสงค์และกรณีการใช้งาน อย่างที่คุณเห็น ไม่ใช่ฐานข้อมูลทั้งหมดที่มีการรับประกันกรด โดยยอมเสียสละเพื่อประสิทธิภาพที่ดีขึ้น ดังนั้น อาจเป็นไปได้ว่ามีการเลือกฐานข้อมูลที่ไม่มี ACID ในโครงการของคุณ และคุณอาจต้องใช้ฟังก์ชัน ACID ที่จำเป็นบางอย่างในด้านแอปพลิเคชัน และถ้าระบบของคุณได้รับการออกแบบให้เป็นไมโครเซอร์วิสหรือแอปพลิเคชันแบบกระจายประเภทอื่นๆ สิ่งที่จะเป็นธุรกรรมในท้องถิ่นตามปกติในบริการหนึ่งจะกลายเป็นธุรกรรมแบบกระจาย และแน่นอนว่าจะสูญเสียความเป็นกรดแม้ว่าฐานข้อมูลของ แต่ละไมโครเซอร์วิสจะเป็นกรด

ฉันไม่ต้องการให้คำแนะนำที่ละเอียดถี่ถ้วนเกี่ยวกับวิธีสร้างตัวจัดการธุรกรรม เพียงเพราะมันใหญ่และซับซ้อนเกินไป และฉันต้องการครอบคลุมเทคนิคพื้นฐานบางอย่างเท่านั้น หากเราไม่ได้พูดถึงแอปพลิเคชันแบบกระจาย ฉันไม่เห็นเหตุผลที่จะพยายามใช้ ACID อย่างเต็มที่ในด้านแอปพลิเคชัน หากคุณต้องการการรับประกันกรด เพราะท้ายที่สุด การใช้โซลูชันสำเร็จรูปจะง่ายกว่าและถูกกว่าในทุกแง่มุม ( นั่นคือฐานข้อมูลที่มีกรด)

แต่ฉันอยากจะแสดงเทคนิคบางอย่างที่จะช่วยคุณในการทำธุรกรรมในฝั่งแอปพลิเคชัน ท้ายที่สุด การรู้เทคนิคเหล่านี้สามารถช่วยคุณได้ในสถานการณ์ต่างๆ แม้กระทั่งสถานการณ์ที่ไม่จำเป็นต้องเกี่ยวข้องกับการทำธุรกรรม และทำให้คุณเป็นนักพัฒนาซอฟต์แวร์ที่ดีขึ้น (หวังว่าจะเป็นเช่นนั้น)

7.2 เครื่องมือพื้นฐานสำหรับผู้ชื่นชอบการทำธุรกรรม

การปิดกั้นในแง่ดีและแง่ร้าย นี่คือการล็อกสองประเภทสำหรับข้อมูลบางอย่างที่สามารถเข้าถึงได้พร้อมกัน

คนมองโลกในแง่ดีถือว่าความน่าจะเป็นของการเข้าถึงพร้อมกันนั้นไม่ดีนัก ดังนั้นจึงทำสิ่งต่อไปนี้: อ่านบรรทัดที่ต้องการ จดจำหมายเลขเวอร์ชัน (หรือประทับเวลา หรือเช็คซัม / แฮช - หากคุณไม่สามารถเปลี่ยนสคีมาข้อมูลและเพิ่มคอลัมน์สำหรับเวอร์ชันได้ หรือการประทับเวลา) และก่อนที่จะเขียนการเปลี่ยนแปลงไปยังฐานข้อมูลสำหรับข้อมูลนี้ ระบบจะตรวจสอบว่าเวอร์ชันของข้อมูลนี้มีการเปลี่ยนแปลงหรือไม่ หากเวอร์ชันมีการเปลี่ยนแปลง คุณต้องแก้ไขข้อขัดแย้งที่สร้างขึ้นและอัปเดตข้อมูล ("คอมมิต") หรือย้อนกลับธุรกรรม ("ย้อนกลับ") ข้อเสียของวิธีนี้คือการสร้างเงื่อนไขที่เอื้ออำนวยสำหรับข้อผิดพลาดที่มีชื่อยาวว่า “time-of-check to time-of-use” ซึ่งเรียกโดยย่อว่า TOCTOU: สถานะอาจเปลี่ยนแปลงในช่วงเวลาระหว่างการตรวจสอบและการเขียน ฉันไม่มีประสบการณ์เกี่ยวกับการล็อกในแง่ดี

ตัวอย่างเช่น ฉันพบเทคโนโลยีหนึ่งจากชีวิตประจำวันของนักพัฒนาซอฟต์แวร์ที่ใช้บางอย่าง เช่น การล็อกในแง่ดี นั่นคือโปรโตคอล HTTP การตอบสนองต่อคำขอ HTTP GET เริ่มต้นอาจมีส่วนหัว ETag สำหรับคำขอ PUT ที่ตามมาจากไคลเอ็นต์ ซึ่งไคลเอ็นต์อาจใช้ในส่วนหัวแบบ If-Match สำหรับเมธอด GET และ HEAD เซิร์ฟเวอร์จะส่งทรัพยากรที่ร้องขอกลับเฉพาะเมื่อตรงกับ ETags ตัวใดตัวหนึ่งที่รู้จัก สำหรับ PUT และวิธีการที่ไม่ปลอดภัยอื่นๆ ก็จะโหลดทรัพยากรในกรณีนี้เช่นกัน หากคุณไม่ทราบวิธีการทำงานของ ETag นี่เป็นตัวอย่างที่ดีในการใช้ไลบรารี "feedparser" (ซึ่งช่วยแยกวิเคราะห์ RSS และฟีดอื่นๆ)


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

ในทางกลับกัน คนมองโลกในแง่ร้ายได้กำไรจากข้อเท็จจริงที่ว่าธุรกรรมมักจะ "ตรงกัน" ในข้อมูลเดียวกัน และเพื่อให้ชีวิตของเขาง่ายขึ้นและหลีกเลี่ยงสภาวะการแข่งขันที่ไม่จำเป็น เขาเพียงบล็อกข้อมูลที่เขาต้องการ ในการใช้กลไกการล็อก คุณจะต้องรักษาการเชื่อมต่อฐานข้อมูลสำหรับเซสชันของคุณ (แทนที่จะดึงการเชื่อมต่อจากพูล ซึ่งในกรณีนี้ คุณมักจะต้องทำงานกับการล็อกในแง่ดี) หรือใช้ ID สำหรับธุรกรรม ซึ่งสามารถใช้ได้โดยไม่คำนึงถึงการเชื่อมต่อ ข้อเสียของการล็อกในแง่ร้ายคือการใช้งานทำให้การประมวลผลธุรกรรมโดยทั่วไปช้าลง แต่คุณสามารถสงบสติอารมณ์เกี่ยวกับข้อมูลและแยกตัวออกจากกันได้อย่างแท้จริง

อย่างไรก็ตาม อันตรายเพิ่มเติมแฝงตัวอยู่ในการหยุดชะงักที่เป็นไปได้ ซึ่งกระบวนการต่างๆ รอให้ทรัพยากรถูกล็อกโดยกันและกัน ตัวอย่างเช่น ธุรกรรมต้องการทรัพยากร A และ B กระบวนการ 1 ครอบครองทรัพยากร A และกระบวนการ 2 ครอบครองทรัพยากร B ทั้งสองกระบวนการไม่สามารถดำเนินการต่อได้ มีหลายวิธีในการแก้ปัญหานี้ - ฉันไม่ต้องการลงรายละเอียดในตอนนี้ ดังนั้นอ่านวิกิพีเดียก่อน แต่ในระยะสั้นมีความเป็นไปได้ที่จะสร้างลำดับชั้นของการล็อก หากคุณต้องการทราบแนวคิดนี้โดยละเอียดยิ่งขึ้น ขอเชิญคุณมาระดมสมองของคุณกับ “ปัญหานักปรัชญาการรับประทานอาหาร” (“ปัญหานักปรัชญาการรับประทานอาหาร”)

นี่คือตัวอย่างที่ดีของการล็อคทั้งสองจะทำงานในสถานการณ์เดียวกัน

เกี่ยวกับการใช้งานล็อค ฉันไม่ต้องการลงรายละเอียด แต่มีตัวจัดการล็อคสำหรับระบบแบบกระจายเช่น: ZooKeeper, Redis, etcd, Consul

7.3 ศักยภาพของการดำเนินงาน

โดยทั่วไปแล้วรหัส Idempotent เป็นแนวปฏิบัติที่ดี และเป็นกรณีที่นักพัฒนาสามารถทำเช่นนี้ได้โดยไม่คำนึงว่าเขาจะใช้ธุรกรรมหรือไม่ก็ตาม Idempotency เป็นคุณสมบัติของการดำเนินการเพื่อสร้างผลลัพธ์เดียวกันเมื่อมีการใช้การดำเนินการนั้นกับวัตถุอีกครั้ง ฟังก์ชั่นถูกเรียกใช้ - ให้ผลลัพธ์ โทรอีกครั้งหลังจากหนึ่งหรือห้าวินาที - ให้ผลลัพธ์เดียวกัน แน่นอนว่าหากข้อมูลในฐานข้อมูลมีการเปลี่ยนแปลงผลลัพธ์ก็จะแตกต่างออกไป ข้อมูลในระบบที่สามอาจไม่ขึ้นอยู่กับฟังก์ชัน แต่สิ่งใดที่ต้องคาดเดาได้

อาจมีอาการหลายอย่างผิดปกติ หนึ่งในนั้นเป็นเพียงคำแนะนำเกี่ยวกับวิธีการเขียนโค้ดของคุณ คุณจำได้ไหมว่าหน้าที่ที่ดีที่สุดคือหน้าที่ที่ทำสิ่งหนึ่ง? และอะไรคือสิ่งที่ดีในการเขียนการทดสอบหน่วยสำหรับฟังก์ชันนี้ หากคุณปฏิบัติตามกฎสองข้อนี้ แสดงว่าคุณเพิ่มโอกาสที่ฟังก์ชันของคุณจะหมดอำนาจแล้ว เพื่อหลีกเลี่ยงความสับสน ฉันจะชี้แจงว่าฟังก์ชัน idempotent ไม่จำเป็นต้อง "บริสุทธิ์" (ในแง่ของ "ฟังก์ชันบริสุทธิ์") ฟังก์ชันบริสุทธิ์คือฟังก์ชันที่ทำงานเฉพาะกับข้อมูลที่ได้รับจากอินพุต โดยไม่มีการเปลี่ยนแปลงแต่อย่างใดและส่งคืนผลลัพธ์ที่ประมวลผล นี่คือฟังก์ชันที่ช่วยให้คุณปรับขนาดแอปพลิเคชันของคุณโดยใช้เทคนิคการเขียนโปรแกรมเชิงฟังก์ชัน เนื่องจากเรากำลังพูดถึงข้อมูลทั่วไปและฐานข้อมูล ฟังก์ชันของเราจึงไม่น่าจะบริสุทธิ์

นี่เป็นฟังก์ชั่นที่บริสุทธิ์:


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

แต่ฟังก์ชั่นนี้ไม่บริสุทธิ์ แต่เป็น idempotent (โปรดอย่าสรุปเกี่ยวกับวิธีที่ฉันเขียนโค้ดจากชิ้นส่วนเหล่านี้):


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

แทนที่จะเป็นคำพูดมากมาย ฉันแค่พูดได้ว่าฉันถูกบังคับให้เรียนรู้วิธีเขียนโปรแกรมไร้อำนาจได้อย่างไร ฉันทำงานหลายอย่างกับ AWS อย่างที่คุณเห็นในตอนนี้ และมีบริการที่เรียกว่า AWS Lambda Lambda ช่วยให้คุณไม่ต้องดูแลเซิร์ฟเวอร์ แต่เพียงโหลดโค้ดที่จะรันตามเหตุการณ์บางอย่างหรือตามกำหนดการ เหตุการณ์สามารถเป็นข้อความที่ส่งโดยนายหน้าข้อความ ใน AWS โบรกเกอร์นี้คือ AWS SNS ฉันคิดว่าสิ่งนี้ควรชัดเจนแม้กระทั่งสำหรับผู้ที่ไม่ได้ทำงานกับ AWS: เรามีนายหน้าที่ส่งข้อความผ่านช่องทาง ("หัวข้อ") และบริการขนาดเล็กที่สมัครรับข้อมูลจากช่องทางเหล่านี้จะได้รับข้อความและพวกเขาตอบสนองด้วยวิธีใด

ปัญหาคือ SNS ส่งข้อความ "อย่างน้อยหนึ่งครั้ง" ("การส่งอย่างน้อยหนึ่งครั้ง") มันหมายความว่าอะไร? ไม่ช้าก็เร็วรหัสแลมบ์ดาของคุณจะถูกเรียกสองครั้ง และมันเกิดขึ้นจริงๆ มีหลายสถานการณ์ที่ฟังก์ชันของคุณต้องไร้อำนาจ: ตัวอย่างเช่น เมื่อเงินถูกถอนออกจากบัญชี เราสามารถคาดหวังให้ใครบางคนถอนเงินจำนวนเท่ากัน 2 ครั้ง แต่เราต้องแน่ใจว่านี่เป็น 2 ครั้งที่แยกจากกันจริงๆ - กล่าวอีกนัยหนึ่ง ธุรกรรมเหล่านี้เป็น 2 ธุรกรรมที่แตกต่างกัน และไม่ใช่ธุรกรรมซ้ำซ้อนกัน

สำหรับการเปลี่ยนแปลง ฉันจะให้อีกตัวอย่างหนึ่ง - การจำกัดความถี่ของคำขอไปยัง API (“การจำกัดอัตรา”) Lambda ของเราได้รับเหตุการณ์ที่มี user_id บางอย่าง ซึ่งควรทำการตรวจสอบเพื่อดูว่าผู้ใช้ที่มี ID นั้นได้ส่งคำขอที่เป็นไปได้ไปยัง API บางส่วนของเราจนหมดแล้วหรือไม่ เราสามารถจัดเก็บมูลค่าของการโทรใน DynamoDB จาก AWS และเพิ่มมูลค่าด้วยการเรียกไปยังฟังก์ชันของเราแต่ละครั้งได้ 1

แต่จะเกิดอะไรขึ้นถ้าฟังก์ชันแลมบ์ดานี้ถูกเรียกใช้โดยเหตุการณ์เดียวกันสองครั้ง อย่างไรก็ตาม คุณได้ให้ความสนใจกับอาร์กิวเมนต์ของฟังก์ชัน lambda_handler() หรือไม่ อาร์กิวเมนต์ที่สอง บริบทใน AWS Lambda ถูกกำหนดโดยค่าเริ่มต้นและมีข้อมูลเมตาต่างๆ รวมถึง request_id ที่สร้างขึ้นสำหรับการโทรที่ไม่ซ้ำกันแต่ละครั้ง ซึ่งหมายความว่า ในตอนนี้ แทนที่จะเก็บจำนวนการโทรในตาราง เราสามารถจัดเก็บรายการ request_id และในการเรียกแต่ละครั้ง Lambda ของเราจะตรวจสอบว่าคำขอนั้นได้รับการประมวลผลแล้วหรือไม่:

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

เนื่องจากตัวอย่างของฉันนำมาจากอินเทอร์เน็ตจริง ๆ ฉันจะทิ้งลิงก์ไปยังแหล่งที่มาดั้งเดิม โดยเฉพาะอย่างยิ่งเนื่องจากมันให้ข้อมูลเพิ่มเติมเล็กน้อย

จำได้ไหมว่าฉันพูดถึงก่อนหน้านี้ว่าสามารถใช้รหัสธุรกรรมเฉพาะเพื่อล็อกข้อมูลที่แชร์ได้ ตอนนี้เราได้เรียนรู้ว่ามันสามารถใช้เพื่อทำให้ปฏิบัติการไร้อำนาจ มาดูกันว่าคุณสามารถสร้าง ID ดังกล่าวด้วยตัวคุณเองด้วยวิธีใดบ้าง