7.1 為什麼需要

我們已經詳細討論了 ACID 的所有屬性、它們的用途和用例。如您所見,並非所有數據庫都提供 ACID 保證,為了更好的性能而犧牲它們。因此,很可能在您的項目中選擇了不提供 ACID 的數據庫,並且您可能需要在應用程序端實現一些必要的 ACID 功能。而且,如果您的系統被設計為微服務或某種其他類型的分佈式應用程序,那麼一個服務中的正常本地事務現在將變成分佈式事務——當然,將失去其 ACID 特性,即使數據庫每個單獨的微服務都是 ACID。

我不想為您提供關於如何創建事務管理器的詳盡指南,只是因為它太大太複雜,我只想介紹一些基本技術。如果我們不是在談論分佈式應用程序,那麼如果您需要 ACID 保證,我認為沒有理由嘗試在應用程序端完全實現 ACID - 畢竟,採用現成的解決方案在任何意義上都會更容易和更便宜(即,具有 ACID 的數據庫)。

但我想向您展示一些可以幫助您在應用程序端進行交易的技術。畢竟,了解這些技術可以在各種場景中為您提供幫助,甚至是那些不一定涉及事務的場景,並使您成為更好的開發人員(我希望如此)。

7.2 交易愛好者的基本工具

樂觀和悲觀阻塞。這是對某些可以同時訪問的數據的兩種類型的鎖。

樂天派假設並發訪問的可能性不是很大,因此它執行以下操作:讀取所需的行,記住它的版本號(或時間戳,或校驗和/哈希 - 如果您不能更改數據方案並為版本添加一列或時間戳),並且在將此數據的更改寫入數據庫之前,它會檢查此數據的版本是否已更改。如果版本已更改,則您需要以某種方式解決創建的衝突並更新數據(“提交”),或回滾事務(“回滾”)。這種方法的缺點是為長名稱“time-of-check to time-of-use”的bug創造了有利條件,縮寫為TOCTOU:狀態可能在check和write之間的時間段發生變化。我沒有樂觀鎖定的經驗,

例如,我從開發人員的日常生活中發現了一種使用樂觀鎖定之類的技術——這就是 HTTP 協議。對初始 HTTP GET 請求的響應可以包括用於來自客戶端的後續 PUT 請求的 ETag 標頭,客戶端可以在 If-Match 標頭中使用它。對於 GET 和 HEAD 方法,服務器只有在與它知道的 ETag 之一匹配時才會發回請求的資源。對於 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,這兩個進程都無法繼續執行。有多種方法可以解決這個問題——我現在不想詳述,所以先閱讀維基百科,但簡而言之,有創建鎖層次結構的可能性。如果你想更詳細地了解這個概念,那麼請你絞盡腦汁研究“Dinning Philosophers Problem”(“哲學家用餐問題”)。

是一個很好的例子,說明兩個鎖在同一場景中的行為方式。

關於鎖的實現。我不想細說,但是有分佈式系統的鎖管理器,例如:ZooKeeper、Redis、etcd、Consul。

7.3 操作的冪等性

冪等代碼通常是一種很好的做法,這正是開發人員能夠做到這一點的最佳做法,無論他是否使用事務。冪等性是操作的屬性,當該操作再次應用於對象時會產生相同的結果。該函數被調用 - 給出了結果。一秒或五秒後再次調用 - 給出相同的結果。當然,如果數據庫中的數據發生了變化,結果就不一樣了。第三方系統中的數據可能不依賴於函數,但任何依賴於函數的東西都必須是可預測的。

冪等性可以有多種表現形式。其中之一隻是關於如何編寫代碼的建議。你還記得最好的函數是只做一件事的函數嗎?為這個功能編寫單元測試有什麼好處呢?如果你遵守這兩條規則,那麼你已經增加了你的函數是冪等的機會。為避免混淆,我將澄清冪等函數不一定是“純”的(在“函數純度”的意義上)。純函數是那些只對它們在輸入端接收到的數據進行操作的函數,不會以任何方式更改它們並返回處理後的結果。這些函數允許您使用函數式編程技術擴展您的應用程序。由於我們正在談論一些通用數據和數據庫,因此我們的功能不太可能是純粹的,

這是一個純函數:


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

但是這個函數並不純粹,而是冪等的(請不要從這些片段中得出我是如何寫代碼的結論):


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 傳遞消息“至少一次”(“at-least-once delivery”)。這是什麼意思?您的 Lambda 代碼遲早會被調用兩次。它確實發生了。有很多場景需要你的函數是冪等的:例如,當從一個賬戶中取款時,我們可以預期某人取款兩次相同的金額,但我們需要確保這真的是 2 個獨立的時間 -換句話說,這是兩個不同的交易,而不是一個的重複。

作為改變,我將舉另一個例子——限制對 API 的請求頻率(“速率限制”)。我們的 Lambda 接收到一個具有特定 user_id 的事件,應該檢查該事件以查看具有該 ID 的用戶是否已用完他對我們某些 API 的可能請求數。我們可以將調用的值從 AWS 存儲在 DynamoDB 中,並在每次調用我們的函數時將其增加 1。

但是如果這個 Lambda 函數被同一個事件調用兩次怎麼辦?對了,你有沒有註意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"
    	})
	}

由於我的示例實際上是從 Internet 上獲取的,因此我會留下原始來源的鏈接,尤其是因為它提供了更多信息。

還記得我上面提到的類似唯一事務 ID 的東西可以用來鎖定共享數據嗎?我們現在了解到它也可以用來使操作冪等。讓我們找出您可以自己生成此類 ID 的方法。