[直播回顾】 直击MySQL源码本色

2020-06-17 00:00:00 数据 事务 线程 介绍 可以看到

  前言:
领导说既然开分享啦,就分享一些比较干的东西,我想来想去哈,binlog比较合适。为啥呢:
1. binlog mysql实现复制的基础;
2.复制是高可用啊、灾备啊的解决方案的基础(原生的高可用,灾备解决方案)所以binlog 就变成了高可用和灾备解决方案的基础;
3. 很容易获取,同时也好上手。如果上来就整trxlockredo undo log。估计把大家整蒙了,大家也没有兴趣继续研究源码了。
原汁原味,H君连语气都尽可能还原了,就问老板加鸡腿吗?

接下来就介绍下binlog相关的知识

1.binlog mysql 二进制日志


顾名思义,binlog mysql在进行说句或者控制操作时产生的日志文件。使用MySQL的同学都知道binlog可以用来做数据恢复,搭建复制。我们可以用binlog做指定时间点的回复。
但是如果有同学不小心操作错了一条数据,但是这个数据库实例又比较大,如果为了恢复这一条数据而使用整个实例的备份文件,耗时很久还不方便,这个时候呢,其实我们是可以使用binlog 来生成闪回语句进行单条数据的回退操作

2. binlog文件由两部分组成


包括relaylog,此时我们看下mysql的源码
mysqlbinlog.cc:
unction  dump_local_log_entries, check_header()
  if (memcmp(header, BINLOG_MAGIC, sizeof(header)))
{
    error("File is not a binary log file.");
    DBUG_RETURN(ERROR_STOP);
  }
binlog 的header 和events
/* 4 bytes which all binlogs should begin with */
#define BINLOG_MAGIC        "\xfe\x62\x69\x6e"
所有的binlog event 都会以这4个字节开头,这4个字节用于校验该文件是否是binlog 文件。这个事件结束之后,才会是一个个的binlog event。
我们打开binlog 试一下,可以开到开头。我们改掉一个bin中的一个字符,在用mysqlbinlog打开一下这个文件试一下,哈哈,报错了,File is not a binlog file.

3. binlog 是在什么时候产生的呢


a. 首先我们必须在配置文件里打开binlog选项,mysql才会产生binlog;
b.     binlog 有三种formatrowstatements mix;
Row 记录所有数据变化,比如一条delete 语句,binlog 记录的row event解析出来可能是where @1=and @2 = 
 statements delete from t1 where id < 10;
 mix兼具row statements的优点,也就是说也有可能有他们两个的缺点哦。比如对表的操作可能会退化成语句格式,说是DDLstatementDMLrow,但是实际测下来,部分DML还是走了statement。
后续我们所介绍的trx 的构成不明说的话,都是以row 格式哈。
c. 说道binlog的产生时间,就不得不提一个参数,sync_binlog = 1,如果说道sync_binlog等于就得介绍innodb_flush_log_at_trx_commit;
 sync_binlog是针对,binlog落盘的,当sync_binlog = 1时,一个事务mysql或对binlog做一次落盘,innodb_flush_log_at_trx_commit 是针对innodb存储引擎的,它对应的是redo log undo log的落盘。
我们知道binlog 是在mysqlserver层,redo log是存储引擎,因此也需要保证binlog redo log的数据一致性。mysql 使用了内部xa 来保证数据的一致。这个往细里讲的话,也差不多要一节课。
 源文件在binlog.ccordered_commit 函数。今天不是咱们的重点,我就简单讲一下流程。

 准备阶段



 1>. binlog 准备:将上一次commit 队列中大的seq number 写入到本次事务的last_commit中;
2>. innodb 准备:redo 写入os cache, xidundo 以便回滚使用;
 3>. xid 写入到binlog cache 中。

  提交阶段:



1>. innodb_flush_log_at_trx_commit = 1sync redo,从 os cache redo log 到磁盘循环每个事务的binlog cache os cache(有一个小点哦,如果此时sync_binlog != 1会触发 dump线程发送event 给从库);
 2>. binlog os cache 刷到binlog 磁盘,此时,如果sync_binlog = 1 则会触发dump现场发送event。
 -------大家想想如果有复制,是不是 mysql 5.7 after sync的?
根据上面的流程,我们能够看到,其实在提交阶段,第2步,binlog 已经在磁盘上产生了。同时,也在这个时候将binlog 发送给从库。所以我们可以说,after sync 是无损的复制。
上面我们介绍了binlog的一些东西,下面咱们从更细的角度来看binlog
我们在介绍binlog组成的时候,带了一句,binlog 是有binlog header(4字节events组成的,因此我们可以说,event binlog中记录数据操作或者一个事务的小单位了(可能有点绕)。
mysql 是事务数据库,gtid 出现之后,每个事务拥有自己的trx_no,在slave 回放时也是按照事务回放的,因此可以说binlog回放时的小单位事务。我们可以理解trx 是一个
逻辑的概念,这个逻辑的概念怎么落到binlog上的二进制数据上呢,这就需要我们后续介绍的event了。一个trx 是一对events的集合,这些events也是有规律可循的。
events 总共有38中不同的分类(包括unkown_event),其中部分类型已经废弃了,但是为了兼容还是保留着。

mysql 每个event 有三部分构成:


event header 19个字节
fixed data(posted header)format_description_event post_headers中记录该event 对应的长度
 variable data 变动的变量的长度,比如query_event 中 query_sql的长度(后续介绍)
event header 构成如下:
timestamp                0:4
 type_code                4:1
 server_id                  5:4
event_length             9:4
next_position            13:4
 flags                         17:2
我们先介绍一下,每个binlog 中的个事件:format_description_event
head = {
    when = {
      tv_sec = 1591684192
      tv_use =0
    }
    event_type = 15
    event_type_name = Format_desc
    unmasked_server_id = 3
    event_len = 119
    log_pos = 123
    flags = 0
  }
 
  binlog_version = 4
  server_version = "5.7.23-debug-log"
  created = 1591684192
  common_header_len = 19
  post_header_len = std::vector of length 39, capacity 39 = {56,13,0,8,0,18,0,4,4,4,4,18,0,0,95,0,4,26,8,0,0,0,8,8,8,2,0,0,0,10,10,10,42,42,0,18,52,0,1}
  checksum_alg = CRC32
  number_of_event_* = 38
可以看到format_description_event 的结构,其中post_header_len 解析每个event都会用到。

我们再看一个query event
  {
head = {
    when = {
      tv_sec = 1591684196
      tv_use =0
    }
    event_type = 2
    event_type_name = Query
    unmasked_server_id = 3
    event_len = 83
    log_pos = 302
    flags = 8
  }
  query_data_written = 17
  data_len = 12
  thread_id = 2
  query_exec_time = 0
  db_len = 6
  error_code = 0
  status_var_len = 281474976710655
  flags2 = 0
  sql_mode = 1436549152
  catalog_len = 3
  catalog = std
  auto_increment_increment = 0
  auto_incrment_offset = 0
  charset = "!"
  time_zone_len = 0
  time_zone_str =""
  lc_time_names_number = 0
  charset_database_number = 0
  table_map_for_update = 0
  master_data_written = 0
  user_len = 0
  user = ""
  host = ""
  host_len = 0
  mts_accessed_dbs = 1
  mts_accessed_db_names = {
    "testdb
  }
  explicit_defaults_ts = TERNARY_UNSET
  q_len = 5
  query = "BEGIN"
  db = "testdb"
}
可以看到event_type = 2 ,翻一下log_event_type的枚举类,可以看到2 正好是query event, 上面说每个event 还有变长部分,比如query, db 等等。这个工具,是把binlog解析过了。
如果没有解析,我们直接读16进制数据呢,就以query event 为例,我们看看event type 的地址,下标为4开始,占一个字节,是不是也是02。
这样大家对event有一个大致的了解了吧。下面我们介绍下,各种类型的trx 是怎么由 event 组成的。咱们先简单事务,再复杂事务(row格式),我们先介绍DDL吧。

DDL      


咱们创建个测试库,再创建个表。
我们可以看到,创建测试库使用了一个gtid_event,query event,创建表时又使用了gtid_event 和query event. 我们用我们开发的binlog 解析工具看一下明细。
通过明细,我们可以看到个query event 里存储的确实是 create databases语句,第二个query event 里存储的是存储的 query 是create table的语句。
DDL的trx 由两个event 构成:gtid eventquery event

normal trx(dml)


我们接着做一个插入操作:insert into test1(name) values('ggggg');
可以看到,event 一下多了很多哦,从154开始,一个插入操作使用了如下events:
Gtid event
Query event
Table_map
Write_rows
Xid event
(Rotate 咱们不用关心,他是因为我做了flush log导致的。他表示该binlog文件结束,已切换新的binlog 文件。这个事件结束之后,才会是一个个的binlog)

在DML当中,Query event 存储的就不是实际执行的语句,而是存储了一个BEGIN。
那我分别做一个update 和delete 大家看下,event类型和顺序,我们可以看到event 顺序和insert 一样,不同的时,update,rows event由write rows 变成了update rows,
delete 则由write rows 变成了delete rows。

这样大家对简单的DML 和DDL trx 的event 构成大家有概念了吧。

DDL:  Gtid_event, Query_event
DML:  Gtid_event, Qeury_event, Table_map_event, X_rows_event, Xid_event
有了这个概念,我们就介绍后续trx的events 构成了。

顺序我调整一下,我们后面介绍下big trx:
我们会发现,big trxevents组成和单条的DML一样,不同的就是delete_rows_event 多了很多。

后面接着呢,是modify multi table trx ,原谅我的Chinglish哈,我想说的是 multi table dml event,我们看一下顺序,它现在变成了:
table_map_event,write_rows_event, table_map_event, write_rows_event
*注意:table_map_event 的 table_id 和 write_rows_event 的 table_id,会有一一对应的关系。rows event 对应的就是其上面紧接着的 table_map_event 这一点和后面的 trx 有些不同。
接 下 来 我 们 看 下 join update event 
他 和 多 表 dml 已 经 不 一 样 了 。他 的 顺 序 是:

table_map_event、table_map_event、rows_event,、rows_event

*注意 table_id 的对应关系。

接下来我们看trigger 
相同的情况出现了,trigger 和 join update 的 binlog 顺序一样。那怎么区分这两种类型的事务呢?
后,我们来看下 xa trx 
我们可以看到一个 xa 事务,它的 events 顺序是酱紫的 Gtid、Query、 table_map、write_rows query、 XA_prepare、 Gtid、Qeury, 而且它占了两个 gtid 哦。为啥我们要对 trigger event、 join update event 以及 xa event 做特殊介绍呢,因为,根据 他 event 的类型不同,我们是要做不一样的处理的。
比如 xa event,它涉及多个 mysql 实例,如果我想讲这个 xa 事务,放到一个实例上回放该怎么处理?

依赖 binlog 的 replication


a. 搭建方法,时间比较紧,这个 DBA 基本都会,我就不介绍了哈 
b. 复制的启动过程
与复制相关的命令主要包括了如下几个:change master、show slave stat、show master stat、 start slave、stop slave 等命令。
sql_parser.cc
mysql_execute_command case SQLCOM_CHANGE_MASTER: {
if (check_global_access(thd, SUPER_ACL)) goto error;
res=change_master_cmd(thd);
break; }
case SQLCOM_SHOW_SLAVE_STAT: {
/* Accept one of two privileges */
if (check_global_access(thd, SUPER_ACL | REPL_CLIENT_ACL))
goto error;
res=show_slave_status_cmd(thd);
break;
}
case SQLCOM_SHOW_MASTER_STAT: {
/* Accept one of two privileges */
if (check_global_access(thd, SUPER_ACL | REPL_CLIENT_ACL))
goto error;
res = show_master_status(thd);
break;
}
当在从库上,执行 change master to ,但没有 start slave 的时候,实际只是注册了一个信 息,主机没有任何操作,从机会生成 relay_log.000001, 在这个 relay log 文件中里面的 format_description_event 实际都是 slave 本机的。
start slave 之后,slave 做的操作
mysql_execute_command()
|-start_slave_cmd()
|-start_slave()
|-start_slave_threads()
|-start_slave_thread()                 ← 先启动 IO 线程,无误再启动 SQL 线程
| |-handle_slave_io()                   ← IO 线程处理函数
| |-start_slave_thread()
|-handle_slave_sql()                   ← SQL 线程处理函数

io thread 会做如下操作

 handle_slave_io()

 |-my_thread_init()                  ← 0) 线程初始化

  |-init_slave_thread()

  |

  |-safe_connect()                    ← 1) 以标准的连接方式连上master

  |-get_master_version_and_clock()       并获取主库的所需信息

  |-get_master_uuid()

  |-io_thread_init_commands()

  |

  |-register_slave_on_master()       ← 2) 把自己注册到master上去

  | |-net_store_data()                    ←    设置数据包

  | |-simple_command()               ←    S把自己的ID、IP、端口、用户名提交给M,用于注册

  | |                                  ←    **上述会发送COM_REGISTER_SLAVE命令**

  |

  |                                 ###1BEGIN while循环中检测io_slave_killed()

  |

  |-request_dump()                  ← 3) 开始请求数据,向master请求binlog数据

  | |-RUN_HOOK()                    ←    调用relay_io->before_request_transmit()

  | |-int2store()                        ←    会根据是否为GTID作区分

  | |-simple_command()              ←    发送dump数据请求

  | |                               ←    **执行COM_BINLOG_DUMP_GTID/COM_BINLOG_DUMP命令**

  |

  |                                     ###2BEGIN while循环中检测io_slave_killed()

  |

  |-read_event()                    ← 4) 读取event并存放到本地relay log中

  | |-cli_safe_read()               ←    等待主库将binlog数据发过来

  |   |-my_net_read()

  |-RUN_HOOK()                      ←    调用relay_io->after_read_event()

  |

  |-queue_event()                   ← 5) 将接收到的event保存在relaylog中

  |-RUN_HOOK()                      ←    调用relay_io->after_queue_event()

  |-flush_master_info()

当主库收到从库的注册申请时,主库做如下操作:

        bool dispatch_command(THD *thd, const COM_DATA *com_data,

              enum enum_server_command command)

{

  ... ...

  switch (command) {

      ... ...

#ifdef HAVE_REPLICATION

      case COM_REGISTER_SLAVE:    // 注册slave

          if (!register_slave(thd, (uchar*)packet, packet_length))

              my_ok(thd);

          break;

#endif

#ifdef EMBEDDED_LIBRARY

      case COM_BINLOG_DUMP_GTID:

          error= com_binlog_dump_gtid(thd, packet, packet_length);

          break;

      case COM_BINLOG_DUMP:

          error= com_binlog_dump(thd, packet, packet_length);

          break;

#endif

      ... ...

  }

  ... ...

}

接着因为slave 发送了request_dump 命令,主就会通过通过dump线程将binlog 发送给从库

dispatch_command()

 |-com_binlog_dump_gtid()              ← COM_BINLOG_DUMP_GTID

 |-com_binlog_dump()                     ← COM_BINLOG_DUMP

   |-kill_zombie_dump_threads()       ← 如果同一个备库注册,会移除跟该备库匹配的binlog dump线程

   |-mysql_binlog_send()               ← 上述两个命令都会执行到此处

     |                                              ← 会打开文件,在指定位置读取文件,将event按照顺序发给备库

     |-Binlog_sender::run()             ← 调用rpl_binlog_sender.cc中的发送

       |-init()

       | |-init_heartbeat_period()      ← 启动心跳

       | |-transmit_start()                ← RUN_HOOK(),binlog_transmit_delegate

       |

       |                              -###BEGIN while()循环,只要没有错误,线程未被杀死,则一直执行

       |-open_binlog_file()

       |-send_binlog()                   ← 发送二进制日志

       | |-send_events()

       |   |-after_send_hook()

       |     |-RUN_HOOK()               ← 调用binlog_transmit->after_send_event()钩子函数

       |

       |-set_last_file()

       |-end_io_cache()

       |-mysql_file_close()

       |-###END


接着我们介绍从的另一个线程的工作

handle_slave_sql()                                ← ###作为协调线程

 |-my_thread_init()
 |-init_slave_thread()
 |-slave_start_workers()                           MTS(Multi-Threaded Slave)
 | |-init_hash_workers()
 | |-slave_start_single_worker()
 |   |-Rpl_info_factory::create_worker()
 |   |-handle_slave_worker()                    ← ###对于复制的并行执行线程
 |     |-my_thread_init()
 |     |-init_slave_thread()
 |     |-slave_worker_exec_job_group()
 |       |-pop_jobs_item()                      ← 获取具体的event(ev),会阻塞等待==<<<==
 |       |                                      ← 在while循环中执行
 |       |-is_gtid_event()
 |       |-worker->slave_worker_exec_event(ev)
 |         |-ev->do_apply_event_worker()        ← 调用该函数应用event
 |           |-do_apply_event()                 ← 利用C++多态性执行对应的event
 |
 |-### 如下从IO线程中读取数据
 |-sql_slave_killed()                           ← 只要线程未kill则一直执行
   |-exec_relay_log_event()
     |-next_event()                               ← 从cache或者relaylog中读取event
     | |-sql_slave_killed()                      ← 只要线程未kill则一直执行
     | |-Log_event::read_log_event()           ← 读取记录,参数为IO_CACHE
     |   |-my_b_read()                              ← 从磁盘读取头部,并检查头部信息是否合法
     |   |-Log_event::read_log_event()          ← 处理读取到缓存中的数据,个参数为char*
     |     | ... ...
     |     |-Write_rows_log_event()             ← 根据不同的event类型,创建ev对象
     |     |-Update_rows_log_event()
     |     |-Delete_rows_log_event()
     |     | ... ...
     |
     |-apply_event_and_update_pos()        ← 执行event并修改当前读的位置
       |-append_item_to_jobs()                  ← 发送给workers线程==>>>==


主要利用了event 的多态


我们以insert为例,其rows event write rows event

int Rows_log_event::do_apply_event(Relay_log_info const *rli)

{

  ... ...

  table=

    m_table= const_cast<Relay_log_info*>(rli)->m_table_map.get_table(m_table_id);

  ... ...

  if (table)

  {

    ... ...

    if ((m_rows_lookup_algorithm != ROW_LOOKUP_NOT_NEEDED) &&

        !is_any_column_signaled_for_table(table, &m_cols))

    {

      error= HA_ERR_END_OF_FILE;

      goto AFTER_MAIN_EXEC_ROW_LOOP;

    }

    switch (m_rows_lookup_algorithm)

    {

      case ROW_LOOKUP_HASH_SCAN:

        do_apply_row_ptr= &Rows_log_event::do_hash_scan_and_update;

        break;

 

      case ROW_LOOKUP_INDEX_SCAN:

        do_apply_row_ptr= &Rows_log_event::do_index_scan_and_update;

        break;

 

      case ROW_LOOKUP_TABLE_SCAN:

        do_apply_row_ptr= &Rows_log_event::do_table_scan_and_update;

        break;

 

      case ROW_LOOKUP_NOT_NEEDED:

        DBUG_ASSERT(get_general_type_code() == binary_log::WRITE_ROWS_EVENT);

 

        /* No need to scan for rows, just apply it */

        do_apply_row_ptr= &Rows_log_event::do_apply_row;

        break;

 

      default:

        DBUG_ASSERT(0);

        error= 1;

        goto AFTER_MAIN_EXEC_ROW_LOOP;

        break;

    }

 

    do {

      error= (this->*do_apply_row_ptr)(rli);

 

      if (handle_idempotent_and_ignored_errors(rli, &error))

        break;


/* this advances m_curr_row */

      do_post_row_operations(rli, error);

 

    } while (!error && (m_curr_row != m_rows_end));

    ... ...

  }

  ... ...

}

它后直接调用了do_apply_row


do_apply_row()

 |-do_exec_row()

   |-write_row()

     |-ha_start_bulk_insert()

后直接调用引擎层的ha_start_bulk_insert 将数据插进去。

PPT下载:立即点击

视频回放,立即点击:

  1. 神秘的binlog
  2. binlog在不同情况下的记录形式
  3. 基于binlog实现准实时复制


相关文章