社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

mysql 主库更新后,从库都读到最新值了,主库还有可能读到旧值吗?

数据分析与开发 • 3 年前 • 402 次点击  

今天的文章,其实来自真实的面试题,而且还比较有趣,所以忍不住分享出来。

直接开始吧。

我们知道,mysql数据库,为了得到更高性能,一般会读写分离,主库用于写操作,比如用于执行insert,update操作,从库用于读,也就是最常见的select操作。像下面这个图这样。

mysql读写分离

虽然主库一般用于写操作,但也是能读的。那么今天的问题来了。

  • 主库更新后,主库都读到最新值了,从库还有可能读到旧值吗?

  • 主库更新后,从库都读到最新值了,主库还有可能读到旧值吗?

毕竟面试官都这么问了,那当然是有可能的,那至于是为啥,以及怎么做到的,今天我们来好好聊聊。

正常的主从更新流程

比如我在主库和从库都有张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 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线程的存在。

io线程和sql线程

因此总结起来,主从同步的步骤就是

1.执行更新sql语句。

2.主库写成功时,binlog会更新。

3.主库binlog dump 线程将binlog的更新部分发给从库

4.从库io线程收到binlog更新部分,然后写入到relay log中

5.从库sql线程读取relay log内容,重放执行sql,最后主从一致。

mysql主从同步

到这里,我们可以开始回答文章开头的第一个问题。

主库更新后,主库都读到最新值了,从库还有可能读到旧值吗?

这是可能的,上面提到的主从同步的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)进行以下操作。

一个case解释隔离级别

我们执行事务都像上面这样,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了。

所以从结论上来说,出现了从库都读到最新值了,主库却读到了旧值的情况。

从库读到最新值主库却读到旧值

好了这道题到这里就结束了。

意不意外?

这道面试题,通过一个问题,将主从同步,事务隔离级别等知识点都串起来了。

还是有点意思的。

那么问题又来了,这四个隔离级别是挺骚气的,那他们是怎么实现的呢?

- EOF -


推荐阅读   点击标题可跳转

1、面试必备:聊聊 MySQL 的主从

2、MySQL 索引底层:B+ 树详解

3、MySQL + JSON = 王炸!!



看完本文有收获?请转发分享给更多人

推荐关注「数据分析与开发」,提升数据技能

点赞和在看就是最大的支持❤️

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/131247
 
402 次点击