🇬🇧 Read the English version of this article on tanhdev.com

Answer-first: Khi chuyển đổi từ Magento sang Golang, bạn có ba chiến lược cơ sở dữ liệu (database): (A) Dùng chung DB (Shared DB) để tối ưu compute ngay lập tức, (B) CDC + Outbox để tách DB dần dần mà không cần đụng vào code PHP, hoặc (C) Tách biệt hoàn toàn bằng Event Bus. Phương án A là một trạng thái chuyển giao tuyệt vời nhưng lại là một trạng thái cuối cùng thảm họa. Phương án B là con đường được khuyến nghị trong ngành công nghiệp cho hầu hết các hệ thống Magento — nó cho phép Go làm chủ schema của riêng mình và làm phẳng cấu trúc EAV mà không yêu cầu PHP Magento phải phát ra dù chỉ một sự kiện nào. Phương án C chỉ khả thi khi đội ngũ Magento của bạn có thể xây dựng và duy trì một hệ thống phát hành sự kiện (event publisher) đáng tin cậy.

Những gì bạn sẽ học được mà AI không nói cho bạn

  • Tại sao Go chạy trên MySQL của Magento lại nhanh hơn ở tầng compute nhưng vẫn bị nghẽn cổ chai ở tầng truy vấn EAV — và cách giải quyết thực sự là gì.
  • Yếu tố quyết định duy nhất giữa CDC (Phương án B) và Event Bus (Phương án C): ai sở hữu mã nguồn PHP Magento.

Tình thế tiến thoái lưỡng nan của Strangler Fig: Compute vs. Data

Khi chuyển đổi từ khối nguyên khối (monolith) Magento sang backend Golang, các kiến trúc sư phải đối mặt với một quyết định không hề rõ ràng: chúng ta nên chuyển đổi tầng APIcơ sở dữ liệu cùng một lúc không?

Chuyển đổi cả hai đồng thời đòi hỏi phải thiết lập Tính nhất quán cuối (Eventual Consistency), Ghi kép (Dual-Writes), và mô hình Saga ngay từ Ngày 1. Điều này làm chậm thời gian ra mắt (time-to-market) và ngăn doanh nghiệp nhìn thấy sự cải thiện hiệu năng trong 9–18 tháng.

Sự thay thế là Shared Database Quick Win (Thắng nhanh nhờ dùng chung DB) — chuyển đổi tầng compute API trước, giữ nguyên tầng dữ liệu, và câu giờ để thiết kế một chiến lược tách DB chuẩn chỉnh.

Nhưng “câu giờ” chỉ hiệu quả nếu bạn có một kế hoạch rõ ràng cho Giai đoạn 2 (Phase 2). Bài viết này định nghĩa cả ba phương án và cung cấp cho bạn một bộ khung ra quyết định để chọn đúng con đường.


Ba Phương án trên Bàn đàm phán

┌──────────────────┬──────────────────────────┬──────────────────────────┐
│  PHƯƠNG ÁN A     │  PHƯƠNG ÁN B             │  PHƯƠNG ÁN C             │
│  Shared DB       │  Evolutionary CDC        │  Full Separation +       │
│  (Thắng nhanh)   │  (Debezium + Outbox)     │  Event Bus (Kafka)       │
├──────────────────┼──────────────────────────┼──────────────────────────┤
│  Go đọc chung    │  Go làm chủ write DB.    │  Tách riêng hoàn toàn DB.│
│  MySQL của       │  CDC đồng bộ reads từ    │  Giao tiếp qua Kafka.    │
│  Magento trực tiếp│ Magento. Không sửa PHP. │  Magento phát ra sự kiện │
└──────────────────┴──────────────────────────┴──────────────────────────┘

Phương án A — Shared Database (Thắng nhanh)

┌──────────────┐         ┌──────────────────────────────┐
│  Go (magento-go) │──READ──▶│                              │
│              │──WRITE─▶│   MySQL Magento (dùng chung) │
│  PHP Magento │──READ──▶│                              │
│              │──WRITE─▶└──────────────────────────────┘
└──────────────┘

Trong Giai đoạn 1 của mô hình Strangler Fig, bạn đưa một smart router (ví dụ: shadow proxy routemode) vào trước Magento, sau đó viết lại các API có lưu lượng truy cập cao bằng Go — nhưng kết nối Go trực tiếp vào MySQL hiện tại của Magento.

Tại sao nó hiệu quả (Lợi ích ngay lập tức)

Magento PHP có thể mất 100–200ms chỉ để khởi động framework trước khi thực thi bất kỳ câu lệnh SQL nào. Go loại bỏ hoàn toàn sự cồng kềnh này. Được đo lường trong một dự án chuyển đổi Magento→Go thực tế (mag-go), cùng một endpoint xác thực (authentication) đã giảm từ 180ms xuống còn 8ms — giảm tới 22 lần — mà không hề thay đổi câu truy vấn cơ sở dữ liệu nào.

  1. Tách biệt Compute ngay lập tức. Sự cồng kềnh của PHP framework (DI container, ORM hydration, module observers) hoàn toàn biến mất.
  2. Không có vấn đề đồng bộ dữ liệu. Cả hai hệ thống cùng đọc các bản ghi đã commit trên MySQL — không có độ trễ của tính nhất quán cuối.
  3. Rollback cực dễ. Chỉ một thay đổi cấu hình trong routemode là có thể lật luồng traffic quay về PHP.

Mặt tối — Tại sao đây không thể là trạng thái cuối cùng

Mặt tối 1: Magento DB là một “API ẩn” mà bạn không kiểm soát

Schema của Magento không phải là API của bạn — nhưng Go giờ đây lại bị gắn chặt vào nó như thể nó là API.

Nâng cấp Magento 2.4.6 → 2.4.7
  → Đổi tên cột trong bảng customer_entity
  → Go struct binding âm thầm gặp lỗi hoặc panic
  → Gây ra sự cố lúc 2h sáng

Adobe đã ngừng hỗ trợ tính năng “split database” trong Magento 2.4.6+. Mỗi bản nâng cấp lại càng gom chung schema lại với nhau. Mỗi struct trong Go trỏ tới một bảng của Magento là một phụ thuộc không được khai báo và không có SLA (cam kết chất lượng dịch vụ).

Mặt tối 2: Xung đột Ghi (Write Conflict) — Race Conditions trên các bảng dùng chung

Bảng (Table)PHP ghiGo ghiMức độ Rủi ro
customer_entity✅ (cập nhật token, địa chỉ)CAO (HIGH)
oauth_token✅ (magento-go auth module)NGHIÊM TRỌNG (CRITICAL)
customer_address_entityCAO (HIGH)
quote / sales_orderChỉ qua ProxyTrung bình (Medium)

Các cơ chế khóa cấp dòng (row-level locks) của MySQL ngăn chặn việc đọc sai (dirty reads) nhưng không ngăn chặn được các xung đột logic nghiệp vụ. Nếu Go cập nhật token session của khách hàng cùng lúc Magento làm vô hiệu hóa nó, kết quả là trạng thái xác thực bị hỏng — chứ không phải lỗi database.

Mặt tối 3: Cạm bẫy Distributed Monolith (Nguyên khối Phân tán)

Go service ───────▶ Shared MySQL ◀─────── PHP Magento
    ↑                                           ↑
"microservice"                       sự gắn kết nguyên khối trên thực tế

Giờ đây bạn có hai runtime riêng biệt nhưng cùng một miền thất bại (failure domain). Một câu truy vấn Go quét toàn bộ bảng (full-scan) catalog_product_entity_varchar có thể làm suy giảm hiệu năng thanh toán (checkout) của Magento. Về mặt kiến trúc, hệ thống này giống hệt một hệ thống nguyên khối (monolith) — chỉ là khó debug hơn vì dính líu đến hai codebase.

Mặt tối 4: EAV mới là Cổ chai thực sự — Go không giải quyết được điều đó

-- Chỉ để tải một khách hàng kèm tất cả thuộc tính EAV:
SELECT ce.*, 
       cevs.value AS first_name,   -- customer_entity_varchar (JOIN 1)
       cevi.value AS store_id,     -- customer_entity_int (JOIN 2)
       ...
FROM customer_entity ce
JOIN customer_entity_varchar cevs ON ce.entity_id = cevs.entity_id
JOIN customer_entity_int cevi ON ce.entity_id = cevi.entity_id
...  -- Tổng cộng 5–10 JOINs

Go thực thi câu truy vấn này nhanh hơn PHP, nhưng kế hoạch thực thi truy vấn (query plan) là hoàn toàn giống nhau. Điểm nghẽn là ở thiết kế schema EAV, không phải ở ngôn ngữ lập trình. Cách duy nhất để sửa là làm phẳng (flatten) EAV thành một schema riêng do Go làm chủ — điều này đòi hỏi phải tách biệt DB.

Cách thực thi Phương án A an toàn (Các ràng buộc không thể thương lượng)

Nếu bạn cam kết chọn Phương án A như một trạng thái chuyển giao:

  1. Ràng buộc chỉ-đọc (Read-only): Go chỉ được phép SELECT trên các bảng thuộc sở hữu của Magento. Mọi hành động ghi (write) phải được proxy trả về API PHP của Magento.
  2. Chính sách sở hữu bảng: Tài liệu hóa các bảng mà Go được phép đọc. Không được phép có những hành vi đọc không được ghi nhận.
  3. Kiểm tra Schema pinning CI: Chạy so sánh (diff) schema trên mỗi PR nâng cấp Magento. Nếu một cột thay đổi mà Go có tham chiếu đến, hãy chặn build ngay.
  4. Đặt thời hạn cứng cho Giai đoạn 2: Phương án A mà không có ngày triển khai Giai đoạn 2 trên lịch trình thì chắc chắn sẽ trở thành giải pháp vĩnh viễn do sức ỳ.

Phương án B — Evolutionary CDC + Outbox (Con đường được khuyến nghị)

┌──────────────┐              ┌──────────────────┐
│  PHP Magento │──WRITE──────▶│  MySQL Magento   │
└──────────────┘              │  (master)        │
                              └────────┬─────────┘
                                       │ binlog (không cần sửa PHP)
                               ┌──────────────┐
                               │  Debezium    │  ← đọc binlog trực tiếp
                               │  CDC Engine  │
                               └──────┬───────┘
                                      │ stream events
┌──────────────┐  WRITE-OWNED  ┌────────────────────────┐
│  Go (magento-go) │──────────────▶│  Go DB (tách biệt)     │
│              │  READ-SYNCED  │  Schema phẳng           │
│              │◀──────────────│  token, session,        │
└──────────────┘               │  customer_flat...       │
        │                      └────────────────────────┘
        │ Outbox events (Go → Magento nếu cần)
   ┌──────────┐
   │  Kafka   │──▶ Magento consumer
   └──────────┘

Một góc nhìn cốt lõi: Debezium đọc trực tiếp binary log (binlog) của MySQL. Điều này có nghĩa là bạn không bao giờ cần sửa đổi một dòng code PHP Magento nào để stream mọi thay đổi dữ liệu vào database của Go.

Cách thức hoạt động của CDC + Outbox

Bước 1 — Go tạo DB riêng cho dữ liệu mà nó làm chủ:

-- Bảng do Go làm chủ (Go ghi vào đây, Magento không bao giờ chạm tới)
CREATE TABLE magento_customer_token (
  id UUID PRIMARY KEY,
  customer_id BIGINT NOT NULL,
  token_hash VARCHAR(255) NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

CREATE TABLE magento_outbox (
  id UUID PRIMARY KEY,
  event_type VARCHAR(100) NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMP DEFAULT now(),
  published_at TIMESTAMP
);

Bước 2 — Debezium stream các thay đổi của Magento → Go DB:

# debezium-connector.yaml
connector.class: io.debezium.connector.mysql.MySqlConnector
database.hostname: magento-mysql-master
database.include.list: magento
table.include.list: magento.customer_entity, magento.catalog_product_entity
# Debezium đọc binlog — không cần sửa PHP Magento

Bước 3 — Các lệnh ghi (write) của Go sử dụng Outbox Pattern (Nguyên tử):

// Trong một DB transaction duy nhất — không có rủi ro dual-write
tx.Exec(`INSERT INTO magento_customer_token (...) VALUES (...)`)
tx.Exec(`INSERT INTO magento_outbox (event_type, payload)
         VALUES ('TOKEN_CREATED', $1)`, tokenPayload)
// OutboxProcessor xuất bản (publish) lên Kafka mỗi 500ms
// Debezium bắt lấy dòng outbox → định tuyến về Magento nếu cần

Tại sao Phương án B lại chiến thắng đối với hầu hết các đội ngũ

Những gì bạn đạt đượcNhững gì bạn tránh được
Go làm chủ schema của nó — flatten EAV → tốc độ đọc nhanh gấp 5 lầnKhông bao giờ phải chạm vào code PHP Magento
Ghi ACID (an toàn) trên Go DB của riêng nóKhông có độ phức tạp của Saga ở Ngày 1
Debezium stream các thay đổi của Magento về GoKhông có rủi ro chạy đua khi ghi kép (dual-write race conditions)
Triển khai theo từng domain (Auth trước, Checkout sau cùng)Không phải đối mặt với rủi ro Big Bang cutover

Phương án C — Tách riêng hoàn toàn DB + Event Bus

┌─────────────────────────────────────────────────────────────┐
│                     EVENT BUS (Kafka)                        │
│  topic: customer.updated │ order.created │ inventory.changed │
└──────────┬───────────────────────────────┬──────────────────┘
           │ PUBLISH                       │ CONSUME
           ▼                               ▼
┌──────────────────┐          ┌───────────────────────────────┐
│  PHP Magento     │          │  Go Service (magento-go)           │
│                  │          │                                │
│  MySQL Magento   │          │  Go DB (sở hữu riêng, schema phẳng)│
│  (sở hữu hoàn toàn)│        │  CQRS read projections        │
│                  │          │  customer_flat, order_summary  │
│  ← Magento phải  │          │                                │
│    phát ra TẤT CẢ│          └───────────────────────────────┘
│    sự thay đổi   │                         │ PUBLISH
│    trạng thái    │◀────────────────────────┘
│    như là sự kiện│  (các sự kiện bù đắp / compensating events)
└──────────────────┘  

Ở Phương án C, cả hai hệ thống được cô lập hoàn toàn. Việc giao tiếp diễn ra hoàn toàn thông qua event bus. Mỗi dịch vụ sở hữu toàn bộ cơ sở dữ liệu của chính nó.

Những gì Phương án C đòi hỏi mà Phương án B thì không

Khác biệt cốt lõi nằm ở chỗ ai là người xuất bản các sự kiện:

Phương án B (CDC)Phương án C (Event Bus)
Ai xuất bản thay đổi?Debezium đọc tự động binlogPHP Magento phải ghi vào bảng outbox
Code PHP Magento có phải sửa?❌ Không yêu cầuBắt buộc — mỗi thay đổi trạng thái cần một sự kiện
Rủi ro nếu team PHP chậm chạpBằng 0 (CDC là hạ tầng)Cao (Go DB sẽ bị thiếu hụt dữ liệu)
Độ trễ đồng bộ50–200ms (độ trễ binlog)10–100ms (nếu Kafka khỏe)
Replay sự kiệnCó (lịch sử binlog)Có (Kafka retention)

Phương án C là kiến trúc đích (target architecture) đúng đắn cho dài hạn. Nhưng nó đòi hỏi đội ngũ PHP Magento phải liên tục phát hành các sự kiện domain một cách đáng tin cậy cho mọi sự thay đổi trạng thái — cập nhật khách hàng, trạng thái đơn hàng, điều chỉnh hàng tồn kho, áp dụng khuyến mãi. Chỉ cần bỏ sót một hạng mục sự kiện, cơ sở dữ liệu của Go sẽ âm thầm lệch pha so với Magento.


Ma trận so sánh 3 chiều

Tiêu chíPhương án A: Shared DBPhương án B: CDC + OutboxPhương án C: Full Event Bus
Mô hình nhất quánStrong ACID ✅ACID writes, eventual reads ⚠️Eventual consistency ❌
Độ kết dính SchemaChặt — Magento sở hữu ❌Giảm dần theo domain ⚠️Lỏng lẻo — event schema ✅
Rủi ro xung đột ghiCao — race conditions ❌Thấp — ghi độc lập ✅Không có — cách ly hoàn toàn ✅
Hiệu suất EAVKhông đổi ❌Flatten theo domain ⚠️Full flatten ✅
Scale độc lậpKhông ❌Một phần ⚠️Toàn diện ✅
Rollback Go serviceDễ — lật routemode ✅Dễ cho mỗi domain ✅Khó — event schema backward compat ❌
Thời gian ra ProductionĐang chạy ngay ✅3–6 tháng ⚠️9–18 tháng ❌
Sửa đổi code MagentoKhông ✅Không (CDC reads binlog) ✅Bắt buộc — event publisher ❌
Hạ tầng mớiKhông ✅Debezium + Kafka ⚠️Kafka + Schema Registry + CQRS ❌
Độ khó DebugThấp ✅Trung bình ⚠️Cao — trace qua nhiều trạm ❌
Kỹ năng Team yêu cầuThấp ✅Trung bình ⚠️Cao — hệ thống phân phân tán ❌
Cô lập lỗi (Fault isolation)Không — 1 DB = tất cả sập ❌Một phần ⚠️Toàn diện ✅
An toàn giao dịch CheckoutACID ✅Yêu cầu Saga ⚠️Saga + bù đắp giao dịch ❌
Rủi ro bán lố tồn khoKhông (ACID) ✅Thấp nếu CDC lag < 100ms ⚠️Cao nếu consumer lag ❌
Khả năng Replay / AuditKhông ❌Một phần (Outbox) ⚠️Toàn diện (Kafka retention) ✅

Bảng rủi ro cho mỗi phương án

Phương án A — Các rủi ro của Shared DB

Rủi roKhả năngTác độngKhắc phục (Mitigation)
Nâng cấp Magento làm vỡ Go structCao (mỗi bản upgrade)Service sậpSchema pinning + kiểm tra CI diff
Race condition khi ghi bảng authTrung bìnhDữ liệu hỏngChính sách sở hữu bảng, Go chỉ đọc
Cổ chai DB khi mở rộngCaoHai bên đều chậmRead replica riêng cho Go
Trở thành Distributed monolith vĩnh viễnChắc chắn nếu không có hạn chótNợ kiến trúc (Debt)Đặt hạn chót rõ ràng cho Giai đoạn 2

Phương án B — Các rủi ro của CDC

Rủi roKhả năngTác độngKhắc phục (Mitigation)
Lỗi đường ống CDCThấp (Debezium HA)Go DB lệch đồng bộGiám sát độ trễ (Lag) + Cảnh báo
Nhận sự kiện trùng lặpTrung bìnhGhi trùng dữ liệuIdempotency key trên mọi consumer
Áp lực lưu trữ MySQL binlogThấpChi phí StorageChính sách lưu giữ (retention) binlog
Quá trình dual-write tạm thờiTrung bìnhTrôi dạt dữ liệu (Drift)Outbox pattern ngay từ ngày đầu

Phương án C — Các rủi ro của Full Event Bus

Rủi roKhả năngTác độngKhắc phục (Mitigation)
Magento phát thiếu sự kiệnRất CaoGo DB thiếu dữ liệuBộ Integration test, bắt buộc
Thay đổi Schema sự kiệnCao (do nghiệp vụ)Consumer sậpSchema Registry + backward compat
Saga lỗi → đơn hàng thiếu đồng bộTrung bìnhẢnh hưởng doanh thuDead-letter queue + xử lý thủ công
Bão sự kiện (Event storm) khi flash saleTrung bìnhQuá tải KafkaPhân chia Topic + SLA độ trễ consumer
Độ trễ Read-your-own-writeCaoTrải nghiệm UX tệCập nhật UI theo hướng lạc quan (Optimistic)

⚠️ Lưu ý tối quan trọng cho Phương án C: PHP Magento phải ghi vào bảng outbox nằm chung trong một database transaction với quá trình thay đổi trạng thái kinh doanh. Điều này thường bị triển khai sai — xuất bản thẳng lên Kafka sau khi DB commit sẽ tạo ra một khoảng trống ghi kép (dual-write gap). Nếu Kafka publish bị lỗi, sự kiện đó bị mất tích âm thầm và database của Go sẽ bị lệch vĩnh viễn.


Bộ khung ra quyết định (Decision Framework) — Đâu là lựa chọn đúng cho bạn?

Q1: Team của bạn có 2+ kỹ sư có kinh nghiệm về hệ thống phân tán không?
  └─ KHÔNG → Ở lại Phương án A, bổ sung rào chắn (schema pinning, read-only policy)
  └─ CÓ    → Q2

Q2: Mức độ chịu đựng (tolerance) đối với việc bán lố hàng tồn kho của bạn có bằng 0 không?
  └─ CÓ    → Phương án B (CDC duy trì ACID writes, không rủi ro eventual consistency lúc ghi)
  └─ KHÔNG → Cả B và C đều khả thi

Q3: Bạn có cần Go mở rộng hoàn toàn độc lập với DB của Magento không?
  └─ KHÔNG → Phương án B là đủ và được khuyến nghị
  └─ CÓ    → Q4

Q4: Team PHP Magento có khả năng cam kết xây dựng và duy trì các event publisher không?
  └─ KHÔNG → Bắt buộc chọn Phương án B (CDC không cần sửa PHP)
  └─ CÓ    → Phương án C khả thi (lộ trình: 12–18 tháng)

Ưu tiên Domain — Những gì cần tách biệt trước

Không phải tất cả các domain (miền nghiệp vụ) đều mang rủi ro giống nhau. Hãy di chuyển theo trình tự này bất kể bạn chọn Phương án B hay C:

DomainƯu tiênTại saoRủi ro
Auth / Token (auth/)Đầu tiênGo đã làm chủ logic token, không phụ thuộc MagentoThấp
OIDC (oidc/)Đầu tiênThuộc hoàn toàn về Go, không cần dữ liệu MagentoThấp
Wishlist (wishlist/)Thứ 2Domain đơn giản, lượng dữ liệu nhỏThấp
Customer read (customer/)Thứ 2Làm phẳng EAV → Đọc nhanh 5×, việc ghi vẫn là proxyTrung bình
Customer writeThứ 3Cần có Saga với Magento để đồng bộ địa chỉ/profileTrung bình
Cart / Checkout (cartcheckout/)Cuối cùngRủi ro kinh doanh cao nhất, cần Saga trưởng thànhCao

Không bao giờ di chuyển Cart/Checkout sang một DB riêng cho tới khi mô hình Saga đã được chứng minh hiệu quả trên Production ở các domain ít rủi ro hơn.


Checklist Hạ tầng trước khi Tách biệt DB

Trước khi cam kết theo Phương án B hoặc C, hãy xác thực các điều kiện tiên quyết sau:

required_before_option_b:
  - mysql_binlog_enabled: true          # Kiểm tra bằng: SHOW VARIABLES LIKE 'log_bin'
  - debezium_or_dms: deployed           # Debezium nhúng hoặc chạy riêng
  - kafka_or_redis_streams: deployed    # Nền tảng truyền tải sự kiện CDC
  - outbox_table: created_in_go_db      # Khởi tạo bảng magento_outbox
  - idempotent_consumers: implemented   # Consumer có logic check idempotency key
  - cdc_lag_monitoring: configured      # Cảnh báo nếu lag > 500ms

additional_for_option_c:
  - php_event_publisher: implemented    # Module Magento ghi vào outbox table
  - schema_registry: deployed           # Quản lý schema bằng Avro hoặc Protobuf
  - saga_orchestrator: stable           # Ổn định trên production cho domain nhỏ
  - dead_letter_queue: configured       # Dành cho compensation saga thất bại

0–6 tháng (BÂY GIỜ)          6–12 tháng                 12–24 tháng
────────────────             ───────────                ────────────
Phương án A + Rào chắn  →    Phương án B: CDC           Phương án B → C (dần dần)
                              DB Auth/OIDC trước
Chính sách Sở hữu bảng        Streaming bằng Debezium    Chỉ khi team PHP
Kiểm tra Schema CI            Flatten EAV reads          có thể phát hành events
Chạy ngầm routemode           Wishlist + Customer        Mô hình Saga ổn định
Chốt hạn cứng Giai đoạn 2     tách biệt việc đọc         Cart/Checkout CUỐI CÙNG

Xem thêm