本文共 3907 字,大约阅读时间需要 13 分钟。
我们知道,MySQL有一个老问题,当表上无主键时,那么对于在该表上做的DML,如果是以ROW模式复制,则每一个行记录前镜像在备库都可能产生一次全表扫描(或者二级索引扫描),大多数情况下,这种开销都是非常不可接受的,并且产生大量的延迟。
在MySQL5.6中提供了一个新的参数:, 可以部分解决无主键表导致的复制延迟问题,其基本思路是对于在一个ROWS EVENT中的所有前镜像收集起来,然后在一次扫描全表时,判断HASH中的每一条记录进行更新。
首先来看看性能怎么样,我们使用sbtest表,并将其上面的主键及二级索引全部删除,表中有200万行数据
主库执行随机更新操作:
update table sbtest2 set k = k +15 order by rand() limit [$RECOR_NUM];
备库的最大延迟时间(基本等同执行时间)如下:
随机更新记录数 | TABLE_SCAN,INDEX_SCAN | TABLE_SCAN,INDEX_SCAN,HASH_SCAN |
10 | 26s | 6s |
20 | 51s | 9s |
40 | 96s | 15s |
60 | 147s | 25s |
80 | 187s | 30s |
100 | 228s | 37s |
可以看出来,该特性对于无主键表的复制延迟问题,还是有很大的帮助的。
如何使用:
slave_rows_search_algorithms的文档描述的非常清晰,该变量由三个值的组合组成:TABLE_SCAN,INDEX_SCAN, HASH_SCAN,使用组合包括:
TABLE_SCAN,INDEX_SCAN (默认配置,表示如果有索引就用索引,否则使用全表扫描)
INDEX_SCAN,HASH_SCAN
TABLE_SCAN,HASH_SCAN
TABLE_SCAN,INDEX_SCAN,HASH_SCAN(等价于INDEX_SCAN, HASH_SCAN)
参数组合(摘自log_event.cc: 9633~9648)
/* Decision table: - I --> Index scan / search - T --> Table scan - Hi --> Hash over index - Ht --> Hash over the entire table |--------------+-----------+------+------+------| | Index\Option | I , T , H | I, T | I, H | T, H | |--------------+-----------+------+------+------| | PK / UK | I | I | I | Hi | | K | Hi | I | Hi | Hi | | No Index | Ht | T | Ht | Ht | |--------------+-----------+------+------+------| */
a.决定使用哪种scan方式:
函数:Rows_log_event::decide_row_lookup_algorithm_and_key
调用backtrace:
Rows_log_event::do_apply_event
->do_before_row_operations
->Rows_log_event::row_operations_scan_and_key_setup
->Rows_log_event::decide_row_lookup_algorithm_and_key
没啥好说的,根据上述的参数组合矩阵,来确定使用哪种scan策略,存储在rows log event对象的m_rows_lookup_algorithm中
另外这里也会去看看是否能够使用索引,以及使用哪个索引(this->m_key_index= search_key_in_table),
b.执行过程
在函数Rows_log_event::do_apply_event中,当确定了使用哪种scan策略后,就可以选择对应的接口函数:
11084 switch (m_rows_lookup_algorithm)11085 {11086 case ROW_LOOKUP_HASH_SCAN:11087 do_apply_row_ptr= &Rows_log_event::do_hash_scan_and_update;11088 break;1108911090 case ROW_LOOKUP_INDEX_SCAN:11091 do_apply_row_ptr= &Rows_log_event::do_index_scan_and_update;11092 break;1109311094 case ROW_LOOKUP_TABLE_SCAN:11095 do_apply_row_ptr= &Rows_log_event::do_table_scan_and_update;11096 break;1109711098 case ROW_LOOKUP_NOT_NEEDED:11099 DBUG_ASSERT(get_general_type_code() == WRITE_ROWS_EVENT);1110011101 /* No need to scan for rows, just apply it */11102 do_apply_row_ptr= &Rows_log_event::do_apply_row;11103 break;1110411105 default:11106 DBUG_ASSERT(0);11107 error= 1;11108 goto AFTER_MAIN_EXEC_ROW_LOOP;11109 break;11110 }
这里我们只关心Rows_log_event::do_hash_scan_and_update,主要做几件事
1.将当前行事件的记录前镜像和后镜像存储到一个hash中(Rows_log_event::do_hash_row);
hash表的结构定义在Hash_slave_rows类中
>>hash key的生成基于record[0],也就是前镜像;
>>前镜像记录的起始位置
>>前镜像记录的结束位置
对于hash scan,当有索引时(m_key_index<MAX_KEY),处理方式略微有些不一样:
在将记录加入到hash时,键值列被单独存储下来,存储在m_distinct_key_list中(Rows_log_event::add_key_to_distinct_keyset)
2.如果该EVENT中所有的行记录都解析完毕,开始执行SCAN && UPDATE (Rows_log_event::do_scan_and_update)
>>初始化表或者索引扫描(open_record_scan())
对于索引,需要根据m_distinct_key_list初始化需要查询的键值iterator(m_itr)
>>在一个while循环中:
|–>error= next_record_scan(i == 0); //读取表或索引上的下一条记录
|–>对于全表扫描,直接调用table->file->ha_rnd_next(table->record[0])扫描表记录
|–>对于索引,第一次读是根据key搜索记录,下一次再调用next_record_scan时,先看下一条记录是否和当前m_key是否匹配,如果不匹配,将 m_key指向下一个m_itr,并根据m_key重读index,否则使用这个记录;
这里看起来似乎有优化的余地,因为m_itr并不是有序的,它的成员取自m_distinct_key_list,并且也不会保证存储的Key完全不重复,从函数 add_key_to_distinct_keyset的逻辑可以看出来,每次加入一个新的key,仅仅和上一次的key做对比来判断是否重复,但实际上二级索引key在一个rows log event中可能是无序的,做一次排序 会不会有利于性能呢?(TODO)
|–>根据读取到的记录构建key,查找hash,查看是否存在对应的entry(entry= m_hash.get(table, &m_cols))
|–>如果存在entry,还需要把读取到的记录跟该entry对应的binlog中的行记录进行比较(防止hash collision),符合的话,则从hash中将其删除,并应用该更新(Rows_log_event::do_apply_row)
当发生错误,或者hash中记录删光后,结束扫描,从while退出
可见,对于没有索引的表而言,最多需要一次表扫描。
转载地址:http://thhca.baihongyu.com/