Sửa lỗi DynamoDB TransactionConflictException: Khi các Transaction xung đột

intermediate☁️ AWS2026-07-11| AWS SDK (Boto3, JavaScript SDK), AWS Lambda, DynamoDB API

Error Message

TransactionConflictException: Transaction is ongoing for the item
#dynamodb#aws-lambda#he-thong-phan-tan#backend#boto3

Cuộc gọi lúc 2 giờ sáng

Điện thoại của bạn rung lên. Lúc đó là 2 giờ sáng và bảng điều khiển giám sát (monitoring dashboard) là một "biển đỏ". Một đợt lưu lượng truy cập tăng đột biến vừa kích hoạt hàng loạt lỗi 500 trong dịch vụ xử lý đơn hàng của bạn. Khi bạn kiểm tra CloudWatch logs, một lỗi cụ thể xuất hiện hàng nghìn lần:

ClientError: An error occurred (TransactionConflictException) when calling the TransactWriteItems operation: Transaction is ongoing for the item

Điều này xảy ra do TransactWriteItemsTransactGetItems yêu cầu quyền truy cập độc quyền (exclusive access) vào các item trong quá trình thực hiện. Nếu Transaction A vẫn đang được xử lý và Transaction B cố gắng tác động vào cùng một partition key, AWS sẽ từ chối Transaction B ngay lập tức. Nó không đưa yêu cầu vào hàng đợi; nó chỉ đơn giản là hủy bỏ yêu cầu đó.

Tại sao các Transaction của bạn bị lỗi

Trong sự cố hệ thống thực tế gần đây của chúng tôi, chúng tôi đã cập nhật một item 'Stock' (Kho hàng) và tạo một bản ghi 'Order' (Đơn hàng) trong một bước nguyên tử (atomic step). Trong một chương trình flash sale, hơn 800 thực thi Lambda đồng thời đã cố gắng giảm cùng một item 'Stock' tại cùng một mili giây.

Các transaction trong DynamoDB tuân thủ tính chất ACID, nhưng chúng không được thiết kế cho việc tranh chấp cao (high-contention) trên một dòng duy nhất. Hãy tưởng tượng nó giống như một hành lang hẹp. Nếu một người đang đi qua, những người khác phải đợi bên ngoài. Nếu 100 người cố gắng chen lấn cùng một lúc, DynamoDB sẽ bắt đầu từ chối mọi người để ngăn chặn tình trạng tắc nghẽn.

Giải pháp: Cách tiếp cận ba hướng

Việc tăng provisioned throughput (thông lượng được cấp sẵn) sẽ không giải quyết được vấn đề này. Bạn cần thay đổi cách ứng dụng của mình xử lý tình trạng "tắc nghẽn giao thông" này.

1. Thử lại thông minh với Jitter

Cách khắc phục nhanh nhất là bắt ngoại lệ (exception) và thử lại. Nhưng nếu 100 yêu cầu bị lỗi đều thử lại chính xác sau 100ms, chúng sẽ lại va chạm một lần nữa. Đây là vấn đề "thundering herd" (đám đông ồ ạt). Bạn phải sử dụng cơ chế exponential backoff (thử lại với độ trễ tăng dần) kết hợp với jitter (độ trễ ngẫu nhiên).

Dưới đây là một triển khai mạnh mẽ bằng Python và Boto3:

import boto3
import time
import random
from botocore.exceptions import ClientError

dynamodb = boto3.client('dynamodb')

def update_stock_with_retry(item_id, quantity, max_retries=5):
    for attempt in range(max_retries):
        try:
            return dynamodb.transact_write_items(
                TransactItems=[
                    {
                        'Update': {
                            'TableName': 'Inventory',
                            'Key': {'ID': {'S': item_id}},
                            'UpdateExpression': 'SET stock = stock - :q',
                            'ExpressionAttributeValues': {':q': {'N': str(quantity)}},
                            'ConditionExpression': 'stock >= :q'
                        }
                    }
                ]
            )
        except ClientError as e:
            if e.response['Error']['Code'] == 'TransactionConflictException':
                # Exponential backoff: 50ms, 100ms, 200ms... 
                # Chúng ta thêm jitter ngẫu nhiên để phân tán tải
                wait_time = ((2 ** attempt) * 0.05) + random.uniform(0, 0.02)
                print(f"Collision on {item_id}. Retrying in {wait_time:.3f}s...")
                time.sleep(wait_time)
            else:
                raise e
    raise Exception("Transaction failed after maximum retry attempts.")

2. Thu hẹp phạm vi Transaction

Mỗi item bạn thêm vào một lệnh gọi TransactWriteItems sẽ làm tăng xác suất toán học xảy ra xung đột. Liệu dấu thời gian 'LastLogin' có thực sự cần được cập nhật trong cùng một transaction với một 'Purchase' (Giao dịch mua hàng) tài chính hay không? Chắc là không. Hãy chuyển các cập nhật không quan trọng sang một lệnh gọi UpdateItem riêng biệt. Các cập nhật tiêu chuẩn sử dụng optimistic locking (khóa lạc quan), giúp xử lý đồng thời cao mượt mà hơn nhiều so với một transaction đầy đủ.

3. Sử dụng bộ đếm phân tán (Distributed Counters)

Nếu bạn đang gặp nút thắt cổ chai trên một bộ đếm toàn cục duy nhất, transaction không phải là công cụ phù hợp. Thay vào đó, hãy sử dụng mô hình sharding (phân mảnh). Chia bộ đếm của bạn thành 10 hoặc 20 dòng khác nhau (ví dụ: counter_shard_1, counter_shard_2). Ứng dụng của bạn chọn một shard ngẫu nhiên để cập nhật. Khi bạn cần tổng số lượng, chỉ cần cộng tất cả các shard lại. Điều này giúp phân tán "nhiệt" trên nhiều partition key khác nhau.

Xác minh bản sửa lỗi

Sau khi bạn triển khai logic backoff, hãy theo dõi sát sao các chỉ số (metrics) của mình. Bạn sẽ muốn thấy tỷ lệ lỗi giảm xuống trong khi vẫn duy trì được tính toàn vẹn của dữ liệu.

- **CloudWatch Metrics:** Theo dõi `TransactionCheckFailed`. Sau khi triển khai jitter, con số này có thể vẫn lớn hơn 0, nhưng các lỗi 500 ở cấp độ ứng dụng sẽ biến mất khi các lần thử lại thành công.
- **Latency Monitoring:** Theo dõi Lambda `Duration`. Việc thử lại sẽ cộng thêm vài mili giây vào thời gian thực thi của bạn. Nếu độ trễ (latency) tăng vọt trên 500ms, bạn có thể cần tăng số lượng shard.
- **Stress Testing:** Chạy một bài kiểm tra tải nhanh. Chúng tôi đã sử dụng một script đơn giản để gửi 50 yêu cầu đồng thời vào một item, và tỷ lệ thành công của chúng tôi đã tăng từ 12% lên 100% sau khi thêm các lần thử lại với jitter.

Những điểm chính cần lưu ý

Transaction trong DynamoDB rất mạnh mẽ, nhưng chúng không phải là sự thay thế trực tiếp cho các transaction SQL truyền thống. Chúng đi kèm với các giới hạn nghiêm ngặt về tính đồng thời.

- **Hạn chế Hot Keys:** Nếu một item được cập nhật hơn 5-10 lần mỗi giây, các transaction sẽ có khả năng gặp khó khăn.
- **Kiểm tra cài đặt SDK:** Nhiều bộ AWS SDK có các cơ chế thử lại mặc định, nhưng chúng thường không thử lại đối với lỗi `TransactionConflictException`. Luôn xác minh hành vi của bộ SDK cụ thể mà bạn đang dùng.
- **Đảm bảo tính Idempotency:** Sử dụng tham số `ClientRequestToken`. Điều này đảm bảo rằng nếu một lần thử lại thành công nhưng mạng bị timeout, bạn sẽ không vô tình tính phí khách hàng hai lần.

Related Error Notes