阶段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,114 @@
# requires: pymysql
"""对比 LIMIT offset 深分页和游标(keyset)分页。
运行:MYSQL_URL=mysql://user:pass@127.0.0.1:3306/db python paginate.py
没有设置 MYSQL_URL 时只打印提示并正常退出。会创建并删除临时表 demo_page_orders。
"""
import os
import sys
import time
from urllib.parse import urlparse
import pymysql
TABLE = "demo_page_orders"
ROWS = 100_000
PAGE = 20
def connect(url: str) -> pymysql.connections.Connection:
u = urlparse(url)
return pymysql.connect(host=u.hostname, port=u.port or 3306, user=u.username, password=u.password or "",
database=u.path.lstrip("/"), autocommit=True)
def seed(cur) -> None:
cur.execute(f"DROP TABLE IF EXISTS {TABLE}")
cur.execute(f"""CREATE TABLE {TABLE} (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
created_at DATETIME NOT NULL,
note VARCHAR(100) NOT NULL,
KEY idx_user_time_id (user_id, created_at, id)
) ENGINE = InnoDB""")
batch = []
for i in range(ROWS):
batch.append((1, f"note-{i}")) # 全部属于 user 1,便于构造深分页
if len(batch) == 5000:
cur.executemany(f"INSERT INTO {TABLE} (user_id, created_at, note) VALUES (%s, '2026-01-01', %s)", batch)
batch.clear()
# 时间按 id 递增;让每 3 行共用一个时间,演示“时间相同按 id 排序”
cur.execute(f"UPDATE {TABLE} SET created_at = '2026-01-01' + INTERVAL (id DIV 3) SECOND")
cur.execute(f"ANALYZE TABLE {TABLE}")
cur.fetchall()
def offset_page(cur, offset: int) -> list[tuple]:
cur.execute(f"SELECT id, created_at, note FROM {TABLE} WHERE user_id = 1 "
f"ORDER BY created_at DESC, id DESC LIMIT %s OFFSET %s", (PAGE, offset))
return list(cur.fetchall())
def deferred_join_page(cur, offset: int) -> list[tuple]:
# 延迟关联:先在覆盖索引上找出 id,再回表取这一页的整行
cur.execute(f"""SELECT o.id, o.created_at, o.note FROM {TABLE} o
JOIN (SELECT id FROM {TABLE} WHERE user_id = 1 ORDER BY created_at DESC, id DESC LIMIT %s OFFSET %s) t
ON o.id = t.id ORDER BY o.created_at DESC, o.id DESC""", (PAGE, offset))
return list(cur.fetchall())
def keyset_page(cur, after: tuple | None) -> list[tuple]:
if after is None:
cur.execute(f"SELECT id, created_at, note FROM {TABLE} WHERE user_id = 1 "
f"ORDER BY created_at DESC, id DESC LIMIT %s", (PAGE,))
else:
created_at, last_id = after
cur.execute(f"SELECT id, created_at, note FROM {TABLE} WHERE user_id = 1 "
f"AND (created_at < %s OR (created_at = %s AND id < %s)) "
f"ORDER BY created_at DESC, id DESC LIMIT %s", (created_at, created_at, last_id, PAGE))
return list(cur.fetchall())
def timed(fn, *args) -> tuple[float, list[tuple]]:
t = time.perf_counter()
rows = fn(*args)
return (time.perf_counter() - t) * 1000, rows
def main() -> int:
url = os.environ.get("MYSQL_URL")
if not url:
print("未设置 MYSQL_URL,跳过(示例:mysql://app:app@127.0.0.1:3306/learn)")
return 0
conn = connect(url)
cur = conn.cursor()
try:
seed(cur)
offset = ROWS - 1000 # 很深的一页
ms1, a = timed(offset_page, cur, offset)
ms2, b = timed(deferred_join_page, cur, offset)
# 游标分页:拿到上一页最后一行 (created_at, id),直接从那里继续
prev = offset_page(cur, offset - PAGE)[-1]
ms3, c = timed(keyset_page, cur, (prev[1], prev[0]))
assert a == b == c, "三种写法结果应该一致"
print(f"OFFSET {offset}: {ms1:.1f} ms;延迟关联: {ms2:.1f} ms;游标分页: {ms3:.1f} ms(耗时与机器有关,看相对差异)")
# 游标分页逐页翻完不重不漏
seen, after = 0, None
while True:
rows = keyset_page(cur, after)
if not rows:
break
seen += len(rows)
after = (rows[-1][1], rows[-1][0])
if seen >= 2000:
break
assert seen == 2000
print("游标分页连续翻页正确")
return 0
finally:
cur.execute(f"DROP TABLE IF EXISTS {TABLE}")
conn.close()
if __name__ == "__main__":
sys.exit(main())