Điều kiện tiên quyết: Đây là Phần 8 của Khóa Học System Design. Lục lọi lại Phần 7: Chế Đồ API Cản Đúp trước đi — mớ đòn giao dịch chuộc lỗi (compensating transactions) trong lò Saga bắt buộc phải đính kèm bùa cản đúp (idempotent).
Answer-first: Khung rập Saga Pattern cầm trịch giàn xếp mớ bòng bong giao dịch lộn xộn (distributed transactions) vắt ngang dọc bầy microservices bằng ngón đòn băm nát một cục giao dịch chà bá thành một lốc giao dịch lẻ tẻ (local transactions). Lỡ xui một nháy đứt gánh (fails), nguyên hệ thống tự động bóp cò nhả đạn giao dịch chuộc lỗi (compensating transactions) dội ngược theo chiều tua lùi (reverse order) đặng dọn sạch rác (undo) mấy khâu nãy giờ đã dọn mâm xong. Mỗi cục giao dịch cỏn con bắt buộc phải biết cản đúp (idempotent).
Cái Trò 2PC Báo Đời Hại Ruột Gì Ở Xứ Microservices?
Answer-first: Đòn khóa mỏ 2 Pha (Two-Phase Commit - 2PC) rặt là một phường lề luật giam hãm (blocking protocol) treo cục nợ tử huyệt thắt cổ đứt bóng một phát (single point of failure) lù lù ngay thằng quản trò (coordinator). Hễ ông nội quản trò này đột tử lúc đang kẹt giữa mẻ Chuẩn bị (Prepare) với Chốt đơn (Commit), nguyên bầy tớ tham gia cứng họng giam chân mốc mỏ vĩnh viễn với mớ ổ còng khóa đang đeo trên cổ — thảm họa tận thế cút xéo (catastrophic failure mode) trong làng microservices. Rặt nguyên xi y xì đúc mớ địa ngục giao dịch bủa vây lõi nhà băng (core banking distributed transaction challenges) thối nát bên mấy mỏ đồ cổ legacy.
Hố Xí Sập Hầm 2PC (2PC Failure Scenario)
sequenceDiagram
participant Coord as Thằng Quản Trò (Coordinator)
participant S1 as Xưởng Đơn Hàng (Order)
participant S2 as Bốt Thu Tiền (Payment)
participant S3 as Kho Chứa Hàng (Inventory)
Coord->>S1: Kêu Hét Chuẩn bị (Prepare)
Coord->>S2: Kêu Hét Chuẩn bị (Prepare)
Coord->>S3: Kêu Hét Chuẩn bị (Prepare)
S1-->>Coord: Hò reo Sẵn sàng (Ready)
S2-->>Coord: Hò reo Sẵn sàng (Ready)
S3-->>Coord: Hò reo Sẵn sàng (Ready)
Note over Coord: 💥 Thằng Quản Trò ĐỘT TỬ (CRASHES) sau khi Kêu Prepare, chưa kịp hú Commit
Note over S1,S3: Toàn dân bá tánh á khẩu (blocked)! Chìa khóa còng tay bám chết vĩnh viễn!
Note over S1,S3: chẳng một mống nào biết đường mà lùi (rollback) hay bồi thêm (commit)!
Thêm nùi phốt (Additional problems):
- Trói cổ tập thể (Blocking): Tất thảy đứng chầu chực há mỏ đợi lệnh thằng quản trò — bệnh ngáp ruồi lây lan (unavailability propagates).
- Tử huyệt chết chùm (Single point of failure): Thằng cầm trịch đứt bóng là kéo gãy cổ cả hệ thống (Achilles heel).
- Đạp chân chửi lộn (Cross-team incompatibility): Đám bậu xậu nuôi dưới trướng mấy băng nhóm (teams) khác biệt nhai ọc mớ DB thập cẩm hầm bà lằng cạch mặt chẳng thể xơi chung nồi cháo quản trò 2PC.
Dàn Nhạc Giao Hưởng Orchestration vs Điệu Múa Rừng Rú Choreography Cõi Saga
Answer-first: Phái Giao hưởng (Orchestration) rước một thằng nhạc trưởng chóp bu (central coordinator - vd lõi Temporal workflow) ới gọi rành rẽ từng thằng lính ra sân gõ nhịp (explicitly calls each service step in sequence) — mượt mà dọn rác lỗi (easier to debug), nhòm thấu ngọc ngà (full state visibility). Phái Múa rừng (Choreography) thì nã đạn hò hét sự kiện (event reactions) — thằng nào cắn xong ợ lên 1 tiếng event kích mông thằng kế tiếp vác súng ra trận — thả rông xé lẻ (more decoupled) nhưng nhọc xác hộc máu vạch trần tháo dỡ lúc rác văng miểng (harder to trace).
Bản Đồ Mò Đường Saga (Saga Flow Diagram)
graph LR
T1["T1: Nặn Ra Đơn Hàng ✅"] --> T2["T2: Xí Chỗ Giam Kho ✅"]
T2 --> T3["T3: Cấu Xé Vét Tiền ❌"]
T3 --> C2["C2: Nhả Nhốt Kho Ra\n(bồi đền - compensation)"]
C2 --> C1["C1: Đập Nát Đơn Hàng\n(bồi đền - compensation)"]
style T3 fill:#f8d7da,stroke:#dc3545
style C2 fill:#fff3cd,stroke:#f0a500
style C1 fill:#fff3cd,stroke:#f0a500
style T1 fill:#d4edda,stroke:#28a745
style T2 fill:#d4edda,stroke:#28a745
Phẩm hạnh nết na Saga (Saga properties):
- Chơi hốc ACD rụng mẻ I (ACD without I): Đập 1 cục Nguyên khối (Atomic - bấu víu bù trừ) + Chắc Cú Nhất Quán (Consistent) + Lì Lợm Cứng Ngắc (Durable). Văng miểng cút xéo mất dạy (No Isolation) — mấy đoạn ngã tư đường nham nhở (intermediate states) lòi lồ lộ ra trước mặt mấy trận chiến giao dịch khác.
- Dùng dằng kết cục (Eventual consistency): Guồng máy lê lết chốt số lại nếp cũ nhẵn thín láng o hễ mớ bồi đền bù trừ húp trọn (all compensations complete).
- Trò bồi đền chuộc lỗi thề sống thề chết phải có khiên cản đúp (idempotent): Rủi mà cái mẻ đi sửa sai (compensation) cũng lăn quay ra xui ngỏm củ tỏi vác xác đi làm lại (retried), thì nó cũng bắt buộc phải rặn ra 1 cái đích tới y boong bóc.
Bấu Víu Temporal Go SDK — Bao Trọn Gói Phường Dàn Nhạc (Full Orchestration Implementation)
Answer-first: Con hàng workflow.Saga của lò Temporal dí thẳng họng súng bồi đền theo luật vô-sau-ra-trước (LIFO - Last In, First Out) chùi dọn tự động rụp rụp (automatic execution) — đấm vỡ tháo gỡ thằng lính dọn xong chót vót (last successful step) đầu tiên, rồi lùi dần đè đầu thằng kề chót, lùi sâu tận mạng. Y xì boong cái lẽ đời bán buôn (business logic): ối giời thì phải nhả thối tiền lại (refund) trước rồi mới hé cửa lòi hàng khỏi kho (releasing inventory), rồi vứt sọt rác cái đơn (cancel order).
package saga
import (
"fmt"
"time"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
)
type OrderSagaInput struct {
OrderID string
UserID string
Items []OrderItem
Amount float64
Currency string
}
type OrderItem struct {
ProductID string
Quantity int
}
// OrderSagaWorkflow bóp cổ dàn xếp mượt mà nguyên mớ rác hầm bà lằng gò bó đơn hàng saga
func OrderSagaWorkflow(ctx workflow.Context, input OrderSagaInput) error {
activityOpts := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 5,
InitialInterval: time.Second,
MaximumInterval: 30 * time.Second,
BackoffCoefficient: 2.0,
// CẤM TỚI BẾN chẳng được rống cổ húc lại (retry) với mấy cái lỗi hãm l tài phiệt (business errors) — chóc đành đạch nhắm mấy cái lỗi té ngã xàm xí (transient failures) thôi
NonRetryableErrorTypes: []string{"PAYMENT_DECLINED", "INVENTORY_PERMANENTLY_UNAVAILABLE"},
},
}
ctx = workflow.WithActivityOptions(ctx, activityOpts)
var saga workflow.Saga
saga.SetParallelCompensation(false) // Chuỗi móc xích chuộc lỗi nối đuôi dập lùi (Sequential compensation - LIFO order)
// ─── Nháy 1 (Step 1): Đúc Đơn Hàng (Create Order) ─────────────────────────────────────────────
var orderResult CreateOrderResult
if err := workflow.ExecuteActivity(ctx, CreateOrderActivity, input).Get(ctx, &orderResult); err != nil {
return fmt.Errorf("thọt lòi đúc đơn: %w", err)
}
// Găm bùa dọn rác bồi đền NGAY VÀ LUÔN (IMMEDIATELY) sau khi ăn mảnh ngọt sớt ở một đoạn
saga.AddCompensation(CancelOrderActivity, orderResult.OrderID)
// ─── Nháy 2 (Step 2): Xí Cỗ Bịt Kho (Reserve Inventory) ────────────────────────────────────────
var reserveResult ReserveInventoryResult
if err := workflow.ExecuteActivity(ctx, ReserveInventoryActivity, orderResult.OrderID, input.Items).Get(ctx, &reserveResult); err != nil {
saga.Compensate(ctx) // Sút họng nhả bùa: CancelOrderActivity
return fmt.Errorf("thọt nghẽn xí kho: %w", err)
}
saga.AddCompensation(ReleaseInventoryActivity, reserveResult.ReservationID)
// ─── Nháy 3 (Step 3): Rút Máu Chấn Tiền (Process Payment) ──────────────────────────────────────────
var paymentResult ProcessPaymentResult
if err := workflow.ExecuteActivity(ctx, ProcessPaymentActivity, orderResult.OrderID, input.Amount).Get(ctx, &paymentResult); err != nil {
saga.Compensate(ctx) // Dội ngược pháo (LIFO): ReleaseInventoryActivity → rớt xuống CancelOrderActivity
return fmt.Errorf("lủng cống hút máu payment: %w", err)
}
saga.AddCompensation(RefundPaymentActivity, paymentResult.TransactionID)
// ─── Nháy 4 (Step 4): Hét Báo Mồi Nấu Đơn (Notify Fulfillment) ───────────────────────────────────────
if err := workflow.ExecuteActivity(ctx, NotifyFulfillmentActivity, orderResult.OrderID).Get(ctx, nil); err != nil {
saga.Compensate(ctx) // Giật súng lùi cọc (LIFO): RefundPaymentActivity → dập ReleaseInventory → trảm CancelOrder
return fmt.Errorf("ngọng họng loan báo: %w", err)
}
workflow.GetLogger(ctx).Info("Đóng hòm saga đơn hàng êm xuôi", "orderID", orderResult.OrderID)
return nil
}
// ─── Hình nhân thế mạng Activity stubs (nhớ lôi cổ ra đóng mác tròng vô Temporal Worker nha cưng) ─────────────
type CreateOrderResult struct{ OrderID string }
type ReserveInventoryResult struct{ ReservationID string }
type ProcessPaymentResult struct{ TransactionID string }
func CreateOrderActivity(input OrderSagaInput) (CreateOrderResult, error) {
// Đâm chém DB INSERT INTO orders ... ON CONFLICT DO NOTHING (cản đúp idempotent)
return CreateOrderResult{OrderID: "order-uuid"}, nil
}
func CancelOrderActivity(orderID string) error {
// Táng búa UPDATE orders SET status='cancelled' WHERE id=orderID (cản đúp idempotent)
return nil
}
func ReserveInventoryActivity(orderID string, items []OrderItem) (ReserveInventoryResult, error) {
return ReserveInventoryResult{ReservationID: fmt.Sprintf("res-%s", orderID)}, nil
}
func ReleaseInventoryActivity(reservationID string) error {
// Gõ phím UPDATE inventory_reservations SET status='released' WHERE id=reservationID (cản đúp idempotent)
return nil
}
func ProcessPaymentActivity(orderID string, amount float64) (ProcessPaymentResult, error) {
return ProcessPaymentResult{TransactionID: fmt.Sprintf("tx-%s", orderID)}, nil
}
func RefundPaymentActivity(transactionID string) error {
// Thọc gậy POST /v1/refunds {transaction_id: transactionID} (bọc nón cản đúp thông chốt idempotency key)
return nil
}
func NotifyFulfillmentActivity(orderID string) error {
return nil
}
[!IMPORTANT] Thứ bậc ưu tiên dọn rác bồi đền (Compensation order) = Bào từ chóp lên đầu (LIFO). Lệnh bóp cò
saga.Compensate()của Temporal cắn xé quật ngược quy trình nổ mìn dọn hầm y chang trình tự xoay chóp tụt lùi lúc bế xếp mâm. Dọn bãi Nháy 3 đi trước Nháy 2, rồi lăn lóc tới Nháy 1. Thằng này trúng mánh kinh doanh cọp (correct business logic): ói trả mẹ tiền (refund) xong rồi mới bung rào kho (releasing), chốt cửa gạt bỏ luôn tờ đơn (cancel).
Chiêu Trò Móc Hũ Trữ Transactional Outbox Pattern — Giấy Gọi Nhập Ngũ Sự Kiện Bao Chót Hố (Guaranteed Event Delivery)
Answer-first: Đấu pháp Hũ Trữ (Transactional Outbox Pattern) thề non hẹn biển bóp chết (guarantees) phán rằng mớ sớ rác Kafka kiện hàng phải được nhét phóng ra (published) kết dính đóng gạch tịt luôn một nùi nguyên khối chóc với cái nháy bôi DB write (atomically) — nếu nhát cắn hót lụm ghi chép chốt (commits), bọc hàng thể quái nào cũng trồi đầu ói ra phát tán. Ngộ nhỡ ổ chứa xẹp lép tạch đột tử (crashes) sau nháy đóng gạch (committing) mà ứ nghẹn chưa rặn bắn ra kịp (before publishing), cục nam châm CDC connector (lão Debezium) vác cày tới hốt quét cái mớ tàn tích hũ rác nằm chình ình từ hầm WAL và quăng tuốt luốt rải thảm rác nốt lúc cựa quậy tỉnh giấc phục sinh.
Tới Nước rác Gì Mà Chú Em Lải Nhải Đòi Xài Nó (Why You Need It)
Lọt hố sình lầy nếu chẳng xài Outbox (Problem without Outbox):
- Nhát vả DB giao dịch đóng hòm gọn lỏn (commits - đúc đơn ngon ơ).
- Tạch đứt hơi, sập (crashes) ngọng mẹ mõm trước cái giây gõ còi
kafka.Produce(event). - Đơn hàng hiện hồn trên nóc DB nhưng đám lính đánh thuê bu nhung nhúc ở hạ lưu (downstream) chẳng bắt được mớ sớ nào (never receive the event).
- Cả mâm nhà kho (Inventory), loa kẹo kéo (notifications), rải cọc soi bói (analytics) chéo ngoe cắn lộn vênh vẹo nhão nhét — bó tay chịu chết cmn luôn (no way to recover).
Thuốc Giải Độc với bùa Outbox (Solution with Outbox):
- DB giao dịch băm bổ đúc cùng một nhát cả mảng cục chéo order VÀ ọc thêm nùi ứ hũ rác event sập nắp xích dính lẹo (atomically).
- Ông ngáo Debezium chọc ngoáy nhai rỉa mớ bãi nôn Outbox móc lên từ đống xình lầy PostgreSQL WAL.
- Debezium tung hê ói nôn cái bọc sự kiện (event) tọng vào họng Kafka.
- Rủi nhọ ông kẹ Debezium đột quỵ chết ngắc, chả sao sất, lúc mò sống lại kéo cày tiếp tục nhấp nhổm cắn rỉa vạch đích hố WAL chót vót — chẳng thất thoát sứt mẻ 1 hột bụi rác nào (no event lost).
-- Lò đúc Hũ Trữ (Outbox table)
CREATE TABLE outbox_table (
id UUID NOT NULL DEFAULT gen_random_uuid(),
aggregate_type VARCHAR(100) NOT NULL, -- e.g., rặng danh hão 'order', 'payment'
aggregate_id VARCHAR(255) NOT NULL, -- e.g., mác chóp UUID đơn hàng (order UUID)
event_type VARCHAR(100) NOT NULL, -- e.g., hò hét 'ORDER_CREATED'
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
// Tọng mớ cặn bã nhét chui gầm nồi Application code — búa đóng cọc một nhát (atomic write): chốt order + đục luôn outbox event chui tuốt lọt thỏm 1 phát giao dịch (one transaction)
func (s *OrderService) CreateOrder(ctx context.Context, userID string, amount float64) (string, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback()
// 1. Phập (Insert) bãi nôn nghiệp vụ cơm áo (business record)
var orderID string
err = tx.QueryRowContext(ctx,
`INSERT INTO orders (user_id, amount, status) VALUES ($1, $2, 'pending') RETURNING id`,
userID, amount,
).Scan(&orderID)
if err != nil {
return "", err
}
// 2. Chích dộng thêm (Insert) cục rác outbox event — Y CHANG MỘT MÂM (SAME TRANSACTION)
payload, _ := json.Marshal(map[string]interface{}{
"order_id": orderID, "user_id": userID, "amount": amount,
})
_, err = tx.ExecContext(ctx,
`INSERT INTO outbox_table (aggregate_type, aggregate_id, event_type, payload)
VALUES ('order', $1, 'ORDER_CREATED', $2)`,
orderID, payload,
)
if err != nil {
return "", err
}
// 3. Khóa lẫy (committed) dính nhằng chặt nghẹt — Mụ phù thủy Debezium bò mẫm dọn chùi nốt cục cặn bã outbox
return orderID, tx.Commit()
}
Nêm Nếm Công Thức Nhồi Debezium PostgreSQL Outbox EventRouter Đem Phơi Production
{
"name": "postgres-outbox-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres-db.internal",
"database.port": "5432",
"database.user": "debezium",
"database.password": "${file:/secrets/debezium.properties:db.password}",
"database.dbname": "orders_db",
"database.server.name": "orders-dbserver",
"plugin.name": "pgoutput",
"slot.name": "debezium_outbox_slot",
"table.include.list": "public.outbox_table",
"tombstones.on.delete": "false",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.table.field.event.id": "id",
"transforms.outbox.table.field.event.key": "aggregate_id",
"transforms.outbox.table.field.event.payload": "payload",
"transforms.outbox.route.by.field": "aggregate_type",
"transforms.outbox.route.topic.replacement": "events.${routedByValue}",
"transforms.outbox.table.expand.json.payload": "true",
"key.converter": "org.apache.kafka.connect.storage.StringConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false"
}
}
[!NOTE] Bùa bốc
route.topic.replacement:aggregate_type = 'order'văng xẹt vô hũ → topicevents.order. Nếu lòiaggregate_type = 'payment'→ ném xéo topicevents.payment. Nhắm mắt chọt đại (Auto-routing) trơn láng chẳng thèm rườm rà bấu víu rác rưởi Kafka Streams (extra logic).Lên nòng chĩa súng PostgreSQL WAL config (postgresql.conf):
wal_level = logical max_wal_senders = 4 max_replication_slots = 4
Hỏi Nhanh Đáp Gọn (FAQ)
Rốt Cục Chọc Gậy Hai Lão Nhạc Trưởng (Saga Orchestration) Và Bọn Múa Khỉ (Choreography) Cái chẳng Gì Khác Nhau?
Lò Dàn Nhạc (Orchestration) (Kẹp nách Temporal): Thằng tướng chóp bu ở giữa thông não thấu tỏ nắm trọn guồng quay. Nùi sổ đỏ cục diện (All state) đóng hòm khóa kỹ vô két sắt lịch sử hành trình (workflow history). Ngon ăn thọc gậy sờ gáy gỡ mìn (Easy to debug) — Đồ chơi Temporal UI bới lông tìm vết lột trần truồng từng chân tơ kẽ tóc. Vạch mặt trân tráo trọi lỏi mớ bồi đền chuộc lỗi (Full compensation visibility). Điểm mặt đặt tên ném vô mấy cái đường dây làm ăn bốc mùi tử địa máu me sặc sụa (complex business-critical flows - như dọn chùi lên đơn hàng order fulfillment, lột da đắp mỡ xẻ thịt tiền tệ financial transfers).
Phái Khỉ Múa (Choreography): Xé lẻ mạnh thân ai nấy ré văng (emits events); bọn lính lác bên rào vểnh tai hóng cắn rỉa (react to them). Rạch ròi chẳng lòi bóng dáng 1 thằng bấu đầu ngồi xổm ở trển hay vướng bã SPOF. Xổ lồng tung bay loạn xạ phang nhây (More decoupled) khổ nỗi chóp bu mà muốn mò rác lau dọn (debugging failures) phải nai lưng chổng mông bò rúc bới tung rành rẽ bầy sớ nhằng nhịt Kafka topics (tracing events across multiple topics). Đắp mền nện vô mấy chốn quăng chài tỏa nón rải rác fan-out rẻ tiền tôm tép chẳng màng dọn rác bồi đền (no compensation needed).
Giở Chiêu Đúc Nặn Khuôn Mẫu Bồi Đền Bù Trừ (Compensating Transactions) Ra Làm Sao?
Lò chuộc lỗi buộc phải rập khuôn xăm chữ: (1) Cản Đúp (Idempotent) — múa may quay cuồng cày nát n bận cũng chỉ phọt ra y cái đích 1 cục đích duy nhất. Lận lưng bóp nghẹt ON CONFLICT DO NOTHING rà soi đo mớ bùa ngải (status checks) trước lúc múa cọ. (2) Ăn Rơ Gõ Nhịp Chuẩn Bệnh (Semantically correct) — chẳng phải chiêu dội máy chém văng mạng SQL ROLLBACK nhạt nhẽo mà là hất cẳng tháo ngòi đảo ngược cục diện ở chóp đỉnh làm ăn (business-level reversal) (giựt sập đơn, nới cổ nhả hàng, khạc ói bồi tiền). (3) Chày Cối Tới Cùng Lì Lợm Khớp Ngàm (Eventually complete) — Đạo luật húc đập lì lợm chọc ngoáy retry policy của Temporal đè đầu ấn cổ ép đống bồi đền bấu víu rốt cục kiểu mẹ gì cũng gặm trái ngọt (will eventually succeed) chấp luôn mấy cái đợt té hầm gãy giò (transient failures).
Ngứa Cựa Chỗ Nào Đè Phép Outbox Pattern Ra Chơi?
Ngậm đắng nuốt xài Outbox khi mà kề dao ngang cổ bắt thề thốt: “hễ mẻ đóng gạch DB nháy đèn xanh (commits), cục rác Kafka dứt khoát SẼ ĐƯỢC ỌC BẮN THẢ RA (WILL be published).” Cấm chỉ liếm mép chơi ngu hễ: cắn răn ngậm miệng dễ dãi ậm ờ mớ rác rớt lọt rơi vãi ném lơ (best-effort event publishing is acceptable), hay là cái độ rùa bò trễ tràng của cái vòi hút bùn CDC pipeline (chòng chành lê thê bét nhét 100–500ms) vắt kiệt đứt cuống họng đòi hỏi của mày (is too high) (đánh võng ôm luôn pháo bắn sống văng thẳng tay synchronous event publishing rước mẹ luôn cái mìn ngậm rác Message loss khi sập hầm).
🔗 Bay Sang Bài Tới: Phần 9: Phân Mảnh Nhất Quán Khớp Ngàm Bánh Răng (Consistent Hashing) — Hạt Vỡ Mộng Ảo (Virtual Nodes) & Rập Khuôn Vòng Băm CRC32 Trong Go — Bới lông chọc lỗ xem trò băm chẻ dư (modulo hashing) thọt ngáo hầm làm sao, nhẩm nhức nách con tính phơi bụng nứt hạt ảo (virtual node variance math), và rèn cái khiên sắt bọc thép thread-safe GetN vung búa.