我们知道,mysql 数据库,为了得到更高性能,一般会读写分离,主库用于写操作,比如用于执行insert,update
操作,从库用于读,也就是最常见的select
操作。像下面这个图这样。
虽然主库一般用于写操作,但也是能读的。那么今天的问题来了。
-
主库更新后,主库都读到最新值了,从库还有可能读到旧值吗?
-
主库更新后,从库都读到最新值了,主库还有可能读到旧值吗?
毕竟面试官都这么问了,那当然是有可能的,那至于是为啥,以及怎么做到的,今天我们来好好聊聊。
正常的主从更新流程
比如我在主库和从库都有张 user 表,此时有以下两条数据。
正常情况下,我们往主库执行写操作,比如更新一条数据,执行
update user set age = 50 where id = 1;
虽然这是一个单条写操作,但本质上可以理解为单条语句的事务。等同于下面这样
begin;
update user set age = 50 where id = 1;
commit;
这个事务如果执行成功了,数据会先写入到主库的 binlog 文件中,然后再刷入磁盘。
binlog 文件是 mysql 的 server 层日志,记录了用户对数据库有哪些变更操作,比如建数据库表加字段,对某些行的增删改等。
它的位置可以通过下面的查询语句看到。
mysql> show variables like "%log_bin%";
+---------------------------------+--------------------------------------+
| Variable_name | Value |
+---------------------------------+--------------------------------------+
| log_bin | ON |
| log_bin_basename | /var/lib/mysql/mysql-slave-bin |
| log_bin_index | /var/lib/mysql/mysql-slave-bin.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+--------------------------------------+
6 rows in set (0.04 sec)
其中 binlog 在 /var/lib/mysql/
下,命名会类似mysql-bin.00000x
。感兴趣的可以到这个目录下直接查看文件内容长什么样子。
如果两个 mysql 配置好了主从的关系,那么他们之间会建立一个tcp 长连接,主要用于传输同步数据。
除此之外,主库还会再起一个binlog dump 线程将 binlog 文件的变更发给从库。
可以在主库中通过 show full processlist;
查询到 binlog dump 线程的存在。
以上,主库的工作就结束了,我们说说从库的。
从库在收到 binlog 后,会有一个io 线程负责把收到的数据写入到relay log(中继日志)中。
然后再有一个sql 线程,来读取 relay log 的内容,然后对从库执行 sql 语句操作,从结果上来看就是将主库执行过的写操作,在从库上也重放一遍,这样主从数据就一致了。
是不是感觉 relay log 有些多余?
为什么要先写一遍 relay log 然后再写从库,直接将数据写入到从库不好吗?
在这里 relay log 的作用就类似于一个中间层,主库是多线程并发写的,从库的 sql 线程是单线程串行执行的,所以这两边的生产和消费速度肯定不同。当主库发的 binlog 消息过多时,从库的 relay log 可以起到暂存主库数据的作用,接着从库的 sql 线程再慢慢消费这些 relay log 数据,这样既不会限制主库发消息的速度,也不会给从库造成过大压力。
可以通过在从库中执行 show full processlist;
确认 io 线程和 sql 线程的存在。
因此总结起来,主从同步的步骤就是
1.执行更新 sql 语句。
2.主库写成功时,binlog 会更新。
3.主库 binlog dump 线程将 binlog 的更新部分发给从库
4.从库 io 线程收到 binlog 更新部分,然后写入到 relay log 中
5.从库 sql 线程读取 relay log 内容,重放执行 sql,最后主从一致。
到这里,我们可以开始回答文章开头的第一个问题。
主库更新后,主库都读到最新值了,从库还有可能读到旧值吗?
这是可能的,上面提到的主从同步的 5 个步骤里,第 3 到第 5 步骤,都需要时间去执行,而这些步骤的执行时间总和,就是我们常说的主从延迟。
当更新一行数据后,立马去读主库,主库的数据肯定是最新值,这点没什么好说的,但如果此时主从延迟过大,这时候读从库,同步可能还没完成,因此读到的就是旧值。
在实际的开发当中,主从延迟也非常常见,当数据库压力稍微大点,主从延迟就能到 100ms 甚至 1s 以上。
具体的主从延迟时间可以在从库中执行 show slave status \G;
来查看,其中里面的Seconds_Behind_Master
则是主从延迟的时间,单位是秒。
mysql> show slave status \G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 172.17.0.2
Master_User: slave
Connect_Retry: 30
Master_Log_File: mysql-bin.000002
Read_Master_Log_Pos: 756
Relay_Log_File: edu-mysql-relay-bin.000004
Relay_Log_Pos: 969
Relay_Master_Log_File: mysql-bin.000002
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 2
所以如果你有写数据后就立马要读数据的场景,要是此时读的是从库,很有可能会读到更新前的旧数据,如果你对数据一致性有较高要求,这种时候建议读主库。
主库更新后,从库都读到最新值了,主库还有可能读到旧值吗?
那另一个问题就来了,如果从库都读到最新值了,那说明主库肯定已经更新完成了,那此时读主库是不是只能读到最新值呢?
还真不是的,待会我给大家复现下,但在这之前我们了解一些前置知识点。
mysql 的四种隔离级别
这个绝对是面试八股文老股了。mysql 有四种隔离级别,分别是读未提交(Read uncommitted),读提交(Read committed),可重复读(Repeatable read)和串行化(Serializable)。在不同的隔离级别下,并发读写效果会不太一样。
当前数据库处于什么隔离级别可以通过执行 select @@tx_isolation;
查看到。
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.01 sec)
也可以通过下面的语句去修改隔离级别。
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE-READ;
下面用一个 case 来让大家直观点的理解这四个隔离级别的区别。
假设我们有两个线程同时对某行数据 A(A=1)进行以下操作。
我们执行事务都像上面这样,begin 可以开启事务,commit 会提交事务,上面两个线程,各执行一个事务,且此时是并发执行。
线程 1 会将某行的 A 这个字段从 1 更新为 2。
线程 2 啥也不干,就读 A。重点关注 2 线程的三次读 A 的行为,它们会根据隔离级别的不同,读到不同的值。
第 1 次读 A:
- 如果是读未提交,那么会读到 2,顾名思义,就算线程 1 未提交,线程 2 也能读到最新的值。
- 如果是读提交或者可重复读,那读到的都是 1,读提交只认事务提交后的数据,而可重复读只要线程 2 的事务内没有执行对 A 的更新 sql 语句,那读 A 的数据就会一直不变。
第 2 次读 A:时机正好在线程 1 提交了事务之后
- 如果是读未提交,前面都读到 2 了,现在读到的还是 2,这个没啥好说的。
- 如果是读提交,那读到的都是 2 了,因为线程 1 的事务提交了,读提交只认提交后的数据,所以此时线程 2 能读到最新数据。
- 如果是可重复读那就还是 1,理由跟上面一样。
第 3 次读 A:时机正好在线程 2 提交了事务之后
- 如果是读未提交或读已经提交,结果跟前面一样,还是 2。
- 如果是可重复读,那就变成了 2,因为线程 2 前面的事务结束了,在同一个事务内 A 的值重复多次读都是一致的,但当事务结束了之后,新的查询不再需要受限于上一次开事务时的值。
上面的情况没有将串行化纳入讨论范围,只讨论了读未提交,读提交和可重复读这三个隔离级别,因为在这三个隔离级别下都有可能出现两个事务并发执行的场景,而在串行化的隔离级别中则不会出现,多个事务只会一个挨着一个依次串行执行,比如线程 1 的事务执行完了之后,线程 2 的事务才执行,因此不会产生并发查询更新的问题。
有了这个知识背景之后,我们就可以回到第二个问题里了。数据库原始状态如下,此时主从都一样。
假设当前的数据库事务隔离级别是可重复读,现在主库有 A,B 两个线程,同时执行 begin,开启事务。
此时主库的线程 2,先读一次 id=1 的数据,发现 age=72,由于当前事务隔离级别是可重复读,那么只要线程 2 在事务内不做更新操作的话,那么不管重复读多少次,age 都是 72。在这之后主库的线程 1 将 age 更新为 100 且执行 commit 提交了事务。
主库线程 1 的事务提交成功之后 binlog 就会顺利产生,然后同步给从库。此时从库去查询就能查到最新值 age=100。回过头来,此时主库的线程 2 因为还没提交事务,所以一直读到的都是旧值 age=72。但如果这时候线程 2 执行 commit 提交了事务,那么再查询,就能拿到最新值 age=100 了。
所以从结论上来说,出现了从库都读到最新值了,主库却读到了旧值的情况。
好了这道题到这里就结束了。
意不意外?
这道面试题,通过一个问题,将主从同步,事务隔离级别等知识点都串起来了。
还是有点意思的。
那么问题又来了,这四个隔离级别是挺骚气的,那他们是怎么实现的呢?
暂无评论内容