参考:https://golangguide.top/
对于 MySQL 数据库,为了得到更高的性能,一般会搭建 MySQL 集群实现读写分离,主库用于写操作,从库用于读操作。虽然主库一般用于写,但也是能读的。那么就有这样一个问题:在 MySQL 集群中,在从库已经读到了最新值的情况下,主库还有可能读到旧值吗?
正常的主从更新流程
假设在主库和从库中都有一张 user 表,此时有以下数据:
id | name | age |
---|---|---|
1 | 小王 | 72 |
2 | 小李 | 60 |
我们往主库执行写操作时,一般都能理解成单条语句的事务,比如下面两段 SQL 效果相同:
update user set age = 50 where id = 1;
begin;
update user set age = 50 where id = 1;
commit;
如果事务执行成功了,数据会先写入到主库的 binlog 文件中,然后再刷入磁盘。
binlog 文件是 MySQL 的 server 层日志,记录了用户对数据库有哪些变更操作,比如建数据库表、加字段,以及对某些行的增删改等。
如果两个 MySQL 节点配置好了主从关系,那么它们之间会建立一个 TCP 长连接,主要用于传输同步数据。
除此之外,主库还会再创建一个 binlog dump 线程,将 binlog 文件的变更发送给从库。以上,主库的工作就结束了。
当从库通过之前创建的 TCP 长连接收到 binlog 后,会有一个 IO 线程负责把收到的数据写入到 relay log(中继日志)中,然后再有一个 SQL 线程来读取 relay log 的内容,接下来对从库执行 SQL 语句操作,完成数据的主从同步。
为什么要先写一遍 relay log 然后再写从库?
relay log 的作用就类似一个中间层,主库是多线程并发写的,从库的 SQL 线程是单线程串行执行的,所以两边的生产和消费速度肯定不同。当主库的 binlog 消息过多时,从库的 relay log 可以起到暂存主库数据的作用,接着从库的 SQL 线程再慢慢消费这些 relay log 数据,这样既不会限制主库发消息的速度,也不会给从库造成过大的压力。
因此总结起来,主从同步的步骤如下:
- 执行更新 SQL 语句
- 主库写成功时,更新 binlog
- 主库 binlog dump 线程将 binlog 的更新部分发给从库
- 从库 IO 线程收到 binlog 更新部分,写入到 relay log 中
- 从库 SQL 线程读取 relay log 内容,重放执行 SQL,最后主从一致
主库更新后,从库都读到最新值了,主库还有可能读到旧值吗?
答案是会的,这里需要先了解 MySQL 的四种隔离级别,分别是:读未提交(Read uncommitted),读已提交(Read committed),可重复读(Repeatable read)和串行化(Serializable)。在不同的隔离级别下,并发读写效果是不一样的。
四种隔离级具体可以看这篇:https://yuk1pedia.github.io/2024/11/MySQL-Principles/
掌握了 MySQL 的四种隔离级别后,就可以回到这个问题:主库更新后,从库都读到最新值了,主库还有可能读到旧值吗?
我们还是以这张表为例:
id | name | age |
---|---|---|
1 | 小王 | 72 |
2 | 小李 | 60 |
假设当前数据库事务的隔离级别是可重复读,主库中有 A、B 两个线程,同时执行 begin 开启事务,此时主库的线程 2 先读一次 id = 1 的数据,发现 age = 72,由于当前事务隔离级别是可重复读,那么只要线程 2 在它提交之前不做任何更新操作,不管重复读多少次,age 都是 72。
在这之后主库的线程 1 将 age 更新为 100,且执行 commit 提交了事务,那么主库线程 1 就会产生 binlog,然后同步给从库,此时从库去查询就能查到 age = 100。
回过头来,此时主库中的线程 2 还没有提交事务,所以就会一直读到旧值 age = 72。当线程 2 提交了事务,再查询就能查到最新的数据 age = 100了。
从结论上来说,出现了从库都读到最新值了,主库却读到了旧值的情况。