mysql运维-slave_skip_errors

本文介绍了在MySQL主从同步中,如何处理从库复制错误的问题。通过设置`slave_skip_errors`参数,可以在从库遇到错误时跳过,保持主从一致。同时,文章讨论了在使用mysql企业版备份工具MEB进行在线热备时,如何应对因DDL操作导致的从库恢复异常情况,并提供了配置建议。
1 简介
    mysql在主从复制过程中,由于各种的原因,从服务器可能会遇到执行BINLOG中的SQL出错的情况,在默认情况下,服务器会停止复制进程,不再进行同步,等到用户自行来处理。
    slave-skip-errors的作用就是用来定义复制过程中从服务器可以自动跳过的错误号,当复制过程中遇到定义的错误号,就可以自动跳过,直接执行后面的SQL语句。
2 官方参考

Command-Line Format

--slave-skip-errors=name

System Variable Name

slave_skip_errors

Variable Scope

Global

Dynamic Variable

No

 

Permitted Values

Type

string

Default

OFF

Valid Values

OFF

[list of error codes]

all

ddl_exist_errors

    slave_skip_errors选项有四个可用值,分别为: off,all,ErorCode,ddl_exist_errors
     默认情况下该参数值是off,我们可以列出具体的error code,也可以选择all,mysql5.6及MySQL Cluster NDB 7.3以及后续版本增加了参数ddl_exist_errors,该参数包含一系列error code(1007,1008,1050,1051,1054,1060,1061,1068,1094,1146)
    一些error code代表的错误如下:
    1007:数据库已存在,创建数据库失败
    1008:数据库不存在,删除数据库失败
    1050:数据表已存在,创建数据表失败
    1051:数据表不存在,删除数据表失败
    1054:字段不存在,或程序文件跟数据库有冲突
    1060:字段重复,导致无法插入
    1061:重复键名
    1068:定义了多个主键
    1094:位置线程ID
    1146:数据表缺失,请恢复数据库
    1053:复制过程中主服务器宕机
    1062:主键冲突 Duplicate entry '%s' for key %d

    my.cnf中的写法:
slave_skip_errors=1062,1053
slave_skip_errors=all
slave_skip_errors=ddl_exist_errors
    作为mysql启动参数的写法:
--slave-skip-errors=1062,1053
--slave-skip-errors=all
--slave-skip-errors=ddl_exist_errors

    从数据库中查看该参数的值:

mysql> show variables like 'slave_skip%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| slave_skip_errors | 1007  |
+-------------------+-------+

3 举例分析

    3.1 测试说明
    配置好mysql主从同步,然后在从上写入数据,造成主从不一致。
    3.2 准备测试表结构
    在主机上创建表:

create table replication (c1 int not null primary key, c2 varchar(10));
    3.3 准备测试数据
    在主机上插入基础数据
mysql> insert into replication values (1, 'test1');
mysql> insert into replication values (2, 'test2');

    此时,主机从机replication表里面都有两条记录
    3.4 开始测试
    从机插入一条记录

mysql> insert into replication values (3, 'test3');
    然后在主机上执行相同的操作

mysql> insert into replication values (3, 'test3');
    在从机上查看复制状态

mysql> show slave status \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.1.222
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 16700
               Relay_Log_File: mysql-relay-bin.000003
                Relay_Log_Pos: 16595
        Relay_Master_Log_File: mysql-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: No
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: mysql.ibbackup_binlog_marker
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: mysql.backup_%
                   Last_Errno: 1062
                   Last_Error: Error 'Duplicate entry '3' for key 'PRIMARY'' on query. Default database: 'test'. Query: 'insert into replication values (3, 'test3')'
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 16425
              Relay_Log_Space: 17544
    可以看到:sql线程已经停止工作 Slave_SQL_Running: No
                        错误号为:Last_Errno: 1062
                        错误信息为:Last_Error: Error 'Duplicate entry '3' for key 'PRIMARY'' on query. Default database: 'test'. Query: 'insert into replication values (3, 'test3')'
    如果我们在my.cnf中加入如下选项,则可跳过此错误,数据同步继续进行。

[mysqld]
slave_skip_errors=1062
    具体测试方法同上,大家可自己验证。

4 从backup恢复时从机复制出错的一些解释

    mysql企业版备份工具meb提供在线热备功能,如果在备份过程中执行ddl操作,从机需要从主机的备份恢复时可能会异常,从而导致从机同步数据失败。原因是从机恢复时需要先从备份文件恢复(包含备份过程中执行的ddl语句),同步时不是从全备后的最后一个位置同步,而是从ddl的上个位置同步,如果再次执行该ddl语句在从机上不会造成冲突,则同步继续,如果会造成冲突,同步终止。解决此冲突的办法是在my.cnf文件中加入一行

[mysqld]
slave_skip_errors=ddl_exist_errors

5 注意事项
    5.1 该参数为全局静态参数,不能动态调整,可在my.cnf中加入该参数列表后重启mysql服务器生效。
    5.2 必须注意的是,启动这个参数,如果处理不当,很可能造成主从数据库的数据不同步,在应用中需要根据实际情况,如果对数据完整性要求不是很严格,那么这个选项确实可以减轻维护的成本

****************************************************************************************
    原文地址:http://blog.csdn.net/jesseyoung/article/details/40585809
    博客主页:http://blog.csdn.net/jesseyoung
****************************************************************************************

<think>我们注意到在参数排序比较函数中,需要获取从机地址(slave address)来进行排序。在ParamLocation结构体中,我们已经保存了device_index,而每个device_index对应一个设备(DEVICE_NUMBER),其中包含了从机地址(Number字段)。因此,我们可以通过device_index来获取从机地址。 修改思路: 1. 在compare_params函数中,我们可以通过ParamLocation结构体中的device_index来访问设备参数结构体(DEVICE_PARA)中的DeviceNumber数组,从而获取从机地址。 2. 但是,compare_params函数是qsort的回调函数,它的参数是两个待比较元素的指针,而我们在比较函数内部无法直接访问全局的device_para结构。因此,我们需要将设备参数结构体作为额外信息传入比较函数。 解决方案: 我们可以使用qsort_r函数(如果支持)来传递额外参数。然而,qsort_r不是标准C函数,可移植性较差。另一种方法是使用全局变量,但要注意线程安全。考虑到这个程序是单线程的,我们可以使用一个全局变量来指向device_para结构体。 步骤: 1. 定义一个全局变量:DEVICE_PARA *g_device_para; 2. 在调用qsort之前,将这个全局变量设置为当前的device_para指针。 3. 在compare_params函数中,通过这个全局变量和ParamLocation中的device_index来获取从机地址。 但是,使用全局变量在多线程环境下会有问题,这里假设单线程。 另一种方法是,在收集参数位置信息时,将slave_addr也存入ParamLocation结构体中,这样在比较时就可以直接使用。 这里我们选择第二种方法,因为这样更清晰,且不需要全局变量。 修改ParamLocation结构体: ```c typedef struct { DEVICEID_PARAID *param; // 参数指针 int device_index; // 设备在DeviceNumber数组中的索引 int data_index; // 设备数据在DeviceData数组中的索引 int param_index; // 参数在DeviceParaID数组中的索引 int slave_addr; // 从机地址 int portfd; // 端口fd } ParamLocation; ``` 这样,在创建ParamLocation数组时,我们就可以填充slave_addr和portfd(如果后续排序需要的话)。然后,在比较函数中就可以直接使用这些字段。 因此,我们需要修改初始化param_locations数组的代码,在填充每个ParamLocation时,通过device_index获取设备,然后得到从机地址和端口fd(如果需要按端口排序的话)。 由于我们之前已经讨论过排序规则:先按端口fd,再按从机地址,最后按基地址。所以我们在ParamLocation中同时存储portfd和slave_addr。 具体修改如下: 1. 扩展ParamLocation结构体,增加slave_addr和portfd字段。 2. 在收集参数位置信息时,计算并存储这两个值。 3. 在比较函数中,直接使用这两个字段进行比较。 这样,在比较函数中就可以这样写: ```c int compare_params(const void *a, const void *b) { const ParamLocation *pa = (const ParamLocation *)a; const ParamLocation *pb = (const ParamLocation *)b; // 先按端口fd排序 if (pa->portfd != pb->portfd) { return pa->portfd - pb->portfd; } // 然后按从机地址排序 if (pa->slave_addr != pb->slave_addr) { return pa->slave_addr - pb->slave_addr; } // 最后按基地址排序 AddrParseResult res_a = parse_address(pa->param->RegAddress); AddrParseResult res_b = parse_address(pb->param->RegAddress); return res_a.base_addr - res_b.base_addr; } ``` 注意:在收集ParamLocation时,portfd和slave_addr的获取: - slave_addr: 通过device_index找到DEVICE_NUMBER,然后取Number字段。 - portfd: 通过device_index和device_argv,调用get_portfd函数。 但是,在初始化分组函数中,device_argv是已知的,所以我们可以传入device_argv。 修改后的init_addr_groups函数中,收集参数位置信息的循环: ```c for (t1 = 0; t1 < device_para->DeviceNumberIndex; t1++) { DEVICE_NUMBER *dev_num = &device_para->DeviceNumber[t1]; for (t2 = 0; t2 < dev_num->DeviceDataIndex; t2++) { DEVICE_DATA *dev_data = &dev_num->DeviceData[t2]; for (t3 = 0; t3 < dev_data->DeviceIdIndex; t3++) { param_locations[index].param = &dev_data->DeviceParaID[t3]; param_locations[index].device_index = t1; param_locations[index].data_index = t2; param_locations[index].param_index = t3; // 获取从机地址 param_locations[index].slave_addr = dev_num->Number; // 获取端口fd param_locations[index].portfd = get_portfd(t1, device_argv); index++; } } } ``` 这样,在比较函数中就可以直接使用这些字段,无需再通过全局变量。 注意:get_portfd可能会返回-1,这可能会影响排序(将-1放在前面还是后面?)。由于我们后续会跳过portfd为-1的参数,所以在排序时,我们可以将无效的端口排到最后,这样在分组时就不会干扰有效的参数。但根据我们的逻辑,portfd为-1的参数在后续会被跳过,所以排序时位置无关紧要。 因此,我们按照上述方式修改代码。 下面给出修改后的关键部分代码:</think>要解决在排序函数中获取从机地址的问题,我们需要修改参数位置信息结构体,并在初始化时填充从机地址和端口信息。以下是完整的解决方案: ```c /* 定义参数位置信息结构体 */ typedef struct { DEVICEID_PARAID *param; // 参数指针 int device_index; // 设备在DeviceNumber数组中的索引 int data_index; // 设备数据在DeviceData数组中的索引 int param_index; // 参数在DeviceParaID数组中的索引 int slave_addr; // 从机地址(新增) int portfd; // 端口fd(新增) } ParamLocation; /* 参数排序比较函数 */ int compare_params(const void *a, const void *b) { const ParamLocation *pa = (const ParamLocation *)a; const ParamLocation *pb = (const ParamLocation *)b; // 1. 先按端口fd排序 if (pa->portfd != pb->portfd) { return pa->portfd - pb->portfd; } // 2. 然后按从机地址排序 if (pa->slave_addr != pb->slave_addr) { return pa->slave_addr - pb->slave_addr; } // 3. 最后按寄存器基地址排序 AddrParseResult res_a = parse_address(pa->param->RegAddress); AddrParseResult res_b = parse_address(pb->param->RegAddress); return res_a.base_addr - res_b.base_addr; } /* 初始化地址分组 */ void init_addr_groups(DEVICE_PARA *device_para, DEVICE_ARGV *device_argv, int protocol_type) { // ... [其他代码不变] ... /* 第一步:收集所有参数位置信息 */ int total_params = 0; int t1, t2, t3; for (t1 = 0; t1 < device_para->DeviceNumberIndex; t1++) { DEVICE_NUMBER *dev_num = &device_para->DeviceNumber[t1]; for (t2 = 0; t2 < dev_num->DeviceDataIndex; t2++) { DEVICE_DATA *dev_data = &dev_num->DeviceData[t2]; total_params += dev_data->DeviceIdIndex; } } if (total_params == 0) return; // 创建参数位置数组 ParamLocation *param_locations = (ParamLocation *)malloc(total_params * sizeof(ParamLocation)); if (!param_locations) return; int index = 0; for (t1 = 0; t1 < device_para->DeviceNumberIndex; t1++) { DEVICE_NUMBER *dev_num = &device_para->DeviceNumber[t1]; int slave_addr = get_slave_addr(dev_num); // 获取从机地址 int portfd = get_portfd(t1, device_argv); // 获取端口fd for (t2 = 0; t2 < dev_num->DeviceDataIndex; t2++) { DEVICE_DATA *dev_data = &dev_num->DeviceData[t2]; for (t3 = 0; t3 < dev_data->DeviceIdIndex; t3++) { param_locations[index].param = &dev_data->DeviceParaID[t3]; param_locations[index].device_index = t1; param_locations[index].data_index = t2; param_locations[index].param_index = t3; param_locations[index].slave_addr = slave_addr; // 填充从机地址 param_locations[index].portfd = portfd; // 填充端口fd index++; } } } /* 第二步:按端口、从机地址和基地址排序 */ qsort(param_locations, total_params, sizeof(ParamLocation), compare_params); // ... [其余代码不变] ... } ``` ### 代码解释: 1. **修改ParamLocation结构体**: - 新增`slave_addr`字段存储从机地址 - 新增`portfd`字段存储端口文件描述符 - 这样在排序函数中可以直接访问这些值 2. **改进初始化过程**: - 在创建`param_locations`数组时,为每个参数位置信息填充: - `slave_addr`:通过`get_slave_addr()`获取 - `portfd`:通过`get_portfd()`获取 - 这样每个参数位置信息都包含了完整的分组关键信息 3. **优化排序函数**: - 现在可以直接访问`pa->portfd`和`pa->slave_addr` - 不需要调用额外的函数获取这些值 - 排序逻辑更清晰高效 4. **排序优先级**: ```c // 1. 端口fd (portfd) if (pa->portfd != pb->portfd) { return pa->portfd - pb->portfd; } // 2. 从机地址 (slave_addr) if (pa->slave_addr != pb->slave_addr) { return pa->slave_addr - pb->slave_addr; } // 3. 寄存器基地址 (base_addr) AddrParseResult res_a = parse_address(pa->param->RegAddress); AddrParseResult res_b = parse_address(pb->param->RegAddress); return res_a.base_addr - res_b.base_addr; ``` ### 完整排序函数实现: ```c /* 参数排序比较函数 */ int compare_params(const void *a, const void *b) { const ParamLocation *pa = (const ParamLocation *)a; const ParamLocation *pb = (const ParamLocation *)b; // 1. 首先按端口fd排序 (最高优先级) if (pa->portfd != pb->portfd) { return pa->portfd - pb->portfd; } // 2. 相同端口时,按从机地址排序 if (pa->slave_addr != pb->slave_addr) { return pa->slave_addr - pb->slave_addr; } // 3. 相同端口和从机时,按寄存器基地址排序 AddrParseResult res_a = parse_address(pa->param->RegAddress); AddrParseResult res_b = parse_address(pb->param->RegAddress); // 确保解析成功 if (res_a.base_addr < 0 || res_b.base_addr < 0) { return 0; } return res_a.base_addr - res_b.base_addr; } ``` ### 为什么这样设计更好: 1. **效率**: - 预先计算并存储关键值,避免在排序过程中重复解析 - 减少函数调用次数,提高排序性能 2. **可维护性**: - 所有分组相关信息集中在一个结构体中 - 排序逻辑清晰,易于理解和修改 3. **正确性**: - 确保分组标准(端口 > 从机 > 地址)严格遵循 - 避免在排序过程中可能出现的错误或不一致 4. **可扩展性**: - 如果需要添加新的排序标准,只需在结构体中添加新字段 - 修改排序函数逻辑即可,不影响其他代码 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值