📖 Bản tiếng Anh (English Edition)
Bài trước: Phần 4 — Thực Tiễn SRE & Kiểm Thử Hỗn Loạn Chaos Mesh | Danh mục Series | Bài tiếp: Phần 6 — Nền Tảng AI: Phát Hiện Gian Lận Thời Gian Thực & LLM Hub
Answer-first: Xử lý bùng nổ traffic chiến dịch hoàn tiền đòi hỏi phân tách hoàn toàn khuyến mãi khỏi sổ cái thanh toán. PayPay triển khai Phòng Chờ Ảo Edge điều tiết tải, dùng Redis Lua script nguyên tử chống bội chi ngân sách trong bộ nhớ mili-giây, và trả thưởng bất đồng bộ kèm đối soát tự động ba bên.
Điều kiện tiên quyết: Nắm vững cơ chế race condition trong bộ nhớ đệm, tính nguyên tử của Redis Lua script đơn luồng, giải thuật giới hạn tốc độ token bucket và quy trình đối soát kế toán kép.
1. Bản Chất Đợt Sóng Thần Lưu Lượng Từ Siêu Chiến Dịch Khuyến Mãi
Vào tháng 12 năm 2018, PayPay đã châm ngòi cho cuộc cách mạng thanh toán không dùng tiền mặt tại Nhật Bản bằng “Chiến Dịch Tặng 10 Tỷ Yên” (100億円あげちゃうキャンペーン). Chiến dịch này mang đến cho người tiêu dùng mức hoàn tiền chưa từng có tiền lệ: tặng ngay 20% giá trị cho mọi giao dịch thanh toán, cùng cơ hội trúng thưởng 1-trên-40 nhận hoàn tiền 100% lên tới 100.000 Yên.
Phản ứng bùng nổ của thị trường đã làm tê liệt hạ tầng viễn thông trên toàn Nhật Bản:
- Lưu lượng truy cập tăng vọt từ mức cơ sở 150 giao dịch mỗi giây (TPS) lên vượt mức 2.500 TPS chỉ trong vòng chưa đầy 30 giây.
- Hàng triệu người tiêu dùng đồng loạt đổ xô về các trung tâm thương mại điện máy lớn tại Tokyo và Osaka để mua sắm các thiết bị giá trị cao (laptop, máy ảnh) trước khi tổng quỹ 10 tỷ Yên cạn kiệt.
- Các kết nối cơ sở dữ liệu quan hệ bị đóng băng hoàn toàn do tranh chấp khóa dòng (row-level lock contention) trên các bảng đếm số dư ngân sách toàn cục. Cụm cơ sở dữ liệu ban đầu bị tê liệt dưới làn sóng thay đổi số dư đồng thời.
Để có thể tổ chức thành công các siêu chiến dịch tiếp theo mà không gây suy giảm chất lượng dịch vụ, ban lãnh đạo kỹ thuật của PayPay đã thiết lập hai nguyên lý kiến trúc bất di bất dịch:
Hai Ràng Buộc Bất Biến Của Cỗ Máy Chiến Dịch PayPay:
┌─────────────────────────────────────────────────────────────┐
│ 1. Tính Độc Lập Tuyệt Đối Của Giao Dịch Thanh Toán Cốt Lõi: │
│ Việc một khách hàng trả tiền mua bánh mì tại cửa hàng tiện │
│ lợi TUYỆT ĐỐI KHÔNG BAO GIỜ được phép thất bại hoặc timeout │
│ chỉ vì hệ thống tính điểm thưởng khuyến mãi đang bị quá tải.│
├─────────────────────────────────────────────────────────────┤
│ 2. Cam Kết Không Vượt Quá Ngân Sách Đến Từng Mili-giây: │
│ Khi trần ngân sách chiến dịch (ví dụ 10 tỷ Yên) chạm đích, │
│ toàn bộ chiến dịch phải kết thúc trên toàn cầu ngay lập tức. │
│ Bội chi ngân sách khuyến mãi là tổn thất tài chính cực nặng.│
└─────────────────────────────────────────────────────────────┘
2. Phòng Chờ Ảo Tại Tầng Biên (Edge Virtual Waiting Room) & San Phẳng Đỉnh Tải
Thay vì để hàng triệu kết nối HTTP đồng thời từ ứng dụng di động ồ ạt dội thẳng vào các gateway Kubernetes và làm kiệt quệ connection pool của database, PayPay triển khai hệ thống Phòng Chờ Ảo Tại Tầng Biên (Edge Virtual Waiting Room) thông qua AWS CloudFront, Lambda@Edge và Envoy:
flowchart TD
subgraph Users["Làn Sóng Người Dùng Ứng Dụng Di Động"]
U1["Người Dùng 1 (Đã Có Token Phiên Thanh Toán)"]
U2["Người Dùng 2 (Tham Gia Chiến Dịch Mới)"]
U3["Người Dùng 3 (Lưu Lượng Tăng Đột Biến)"]
end
subgraph EdgeLayer["Cổng Điều Tiết Tầng Biên (AWS CloudFront + Lambda@Edge)"]
CHECK["Kiểm Tra Cookie Token Cấp Phép<br/>(Chữ Ký Mật Mã HMAC-SHA256)"]
QUEUE["Phòng Chờ Ảo Tầng Biên<br/>(Hàng Đợi Hàng Ngang SSE / WebSocket)"]
ADMIT["Cỗ Máy Cấp Phép Lũy Tiến<br/>(Tốc Độ: 500 Người / Giây)"]
end
subgraph GatewayTier["API Gateway & Cụm EKS Trung Tâm"]
GW["Envoy Ingress Gateway (Token Bucket Rate Limiter)"]
PAY_CORE["Hạm Đội Microservices Thanh Toán Cốt Lõi"]
TIDB["Cụm Lưu Trữ Sổ Cái Tài Chính TiDB"]
end
U1 -->|Gửi Kèm Cookie Hợp Lệ| CHECK
U2 -->|Chưa Có Token / Quá Tải| QUEUE
U3 -->|Chưa Có Token / Quá Tải| QUEUE
CHECK -->|Chữ Ký Hợp Lệ| GW
QUEUE -->|Đến Lượt Theo Trình Tự Thời Gian| ADMIT
ADMIT -->|Cấp Cookie Chữ Ký Có Hạn 15 Phút| GW
GW --> PAY_CORE
PAY_CORE --> TIDB
Cơ Chế Vận Hành Của Phòng Chờ Ảo
- Xác Thực Cấp Quyền Bằng Mật Mã Tại Tầng Biên: Khi một yêu cầu HTTP gửi đến CloudFront, hàm Lambda@Edge kiểm tra sự tồn tại của cookie mã hóa
paypay_admission_jwt. Nếu token chứa chữ ký mật mã hợp lệ được ký bởi private key của hệ thống và còn trong thời hạn 15 phút, yêu cầu được chuyển tiếp ngay tới Envoy Gateway nội bộ. - Xếp Hàng FIFO Có Thứ Tự Tuyệt Đối: Nếu không có token hợp lệ và hệ thống ghi nhận hạ tầng backend đang chịu áp lực cao (ví dụ độ trễ P99 vượt 40ms hoặc CPU TiDB vượt 65%), người dùng được điều hướng tới trang web phòng chờ tĩnh lưu trữ trên các PoP của CDN. Ứng dụng client duy trì kết nối Server-Sent Events (SSE), nhận số thứ tự xếp hàng xác định dựa trên dấu thời gian vào hàng.
- Điều Tiết Tốc Độ Mở Cửa Linh Hoạt: Bộ điều phối tự động liên tục theo dõi hàng đợi đĩa của TiKV và connection pool gRPC. Khi dung lượng xử lý an toàn, hệ thống mở cửa cho từng đợt người dùng (ví dụ 500 người/giây) và cấp cookie phiên hợp lệ.
3. Theo Dõi Ngân Sách Thời Gian Thực & Xử Lý Bất Biến Bằng Redis Lua
Điểm yếu chí mạng trong các đợt bùng nổ khuyến mãi là hiện tượng race condition gây bội chi ngân sách. Nếu nhiều worker cùng thực thi câu truy vấn kiểm tra ngân sách đồng thời (SELECT budget WHERE budget > 0), tất cả worker đều thấy ngân sách còn tiền và cùng chấp thuận giảm giá, khiến tổng số tiền hoàn vượt hàng trăm triệu Yên so với hạn mức 10 tỷ Yên.
PayPay giải quyết triệt để rủi ro này bằng cách thực thi việc kiểm tra và trừ ngân sách bên trong một Redis Lua Script nguyên tử trên bộ nhớ in-memory:
sequenceDiagram
autonumber
participant App as Cỗ Máy Tính Thưởng Chiến Dịch (Go 1.25)
participant Redis as Cụm Redis Sentinel Master (In-Memory)
participant Kafka as Hàng Đợi Sự Kiện Apache Kafka
participant Ledger as Sổ Cái Tài Chính Cốt Lõi TiDB
App->>Redis: EVALSHA deduct_budget.lua (campaign_id, requested_reward)
Note over Redis: Thực Thi Lua Script Đơn Luồng Nguyên Tử (Dưới 300 Micro-giây)
alt Ngân Sách Còn Đủ (Còn Lại >= Yêu Cầu)
Redis-->>Redis: Trừ Tiền: remaining_budget -= requested_reward
Redis-->>App: Trả Về Trạng Thái 1 (APPROVED, số dư mới = 894500 JPY)
App->>Kafka: Bắn Bản Tin RewardGrantedEvent (Bất Đồng Bộ)
App-->>Ledger: Cho Phép Tiếp Tục Thanh Toán (Đã Áp Dụng Khuyến Mãi)
else Ngân Sách Đã Cạn (Còn Lại < Yêu Cầu)
Redis-->>Redis: Ghi Đè campaign:status = "CLOSED"
Redis-->>Redis: Bắn Kênh Pub/Sub: "CAMPAIGN_EXHAUSTED"
Redis-->>App: Trả Về Trạng Thái 0 (REJECTED_BUDGET_EXHAUSTED)
App->>Kafka: Bắn Bản Tin CampaignClosedEvent
App-->>Ledger: Cho Phép Tiếp Tục Thanh Toán (Giá Gốc Không Giảm)
end
Đoạn Mã Redis Lua Chuẩn Môi Trường Sản Xuất: Trừ Ngân Sách Nguyên Tử
-- Script Trừ Ngân Sách Chiến Dịch Nguyên Tử Tại PayPay
-- KEYS[1]: campaign:budget:{campaign_id}
-- KEYS[2]: campaign:status:{campaign_id}
-- ARGV[1]: requested_deduction_amount (Số nguyên Yên Nhật)
local current_status = redis.call('GET', KEYS[2])
if current_status == 'CLOSED' then
return 0 -- Chiến dịch đã đóng toàn cầu
end
local current_budget = redis.call('GET', KEYS[1])
if not current_budget then
return -1 -- Khóa chiến dịch không tồn tại hoặc chưa khởi tạo
end
local budget_num = tonumber(current_budget)
local deduct_num = tonumber(ARGV[1])
if budget_num >= deduct_num then
local remaining = redis.call('DECRBY', KEYS[1], deduct_num)
if remaining <= 0 then
redis.call('SET', KEYS[2], 'CLOSED')
redis.call('PUBLISH', 'campaign:lifecycle:events', 'CAMPAIGN_EXHAUSTED')
end
return 1 -- Thành công: Đã trừ ngân sách an toàn
else
-- Ngân sách cạn kiệt: Lập tức đóng chiến dịch để từ chối các yêu cầu tiếp theo
redis.call('SET', KEYS[2], 'CLOSED')
redis.call('PUBLISH', 'campaign:lifecycle:events', 'CAMPAIGN_EXHAUSTED')
return 0 -- Bị từ chối: Không đủ ngân sách
end
Vì Redis thực thi Lua script theo cơ chế đơn luồng tuần tự (single-threaded), tuyệt đối không thể có hai yêu cầu đồng thời cùng đọc hoặc trừ giá trị budget_num. Thời gian thực thi chỉ mất từ 150 đến 300 micro-giây, loại bỏ hoàn toàn hiện tượng nghẽn lock trên cơ sở dữ liệu quan hệ.
4. Mã Nguồn Golang 1.25+ Cỗ Máy Quản Lý Chiến Dịch Production
Dưới đây là mã nguồn Golang 1.25+ hoàn chỉnh cho gói campaign, quản lý connection pool Redis Sentinel, nạp trước mã băm Lua SHA (ScriptLoad), đánh giá hạn mức, bắn sự kiện trả thưởng bất đồng bộ lên Kafka và tự động giáng cấp (graceful fallback) an toàn khi Redis gặp sự cố:
// Package campaign triển khai cỗ máy quản lý ngân sách khuyến mãi hiệu năng cao cho PayPay.
package campaign
import (
"context"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"time"
"github.com/IBM/sarama"
"github.com/redis/go-redis/v9"
)
type EvaluationResult int
const (
ResultRejectedExhausted EvaluationResult = 0
ResultApproved EvaluationResult = 1
ResultCampaignMissing EvaluationResult = -1
ResultFallbackBypass EvaluationResult = -2
)
type RewardGrantedEvent struct {
EventID string `json:"event_id"`
CampaignID string `json:"campaign_id"`
UserID int64 `json:"user_id"`
TransactionSN string `json:"transaction_sn"`
RewardAmount int64 `json:"reward_amount"`
Timestamp time.Time `json:"timestamp"`
}
type Engine struct {
logger *slog.Logger
redisClient *redis.Client
kafkaProducer sarama.SyncProducer
kafkaTopic string
luaSHA string
}
const deductLuaScript = `
local current_status = redis.call('GET', KEYS[2])
if current_status == 'CLOSED' then
return 0
end
local current_budget = redis.call('GET', KEYS[1])
if not current_budget then
return -1
end
local budget_num = tonumber(current_budget)
local deduct_num = tonumber(ARGV[1])
if budget_num >= deduct_num then
local remaining = redis.call('DECRBY', KEYS[1], deduct_num)
if remaining <= 0 then
redis.call('SET', KEYS[2], 'CLOSED')
redis.call('PUBLISH', 'campaign:lifecycle:events', 'CAMPAIGN_EXHAUSTED')
end
return 1
else
redis.call('SET', KEYS[2], 'CLOSED')
redis.call('PUBLISH', 'campaign:lifecycle:events', 'CAMPAIGN_EXHAUSTED')
return 0
end
`
func NewEngine(
logger *slog.Logger,
redisClient *redis.Client,
kafkaProducer sarama.SyncProducer,
kafkaTopic string,
) (*Engine, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
sha, err := redisClient.ScriptLoad(ctx, deductLuaScript).Result()
if err != nil {
return nil, fmt.Errorf("không thể nạp trước script Lua trừ ngân sách vào Redis: %w", err)
}
logger.Info("script Lua chiến dịch đã được nạp thành công vào Redis", slog.String("sha", sha))
return &Engine{
logger: logger,
redisClient: redisClient,
kafkaProducer: kafkaProducer,
kafkaTopic: kafkaTopic,
luaSHA: sha,
}, nil
}
// EvaluateAndDeduct thực hiện trừ ngân sách nguyên tử; nếu Redis lỗi, tự động giáng cấp an toàn không chặn thanh toán.
func (e *Engine) EvaluateAndDeduct(
ctx context.Context,
campaignID string,
userID int64,
txSN string,
requestedReward int64,
) (EvaluationResult, error) {
budgetKey := fmt.Sprintf("campaign:budget:%s", campaignID)
statusKey := fmt.Sprintf("campaign:status:%s", campaignID)
evalCtx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
res, err := e.redisClient.EvalSha(evalCtx, e.luaSHA, []string{budgetKey, statusKey}, requestedReward).Result()
if err != nil {
// Cơ Chế Giáng Cấp An Toàn: Tuyệt đối không bao giờ chặn thanh toán cốt lõi vì lỗi Redis!
e.logger.ErrorContext(ctx, "đánh giá ngân sách Redis thất bại, kích hoạt cơ chế bỏ qua khuyến mãi an toàn",
slog.String("campaign_id", campaignID),
slog.Int64("user_id", userID),
slog.String("error", err.Error()),
)
return ResultFallbackBypass, nil
}
statusInt, ok := res.(int64)
if !ok {
return ResultCampaignMissing, errors.New("kiểu dữ liệu trả về từ Redis Lua không phải số nguyên")
}
result := EvaluationResult(statusInt)
if result == ResultApproved {
e.publishRewardGrantedAsync(campaignID, userID, txSN, requestedReward)
}
return result, nil
}
func (e *Engine) publishRewardGrantedAsync(campaignID string, userID int64, txSN string, amount int64) {
event := RewardGrantedEvent{
EventID: fmt.Sprintf("rwd-%s-%d", txSN, time.Now().UnixNano()),
CampaignID: campaignID,
UserID: userID,
TransactionSN: txSN,
RewardAmount: amount,
Timestamp: time.Now().UTC(),
}
payload, err := json.Marshal(event)
if err != nil {
e.logger.Error("không thể tuần tự hóa sự kiện trả thưởng", slog.String("tx_sn", txSN), slog.String("error", err.Error()))
return
}
msg := &sarama.ProducerMessage{
Topic: e.kafkaTopic,
Key: sarama.StringEncoder(fmt.Sprintf("%d", userID)),
Value: sarama.ByteEncoder(payload),
}
// Bắn bản tin bất đồng bộ trong goroutine riêng để không ảnh hưởng độ trễ thanh toán
go func() {
_, _, sendErr := e.kafkaProducer.SendMessage(msg)
if sendErr != nil {
e.logger.Error("không thể đẩy sự kiện thưởng vào Kafka",
slog.String("tx_sn", txSN), slog.String("error", sendErr.Error()))
}
}()
}
5. Kiến Trúc Đối Soát Tài Chính Tự Động Ba Bên (Three-Way Financial Reconciliation)
Bởi vì các khoản giảm trừ khuyến mãi, cổng thanh toán ngân hàng và giao dịch quẹt thẻ tại quầy hoạt động qua các ranh giới mạng bất đồng bộ, hệ thống thực tế luôn phải đối mặt với nguy cơ sai lệch số liệu do chập chờn mạng hoặc worker bị restart.
PayPay vận hành Đường Ống Đối Soát Tài Chính Ba Bên Tự Động định kỳ vào lúc 02:00 sáng hàng ngày (Giờ Tokyo):
flowchart TD
subgraph DataSources["Ba Nguồn Dữ Liệu Tài Chính Độc Lập"]
SRC_TIDB["Sổ Cái Nội Bộ TiDB<br/>(Dòng Ghi Nợ/Có Cốt Lõi)"]
SRC_BANK["Dữ Liệu Bù Trừ Ngân Hàng (Zengin / CAFIS)<br/>(Sao Kê Ngân Hàng Ngoài Chính Thức)"]
SRC_POS["Nhật Ký Máy POS Điểm Bán<br/>(File Log Máy Quẹt Thẻ Cửa Hàng)"]
end
subgraph DataLakeIngress["Thu Thập Hồ Dữ Liệu Tài Chính (AWS S3)"]
S3_RAW["Hồ Dữ Liệu S3 Được Mã Hóa (Bảng Parquet)"]
end
subgraph ReconEngine["Cỗ Máy Đối Soát Phân Tán (PySpark + TiFlash)"]
STAGE1["Giai Đoạn 1: Khớp Mã Giao Dịch & Transaction SN"]
STAGE2["Giai Đoạn 2: Kiểm Tra Chênh Lệch Số Tiền & Tiền Thưởng"]
STAGE3["Giai Đoạn 3: Tự Động Phân Loại Sai Lệch"]
end
subgraph ResolutionOutput["Kết Quả Quyết Toán & Hòa Giải"]
MATCH["Khớp 100% Không Lệch Đồng Nào<br/>(Chuyển Tiền Quyết Toán Cho Shop)"]
DISCREP["Phát Hiện Chênh Lệch (> 0,01 JPY)"]
AUTO_REV["Bút Toán Bù Trừ Tự Động Cân Bằng Sổ Cái"]
SRE_REV["Bàn Kiểm Toán Thủ Công Của Đội Tài Chính & SRE"]
end
SRC_TIDB --> S3_RAW
SRC_BANK --> S3_RAW
SRC_POS --> S3_RAW
S3_RAW --> STAGE1
STAGE1 --> STAGE2
STAGE2 --> STAGE3
STAGE3 -->|Chênh Lệch = 0,00 JPY| MATCH
STAGE3 -->|Chênh Lệch > 0,00 JPY| DISCREP
DISCREP -->|Xác Định Do Rớt Gói Tin Mạng| AUTO_REV
DISCREP -->|Tranh Chấp Tài Chính Phức Tạp| SRE_REV
Ba Trụ Cột Dữ Liệu Đối Soát
- Sổ Cái Nội Bộ TiDB: Toàn bộ dòng ghi nợ (debit) và ghi có (credit) được lưu giữ trong cụm TiDB thời gian thực khi giao dịch diễn ra.
- File Nhật Ký Máy POS Cửa Hàng: Tổng hợp các giao dịch được lưu trữ cục bộ trên máy POS hoặc thiết bị quẹt mã QR tĩnh tại cửa hàng vào cuối ngày bán hàng.
- Sao Kê Mạng Lưới Ngân Hàng: Dữ liệu thanh quyết toán bù trừ điện tử truyền từ hệ thống viễn thông liên ngân hàng Zengin và cổng thẻ CAFIS.
Hệ thống phân tán đối soát sẽ quét và so sánh từng giao dịch trên cả 3 nguồn. Nếu phát hiện sự cố — ví dụ mạng di động của khách bị rớt sau khi tài khoản ví đã bị trừ tiền nhưng máy POS tại cửa hàng chưa kịp nhận tín hiệu xác nhận — cỗ máy đối soát sẽ tự động tạo bút toán đảo ngược hoàn tiền lại cho khách hàng vào sáng hôm sau mà không cần khách phải tạo ticket hỗ trợ.
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 hệ thống chiến dịch khuyến mãi tải cao đòi hỏi các quyết định đánh đổi kỹ thuật dứt khoát:
| Chiều Hướng Kiến Trúc | Chiến Lược Được Chọn | Phương Án Bị Bác Bỏ | Lý Do Đánh Đổi Cốt Lõi |
|---|---|---|---|
| Lưu Trữ Ngân Sách | Redis Lua đơn luồng in-memory | Transaction khóa hàng trên SQL | Tranh chấp khóa dòng trên database quan hệ làm nghẽn connection pool khi vượt 2.500 TPS. |
| Kiểm Soát Tải Đỉnh | Phòng Chờ Ảo tại Biên (CloudFront) | Nginx rate limit tại cụm K8s | Chặn đứng hàng triệu kết nối ngay tại tầng CDN trước khi chạm tới cụm Kubernetes. |
| Chu Trình Trả Thưởng | Tách rời 2 giai đoạn bất đồng bộ | Trả thưởng đồng bộ trong thanh toán | Đảm bảo thanh toán cốt lõi hoàn tất dưới 35ms; sự cố tính thưởng không làm nghẽn thu ngân. |
| Giải Quyết Lệch Dữ Liệu | Chạy lô đối soát tự động 3 bên đêm | Two-Phase Commit 2PC đồng bộ | 2PC xuyên các mạng ngân hàng bên ngoài là bất khả thi vì độ trễ cao (>500ms) và nguy cơ kẹt lock. |
Câu Hỏi Thường Gặp (FAQ)
PayPay làm thế nào để ngăn chặn tình trạng bội chi ngân sách khuyến mãi dưới lưu lượng vi mô đồng thời?
PayPay ngăn chặn bội chi ngân sách bằng cách tập trung hóa biến số dư chiến dịch trong Redis thông qua một Lua script nguyên tử (EVALSHA):
- Do Redis thực thi Lua script một cách tuần tự và nguyên tử mà không bị xen kẽ bởi bất kỳ lệnh nào khác, tuyệt đối không có hai request nào có thể đọc cùng một giá trị số dư ngân sách.
- Khi số dư còn lại thấp hơn số tiền thưởng được yêu cầu, script sẽ lập tức đổi trạng thái chiến dịch thành
CLOSEDvà phát một sự kiện ra toàn bộ edge cache, đóng ngay chiến dịch trên toàn cầu trong vòng 5 mili-giây.
Điều gì sẽ xảy ra nếu kết nối mạng của người dùng bị rớt khi đang đứng đợi trong phòng chờ ảo?
Hệ thống phòng chờ ảo có khả năng phục hồi trạng thái kết nối rất cao:
- Vị trí hàng đợi của người dùng được lưu trữ đồng thời dưới dạng cookie JWT mã hóa trên thiết bị và dấu thời gian gọn nhẹ trong Sorted Set (ZSET) trên cụm máy chủ.
- Nếu ứng dụng di động bị ngắt kết nối hoặc người dùng vô tình tải lại trang, client sẽ xuất trình lại token mã hóa khi kết nối lại. Cổng tầng biên đọc lại dấu thời gian gốc và phục hồi chính xác vị trí hàng đợi mà không phạt người dùng phải xếp hàng lại từ đầu.
Hệ thống xử lý như thế nào khi có sự sai lệch số liệu giữa máy POS tại cửa hàng và sổ cái trung tâm?
Sai lệch được xử lý triệt để qua đường ống đối soát tự động ba bên:
- Trong phiên đối soát ban đêm, PayPay khớp dữ liệu sổ cái nội bộ với bản dump nhật ký giao dịch từ máy POS cửa hàng.
- Nếu máy POS ghi nhận giao dịch nhưng chưa kịp nhận xác nhận thành công do rớt mạng, giao dịch sẽ được đối chiếu với log mạng lưới ngân hàng. Nếu tài khoản người dùng đã bị trừ tiền, giao dịch được công nhận; nếu chưa, hệ thống kích hoạt luồng bồi hoàn tự động.
Tại sao PayPay lại sử dụng Redis Lua script đơn luồng thay vì giao dịch cơ sở dữ liệu phân tán để kiểm soát ngân sách chiến dịch?
Quyết định kiến trúc này xuất phát từ bản chất vật lý của hiện tượng tranh chấp khóa cơ sở dữ liệu:
- Trong một cơ sở dữ liệu quan hệ phân tán (như MySQL hay TiDB), việc kiểm soát một biến ngân sách dùng chung duy nhất đòi hỏi phải chiếm khóa độc quyền cấp dòng (
SELECT ... FOR UPDATE). Dưới áp lực trên 2.500 TPS, hàng nghìn thread đồng thời tranh giành một dòng dữ liệu duy nhất, gây nghẽn hàng đợi thread pool, timeout câu truy vấn và sập kết nối theo hiệu ứng domino. - Ngược lại, Redis thực thi Lua script hoàn toàn trên bộ nhớ RAM theo cơ chế đơn luồng. Vì không có chi phí chuyển đổi ngữ cảnh (context switch) hay ghi đĩa I/O trong luồng xử lý chính, mỗi script hoàn tất chỉ trong 150 đến 300 micro-giây. Redis có thể xử lý mượt mà hơn 40.000 thao tác trừ ngân sách nguyên tử mỗi giây trên một node đơn lẻ mà không cần tới khóa dòng hay sinh ra race condition.
📖 Bản tiếng Anh (English Edition)
Bài trước: Phần 4 — Thực Tiễn SRE & Kiểm Thử Hỗn Loạn Chaos Mesh | Danh mục Series | Bài tiếp: Phần 6 — Nền Tảng AI: Phát Hiện Gian Lận Thời Gian Thực & LLM Hub
