📖 Bản tiếng Anh (English Edition)

Bài trước: Phần 2 — Kiến Trúc Hướng Sự Kiện & Quản Trị Kafka Siêu Quy Mô | Danh mục Series | Bài tiếp: Phần 4 — Thực Tiễn SRE & Kiểm Thử Hỗn Loạn Chaos Mesh


Answer-first: Trước giới hạn ghi đơn điểm của Amazon Aurora MySQL trong đợt khuyến mãi, PayPay chuyển đổi sổ cái sang TiDB Distributed SQL. Bằng phân tán Multi-Raft trên TiKV, áp dụng khóa chính AUTO_RANDOM xóa điểm nóng ghi và giao dịch Percolator, TiDB mang lại khả năng mở rộng tuyến tính, online DDL không downtime và P99 dưới 15ms.

Điều kiện tiên quyết: Nắm vững giải thuật đồng thuận phân tán Raft, các cấp độ cô lập giao dịch ACID, cơ chế commit hai pha (2PC) và kiến trúc cơ sở dữ liệu NewSQL.


1. Tại Sao Amazon Aurora MySQL Chạm Ngưỡng Giới Hạn Vật Lý

Trong giai đoạn đầu tăng trưởng vũ bão từ năm 2018 đến 2020, PayPay đặt trọn niềm tin vào Amazon Aurora MySQL làm cơ sở dữ liệu giao dịch cốt lõi. Kiến trúc tách biệt tính toán và lưu trữ cùng hệ thống volume phân tán của Aurora hỗ trợ mở rộng đọc rất tốt thông qua các read replica. Tuy nhiên, khi lượng người dùng vượt mốc 40 triệu tài khoản và các chiến dịch khuyến mãi hoàn tiền quy mô toàn quốc bùng nổ, kiến trúc đơn điểm ghi (single-master) của Aurora đã đâm sầm vào bức tường giới hạn vật lý:

Nút Thắt Vận Hành Của Aurora MySQL Đơn Điểm Ghi:
┌──────────────────────────────────────────────┐
│ Giới Hạn Ghi Đơn Điểm (Single-Writer):       │
│ Mọi lệnh INSERT và UPDATE sổ cái đều dồn vào │ ──► CPU Master chạm ngưỡng 98%,
│ đúng 1 Node Master (Không thể scale out ghi) │     hàng đợi I/O tắc nghẽn hoàn toàn
├──────────────────────────────────────────────┤
│ Độ Trễ Đồng Bộ Giữa Các Read Replica:       │
│ Dưới áp lực ghi khổng lồ, độ trễ sao chép   │ ──► Người dùng thấy số dư ví cũ,
│ (replica lag) tăng vọt từ 5 đến 8 giây       │     bấm thanh toán lại gây trừ tiền kép
├──────────────────────────────────────────────┤
│ Ác Mộng Vận Hành Phân Mảnh Thủ Công:         │
│ Sharding theo user_id làm gãy truy vấn shop; │ ──► Giao dịch phân tán 2PC xuyên shard
│ re-sharding đòi hỏi bảo trì ngừng hệ thống   │     gây trễ nghiêm trọng (>300ms)
└──────────────────────────────────────────────┘

Khi các chiến dịch quốc gia như “Chiến Dịch Tặng 10 Tỷ Yên” diễn ra, lưu lượng ghi tăng vọt vượt mốc 1.250 giao dịch mỗi giây (TPS). Do các nghiệp vụ kế toán tài chính đòi hỏi tính nhất quán đọc ngay sau khi ghi (read-after-write consistency), hệ thống không thể chuyển các truy vấn kiểm tra số dư sang các read replica bất đồng bộ. Mọi thao tác khóa dòng số dư, kiểm tra hạn mức, trừ tiền ví người dùng và chèn dòng sổ cái đều dồn hết vào một node master duy nhất. Việc nâng cấp cấu hình máy chủ lên loại lớn nhất của AWS (db.r5.24xlarge) chỉ kéo dài thêm thời gian trước khi chạm trần; rõ ràng một cơ sở dữ liệu quan hệ bị trói buộc vào một nút ghi đơn lẻ không thể đáp ứng quy mô thanh toán toàn dân của Nhật Bản.


2. Kiến Trúc Cơ Sở Dữ Liệu Phân Tán TiDB NewSQL

Để đạt được khả năng mở rộng ngang năng lực ghi mà vẫn bảo toàn 100% tính toàn vẹn giao dịch ACID và độ tương thích hoàn hảo với giao thức MySQL, PayPay đã chuyển đổi toàn bộ tầng dữ liệu tài chính sang TiDB và TiKV (hệ sinh thái NewSQL phân tán nguồn mở từ PingCAP):

flowchart TD
    subgraph ClientLayer["Hạm Đội Microservices Thanh Toán (AWS Tokyo)"]
        SVC1["Payment Core Pod A (Go 1.25)"]
        SVC2["Payment Core Pod B (Go 1.25)"]
        SVC3["Dịch Vụ Điểm Bán Pod C (Java)"]
    end

    subgraph ComputeLayer["Tầng Tính Toán Không Trạng Thái: TiDB"]
        TIDB1["TiDB Node 1 (Phân Tích Cú Pháp SQL & Tối Ưu Hóa CBO)"]
        TIDB2["TiDB Node 2 (Phân Tích Cú Pháp SQL & Tối Ưu Hóa CBO)"]
        TIDB3["TiDB Node 3 (Phân Tích Cú Pháp SQL & Tối Ưu Hóa CBO)"]
    end

    subgraph CoordinatorTier["Bộ Não Cụm & Cấp Dấu Thời Gian: Placement Driver (PD)"]
        PD_LEAD["PD Leader (Timestamp Oracle - TSO)"]
        PD_FOL1["PD Follower 1 (Raft Quorum)"]
        PD_FOL2["PD Follower 2 (Raft Quorum)"]
    end

    subgraph StorageTier["Tầng Lưu Trữ Key-Value Giao Dịch: TiKV (Multi-Raft)"]
        TIKV_A["TiKV Node A<br/>[Region 1 Leader, Region 2 Follower]"]
        TIKV_B["TiKV Node B<br/>[Region 1 Follower, Region 2 Leader]"]
        TIKV_C["TiKV Node C<br/>[Region 1 Follower, Region 2 Follower]"]
    end

    subgraph AnalyticalTier["Cỗ Máy Cột Phân Tích Thời Gian Thực: TiFlash"]
        TIFLASH["TiFlash Columnar Engine<br/>(Raft Learner - Phân Tích OLAP Không Chiếm Lock)"]
    end

    SVC1 --> TIDB1
    SVC2 --> TIDB2
    SVC3 --> TIDB3

    TIDB1 <--> PD_LEAD
    TIDB2 <--> PD_LEAD
    TIDB3 <--> PD_LEAD

    TIDB1 --> TIKV_A
    TIDB2 --> TIKV_B
    TIDB3 --> TIKV_C

    TIKV_A -. Luồng Sao Chép Bất Đồng Bộ Raft Learner .-> TIFLASH
    TIKV_B -. Luồng Sao Chép Bất Đồng Bộ Raft Learner .-> TIFLASH

    PD_LEAD <--> PD_FOL1
    PD_LEAD <--> PD_FOL2

Phân Tách Trách Nhiệm Rõ Ràng Trong Kiến Trúc TiDB

  1. Tầng Tính Toán Không Trạng Thái (TiDB Compute Tier): Giao tiếp chuẩn giao thức dây của MySQL, tiếp nhận câu lệnh SQL, tối ưu hóa kế hoạch thực thi dựa trên chi phí (Cost-Based Optimizer - CBO) và phân phối các thao tác quét dải dữ liệu xuống tầng lưu trữ. Do không lưu giữ trạng thái trên đĩa, các node TiDB có thể tự động co giãn sau AWS Network Load Balancer chỉ trong vài chục giây mà không gây gián đoạn phiên kết nối.
  2. Cụm Điều Phối Placement Driver (PD Cluster): Đóng vai trò bộ não trung tâm của toàn bộ cụm. PD liên tục phát hành dấu thời gian đơn điệu tăng dần thông qua Timestamp Oracle (TSO) phục vụ cách ly giao dịch phân tán Snapshot Isolation (mô hình Percolator). Ngoài ra, PD liên tục giám sát heartbeat của các node TiKV để tự động điều phối, chia nhỏ (split) và cân bằng tải các Region dữ liệu giữa các node vật lý.
  3. Tầng Lưu Trữ Phân Tán Multi-Raft (TiKV): Lưu giữ các dòng bảng và chỉ mục dưới dạng các cặp key-value được sắp xếp thứ tự trong RocksDB. Dữ liệu được chia thành các Region liên tục có kích thước 96MB. Mỗi Region tạo thành một nhóm đồng thuận Raft độc lập, được nhân bản qua 3 đến 5 node vật lý khác nhau.
  4. Cỗ Máy Cột Thời Gian Thực (TiFlash): Kết nối vào các cụm TiKV với vai trò Raft Learner bất đồng bộ. TiFlash nhận luồng nhật ký Raft và chuyển đổi dữ liệu dòng sang cấu trúc dạng cột (columnar format), cho phép thực thi các câu truy vấn báo cáo tài chính nặng hoặc quét mẫu gian lận mà không gây ảnh hưởng tới các giao dịch thanh toán OLTP.

3. Cơ Chế Multi-Raft Region Split & Triệt Tiêu Điểm Nóng Ghi (Write Hotspot)

Trong các hệ thống lưu trữ key-value phân tán, việc sử dụng các khóa chính tăng tuần tự (chẳng hạn như AUTO_INCREMENT truyền thống của MySQL) là một lỗi kiến trúc nghiêm trọng. Bởi vì các số tăng dần liên tục sẽ rơi vào cùng một dải byte liên tiếp, toàn bộ các lệnh ghi INSERT mới sẽ bị dồn vào đúng duy nhất một Region 96MB của TiKV, biến node chứa Raft Leader của Region đó thành điểm nóng (hotspot) nghẽn CPU, trong khi hàng chục node lưu trữ khác trong cụm hoàn toàn rảnh rỗi.

PayPay loại bỏ hoàn toàn hiện tượng này bằng cách kết hợp cơ chế khóa chính AUTO_RANDOM(5) với giải thuật tự động chia tách Region:

flowchart TD
    subgraph SequentialFailure["Điểm Nóng Ghi AUTO_INCREMENT Tuần Tự (Lỗi Thiết Kế)"]
        SEQ_IN["Luồng Ghi: ID 10001, 10002, 10003..."]
        SEQ_R1["TiKV Region 1 (Leader trên Node A)<br/>[100% Lưu Lượng Dồn Vào 1 Điểm - Sập Node!]"]
        SEQ_R2["TiKV Region 2 (Rảnh Rỗi - 0% CPU)"]
        SEQ_R3["TiKV Region 3 (Rảnh Rỗi - 0% CPU)"]
        SEQ_IN --> SEQ_R1
    end

    subgraph AutoRandomScattering["Cơ Chế Phân Tán AUTO_RANDOM(5) Trên Multi-Raft"]
        AR_IN["Luồng Ghi: Gắn Tiền Tố 5-Bit Ngẫu Nhiên (0..31)"]
        AR_R1["TiKV Region A [Tiền Tố 00000..00011]<br/>Raft Group 1 Leader (Node 1)"]
        AR_R2["TiKV Region B [Tiền Tố 00100..00111]<br/>Raft Group 2 Leader (Node 2)"]
        AR_R3["TiKV Region C [Tiền Tố 01000..01011]<br/>Raft Group 3 Leader (Node 3)"]
        AR_R4["TiKV Region D [Tiền Tố 11100..11111]<br/>Raft Group 32 Leader (Node 32)"]
        
        AR_IN -->|Băm Ngẫu Nhiên| AR_R1
        AR_IN -->|Băm Ngẫu Nhiên| AR_R2
        AR_IN -->|Băm Ngẫu Nhiên| AR_R3
        AR_IN -->|Băm Ngẫu Nhiên| AR_R4
    end

Cơ Chế Hoạt Động Của AUTO_RANDOM(5)

Khi khai báo BIGINT AUTO_RANDOM(5), TiDB sử dụng 5 bit cao nhất của số nguyên 64-bit làm tiền tố ngẫu nhiên ($2^5 = 32$ thùng phân tán độc lập), trong khi 59 bit còn lại tăng tự động.

Nhờ đó:

  • Các thao tác insert liên tiếp từ hàng trăm pod microservices được phân tán đều đặn vào 32 Region TiKV hoàn toàn tách biệt.
  • Áp lực I/O ghi và sao chép log Raft được chia đều cho tất cả các node lưu trữ trong cụm.
  • Khi một Region chạm ngưỡng 96MB, Placement Driver tự động bẻ đôi thành 2 Region 48MB và điều chuyển Raft Leader sang node rảnh rỗi hơn, đạt khả năng mở rộng tuyến tính hoàn hảo.

Bảng so sánh thực nghiệm hiệu năng giữa các kiến trúc cơ sở dữ liệu tại PayPay:

Chỉ Số Đánh GiáAWS Aurora MySQLSharded MySQL (Vitess)TiDB Multi-Raft NewSQL
Thông Lượng Ghi Tối Đa~1.800 TPS (1 Master)~15.000 TPS (Đã chia shard)50.000+ TPS (Mở rộng ngang)
Giao Dịch ACID Xuyên Thực ThểChỉ trên 1 node cục bộ2PC phân tán rất chậmNative Percolator 2PC (<15ms)
Thay Đổi Cấu Trúc Bảng OnlineKhóa bảng / nghẽn I/OPhức tạp trên từng shardGoogle F1 DDL không chiếm khóa
Thời Gian RTO Khi Sập Node30 đến 120 giâyBiến động theo từng shardDưới 3 giây (Raft Quorum)

4. Quy Trình Chuyển Đổi Dữ Liệu Trực Tiếp Không Gián Đoạn (Zero-Downtime Migration)

Chuyển đổi hàng tỷ bản ghi lịch sử và số dư của hàng chục triệu khách hàng từ Aurora MySQL sang TiDB mà không được phép ngừng hệ thống dù chỉ 1 giây đòi hỏi một quy trình 4 giai đoạn nghiêm ngặt:

sequenceDiagram
    autonumber
    participant App as Hạm Đội Microservices Thanh Toán
    participant Aurora as Nguồn: AWS Aurora MySQL
    participant DM as TiDB Data Migration (Cụm DM)
    participant TiDB as Đích: Cụm TiDB NewSQL
    participant Inspector as Công Cụ sync-diff-inspector

    Note over Aurora, TiDB: Giai Đoạn 1: Trích Xuất Snapshot Nhất Quán & Nạp Bulk
    Aurora->>DM: Dump Dữ Liệu Nhất Quán Toàn Cục (Mydumper)
    DM->>TiDB: Nạp Trực Tiếp Tập Tin RocksDB SST (TiDB Lightning)

    Note over Aurora, TiDB: Giai Đoạn 2: Đồng Bộ Binlog Thời Gian Thực Bắt Kịp Tải
    App->>Aurora: Giao Dịch Thanh Toán Live (Ghi & Cập Nhật Số Dư)
    Aurora->>DM: Đọc Luồng Binlog GTID (Stream Sao Chép Liên Tục)
    DM->>TiDB: Replay Các Thao Tác SQL Tăng Dần (Chế Độ Khóa Bi Quan)

    Note over TiDB, Inspector: Giai Đoạn 3: Kiểm Toán Tính Nhất Quán Dữ Liệu
    Inspector->>Aurora: Tính Mã Băm Khối Dữ Liệu (Data Chunk Hash)
    Inspector->>TiDB: Tính Mã Băm Khối Dữ Liệu (Data Chunk Hash)
    Inspector-->>Inspector: So Khớp 100% Mã Băm Trên Toàn Bộ Region

    Note over App, TiDB: Giai Đoạn 4: Ghi Kép & Chuyển Đổi Không Downtime
    App->>TiDB: Chuyển Toàn Bộ Lưu Lượng Ghi Sang TiDB
    TiDB-->>App: Xác Nhận Giao Dịch Thành Công (Aurora Chuyển Thành Kho Lưu Trữ)

Các Cơ Chế Đảm Bảo An Toàn Dữ Liệu Tuyệt Đối

  • Nạp Tốc Độ Cao Bằng TiDB Lightning: Nhập khẩu dữ liệu lịch sử (>15TB) ở chế độ Local Backend với tốc độ vượt 300GB/giờ. Lightning biến đổi dữ liệu SQL thô thành các file RocksDB SST và đẩy thẳng vào đĩa của TiKV, bỏ qua hoàn toàn chi phí xử lý SQL.
  • Bắt Kịp Luồng Binlog Liên Tục: Cụm TiDB Data Migration theo dõi GTID của Aurora, phát lại các thay đổi gia tăng cho đến khi độ trễ sao chép giảm xuống dưới 50 mili-giây.
  • Kiểm Toán Băm Mật Mã Bằng sync-diff-inspector: Công cụ phân chia bảng thành hàng triệu đoạn khóa chính xác định, tính toán mã băm MD5 song song giữa Aurora và TiDB để chứng minh tính đối xứng 100% trước khi tiến hành trỏ DNS production.

5. Định Nghĩa DDL Chuẩn & Mã Nguồn Go 1.25+ Xử Lý Deadlock Phân Tán

Dưới đây là cú pháp DDL production và mã nguồn Golang 1.25+ hoàn chỉnh cho cỗ máy giao dịch kế toán kép, tích hợp tối ưu hóa connection pool, timeout context và vòng lặp thử lại có backoff ngẫu nhiên xử lý các mã lỗi deadlock của TiDB (ErrDeadlock / WriteConflict):

-- Cú pháp DDL Production cho Bảng Sổ Cái Kế Toán Kép Tại PayPay
CREATE TABLE wallet_ledger (
    ledger_id BIGINT AUTO_RANDOM(5) PRIMARY KEY,
    transaction_sn VARCHAR(64) NOT NULL,
    user_id BIGINT NOT NULL,
    counterparty_id BIGINT NOT NULL,
    amount BIGINT NOT NULL, -- Đơn vị Yên Nhật (số nguyên, tránh sai số số thực)
    currency VARCHAR(8) DEFAULT 'JPY',
    entry_type ENUM('DEBIT', 'CREDIT') NOT NULL,
    balance_after BIGINT NOT NULL,
    status TINYINT NOT NULL DEFAULT 1,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT uq_tx_entry UNIQUE (transaction_sn, entry_type),
    INDEX idx_user_created (user_id, created_at)
)
SHARD_ROW_ID_BITS = 4
PRE_SPLIT_REGIONS = 4;

Mã Nguồn Golang 1.25+ Thực Thi Giao Dịch Tài Chính Với Cơ Chế Thử Lại

// Package ledger triển khai các thao tác sổ cái tài chính chuẩn production trên TiDB.
package ledger

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log/slog"
	"math/rand/v2"
	"strings"
	"time"

	"github.com/go-sql-driver/mysql"
)

type LedgerEntry struct {
	TxSN           string
	UserID         int64
	CounterpartyID int64
	Amount         int64 // Yên Nhật
	EntryType      string
}

type LedgerEngine struct {
	db     *sql.DB
	logger *slog.Logger
}

func NewLedgerEngine(db *sql.DB, logger *slog.Logger) *LedgerEngine {
	// Cấu hình Connection Pool tối ưu cho kiến trúc phân tán của TiDB
	db.SetMaxOpenConns(200)
	db.SetMaxIdleConns(50)
	db.SetConnMaxLifetime(10 * time.Minute)
	db.SetConnMaxIdleTime(3 * time.Minute)

	return &LedgerEngine{
		db:     db,
		logger: logger,
	}
}

// ExecuteTransferWithRetry thực thi giao dịch kế toán kép với cơ chế thử lại lũy thừa khi đụng độ deadlock.
func (e *LedgerEngine) ExecuteTransferWithRetry(
	ctx context.Context,
	debit LedgerEntry,
	credit LedgerEntry,
	maxRetries int,
) error {
	baseBackoff := 25 * time.Millisecond
	maxBackoff := 500 * time.Millisecond

	for attempt := 1; attempt <= maxRetries; attempt++ {
		err := e.executeSingleTransfer(ctx, debit, credit)
		if err == nil {
			return nil
		}

		if !isRetryableTiDBError(err) {
			return fmt.Errorf("lỗi sổ cái tài chính không thể thử lại: %w", err)
		}

		if attempt == maxRetries {
			return fmt.Errorf("vượt quá số lần thử lại tối đa (%d): %w", maxRetries, err)
		}

		// Tính toán thời gian hoãn lũy thừa kết hợp jitter ngẫu nhiên để chống nghẽn đồng loạt
		jitter := time.Duration(rand.Int64N(int64(baseBackoff)))
		sleepDuration := min(baseBackoff*(1<<(attempt-1))+jitter, maxBackoff)

		e.logger.WarnContext(ctx, "phát hiện xung đột giao dịch TiDB, tạm hoãn trước khi thử lại",
			slog.Int("attempt", attempt),
			slog.Duration("backoff", sleepDuration),
			slog.String("tx_sn", debit.TxSN),
			slog.String("error", err.Error()),
		)

		select {
		case <-time.After(sleepDuration):
		case <-ctx.Done():
			return ctx.Err()
		}
	}

	return errors.New("thoát bất thường khỏi vòng lặp thử lại giao dịch")
}

func (e *LedgerEngine) executeSingleTransfer(ctx context.Context, debit LedgerEntry, credit LedgerEntry) error {
	txCtx, cancel := context.WithTimeout(ctx, 4*time.Second)
	defer cancel()

	// Khởi tạo giao dịch bi quan tường minh (chuẩn xử lý tài chính của TiDB)
	tx, err := e.db.BeginTx(txCtx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
	if err != nil {
		return fmt.Errorf("không thể mở transaction TiDB: %w", err)
	}
	defer tx.Rollback()

	// 1. Khóa và kiểm tra số dư ví người trả tiền
	var currentBalance int64
	queryPayer := `SELECT balance FROM user_wallets WHERE user_id = ? FOR UPDATE;`
	if err := tx.QueryRowContext(txCtx, queryPayer, debit.UserID).Scan(&currentBalance); err != nil {
		return fmt.Errorf("lỗi khóa dòng ví người gửi %d: %w", debit.UserID, err)
	}

	if currentBalance < debit.Amount {
		return fmt.Errorf("số dư không đủ: ví=%d, cần trừ=%d", currentBalance, debit.Amount)
	}

	// 2. Trừ tiền ví người trả
	updatePayer := `UPDATE user_wallets SET balance = balance - ?, updated_at = NOW() WHERE user_id = ?;`
	if _, err := tx.ExecContext(txCtx, updatePayer, debit.Amount, debit.UserID); err != nil {
		return fmt.Errorf("lỗi cập nhật ví người trả: %w", err)
	}

	// 3. Cộng tiền ví người nhận
	updatePayee := `UPDATE user_wallets SET balance = balance + ?, updated_at = NOW() WHERE user_id = ?;`
	if _, err := tx.ExecContext(txCtx, updatePayee, credit.Amount, credit.UserID); err != nil {
		return fmt.Errorf("lỗi cập nhật ví người nhận: %w", err)
	}

	// 4. Ghi hai dòng đối ứng nợ/có vào sổ cái tài chính
	insertSQL := `
		INSERT INTO wallet_ledger (
			transaction_sn, user_id, counterparty_id, amount, currency, entry_type, balance_after
		) VALUES (?, ?, ?, ?, 'JPY', ?, ?);`

	newPayerBalance := currentBalance - debit.Amount
	if _, err := tx.ExecContext(txCtx, insertSQL, debit.TxSN, debit.UserID, debit.CounterpartyID, debit.Amount, "DEBIT", newPayerBalance); err != nil {
		return fmt.Errorf("lỗi chèn dòng ghi nợ sổ cái: %w", err)
	}

	if _, err := tx.ExecContext(txCtx, insertSQL, credit.TxSN, credit.UserID, credit.CounterpartyID, credit.Amount, "CREDIT", 0); err != nil {
		return fmt.Errorf("lỗi chèn dòng ghi có sổ cái: %w", err)
	}

	// 5. Commit giao dịch qua cơ chế Percolator 2-Phase Commit
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("lỗi commit Percolator: %w", err)
	}

	return nil
}

func isRetryableTiDBError(err error) bool {
	if err == nil {
		return false
	}

	var mysqlErr *mysql.MySQLError
	if errors.As(err, &mysqlErr) {
		switch mysqlErr.Number {
		case 1213: // ER_LOCK_DEADLOCK (Mã Deadlock chuẩn MySQL / TiDB)
			return true
		case 1105: // Lỗi nội bộ chung TiDB (thường bọc xung đột WriteConflict)
			if strings.Contains(mysqlErr.Message, "WriteConflict") || strings.Contains(mysqlErr.Message, "Lock wait timeout") {
				return true
			}
		case 9007: // TiDB ErrWriteConflict
			return true
		case 9008: // TiDB ErrLockWaitTimeout
			return true
		}
	}

	errMsg := strings.ToLower(err.Error())
	return strings.Contains(errMsg, "deadlock") ||
		strings.Contains(errMsg, "writeconflict") ||
		strings.Contains(errMsg, "try again later")
}

6. Đánh Đổi Kiến Trúc & Kinh Nghiệm Tối Ưu Môi Trường Sản Xuất

Vận hành sổ cái tài chính trên cơ sở dữ liệu phân tán đòi hỏi kỷ luật kỹ thuật rất cao:

Chiều Hướng Kiến TrúcChiến Lược Được ChọnPhương Án Bị Bác BỏLý Do Đánh Đổi Cốt Lõi
Mô Hình Cơ Sở Dữ LiệuTiDB Distributed SQL (Multi-Raft)Sharded MySQL với VitessXóa bỏ sự phức tạp của việc re-sharding thủ công và logic giao dịch phân tán ở tầng ứng dụng.
Thiết Kế Khóa ChínhKhóa phân tán AUTO_RANDOM(5)Khóa tăng tuần tự AUTO_INCREMENTPhân tán đều các lệnh ghi vào 32 Region TiKV khác nhau, triệt tiêu điểm nghẽn ghi tại Raft Leader.
Mô Hình TransactionKhóa bi quan (Pessimistic) + 2PCChỉ dùng lạc quan (Optimistic)Tránh bão rollback giao dịch khi hàng nghìn người cùng mua hàng tại một điểm bán khuyến mãi.
Báo Cáo & Phân TíchCột TiFlash đồng bộ bất đồng bộĐọc từ Read Replica truyền thốngTiFlash vận hành như một Raft Learner, không bao giờ tranh chấp tài nguyên hay lock với tầng ghi OLTP.

Câu Hỏi Thường Gặp (FAQ)

TiDB giải quyết tình trạng deadlock phân tán như thế nào dưới lưu lượng thanh toán đồng thời cực lớn?

TiDB tích hợp sẵn bộ phát hiện deadlock phân tán tự động vận hành bên trong Placement Driver (PD) và các Raft Leader của TiKV:

  • Ở chế độ khóa bi quan (pessimistic), TiDB xây dựng đồ thị chờ khóa động (Wait-For Graph) giữa các giao dịch phân tán trên nhiều node TiKV khác nhau.
  • Nếu phát hiện chu trình phụ thuộc vòng tròn giữa các node (ví dụ: Giao dịch 1 đang giữ khóa ví Người dùng A và chờ khóa Cửa hàng B, trong khi Giao dịch 2 đang giữ khóa Cửa hàng B và chờ khóa Người dùng A), bộ phát hiện deadlock sẽ tìm giao dịch có chi phí rollback nhỏ nhất và hủy bỏ giao dịch đó bằng lỗi ErrDeadlock (MySQL Error 1213).
  • Tầng ứng dụng Go interceptor đón bắt mã lỗi này và tự động thực hiện thử lại với độ trễ ngẫu nhiên (exponential backoff with jitter), giúp giao dịch có mức độ ưu tiên cao hơn hoàn tất thanh toán tức thì mà không ảnh hưởng tới trải nghiệm người dùng.

Tính nhất quán dữ liệu giữa Aurora và TiDB được kiểm chứng như thế nào trước khi trỏ toàn bộ traffic?

PayPay áp dụng công cụ sync-diff-inspector kết hợp thử nghiệm lưu lượng bóng (shadow traffic):

  1. Băm Khối Dữ Liệu Xác Định: Bảng cơ sở dữ liệu được chia thành hàng triệu khối dựa trên dải khóa chính. Mã băm được tính toán đồng thời trên cả Aurora và TiDB; kết quả khớp hoàn hảo từng bit chứng minh dữ liệu đồng nhất tuyệt đối.
  2. Phát Lại Lưu Lượng Bóng: Toàn bộ các thao tác đọc và ghi trên môi trường production được nhân bản bất đồng bộ sang cụm TiDB trong suốt 2 tuần liên tiếp để đánh giá kế hoạch thực thi câu truy vấn, tỷ lệ trúng cache và độ trễ trước khi chính thức chuyển đổi DNS.

Làm thế nào TiFlash đảm bảo không gây suy giảm hiệu năng của các giao dịch thanh toán OLTP cốt lõi?

TiFlash cách ly hoàn toàn các tác vụ phân tích nhờ cơ chế Raft Learner và phân tách phần cứng vật lý:

  • Các node TiFlash tham gia vào cụm đồng thuận Raft với tư cách là Learner, tức là chúng chỉ nhận luồng sao chép nhật ký bất đồng bộ mà không tham gia bỏ phiếu bầu leader hay gửi xác nhận ghi quorum. Một câu truy vấn phân tích nặng chạy chậm trên TiFlash không bao giờ có thể làm chậm quá trình commit ghi của TiKV.
  • Phần cứng chuyên biệt: Các instance TiFlash được triển khai trên các node worker Kubernetes riêng với ổ đĩa NVMe độc lập, cách ly triệt để CPU và RAM khỏi tầng xử lý giao dịch trực tiếp.

Tại sao TiDB lại yêu cầu AUTO_RANDOM thay vì AUTO_INCREMENT cho khóa chính của các bảng có thông lượng ghi cao?

Khóa chính tăng đơn điệu gây ra điểm nghẽn phân tán chết người:

  • Khi sử dụng AUTO_INCREMENT, tất cả các bản ghi mới đều có khóa chính rơi vào Region nằm ở tận cùng bên phải của TiKV. Điều này dồn 100% áp lực ghi I/O và sao chép Raft log vào một máy chủ vật lý duy nhất, khiến toàn bộ các node còn lại bị lãng phí tài nguyên.
  • Ngược lại, AUTO_RANDOM(5) gán 5 bit ngẫu nhiên lên đầu số nguyên 64-bit. Cơ chế này phân tán đồng thời các dòng dữ liệu mới vào $2^5 = 32$ node Raft Leader khác nhau trong cụm.
  • Khi một Region đạt kích thước 96MB, Placement Driver tự động chia tách thành hai Region 48MB và điều chuyển Raft Leader sang node rảnh rỗi hơn, duy trì hiệu năng mở rộng tuyến tính hoàn hảo.

📖 Bản tiếng Anh (English Edition)

Bài trước: Phần 2 — Kiến Trúc Hướng Sự Kiện & Quản Trị Kafka Siêu Quy Mô | Danh mục Series | Bài tiếp: Phần 4 — Thực Tiễn SRE & Kiểm Thử Hỗn Loạn Chaos Mesh