<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MySQL 主从复制交互实验手册 - Docker 在线实战</title>
<meta name="keywords" content="MySQL, 主从复制, Replication, Docker Compose, binlog, 边学边练, 数据库运维">
<meta name="description" content="基于 Docker 的 MySQL 8.0 主从复制交互实验手册,在云端终端逐任务实操:编排部署、位点复制、数据一致性验证、离线追赶演练、ROW 格式过滤真相与 1146 故障修复全流程。">
<meta name="author" content="边学边练">
<style>
:root, [data-theme="light"] {
--bg-base: #f6f8fa;
--bg-card: #ffffff;
--bg-subtle: #f6f8fa;
--border-color: #d0d7de;
--border-muted: #e2e8f0;
--text-primary: #1f2328;
--text-secondary: #656d76;
--text-muted: #8c959f;
--k8s-blue: #326ce5;
--mysql-teal: #00758f;
--mysql-orange: #e48e00;
--primary: #0969da;
--primary-hover: #0550ae;
--primary-bg: #ddf4ff;
--success: #1a7f37;
--success-bg: #dafbe1;
--danger: #cf222e;
--danger-hover: #a40e26;
--danger-bg: #ffebe9;
--code-bg: #161b22;
--code-text: #e6edf3;
--code-prompt: #7d8590;
--output-bg: #f6f8fa;
--output-text: #24292f;
--output-border: #d0d7de;
--btn-bg: #ffffff;
--btn-border: #d0d7de;
--btn-text: #24292f;
--btn-hover: #f3f4f6;
--card-radius: 6px;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
[data-theme="dark"] {
--bg-base: #0d1117;
--bg-card: #161b22;
--bg-subtle: #21262d;
--border-color: #30363d;
--border-muted: #21262d;
--text-primary: #e6edf3;
--text-secondary: #7d8590;
--text-muted: #6e7681;
--k8s-blue: #388bfd;
--mysql-teal: #23a2c8;
--mysql-orange: #f0a84a;
--primary: #2f81f7;
--primary-hover: #58a6ff;
--primary-bg: #033d8b;
--success: #3fb950;
--success-bg: #033a16;
--danger: #f85149;
--danger-hover: #ff7b72;
--danger-bg: #490202;
--code-bg: #0d1117;
--code-text: #e6edf3;
--code-prompt: #7d8590;
--output-bg: #161b22;
--output-text: #e6edf3;
--output-border: #30363d;
--btn-bg: #21262d;
--btn-border: #363b42;
--btn-text: #c9d1d9;
--btn-hover: #30363d;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-base);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.5;
padding: 16px;
-webkit-font-smoothing: antialiased;
}
.lab-container {
max-width: 960px;
margin: 0 auto;
}
/* 顶部导航与元信息 */
.lab-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 14px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-color);
flex-wrap: wrap;
gap: 10px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.mysql-logo {
width: 28px;
height: 28px;
flex-shrink: 0;
}
.lab-title-group h1 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.meta-badges {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
flex-wrap: wrap;
}
.badge {
font-size: 11px;
font-family: var(--font-mono);
padding: 1px 6px;
border-radius: 4px;
background: var(--bg-subtle);
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.badge-primary {
color: var(--primary);
border-color: var(--border-color);
}
.badge-verified {
color: var(--success);
background: var(--success-bg);
border-color: var(--success);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
.theme-toggle {
background: var(--btn-bg);
border: 1px solid var(--btn-border);
color: var(--btn-text);
padding: 4px 8px;
border-radius: var(--card-radius);
font-size: 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: background 0.15s ease;
}
.theme-toggle:hover {
background: var(--btn-hover);
}
/* 进度条 */
.progress-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--card-radius);
padding: 10px 14px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.progress-label {
font-size: 12px;
color: var(--text-secondary);
font-family: var(--font-mono);
white-space: nowrap;
}
.progress-track {
flex: 1;
min-width: 140px;
height: 6px;
background: var(--bg-subtle);
border: 1px solid var(--border-color);
border-radius: 999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: var(--success);
transition: width 0.25s ease;
}
.btn-mini {
background: var(--btn-bg);
border: 1px solid var(--btn-border);
color: var(--btn-text);
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-family: var(--font-mono);
cursor: pointer;
transition: all 0.15s ease;
}
.btn-mini:hover {
background: var(--btn-hover);
}
/* 架构概览区 */
.arch-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--card-radius);
padding: 14px 16px;
margin-bottom: 16px;
}
.arch-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.arch-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.arch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.node-col {
background: var(--bg-subtle);
border: 1px solid var(--border-muted);
border-radius: 4px;
padding: 10px 12px;
}
.node-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-color);
}
.node-name {
font-family: var(--font-mono);
font-weight: 600;
font-size: 12px;
color: var(--text-primary);
}
.node-tag {
font-size: 10px;
font-family: var(--font-mono);
padding: 1px 5px;
border-radius: 3px;
background: var(--success-bg);
color: var(--success);
border: 1px solid currentColor;
}
.node-tag-slave {
background: transparent;
color: var(--mysql-teal);
border: 1px solid currentColor;
}
.comp-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.comp-item {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: space-between;
}
.comp-port {
color: var(--text-muted);
font-size: 10px;
}
.arch-note {
font-size: 11px;
color: var(--text-secondary);
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px;
}
/* 任务卡片体系 */
.task-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--card-radius);
margin-bottom: 16px;
overflow: hidden;
}
.task-card.done {
border-color: var(--success);
}
.task-header {
padding: 10px 14px;
background: var(--bg-subtle);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.task-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.task-id {
font-family: var(--font-mono);
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
background: var(--primary);
color: #ffffff;
font-weight: normal;
}
.task-check {
background: var(--btn-bg);
border: 1px solid var(--btn-border);
color: var(--btn-text);
font-family: var(--font-mono);
font-size: 11px;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.task-check:hover {
background: var(--btn-hover);
}
.task-check.checked {
background: var(--success-bg);
border-color: var(--success);
color: var(--success);
}
.task-body {
padding: 14px;
}
.task-desc {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 12px;
line-height: 1.6;
}
.task-desc code {
font-family: var(--font-mono);
font-size: 11px;
padding: 1px 4px;
border-radius: 3px;
background: var(--bg-subtle);
border: 1px solid var(--border-color);
color: var(--text-primary);
}
/* 代码块与操作区 */
.cmd-block {
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 10px;
overflow: hidden;
background: var(--code-bg);
}
.cmd-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.04);
padding: 6px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
gap: 8px;
flex-wrap: wrap;
}
.cmd-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--code-prompt);
}
.cmd-actions {
display: flex;
align-items: center;
gap: 6px;
}
.btn-action {
background: #21262d;
border: 1px solid #363b42;
color: #c9d1d9;
padding: 2px 8px;
font-size: 11px;
font-family: var(--font-mono);
border-radius: 3px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.15s ease;
}
.btn-action:hover {
background: #30363d;
border-color: #8b949e;
color: #ffffff;
}
.btn-action-primary {
background: #1f6feb;
border-color: #388bfd;
color: #ffffff;
}
.btn-action-primary:hover {
background: #388bfd;
}
.btn-action-danger {
background: #21262d;
border-color: var(--danger);
color: var(--danger);
}
.btn-action-danger:hover {
background: var(--danger);
color: #ffffff;
}
.cmd-content {
padding: 8px 12px;
font-family: var(--font-mono);
font-size: 12px;
color: var(--code-text);
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
cursor: pointer;
}
.cmd-prompt {
color: var(--code-prompt);
user-select: none;
margin-right: 6px;
}
/* 预期输出块 */
.output-box {
background: var(--output-bg);
border: 1px solid var(--output-border);
border-radius: 4px;
padding: 8px 12px;
margin-top: 8px;
margin-bottom: 10px;
}
.output-title {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
}
.output-toggle {
font-size: 9px;
color: var(--text-muted);
}
.output-content {
font-family: var(--font-mono);
font-size: 11px;
color: var(--output-text);
line-height: 1.4;
white-space: pre;
overflow-x: auto;
}
.output-content.collapsed {
display: none;
}
/* 多行预格式文本 */
.multiline-pre {
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--code-text);
max-height: 240px;
overflow-y: auto;
white-space: pre;
line-height: 1.45;
}
/* 提示条 */
.callout {
padding: 10px 12px;
border-radius: 4px;
margin-bottom: 12px;
font-size: 12px;
color: var(--text-primary);
line-height: 1.6;
}
.callout-warn {
background: rgba(227, 179, 65, 0.15);
border-left: 4px solid #d29922;
}
.callout-info {
background: rgba(56, 139, 253, 0.1);
border-left: 4px solid var(--primary);
}
.callout-danger {
background: rgba(207, 34, 46, 0.08);
border-left: 4px solid var(--danger);
}
.callout-ok {
background: rgba(26, 127, 55, 0.1);
border-left: 4px solid var(--success);
}
.callout code {
font-family: var(--font-mono);
font-size: 11px;
padding: 1px 4px;
border-radius: 3px;
background: var(--bg-subtle);
border: 1px solid var(--border-color);
}
/* 底部轻量提示 */
.toast {
position: fixed;
bottom: 16px;
right: 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
font-family: var(--font-mono);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: none;
align-items: center;
gap: 6px;
z-index: 1000;
}
@media (max-width: 600px) {
body {
padding: 10px;
}
.arch-grid {
grid-template-columns: 1fr;
}
.header-right {
width: 100%;
justify-content: flex-end;
}
}
</style>
</head>
<body>
<div class="lab-container">
<!-- 头部规范 -->
<header class="lab-header">
<div class="header-left">
<svg class="mysql-logo" viewBox="0 0 32 32" fill="none">
<circle cx="16" cy="16" r="15" fill="#00758f"/>
<path d="M9 21c1.2 1.4 3 2.2 5 2.2 2.2 0 3.4-1 3.4-2.4 0-1.4-1-2-2.8-2.5l-1.3-.4c-1.5-.4-2.3-1-2.3-2 0-1.2 1-2 2.7-2 1.7 0 3 .7 4 1.9" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>
<ellipse cx="22.5" cy="15" rx="5.5" ry="2.2" stroke="#e48e00" stroke-width="1.6" fill="none"/>
</svg>
<div class="lab-title-group">
<h1>MySQL 8.0 主从复制交互实验手册</h1>
<div class="meta-badges">
<span class="badge">Docker Compose</span>
<span class="badge">mysql:8.0</span>
<span class="badge">binlog + Position</span>
<span class="badge">ROW</span>
<span class="badge badge-verified">全任务已实机验证</span>
</div>
</div>
</div>
<div class="header-right">
<span class="badge" id="progress-text">0 / 18</span>
<button class="theme-toggle" onclick="toggleTheme()" id="theme-toggle-btn">
<span id="theme-text">亮色模式</span>
</button>
</div>
</header>
<!-- 进度条 -->
<div class="progress-card">
<span class="progress-label">实验进度</span>
<div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div>
<button class="btn-mini" onclick="resetProgress()">重置进度</button>
<button class="btn-mini" onclick="scrollPrev()">▲ 上一个未做</button>
</div>
<!-- 架构概览 -->
<article class="arch-card">
<div class="arch-header">
<div class="arch-title">🔷 主从拓扑与实例标识 (Replication Topology)</div>
<span class="badge badge-primary">node1 / Ubuntu 24.04</span>
</div>
<div class="arch-grid">
<div class="node-col">
<div class="node-head">
<span class="node-name">mysql-master</span>
<span class="node-tag">source</span>
</div>
<div class="comp-list">
<div class="comp-item"><span>server-id</span><span class="comp-port">1</span></div>
<div class="comp-item"><span>log-bin</span><span class="comp-port">mysql-bin</span></div>
<div class="comp-item"><span>binlog_format</span><span class="comp-port">ROW</span></div>
<div class="comp-item"><span>sync_binlog</span><span class="comp-port">1</span></div>
<div class="comp-item"><span>Dump Thread</span><span class="comp-port">3306</span></div>
</div>
</div>
<div class="node-col">
<div class="node-head">
<span class="node-name">mysql-slave</span>
<span class="node-tag node-tag-slave">replica</span>
</div>
<div class="comp-list">
<div class="comp-item"><span>server-id</span><span class="comp-port">2</span></div>
<div class="comp-item"><span>relay-log</span><span class="comp-port">mysqld-relay-bin</span></div>
<div class="comp-item"><span>replicate-do-db</span><span class="comp-port">cmdb01</span></div>
<div class="comp-item"><span>relay_log_recovery</span><span class="comp-port">ON</span></div>
<div class="comp-item"><span>IO / SQL Thread</span><span class="comp-port">3307</span></div>
</div>
</div>
</div>
<div class="arch-note">
<span>宿主端口:<code>3306</code> 主库 · <code>3307</code> 从库(容器内仍为 3306)</span>
<span>root 密码 <code>srebro@2024</code> · 复制账号 <code>repluser / srebro@test</code></span>
<span>专用网络 <code>mysql-ms-net</code></span>
</div>
</article>
<!-- Task 0 -->
<article class="task-card" id="task-0">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 0</span><span>环境准备:创建实验工作目录</span></div>
<button class="task-check" onclick="toggleDone(0)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">所有物料集中在 <code>/root/mysql-ms</code>,两节点配置分目录存放,便于挂载区分。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">Shell Command</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="mkdir -p /root/mysql-ms/master /root/mysql-ms/slave && cd /root/mysql-ms && ls -d master slave">执行</button>
<button class="btn-action" onclick="copyCommand('mkdir -p /root/mysql-ms/master /root/mysql-ms/slave && cd /root/mysql-ms && ls -d master slave')">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>mkdir -p /root/mysql-ms/master /root/mysql-ms/slave && cd /root/mysql-ms && ls -d master slave</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">master
slave</div>
</div>
</div>
</article>
<!-- Task 1 -->
<article class="task-card" id="task-1">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 1</span><span>编写编排文件 docker-compose.yml</span></div>
<button class="task-check" onclick="toggleDone(1)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-info">💡 <strong>设计要点:</strong><code>--server-id</code> / <code>--relay-log</code> / <code>--replicate-do-db</code> 通过 <code>command</code> 下发而非写进 my.cnf,一份配置文件可双节点复用。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">docker-compose.yml (heredoc)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" onclick="runBlock('blk-compose')">整段下发</button>
<button class="btn-action" onclick="copyBlock('blk-compose')">复制</button>
</div>
</div>
<pre class="multiline-pre" id="blk-compose"><code>cd /root/mysql-ms && cat > docker-compose.yml <<'EOF'
services:
mysql-master:
image: mysql:8.0
container_name: mysql-master
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "srebro@2024"
TZ: "Asia/Shanghai"
ports:
- "3306:3306"
volumes:
- ./master/my.cnf:/etc/mysql/conf.d/my.cnf:ro
- master-data:/var/lib/mysql
command:
- --server-id=1
- --log-bin=mysql-bin
- --binlog-format=ROW
- --sync-binlog=1
- --innodb-flush-log-at-trx-commit=1
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_general_ci
- --default-time-zone=+8:00
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-uroot", "-psrebro@2024"]
interval: 5s
timeout: 5s
retries: 30
networks: [mysql-ms]
mysql-slave:
image: mysql:8.0
container_name: mysql-slave
restart: unless-stopped
depends_on:
mysql-master:
condition: service_healthy
environment:
MYSQL_ROOT_PASSWORD: "srebro@2024"
TZ: "Asia/Shanghai"
ports:
- "3307:3306"
volumes:
- ./slave/my.cnf:/etc/mysql/conf.d/my.cnf:ro
- slave-data:/var/lib/mysql
command:
- --server-id=2
- --relay-log=mysqld-relay-bin
- --replicate-do-db=cmdb01
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_general_ci
- --default-time-zone=+8:00
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-uroot", "-psrebro@2024"]
interval: 5s
timeout: 5s
retries: 30
networks: [mysql-ms]
volumes:
master-data: {}
slave-data: {}
networks:
mysql-ms:
name: mysql-ms-net
EOF
echo WROTE && wc -l docker-compose.yml</code></pre>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">WROTE
65 docker-compose.yml</div>
</div>
</div>
</article>
<!-- Task 2 -->
<article class="task-card" id="task-2">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 2</span><span>编写两节点配置文件 my.cnf</span></div>
<button class="task-check" onclick="toggleDone(2)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-warn">⚠️ <strong>MySQL 8.0 适配:</strong>参考模板中的 <code>sql_mode=...NO_AUTO_CREATE_USER</code> 在 8.0 已被移除,照抄会导致实例无法启动;<code>skip_ssl</code> 亦已废弃。以下为剔除后的可用版本。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">master/my.cnf + slave/my.cnf (heredoc)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" onclick="runBlock('blk-mycnf')">整段下发</button>
<button class="btn-action" onclick="copyBlock('blk-mycnf')">复制</button>
</div>
</div>
<pre class="multiline-pre" id="blk-mycnf"><code>cd /root/mysql-ms && cat > master/my.cnf <<'EOF'
[mysqld]
max_connections=1000
default-storage-engine=INNODB
character-set-server=utf8mb4
collation_server=utf8mb4_general_ci
skip-name-resolve=1
lower_case_table_names=1
log_timestamps=SYSTEM
default-time-zone=+8:00
max_allowed_packet=512M
binlog_expire_logs_seconds=604800
[mysql]
default-character-set=utf8mb4
[client]
default-character-set=utf8mb4
EOF
cat > slave/my.cnf <<'EOF'
[mysqld]
max_connections=1000
default-storage-engine=INNODB
character-set-server=utf8mb4
collation_server=utf8mb4_general_ci
skip-name-resolve=1
lower_case_table_names=1
log_timestamps=SYSTEM
default-time-zone=+8:00
max_allowed_packet=512M
relay_log_recovery=ON
replicate-ignore-db=mysql
replicate-ignore-db=information_schema
replicate-ignore-db=sys
replicate-ignore-db=performance_schema
[mysql]
default-character-set=utf8mb4
[client]
default-character-set=utf8mb4
EOF
echo OK && wc -l master/my.cnf slave/my.cnf</code></pre>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">OK
17 master/my.cnf
21 slave/my.cnf
38 total</div>
</div>
</div>
</article>
<!-- Task 3 -->
<article class="task-card" id="task-3">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 3</span><span>拉起集群 (Compose Up)</span></div>
<button class="task-check" onclick="toggleDone(3)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">首次执行需拉取镜像,耗时约 30 秒至数分钟。<code>depends_on</code> 使用 <code>condition: service_healthy</code>,Compose 会等主库真正可服务后才创建从库。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">Shell Command</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="cd /root/mysql-ms && docker compose up -d">执行</button>
<button class="btn-action" onclick="copyCommand('cd /root/mysql-ms && docker compose up -d')">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>cd /root/mysql-ms && docker compose up -d</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">[+] up 5/5
✔ Network mysql-ms-net Created 0.1s
✔ Volume mysql-ms_master-data Created 0.0s
✔ Volume mysql-ms_slave-data Created 0.0s
✔ Container mysql-master Healthy 27.6s
✔ Container mysql-slave Created 0.0s</div>
</div>
<div class="callout callout-info">💡 若命令返回超时但容器仍在启动,<strong>不要重复提交</strong>;用下一个 Task 的命令轮询真实状态即可。</div>
</div>
</article>
<!-- Task 4 -->
<article class="task-card" id="task-4">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 4</span><span>查看集群状态与端口映射 (Compose PS)</span></div>
<button class="task-check" onclick="toggleDone(4)">标记完成</button>
</div>
<div class="task-body">
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">Shell Command</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker compose -f /root/mysql-ms/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'">执行</button>
<button class="btn-action" onclick="copyCommand("docker compose -f /root/mysql-ms/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker compose -f /root/mysql-ms/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">NAME SERVICE STATUS PORTS
mysql-master mysql-master Up 45 seconds (healthy) 0.0.0.0:3306->3306/tcp, 33060/tcp
mysql-slave mysql-slave Up 17 seconds (healthy) 33060/tcp, 0.0.0.0:3307->3306/tcp</div>
</div>
</div>
</article>
<!-- Task 5 -->
<article class="task-card" id="task-5">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 5</span><span>主库创建复制账号 (Create Repl User)</span></div>
<button class="task-check" onclick="toggleDone(5)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-danger">🔥 <strong>MySQL 8.0 语法变更(实测报错):</strong>旧笔记里的 <code>grant replication slave on *.* to repluser@'%' identified by '...'</code> 在 8.0 已失效,实测返回 <code>ERROR 1064</code>。8.0 起 <code>GRANT</code> 不再隐式创建用户,必须先 <code>CREATE USER</code>。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">旧语法对照(会报错,勿用于 8.0)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-danger" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e "grant replication slave on *.* to repluser2@'%' identified by 'x';" 2>&1 | tail -2">复现报错</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e "grant replication slave on *.* to repluser2@'%' identified by 'x';" 2>&1 | tail -2</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the right syntax to use near 'identified by 'x'' at line 1</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">✅ 8.0 正确写法</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e "CREATE USER IF NOT EXISTS 'repluser'@'%' IDENTIFIED WITH mysql_native_password BY 'srebro@test'; GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%'; FLUSH PRIVILEGES;" 2>/dev/null && echo OK">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -e \"CREATE USER IF NOT EXISTS 'repluser'@'%' IDENTIFIED WITH mysql_native_password BY 'srebro@test'; GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%'; FLUSH PRIVILEGES;\" 2>/dev/null && echo OK")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e "CREATE USER IF NOT EXISTS 'repluser'@'%' IDENTIFIED WITH mysql_native_password BY 'srebro@test'; GRANT REPLICATION SLAVE ON *.* TO 'repluser'@'%'; FLUSH PRIVILEGES;" 2>/dev/null && echo OK</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">核验账号与认证插件</span>
<div class="cmd-actions">
<button class="btn-action" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e "SELECT CONCAT(user,'@',host,' plugin=',plugin) FROM mysql.user WHERE user='repluser';" 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e \"SELECT CONCAT(user,'@',host,' plugin=',plugin) FROM mysql.user WHERE user='repluser';\" 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e "SELECT CONCAT(user,'@',host,' plugin=',plugin) FROM mysql.user WHERE user='repluser';" 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>预期输出 (Expected Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">OK
repluser@% plugin=mysql_native_password</div>
</div>
</div>
</article>
<!-- Task 6 -->
<article class="task-card" id="task-6">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 6</span><span>获取主库 binlog 位点 (Show Master Status)</span></div>
<button class="task-check" onclick="toggleDone(6)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">位点复制必须拿到当前 binlog 文件名与偏移量,从库据此确定从哪个位置开始拉取。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">Shell Command</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e 'SHOW MASTER STATUS;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e 'SHOW MASTER STATUS;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -N -B -e 'SHOW MASTER STATUS;' 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">mysql-bin.000003 849</div>
</div>
</div>
</article>
<!-- Task 7 -->
<article class="task-card" id="task-7">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 7</span><span>从库建链并启动复制 (Change Master & Start Slave)</span></div>
<button class="task-check" onclick="toggleDone(7)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">脚本自动读取主库当前位点并完成 <code>CHANGE MASTER TO</code>,避免人工抄写位点出错,可重复执行。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">setup-slave.sh (heredoc)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" onclick="runBlock('blk-setup')">整段下发</button>
<button class="btn-action" onclick="copyBlock('blk-setup')">复制</button>
</div>
</div>
<pre class="multiline-pre" id="blk-setup"><code>cd /root/mysql-ms && cat > setup-slave.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
PW='srebro@2024'
read -r MFILE MPOS REST < <(docker exec mysql-master mysql -uroot -p"$PW" -N -B -e 'SHOW MASTER STATUS' 2>/dev/null)
echo "MASTER_FILE=$MFILE MASTER_POS=$MPOS"
docker exec mysql-slave mysql -uroot -p"$PW" -e 'STOP SLAVE;' 2>/dev/null
docker exec mysql-slave mysql -uroot -p"$PW" -e "CHANGE MASTER TO MASTER_HOST='mysql-master', MASTER_USER='repluser', MASTER_PASSWORD='srebro@test', MASTER_PORT=3306, MASTER_LOG_FILE='$MFILE', MASTER_LOG_POS=$MPOS;" 2>/dev/null
docker exec mysql-slave mysql -uroot -p"$PW" -e 'START SLAVE;' 2>/dev/null
echo SLAVE_SETUP_DONE
EOF
bash setup-slave.sh</code></pre>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">MASTER_FILE=mysql-bin.000003 MASTER_POS=849
SLAVE_SETUP_DONE</div>
</div>
<div class="callout callout-info">💡 <code>MASTER_POS</code> 是执行瞬间的 binlog 偏移量,随主库写入推进(首次干净部署实测为 <code>849</code>)。重复执行时这里会变成当前最新位点,属正常现象。</div>
</div>
</article>
<!-- Task 8 -->
<article class="task-card" id="task-8">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 8</span><span>复制链路健康检查 (Health Check)</span></div>
<button class="task-check" onclick="toggleDone(8)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-ok">✅ <strong>健康判据:</strong>只看两个 <code>Yes</code> 不够。必须同时满足 <code>Last_IO_Errno=0</code>、<code>Last_SQL_Errno=0</code>、<code>Seconds_Behind_Master</code> 收敛为 <code>0</code>。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">一键体检</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2>/dev/null | grep -E 'Slave_IO_Running:|Slave_SQL_Running:|Master_Log_File|Read_Master_Log_Pos|Seconds_Behind_Master|Last_IO_Errno|Last_SQL_Errno|Master_Server_Id'">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2>/dev/null | grep -E 'Slave_IO_Running:|Slave_SQL_Running:|Master_Log_File|Read_Master_Log_Pos|Seconds_Behind_Master|Last_IO_Errno|Last_SQL_Errno|Master_Server_Id'")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2>/dev/null | grep -E 'Slave_IO_Running:|Slave_SQL_Running:|Master_Log_File|Read_Master_Log_Pos|Seconds_Behind_Master|Last_IO_Errno|Last_SQL_Errno|Master_Server_Id'</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content"> Master_Log_File: mysql-bin.000003
Read_Master_Log_Pos: 849
Relay_Master_Log_File: mysql-bin.000003
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0
Last_IO_Errno: 0
Last_SQL_Errno: 0
Master_Server_Id: 1</div>
</div>
</div>
</article>
<!-- Task 9 -->
<article class="task-card" id="task-9">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 9</span><span>主库写入测试数据 (Write & Verify)</span></div>
<button class="task-check" onclick="toggleDone(9)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-warn">⚠️ <strong>顺序陷阱(实测报错):</strong>不能用 <code>-D cmdb01</code> 来「顺便建库」。库不存在时客户端直接失败:<code>ERROR 1049 (42000): Unknown database 'cmdb01'</code>。必须<strong>先不带 -D 建库</strong>。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">① 先建库(不带 -D)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE DATABASE IF NOT EXISTS cmdb01 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS cmdb02 DEFAULT CHARACTER SET utf8mb4; SHOW DATABASES LIKE "cmdb%";' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE DATABASE IF NOT EXISTS cmdb01 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS cmdb02 DEFAULT CHARACTER SET utf8mb4; SHOW DATABASES LIKE \"cmdb%\";' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE DATABASE IF NOT EXISTS cmdb01 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS cmdb02 DEFAULT CHARACTER SET utf8mb4; SHOW DATABASES LIKE "cmdb%";' 2>/dev/null</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">② 建表并写入(带 -D 指定默认库)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS host_info(id INT PRIMARY KEY AUTO_INCREMENT, hostname VARCHAR(64) NOT NULL, ip VARCHAR(32), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); INSERT INTO host_info(hostname,ip) VALUES("node-a","10.0.0.11"),("node-b","10.0.0.12"),("node-c","10.0.0.13"); SELECT COUNT(*) AS master_rows FROM host_info;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS host_info(id INT PRIMARY KEY AUTO_INCREMENT, hostname VARCHAR(64) NOT NULL, ip VARCHAR(32), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); INSERT INTO host_info(hostname,ip) VALUES(\"node-a\",\"10.0.0.11\"),(\"node-b\",\"10.0.0.12\"),(\"node-c\",\"10.0.0.13\"); SELECT COUNT(*) AS master_rows FROM host_info;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS host_info(...); INSERT INTO host_info(hostname,ip) VALUES("node-a","10.0.0.11"),("node-b","10.0.0.12"),("node-c","10.0.0.13"); SELECT COUNT(*) AS master_rows FROM host_info;' 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">Database (cmdb%)
cmdb01
cmdb02
master_rows
3</div>
</div>
</div>
</article>
<!-- Task 10 -->
<article class="task-card" id="task-10">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 10</span><span>从库读取验证数据一致性 (Consistency Check)</span></div>
<button class="task-check" onclick="toggleDone(10)">标记完成</button>
</div>
<div class="task-body">
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">Shell Command</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'SELECT id,hostname,ip,created_at FROM host_info ORDER BY id;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'SELECT id,hostname,ip,created_at FROM host_info ORDER BY id;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'SELECT id,hostname,ip,created_at FROM host_info ORDER BY id;' 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">id hostname ip created_at
1 node-a 10.0.0.11 2026-10-30 15:50:54
2 node-b 10.0.0.12 2026-10-30 15:50:54
3 node-c 10.0.0.13 2026-10-30 15:50:54</div>
</div>
</div>
</article>
<!-- Task 11 -->
<article class="task-card" id="task-11">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 11</span><span>验证 replicate-do-db 过滤规则</span></div>
<button class="task-check" onclick="toggleDone(11)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">从库只配了 <code>--replicate-do-db=cmdb01</code>,因此写入白名单外的 <code>cmdb02</code> 不应出现在从库。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">① 主库写 cmdb02</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb02 -e 'CREATE TABLE IF NOT EXISTS filtered_check(id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(64)); INSERT INTO filtered_check(note) VALUES("should-not-replicate"); SELECT COUNT(*) AS master_cmdb02_rows FROM filtered_check;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb02 -e 'CREATE TABLE IF NOT EXISTS filtered_check(id INT PRIMARY KEY AUTO_INCREMENT, note VARCHAR(64)); INSERT INTO filtered_check(note) VALUES(\"should-not-replicate\"); SELECT COUNT(*) AS master_cmdb02_rows FROM filtered_check;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb02 -e 'CREATE TABLE IF NOT EXISTS filtered_check(...); INSERT INTO filtered_check(note) VALUES("should-not-replicate"); SELECT COUNT(*) AS master_cmdb02_rows FROM filtered_check;' 2>/dev/null</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">② 从库确认 cmdb02 不存在</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e "SHOW DATABASES LIKE 'cmdb%';" 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -e \"SHOW DATABASES LIKE 'cmdb%';\" 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e "SHOW DATABASES LIKE 'cmdb%';" 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">master_cmdb02_rows
1
Database (cmdb%)
cmdb01</div>
</div>
</div>
</article>
<!-- Task 12 -->
<article class="task-card" id="task-12">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 12</span><span>离线追赶演练 (Catch-up After Outage)</span></div>
<button class="task-check" onclick="toggleDone(12)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">制造从库断链,主库持续写入,再恢复复制验证断点续传。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">① 停复制 + 主库继续写</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'STOP SLAVE;' 2>/dev/null; docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'INSERT INTO host_info(hostname,ip) VALUES("node-d-offline","10.0.0.14"),("node-e-offline","10.0.0.15"); SELECT COUNT(*) AS master_rows FROM host_info;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'STOP SLAVE;' 2>/dev/null; docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'INSERT INTO host_info(hostname,ip) VALUES(\"node-d-offline\",\"10.0.0.14\"),(\"node-e-offline\",\"10.0.0.15\"); SELECT COUNT(*) AS master_rows FROM host_info;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'STOP SLAVE;' 2>/dev/null; docker exec mysql-master mysql -uroot -p'srebro@2024' -D cmdb01 -e 'INSERT INTO host_info(hostname,ip) VALUES("node-d-offline","10.0.0.14"),("node-e-offline","10.0.0.15"); SELECT COUNT(*) AS master_rows FROM host_info;' 2>/dev/null</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">② 断链期间从库行数(应当冻结)</span>
<div class="cmd-actions">
<button class="btn-action" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_during_outage FROM cmdb01.host_info;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_during_outage FROM cmdb01.host_info;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_during_outage FROM cmdb01.host_info;' 2>/dev/null</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">③ 恢复复制并复查</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_after_recover FROM cmdb01.host_info;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_after_recover FROM cmdb01.host_info;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_rows_after_recover FROM cmdb01.host_info;' 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">master_rows
5
slave_rows_during_outage
3
slave_rows_after_recover
5</div>
</div>
<div class="callout callout-ok">✅ I/O 线程凭持久化的位点向主库续请求,SQL 线程顺次回放未执行事件,约 3 秒内追平。这是复制具备实用价值的核心能力。</div>
</div>
</article>
<!-- Task 13 -->
<article class="task-card" id="task-13">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 13</span><span>ROW 格式下的过滤真相:DDL 与 DML 规则不同</span></div>
<button class="task-check" onclick="toggleDone(13)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-danger">🔥 <strong>最反直觉的一条(本手册实测结论):</strong>在 <code>binlog_format=ROW</code> 下,<code>--replicate-do-db</code> 对 <strong>DML</strong> 按「行所属表所在的库」过滤,对 <strong>DDL</strong> 仍按「会话默认库」过滤。二者规则不同,混用会直接造成从库少表。</div>
<table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:12px">
<tr style="background:var(--bg-subtle)"><th style="border:1px solid var(--border-color);padding:6px">语句类型</th><th style="border:1px solid var(--border-color);padding:6px">无默认库(不用 -D)</th><th style="border:1px solid var(--border-color);padding:6px">有默认库(-D cmdb01)</th></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px">DML 增删改</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--success)">✅ 会复制(按表属库)</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--success)">✅ 会复制</td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px">DDL 建表等</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--danger)">❌ 丢弃</td><td style="border:1px solid var(--border-color);padding:6px;color:var(--success)">✅ 会复制</td></tr>
</table>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">① 不带默认库建表(DDL 被丢弃)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE TABLE cmdb01.drift_test(id INT PRIMARY KEY);' 2>/dev/null; sleep 2; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e "SHOW TABLES IN cmdb01 LIKE 'drift%';" 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE TABLE cmdb01.drift_test(id INT PRIMARY KEY);' 2>/dev/null; sleep 2; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e \"SHOW TABLES IN cmdb01 LIKE 'drift%';\" 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'CREATE TABLE cmdb01.drift_test(id INT PRIMARY KEY);' 2>/dev/null; sleep 2; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e "SHOW TABLES IN cmdb01 LIKE 'drift%';" 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出(空 = DDL 未复制)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">(无输出 —— 从库没有 drift_test 表)</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">② 不带默认库插数据(DML 却会复制 → 触发 1146)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-danger" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'INSERT INTO cmdb01.drift_test VALUES(1);' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2>/dev/null | grep -E 'Slave_SQL_Running:|Last_SQL_Errno'">危险:制造故障</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'INSERT INTO cmdb01.drift_test VALUES(1);' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2>/dev/null | grep -E 'Slave_SQL_Running:|Last_SQL_Errno'")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'INSERT INTO cmdb01.drift_test VALUES(1);' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SHOW SLAVE STATUS\G' 2>/dev/null | grep -E 'Slave_SQL_Running:|Last_SQL_Errno'</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出:SQL 线程已中止</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content"> Slave_SQL_Running: No
Last_SQL_Errno: 1146</div>
</div>
</div>
</article>
<!-- Task 14 -->
<article class="task-card" id="task-14">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 14</span><span>故障定位与修复 (1146 Recovery)</span></div>
<button class="task-check" onclick="toggleDone(14)">标记完成</button>
</div>
<div class="task-body">
<div class="task-desc">多级并行复制下,主状态只给概要。<code>performance_schema.replication_applier_status_by_worker</code> 才有精确到 SQL 的报错。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">① 精确定位报错</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -N -B -e 'SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER<>0;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -N -B -e 'SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER<>0;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -N -B -e 'SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER<>0;' 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">Worker 1 failed executing transaction 'ANONYMOUS' at source log mysql-bin.000003,
end_log_pos 6534; Error executing row event: 'Table 'cmdb01.drift_test' doesn't exist',
Error_code: MY-001146</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">② 容器日志交叉印证</span>
<div class="cmd-actions">
<button class="btn-action" data-cmd="docker logs --tail 8 mysql-slave 2>&1">执行</button>
<button class="btn-action" onclick="copyCommand('docker logs --tail 8 mysql-slave 2>&1')">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker logs --tail 8 mysql-slave 2>&1</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">[Repl] Replica SQL for channel '': Worker 1 failed executing transaction 'ANONYMOUS'
at source log mysql-bin.000003, end_log_pos 6534; Error executing row event:
'Table 'cmdb01.drift_test' doesn't exist', Error_code: MY-001146
[Repl] ... The replica coordinator and worker threads are stopped, possibly leaving
data in inconsistent state. Error_code: MY-001756</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">③ 补齐缺失表并恢复</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS drift_test(id INT PRIMARY KEY);' 2>/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_drift_rows FROM cmdb01.drift_test;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS drift_test(id INT PRIMARY KEY);' 2>/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_drift_rows FROM cmdb01.drift_test;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-slave mysql -uroot -p'srebro@2024' -D cmdb01 -e 'CREATE TABLE IF NOT EXISTS drift_test(id INT PRIMARY KEY);' 2>/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'START SLAVE;' 2>/dev/null; sleep 3; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT COUNT(*) AS slave_drift_rows FROM cmdb01.drift_test;' 2>/dev/null</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出:自动补数并恢复</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">slave_drift_rows
1
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0
Last_SQL_Errno: 0</div>
</div>
<div class="callout callout-info">💡 因为 DDL 从未进入从库,这里手工建表不会与主库事件冲突(不会 1050)。若冲突则用 <code>STOP SLAVE; SET GLOBAL sql_slave_skip_counter=1; START SLAVE;</code> 跳过该事件。</div>
</div>
</article>
<!-- Task 15 -->
<article class="task-card" id="task-15">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 15</span><span>主从一致性总核验 (Final Consistency)</span></div>
<button class="task-check" onclick="toggleDone(15)">标记完成</button>
</div>
<div class="task-body">
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">两侧行数对照</span>
<div class="cmd-actions">
<button class="btn-action btn-action-primary" data-cmd="docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2>/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2>/dev/null">执行</button>
<button class="btn-action" onclick="copyCommand("docker exec mysql-master mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2>/dev/null; docker exec mysql-slave mysql -uroot -p'srebro@2024' -e 'SELECT (SELECT COUNT(*) FROM cmdb01.host_info) AS host_info, (SELECT COUNT(*) FROM cmdb01.drift_test) AS drift_test;' 2>/dev/null")">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>docker exec mysql-master ... ; docker exec mysql-slave ... # 两侧对照 host_info / drift_test 行数</div>
</div>
<div class="output-box">
<div class="output-title" onclick="toggleOutput(this)"><span>实测输出 (Verified Output)</span><span class="output-toggle">收起 ▲</span></div>
<div class="output-content">=== MASTER ===
host_info drift_test
7 1
=== SLAVE ===
host_info drift_test
7 1
Master_Log_File: mysql-bin.000003
Read_Master_Log_Pos: 6073
Exec_Master_Log_Pos: 6073
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0</div>
</div>
<div class="callout callout-ok">✅ <code>Read_Master_Log_Pos</code> 等于 <code>Exec_Master_Log_Pos</code>,说明中继日志已全部回放完毕,两侧行数一致。</div>
</div>
</article>
<!-- Task 16 -->
<article class="task-card" id="task-16">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 16</span><span>排障速查表 (Troubleshooting Cheatsheet)</span></div>
<button class="task-check" onclick="toggleDone(16)">标记完成</button>
</div>
<div class="task-body">
<table style="width:100%;border-collapse:collapse;font-size:11px">
<tr style="background:var(--bg-subtle)"><th style="border:1px solid var(--border-color);padding:6px">现象</th><th style="border:1px solid var(--border-color);padding:6px">原因</th><th style="border:1px solid var(--border-color);padding:6px">处置</th></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px"><code>Slave_IO_Running: Connecting</code></td><td style="border:1px solid var(--border-color);padding:6px">网络不通 / 账号密码错 / 主库端口</td><td style="border:1px solid var(--border-color);padding:6px">在从库容器 <code>mysql -h mysql-master -u repluser -p</code> 验证连通性</td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px"><code>Last_SQL_Errno: 1146</code></td><td style="border:1px solid var(--border-color);padding:6px">从库缺表(DDL 未按预期复制)</td><td style="border:1px solid var(--border-color);padding:6px">补建表后 <code>START SLAVE</code></td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px"><code>Last_SQL_Errno: 1050</code></td><td style="border:1px solid var(--border-color);padding:6px">从库表已存在,重复执行 DDL</td><td style="border:1px solid var(--border-color);padding:6px"><code>SET GLOBAL sql_slave_skip_counter=1</code> 后重启</td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px"><code>ERROR 1064</code> 建账号</td><td style="border:1px solid var(--border-color);padding:6px">8.0 不支持 GRANT 隐式建用户</td><td style="border:1px solid var(--border-color);padding:6px">先 <code>CREATE USER</code> 再 <code>GRANT</code></td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px"><code>ERROR 1049</code> Unknown database</td><td style="border:1px solid var(--border-color);padding:6px">用 <code>-D</code> 指定了尚不存在的库</td><td style="border:1px solid var(--border-color);padding:6px">先不带 <code>-D</code> 建库</td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px">server_id 冲突</td><td style="border:1px solid var(--border-color);padding:6px">主从 <code>server-id</code> 相同</td><td style="border:1px solid var(--border-color);padding:6px">保证全局唯一</td></tr>
<tr><td style="border:1px solid var(--border-color);padding:6px">位点被 purge</td><td style="border:1px solid var(--border-color);padding:6px"><code>binlog_expire_logs_seconds</code> 过期清理</td><td style="border:1px solid var(--border-color);padding:6px">只能重做从库</td></tr>
</table>
</div>
</article>
<!-- Task 17 -->
<article class="task-card" id="task-17">
<div class="task-header">
<div class="task-title"><span class="task-id">Task 17</span><span>环境清理 (Teardown)</span></div>
<button class="task-check" onclick="toggleDone(17)">标记完成</button>
</div>
<div class="task-body">
<div class="callout callout-warn">⚠️ 加 <code>-v</code> 会连数据卷一起删除,下次起 \"从零重建\"。仅停止容器则用不带 <code>-v</code> 的版本。</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">停止并保留数据卷</span>
<div class="cmd-actions">
<button class="btn-action" data-cmd="cd /root/mysql-ms && docker compose down">执行</button>
<button class="btn-action" onclick="copyCommand('cd /root/mysql-ms && docker compose down')">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>cd /root/mysql-ms && docker compose down</div>
</div>
<div class="cmd-block">
<div class="cmd-bar">
<span class="cmd-label">彻底清理(含数据卷)</span>
<div class="cmd-actions">
<button class="btn-action btn-action-danger" data-cmd="cd /root/mysql-ms && docker compose down -v">执行</button>
<button class="btn-action" onclick="copyCommand('cd /root/mysql-ms && docker compose down -v')">复制</button>
</div>
</div>
<div class="cmd-content"><span class="cmd-prompt">$</span>cd /root/mysql-ms && docker compose down -v</div>
</div>
<div class="callout callout-info">💡 <strong>生产化待办:</strong>本手册为演练环境,尚未覆盖 ① 从库 <code>read_only=ON</code> + <code>super_read_only</code> ② 半同步 <code>rpl_semi_sync</code> ③ GTID 模式(免位点管理)④ 从库初始化一致性校验(xtrabackup / mysqldump <code>--single-transaction --master-data=2</code>)。</div>
</div>
</article>
<footer class="arch-note" style="border-top:1px solid var(--border-color);margin-top:20px;padding-top:12px">
<span>MySQL 8.0 主从复制交互实验手册 · 全部命令与输出均在云端 node1 的 0 号终端实机验证 · 2026-10-30</span>
<span class="badge badge-verified">18 / 18 任务已验证</span>
</footer>
</div>
<div class="toast" id="toast"></div>
<script>
// 统一下发:本手册所有命令都跑在宿主原生终端(node1 的 0 号终端)
function dispatchCommand(cmd) {
if (window.sendTerminalCommand) {
window.sendTerminalCommand(cmd, "terminal");
return;
}
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: "LAE_TERMINAL_COMMAND",
command: cmd,
target: "terminal",
autoExecute: true
}, "*");
}
}
// 监听 data-cmd 委托并下发到终端
document.addEventListener("click", function(e) {
if (window.__LAE_SDK_ATTACHED__ || window.__TERMINAL_BRIDGE_LOADED__) {
return;
}
const trigger = e.target.closest("[data-cmd]");
if (trigger) {
const cmd = trigger.getAttribute("data-cmd");
if (cmd) {
if (trigger.classList.contains("btn-copy")) {
copyCommand(cmd);
return;
}
dispatchCommand(cmd);
showToast("命令已发送至 0 号终端");
}
}
});
// 下发多行命令块(读 pre/code 的纯文本)
function blockText(id) {
const el = document.getElementById(id);
return el ? el.textContent : "";
}
function runBlock(id) {
const cmd = blockText(id);
if (cmd) {
dispatchCommand(cmd);
showToast("脚本已发送至 0 号终端");
}
}
function copyBlock(id) {
const cmd = blockText(id);
if (cmd) {
copyCommand(cmd);
}
}
function copyCommand(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
showToast("已复制");
}).catch(() => {
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
const input = document.createElement("textarea");
input.value = text;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
showToast("已复制");
}
// 预期输出折叠
function toggleOutput(titleEl) {
const box = titleEl.parentElement;
const content = box.querySelector(".output-content");
const toggle = titleEl.querySelector(".output-toggle");
const collapsed = content.classList.toggle("collapsed");
if (toggle) {
toggle.textContent = collapsed ? "展开 ▼" : "收起 ▲";
}
}
// 进度管理
const STORE_KEY = "mysql_ms_lab_progress";
const TOTAL = 18;
function loadDone() {
try {
const raw = localStorage.getItem(STORE_KEY);
return raw ? JSON.parse(raw) : [];
} catch (e) {
return [];
}
}
function saveDone(list) {
try {
localStorage.setItem(STORE_KEY, JSON.stringify(list));
} catch (e) {}
}
function renderProgress() {
const done = loadDone();
const fill = document.getElementById("progress-fill");
const text = document.getElementById("progress-text");
if (fill) fill.style.width = Math.round((done.length / TOTAL) * 100) + "%";
if (text) text.textContent = done.length + " / " + TOTAL;
for (let i = 0; i < TOTAL; i++) {
const card = document.getElementById("task-" + i);
if (!card) continue;
const btn = card.querySelector(".task-check");
const isDone = done.indexOf(i) !== -1;
card.classList.toggle("done", isDone);
if (btn) {
btn.classList.toggle("checked", isDone);
btn.textContent = isDone ? "✓ 已完成" : "标记完成";
}
}
}
function toggleDone(id) {
const done = loadDone();
const idx = done.indexOf(id);
if (idx === -1) {
done.push(id);
} else {
done.splice(idx, 1);
}
saveDone(done);
renderProgress();
}
function resetProgress() {
try {
localStorage.removeItem(STORE_KEY);
} catch (e) {}
renderProgress();
showToast("进度已重置");
}
function scrollPrev() {
const done = loadDone();
for (let i = 0; i < TOTAL; i++) {
if (done.indexOf(i) === -1) {
const card = document.getElementById("task-" + i);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "start" });
showToast("跳转到 Task " + i);
return;
}
}
}
showToast("全部任务已完成");
}
let toastTimer = null;
function showToast(msg) {
const toast = document.getElementById("toast");
if (toast) {
toast.textContent = msg;
toast.style.display = "inline-flex";
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toast.style.display = "none";
}, 1500);
}
}
// 主题切换管理
function initTheme() {
const urlParams = new URLSearchParams(window.location.search);
const urlTheme = urlParams.get("theme");
const savedTheme = localStorage.getItem("mysql_ms_theme");
setTheme(urlTheme || savedTheme || "light", false);
window.addEventListener("message", (event) => {
if (event.data && (event.data.type === "THEME_CHANGE" || event.data.type === "SET_THEME")) {
const t = event.data.theme;
if (t === "dark" || t === "light") {
setTheme(t, true);
}
}
});
}
function setTheme(theme, save = true) {
document.documentElement.setAttribute("data-theme", theme);
const text = document.getElementById("theme-text");
if (text) {
text.textContent = theme === "dark" ? "暗色模式" : "亮色模式";
}
if (save) {
localStorage.setItem("mysql_ms_theme", theme);
}
}
function toggleTheme() {
const curr = document.documentElement.getAttribute("data-theme") || "light";
setTheme(curr === "dark" ? "light" : "dark", true);
}
initTheme();
renderProgress();
</script>
</body>
</html>
💻 配套实训环境与动手练习
本文涉及的相关命令操作、开发环境与容器编排已内置在我们的在线脱机环境中,无需安装依赖,随时在浏览器中实践体验:
📚 热门技术专题与实战文库精选 深度技术实战沉淀 · 覆盖7,600+篇开发者指南
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有