7.1 Tại sao cần thiết

Chúng ta đã thảo luận một số chi tiết về tất cả các thuộc tính của ACID, mục đích và các trường hợp sử dụng của chúng. Như bạn có thể thấy, không phải tất cả các cơ sở dữ liệu đều cung cấp bảo đảm ACID, hy sinh chúng để có hiệu suất tốt hơn. Do đó, rất có thể xảy ra trường hợp cơ sở dữ liệu không cung cấp ACID được chọn trong dự án của bạn và bạn có thể cần triển khai một số chức năng ACID cần thiết ở phía ứng dụng. Và nếu hệ thống của bạn được thiết kế dưới dạng microservice hoặc một số loại ứng dụng phân tán khác, thì giao dịch cục bộ bình thường trong một dịch vụ giờ đây sẽ trở thành giao dịch phân tán - và tất nhiên, sẽ mất đi bản chất ACID của nó, ngay cả khi cơ sở dữ liệu của mỗi microservice riêng lẻ sẽ là ACID.

Tôi không muốn cung cấp cho bạn một hướng dẫn toàn diện về cách tạo trình quản lý giao dịch, đơn giản vì nó quá lớn và phức tạp, và tôi chỉ muốn trình bày một số kỹ thuật cơ bản. Nếu chúng ta không nói về các ứng dụng phân tán, thì tôi thấy không có lý do gì để cố gắng triển khai đầy đủ ACID ở phía ứng dụng nếu bạn cần đảm bảo ACID - xét cho cùng, việc sử dụng một giải pháp làm sẵn sẽ dễ dàng và rẻ hơn theo mọi nghĩa ( nghĩa là cơ sở dữ liệu có ACID).

Nhưng tôi muốn chỉ cho bạn một số kỹ thuật sẽ giúp bạn thực hiện các giao dịch ở phía ứng dụng. Xét cho cùng, việc biết những kỹ thuật này có thể giúp bạn trong nhiều tình huống khác nhau, ngay cả những tình huống không nhất thiết liên quan đến giao dịch và giúp bạn trở thành nhà phát triển tốt hơn (tôi hy vọng vậy).

7.2 Các công cụ cơ bản dành cho những người yêu thích giao dịch

Chặn lạc quan và bi quan. Đây là hai loại khóa trên một số dữ liệu có thể được truy cập cùng một lúc.

người lạc quangiả định rằng xác suất truy cập đồng thời không quá lớn và do đó, nó thực hiện như sau: đọc dòng mong muốn, ghi nhớ số phiên bản của nó (hoặc dấu thời gian hoặc tổng kiểm tra / hàm băm - nếu bạn không thể thay đổi lược đồ dữ liệu và thêm một cột cho phiên bản hoặc dấu thời gian) và trước khi ghi các thay đổi vào cơ sở dữ liệu cho dữ liệu này, nó sẽ kiểm tra xem phiên bản của dữ liệu này đã thay đổi chưa. Nếu phiên bản đã thay đổi, thì bạn cần giải quyết bằng cách nào đó xung đột đã tạo và cập nhật dữ liệu (“cam kết”) hoặc khôi phục giao dịch (“rollback”). Nhược điểm của phương pháp này là tạo điều kiện thuận lợi cho bug có tên dài “time-of-check to time-of-use”, viết tắt là TOCTOU: trạng thái có thể thay đổi trong khoảng thời gian giữa check và write. Tôi không có kinh nghiệm với khóa lạc quan,

Ví dụ: tôi đã tìm thấy một công nghệ trong cuộc sống hàng ngày của nhà phát triển sử dụng thứ gì đó như khóa lạc quan - đây là giao thức HTTP. Phản hồi cho yêu cầu HTTP GET ban đầu CÓ THỂ bao gồm tiêu đề ETag cho các yêu cầu PUT tiếp theo từ máy khách, mà máy khách CÓ THỂ sử dụng trong tiêu đề If-Match. Đối với các phương thức GET và HEAD, máy chủ sẽ chỉ gửi lại tài nguyên được yêu cầu nếu nó khớp với một trong các ETags mà nó biết. Đối với PUT và các phương thức không an toàn khác, nó cũng sẽ chỉ tải tài nguyên trong trường hợp này. Nếu bạn không biết cách thức hoạt động của ETag, thì đây là một ví dụ hay khi sử dụng thư viện "feedparser" (giúp phân tích cú pháp RSS và các nguồn cấp dữ liệu khác).


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

Mặt khác, người bi quan xuất phát từ thực tế là các giao dịch sẽ thường “gặp nhau” trên cùng một dữ liệu và để đơn giản hóa cuộc sống của anh ta cũng như tránh các điều kiện chạy đua không cần thiết, anh ta chỉ cần chặn dữ liệu mình cần . Để triển khai cơ chế khóa, bạn cần duy trì kết nối cơ sở dữ liệu cho phiên của mình (thay vì lấy kết nối từ nhóm - trong trường hợp đó, rất có thể bạn sẽ phải làm việc với khóa lạc quan) hoặc sử dụng ID cho giao dịch , có thể được sử dụng bất kể kết nối. Nhược điểm của khóa bi quan là việc sử dụng nó làm chậm quá trình xử lý giao dịch nói chung, nhưng bạn có thể yên tâm về dữ liệu và nhận được sự cô lập thực sự.

Tuy nhiên, một mối nguy hiểm bổ sung ẩn nấp trong bế tắc có thể xảy ra, trong đó một số quy trình chờ tài nguyên bị khóa bởi nhau. Ví dụ: một giao dịch yêu cầu tài nguyên A và B. Tiến trình 1 đã chiếm tài nguyên A và tiến trình 2 đã chiếm tài nguyên B. Cả hai tiến trình đều không thể tiếp tục thực hiện. Có nhiều cách khác nhau để giải quyết vấn đề này - tôi không muốn đi vào chi tiết ngay bây giờ, vì vậy hãy đọc Wikipedia trước, nhưng tóm lại, có khả năng tạo một hệ thống phân cấp khóa. Nếu bạn muốn tìm hiểu chi tiết hơn về khái niệm này, thì mời bạn vắt óc suy nghĩ về “Vấn đề các nhà triết học ăn uống” (“bài toán các nhà triết học ăn uống”).

Đây là một ví dụ điển hình về cách cả hai khóa sẽ hoạt động trong cùng một tình huống.

Về việc triển khai khóa. Tôi không muốn đi sâu vào chi tiết, nhưng có những trình quản lý khóa cho các hệ thống phân tán, ví dụ: ZooKeeper, Redis, v.v., Consul.

7.3 Tính không ổn định của các hoạt động

Mã bình thường nói chung là một phương pháp hay và đây chính xác là trường hợp tốt cho nhà phát triển để có thể làm điều này, bất kể anh ta có sử dụng giao dịch hay không. Idempotency là thuộc tính của một hoạt động để tạo ra kết quả tương tự khi hoạt động đó được áp dụng lại cho một đối tượng. Hàm được gọi - đưa ra kết quả. Được gọi lại sau một hoặc năm giây - cho kết quả tương tự. Tất nhiên, nếu dữ liệu trong cơ sở dữ liệu đã thay đổi, kết quả sẽ khác. Dữ liệu trong các hệ thống thứ ba có thể không phụ thuộc vào một chức năng, nhưng bất kỳ thứ gì phụ thuộc vào nó đều phải dự đoán được.

Có thể có một số biểu hiện của sự bất lực. Một trong số đó chỉ là đề xuất về cách viết mã của bạn. Bạn có nhớ rằng chức năng tốt nhất là chức năng thực hiện một việc không? Và điều gì sẽ là một điều tốt để viết các bài kiểm tra đơn vị cho chức năng này? Nếu bạn tuân thủ hai quy tắc này, thì bạn đã tăng khả năng các chức năng của mình trở nên bình thường. Để tránh nhầm lẫn, tôi sẽ làm rõ rằng các hàm idempotent không nhất thiết phải là "thuần khiết" (theo nghĩa là "độ tinh khiết của hàm"). Các hàm thuần túy là những hàm chỉ hoạt động trên dữ liệu mà chúng nhận được ở đầu vào mà không thay đổi chúng theo bất kỳ cách nào và trả về kết quả đã xử lý. Đây là những chức năng cho phép bạn mở rộng ứng dụng của mình bằng các kỹ thuật lập trình chức năng. Vì chúng ta đang nói về một số dữ liệu chung và cơ sở dữ liệu, nên các chức năng của chúng ta có thể không thuần túy,

Đây là một chức năng thuần túy:


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

Nhưng chức năng này không thuần túy, mà bình thường (vui lòng không đưa ra kết luận về cách tôi viết mã từ những phần này):


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

Thay vì nhiều lời, tôi chỉ có thể nói về việc tôi đã buộc phải học cách viết các chương trình idempotent như thế nào. Tôi làm rất nhiều việc với AWS, như bạn có thể thấy bây giờ, và có một dịch vụ gọi là AWS Lambda. Lambda cho phép bạn không cần chăm sóc máy chủ mà chỉ cần tải mã sẽ chạy theo một số sự kiện hoặc theo lịch trình. Một sự kiện có thể là các tin nhắn được gửi bởi một nhà môi giới tin nhắn. Trong AWS, nhà môi giới này là AWS SNS. Tôi nghĩ rằng điều này nên rõ ràng ngay cả đối với những người không làm việc với AWS: chúng tôi có một nhà môi giới gửi tin nhắn qua các kênh (“chủ đề”) và các dịch vụ siêu nhỏ đã đăng ký các kênh này sẽ nhận được tin nhắn và bằng cách nào đó chúng sẽ phản ứng lại.

Vấn đề là SNS gửi tin nhắn "ít nhất một lần" ("at-least-once delivery"). Nó có nghĩa là gì? Điều đó sớm hay muộn mã Lambda của bạn sẽ được gọi hai lần. Và nó thực sự xảy ra. Có một số tình huống mà chức năng của bạn cần phải bình thường: ví dụ: khi tiền được rút từ tài khoản, chúng tôi có thể yêu cầu ai đó rút cùng một số tiền hai lần, nhưng chúng tôi cần đảm bảo rằng đây thực sự là 2 lần độc lập - nói cách khác, đây là 2 giao dịch khác nhau và không phải là sự lặp lại của một giao dịch.

Để thay đổi, tôi sẽ đưa ra một ví dụ khác - giới hạn tần suất yêu cầu đối với API (“giới hạn tốc độ”). Lambda của chúng tôi nhận được một sự kiện với một user_id nhất định. Sự kiện này sẽ được thực hiện để kiểm tra xem liệu người dùng có ID đó đã sử dụng hết số lượng yêu cầu có thể có của anh ấy đối với một số API của chúng tôi hay chưa. Chúng ta có thể lưu trữ trong DynamoDB từ AWS giá trị của các lệnh gọi được thực hiện và tăng giá trị đó lên 1 sau mỗi lệnh gọi hàm của chúng ta.

Nhưng nếu hàm Lambda này được gọi bởi cùng một sự kiện hai lần thì sao? Nhân tiện, bạn có để ý đến các đối số của hàm lambda_handler() không. Đối số thứ hai, ngữ cảnh trong AWS Lambda được cung cấp theo mặc định và chứa nhiều siêu dữ liệu khác nhau, bao gồm cả request_id được tạo cho mỗi lệnh gọi duy nhất. Điều này có nghĩa là bây giờ, thay vì lưu số lượng lệnh gọi được thực hiện trong bảng, chúng ta có thể lưu trữ một danh sách request_id và trên mỗi lệnh gọi, Lambda của chúng ta sẽ kiểm tra xem yêu cầu đã cho đã được xử lý chưa:

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

Vì ví dụ của tôi thực sự được lấy từ Internet, nên tôi sẽ để lại một liên kết đến nguồn ban đầu, đặc biệt là vì nó cung cấp thêm một chút thông tin.

Hãy nhớ cách tôi đã đề cập ở trên rằng một thứ như ID giao dịch duy nhất có thể được sử dụng để khóa dữ liệu được chia sẻ? Bây giờ chúng ta đã biết rằng nó cũng có thể được sử dụng để thực hiện các hoạt động bình thường. Hãy cùng tìm hiểu xem bạn có thể tự tạo những ID như vậy theo những cách nào.