🛢️ 第2章 初识MySQL
📄 一句话概括

单机、主从、集群,不是「初级中级高级」,是三种不同的账。 主从分走的是读,分不走写;写压满了,只能把数据切开。 选哪个不看想不想高级,看「写能不能分散」这一条。

📝 本章和前面几章的关系

上一篇看清了 8.0 的新东西,这一篇看机器怎么摆。 它是第 24 章项目实战的背景:那个网络商城为什么要做主从,答案在这一篇。 和第 12 章(索引与优化)的顺序不能反。 先优化 SQL,再谈加机器——反过来做,等于用钱买自己写错的 SQL。

一、单机:一台机器全包

单机就是一台机器上装着服务端,应用也在这台或者隔壁。 它的上限很清楚:这一台机器的 CPU、内存、磁盘。 最难受的地方是读写抢同一套资源。 一个大查询扫全表,能把热数据全挤出缓冲池,后面所有写都跟着变慢。 教材把这种部署叫单机模式,适合开发、测试、小站点。 它不是「低端」:很多内部系统一辈子就一台机器,够用就是好。 判断要不要升级,看一个信号:读请求把 CPU 打满了,而写还很闲。 这个信号说明该上主从了——不是因为慢,是因为读和写在互相挤。

二、主从:把读分出去

主从=一台主库负责写,若干从库跟着同步,读的请求分给从库。

角色 能做什么 怎么同步
主库(master) 读写都行 把每次改动记进 binlog
从库(slave) 默认只读 拉主库的 binlog 重放一遍

「默认只读」这个设定很关键:在从库上执行写会报 ERROR 1290。 它是保护不是限制——从库的数据是主库的影子,自己改了就跟主库对不上。 主从解决的是读的压力,不是写的压力。 写永远只有主库那一台在扛,加十台从库也不会让写变快。 这一条是整篇的枢纽,记住它,后面所有取舍都顺。

三、复制延迟:主从的硬伤

从库不是实时镜像,它总比主库慢一点。慢的原因有三个。

原因 怎么回事
网络往返 binlog 要从主库传到从库,跨机房就是几十毫秒
重放要时间 从库得把主库干过的活再干一遍
大事务 删十万行主库三秒,从库重放可能三十秒

所以「写完立刻读」是个陷阱:写入落在主库,读去了从库,读不到。 用户看到的就是「刚下的单不见了」。 解法只有一个方向:这类读强制走主库。 常见做法是在同一个事务里读,或者给这条查询加注释提示路由层。 具体延迟多大、影响多少请求,Demo 里有一张档位表。

四、分片:把写也分出去

读能靠加从库分担,写不能。写到顶了,只能把数据切开,一片一台机器。 切开就得先回答一个问题:一条记录该去哪个库? 最常见的答案是取模:uid % 库数。

NODES = ["db0", "db1", "db2", "db3"]
def route(uid):
    return NODES[uid % len(NODES)]

bucket = {n: 0 for n in NODES}
for uid in range(1, 10001):
    bucket[route(uid)] += 1
print("1 万个用户按 uid % 4 分到 4 个库:")
for n in NODES:
    print("  %s  %5d 人  占 %.1f%%" % (n, bucket[n], bucket[n] / 100.0))

print("再试按 uid 的尾号分:")
bucket2 = {n: 0 for n in NODES}
for uid in range(1, 10001):
    bucket2[NODES[uid % 10 % len(NODES)]] += 1
for n in NODES:
    print("  %s  %5d 人" % (n, bucket2[n]))

uid % 4 的结果是均匀的,四个库各 2500 人。 按「尾号」分就出问题了:0 到 9 十个尾号映射到 4 个库,必然有两个库多分一份。 这就是分片键的全部难点:选一个「取值均匀、且查询总带着它」的列。 不均匀会让某个库先爆;查询不带它,就得广播到所有库再拼结果,慢得没法用。 还有一条常被忽略:分片之后,跨片的事务和跨片的 join 都做不了了。 这两样一丢,很多业务逻辑得改成「最终一致」,工作量不在数据库这边。

五、怎么选:三档账

三档总量、三种方案各要几台,Demo 里算过一遍,这里只留结论。

看到的信号 该做什么
读多写少,单机 CPU 打满 上主从,把读分出去
写已经压满主库 加从库没用,只能分片
查询跨很多表、要强一致 别急着分片,先优化 SQL 和索引

顺序永远是:优化 SQL 和索引、加缓存、主从、分片。 每一步都比下一步便宜一个数量级。 最容易犯的错是从最后一步开始做。 上来就分片,等于把「一条 SQL 该建的索引」这个简单问题, 做成了「跨片查询怎么拼」这个复杂问题。

六、动手 Demo:三档总量,单机/主从/集群各要几台

这一课用一个容量模型算账,不是算命。 假设单机能扛 8000 QPS 的读、2500 QPS 的写,业务里读占八成。 三档总量:五千、五万、五十万。看三种方案各要几台机器。

READ, WRITE = 8000, 2500        # 单机读、写能力

def plan(total):
    reads = int(total * 0.8)
    writes = total - reads
    if writes <= WRITE:
        master = 1 + -(-reads // READ)   # 主 + 若干从
        s2 = "%d 台" % master
    else:
        s2 = "写爆,加从库没用"
    shards = -(-writes // WRITE)
    s3 = "%d 台(%d 片)" % (shards * 2, shards)
    return reads, writes, s2, s3

for total in (5000, 50000, 500000):
    r, w, s2, s3 = plan(total)
    print("%7d  读 %6d 写 %6d  主从 %-14s 集群 %s"
          % (total, r, w, s2, s3))

实跑输出:

--------------------------------------------------------------------
第六课:单机、主从、集群,分别能扛多少
--------------------------------------------------------------------

  **教材把应用环境分三种:单机、主从(读写分离)、集群。**
  **它们不是「高级/低级」的关系,是三种不同的账。**
  **这一课用一个容量模型算清楚:三档总量下,三种方案各要几台机器。**

  一、先定几个数
  --------------------------------------------------------------------
  **假设单机能扛 8000 QPS 的读、2500 QPS 的写(这是台不错的机器,真实值要看 SQL 复杂度)。**
  **业务里写占两成、读占八成。下面按三档总量各算一遍。**

  二、三档总量,三种方案各要几台
  --------------------------------------------------------------------

  总量        读/写          单机负载           主从              集群(每片一主一从)
  -------------------------------------------------------------------------------------
  5000 QPS    4000/1000      读 50% 写 40%      2 台              2 台(1 片)
  50000 QPS   40000/10000    读 500% 写 400%    写爆,加从库没用  8 台(4 片)
  500000 QPS  400000/100000  读 5000% 写 4000%  写爆,加从库没用  80 台(40 片)
  **竖着读这张表,能看出三件事:**
  **第一,五千这档,单机一台就够,连主从都不必要——加机器是浪费。**
  **第二,五万这档,写已经超过单机能扛的 2500 QPS,主从直接出局。**
  **因为写不能靠加从库分担:从库是只读的,写全压在主库那一台上。**
  **第三,要到五十万这档,集群才成为唯一选择,代价是 80 台机器和一堆运维活。**
  **所以顺序永远是:先把 SQL 和索引优化到不能再优化,再看主从,最后才谈分片。**
  **五千这档就上集群,是把简单问题做成了复杂问题。**

  三、主从真正换来的东西
  --------------------------------------------------------------------
  **主从不是为了更快,是为了【读写互不干扰】。**
  **单机上读写共用一套缓冲池,一个大查询扫全表,能把热数据全挤出内存,后面所有写都变慢。**
  **拆开之后,从库上的慢查询拖不动主库上的写。**

  四、主从的代价:延迟
  --------------------------------------------------------------------
  **主从也不是免费午餐,代价是【复制延迟】——从库的数据比主库晚一点。**

  延迟    刚下单的人去查订单  该怎么办
  --------------------------------------------------------
  0.1 秒  九成能看到          一般可接受
  1 秒    约一半看不到        下单后强制读主库
  5 秒    基本看不到          必须读主库
  30 秒   肯定看不到          主从已经出问题了,查延迟原因
  **第二列是我按「用户 1 秒内会刷新一次」估的,不是实测。**
  **但结论是真的:只要走主从,就必须定一条规矩。**
  **「写完立刻要读」的那些查询,得钉死在主库上。**
  **MySQL 里这叫「强制走主库」,常见做法是在同一个事务里读,或者用 hint 指定。**

  五、集群不是「主从加几台」
  --------------------------------------------------------------------

  维度        主从                  集群/分片
  --------------------------------------------------------------
  解决什么    读的压力              读写都能扩
  数据在哪    每台都有一份全量      每台只有一片
  扩容        加从库,加完就有全量  要搬数据,最麻烦的一步
  跨片查询    不存在这问题          要么应用层拼,要么放弃
  故障切换    主挂了提升一个从      那一片的主挂了,那一片不可用
  运维复杂度  低                    高一个量级
  **最后两行才是关键:集群换来的是容量,付出的是「跨片查询做不了」和「运维复杂度上一个量级」。**

先看五千那一档:单机负载才 50%,一主一从两台就够,集群也就两台。 台数一样,那当然选简单的——这就是「五千这档上集群是把简单问题做复杂了」。 五万那一档是关键转折。 写请求一万,单机只能扛 2500,主从方案直接出局——因为从库是只读的,写不会因为加了从库而分散。 所以那张表里「写爆,加从库没用」出现了两次,不是偷懒,是想让你看见两次。 五十万那档更明显:集群要 80 台机器,代价是运维复杂一个量级。 延迟那张档位表里的第二列是我按「用户一秒内会刷新一次」估的,不是实测。 但结论是真的:只要走主从,「写完立刻要读」的查询就得钉死在主库上。 最后那张集群对照表,最后两行才是关键。 集群换来的是容量,付出的是「跨片查询做不了」和「运维复杂一个量级」。

30 秒实验

  1. 把 Demo 里的写比例从 20% 改成 50%,再跑一遍。 五万那一档的写变成两万五,是单机能力的十倍,主从一样出局。 结论没变,但分片从 4 片变成 10 片——代价涨得更快。
  2. 把单机写能力从 2500 改成 5000(换块 SSD、加内存),五万那档还得出局吗? 两万五除以五千还是 5,照样出局。 单机能力提一倍,往往只是把分片的临界点往后推一档。
  3. 想一个你熟悉的系统,估它的读写比例。 新闻、朋友圈这类读远多于写;下单、打卡这类写占比高得多。 比例决定了该先解决哪一头。

常见误区

  1. 「上了主从,写就快了。」 不会。写只在主库发生,从库是只读的。 主从改善的是读的吞吐,以及读写互不干扰。
  2. 「分片以后查询一定更快。」 只有带分片键的查询更快——因为它只落在一个片上,表小了。 不带分片键的查询要广播到所有片再合并,比不分片更慢。
  3. 「等慢了再优化 SQL 也来得及。」 慢 SQL 在每一片上都是慢的,分片之后还会被放大——原来扫一张表,现在扫 N 张表。
自测三问(点开对答案) 1. 主从架构里,写操作发生在哪一台? —— **只有主库。** **从库默认只读,在从库上写会报 `ERROR 1290`。** 2. 复制延迟 5 秒,用户下单后立刻刷新订单页,会看到什么? —— **大概率看不到自己刚下的单**——因为读走了从库,而从库还差 5 秒。**这类查询得强制走主库。** 3. 十个尾号分到四个库,为什么分不均? —— **因为 10 不是 4 的倍数。** **0、4、8 三个尾号落在一个库,2、6 两个落在另一个库,自然是 3000 对 2000。**

下一篇是本章唯一标两星的一篇:装 MySQL 时向导问的那一路问题, 到底在定什么,以及那个 my.ini 里最该改的是哪个参数。

相关笔记