阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例

This commit is contained in:
Shikui Wang committed 2026-10-23 15:24:57 +08:00
1 parent 0bae9916a9
commit 5eb67c5d08
65 files changed
+6457 -77

No files matched your search

@@ -0,0 +1,105 @@
"""发件箱(Outbox)模式:业务数据和“待发送消息”写在同一个数据库事务里,由后台程序投递。
只用标准库(sqlite3)演示,直接运行:python outbox_demo.py
真实项目里把 sqlite 换成 MySQL / PostgreSQL,把 FakeBroker 换成 RabbitMQ / Kafka 客户端。
"""
import json
import sqlite3
class FakeBroker:
"""模拟消息队列:fail_times 次之内发送失败,用来演示重试。"""
def __init__(self, fail_times: int = 0) -> None:
self.sent: list[dict] = []
self.fail_times = fail_times
def publish(self, topic: str, msg: dict) -> None:
if self.fail_times > 0:
self.fail_times -= 1
raise ConnectionError("broker 暂时不可用")
self.sent.append({"topic": topic, **msg})
def init(db: sqlite3.Connection) -> None:
db.executescript("""
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, amount INTEGER NOT NULL);
CREATE TABLE outbox (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
payload TEXT NOT NULL,
sent_at TEXT, -- NULL 表示还没发送
attempts INTEGER NOT NULL DEFAULT 0
);
""")
def create_order(db: sqlite3.Connection, user_id: int, amount: int, fail_after_insert: bool = False) -> int:
with db: # 一个事务:订单和消息要么都写入,要么都不写入
cur = db.execute("INSERT INTO orders (user_id, amount) VALUES (?, ?)", (user_id, amount))
order_id = cur.lastrowid
db.execute("INSERT INTO outbox (topic, payload) VALUES (?, ?)",
("order.created", json.dumps({"order_id": order_id, "user_id": user_id, "amount": amount})))
if fail_after_insert:
raise RuntimeError("模拟业务异常:事务回滚")
return order_id
def relay(db: sqlite3.Connection, broker: FakeBroker, batch: int = 100) -> int:
"""后台投递:取未发送的消息,发送成功后标记。可能重复发送(至少一次),消费者要幂等。"""
rows = db.execute("SELECT id, topic, payload FROM outbox WHERE sent_at IS NULL ORDER BY id LIMIT ?",
(batch,)).fetchall()
sent = 0
for mid, topic, payload in rows:
try:
broker.publish(topic, {"message_id": mid, **json.loads(payload)})
except ConnectionError:
with db:
db.execute("UPDATE outbox SET attempts = attempts + 1 WHERE id = ?", (mid,))
break # 保持顺序:前面的没发出去,后面的先不发
with db:
db.execute("UPDATE outbox SET sent_at = datetime('now') WHERE id = ?", (mid,))
sent += 1
return sent
class Consumer:
"""幂等消费者:按 message_id 去重(真实项目里用数据库唯一约束记录已处理的消息)。"""
def __init__(self) -> None:
self.processed: set[int] = set()
self.points: dict[int, int] = {}
def handle(self, msg: dict) -> None:
if msg["message_id"] in self.processed:
return
self.points[msg["user_id"]] = self.points.get(msg["user_id"], 0) + msg["amount"] // 100
self.processed.add(msg["message_id"])
def main() -> None:
db = sqlite3.connect(":memory:")
init(db)
create_order(db, user_id=1, amount=19900)
try:
create_order(db, user_id=2, amount=500, fail_after_insert=True)
except RuntimeError:
pass
create_order(db, user_id=1, amount=10000)
assert db.execute("SELECT count(*) FROM orders").fetchone()[0] == 2
assert db.execute("SELECT count(*) FROM outbox").fetchone()[0] == 2 # 回滚的订单没有留下消息
broker = FakeBroker(fail_times=1)
assert relay(db, broker) == 0 # 第一次 broker 不可用
assert relay(db, broker) == 2 # 重试成功
assert relay(db, broker) == 0 # 没有待发送的了
consumer = Consumer()
for m in broker.sent + broker.sent[:1]: # 模拟第一条消息被重复投递
consumer.handle(m)
assert consumer.points == {1: 199 + 100}, consumer.points
print("发件箱:订单和消息同事务写入;投递失败可重试;消费者幂等,重复消息不会重复加积分")
if __name__ == "__main__":
main()