通过Canal保证某网站的Redis与MySql的数据自动同步

10 篇文章 0 订阅

前置文章: 某网站Redis与MySql同步方案分析


使用Canal的主要目的:让自动同步代替部分手动同步,降低开发人员工作量,避免部分数据一致性问题。

本文主要讲解如何配置Canal,以保证某网站的Redis与MySql的数据自动同步。

1.Java开发原则

下面列出一些本项目的开发原则:

1.1.Redis的KEY命名规范

项目名称-模块名称-对象名称-主键id

例如:baidu-news-user-0000000001。

1.2.Redis初始化

有部分数据需要提前初始化到Redis之中,所以需要提供一个Redis初始化方法redisInit().

1.3.对于时效性要求较高的数据

对于时效性要求较高的数据,采用手动编码的方式保证Redis与MySql的同步:

  • 对于读操作
    • 先读Redis,如果有记录则返回结果;如果没有记录,则读取MySql。
    • 读取MySql,如果有记录则更新Redis,并返回结果;如果没有记录,则返回无结果。
  • 对于写操作
    • 先写MySql,再更新Redis。
4.对于时效性要求一般的数据

对于时效性要求一般的数据,通过Canal的方式保证Redis与MySql的同步:

  • 对于读操作
    • 读取Redis,如果有记录则返回结果,如果没有记录,则返回无结果。
  • 对于写操作
    • 直接写MySql。
  • 通过Canal解析MySql的binlog,来进行Redis和MySql的自动同步。

2.MySql设计原则

经过前面的分析,我们已经知道:Canal是根据MySql中的数据表的增量变化情况去更新Redis数据库的。

所以,如果MySql表结构Redis缓存结构一一对应,则我们在编写缓存自动同步方法时,会节省很多工作。

所以,在某网站项目中,我们统一要求:

  • 设计MySql表结构时,尽量与Redis缓存结构一一对应。
  • 如果因为某些原因无法做到一一对应,则添加一张专用的缓存中间表
  • 缓存中间表缓存结构一一对应,专门用来存储经过业务处理产生的最终需要缓存的数据。

举例说明缓存中间表的作用:

  • 背景
    • MySql中有人员基础信息表person[id,number,name],分别存储了字段:个人编号、身份证号码和姓名。
    • MySql中有人员考试成绩信息表score[id,score],分别存储了字段:个人编号、考试分数。
    • Redis设计了一个缓存结构{key:sfzhm,value=score},用于通过身份证号码快速查询考试分数,其字段为:身份证号码和考试分数。
  • 如果不设计缓存中间表
    • Canal读取增量数据变化,发现person[grbh,sfzhm,name]发生变化。
    • 这时,需要去做其他业务计算,判断是否同步修改Redis中{key:sfzhm,value=score}
    • 因为引起person[grbh,sfzhm,name]变化的原因可能有多种,与缓存结构{key:sfzhm,value=score}相关的可能只是一种情况。
  • 如果添加了缓存中间表person_score[sfzhm,score]
    • Canal读取增量数据变化,发现person[grbh,sfzhm,name]发生变化,无需理会。
    • Canal读取增量数据变化,发现person_score[sfzhm,score]发生变化,则直接修改Redis中的{key:sfzhm,value=score}即可。
    • 因为person_score[sfzhm,score]变化的肯定与{key:sfzhm,value=score}相关。

3.Canal同步方案的部署教程

下面对Canal同步方案的使用方法进行说明,主要分为以下几个步骤:

  • 配置Mysql开启binlog模式
  • 配置Mysql创建并授权Canal用户
  • 部署并启动Canal
  • 编写Redis自动同步服务

备注:此说明参考了其他文献,但是当时没有记录转载作者的好习惯,故而这里没有提及。如果有相关建议,请多多指教

3.1.配置Mysql开启binlog模式

因为Canal是伪装成MySql Slave去收集MySql Master发送的binlog(Binary Logs),所以,需要开启MySql的binlog模式。

切换到mysql的安装路径,找到my.cnf(Linux)/my.ini(windows),加入如下内容:

[mysqld]
log-bin=mysql-bin #添加这一行就ok 开启binary log
binlog-format=ROW #选择row模式 不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了,修改成什么样了
server_id=1 #配置主从复制(mysql replaction)需要定义,不能和canal的slaveId(默认1234)重复

配置完成后,需要重启数据库。

3.2.配置Mysql创建并授权Canal用户

创建canal用户,用来管理canal的访问权限。

# 创建canal用户,用来管理canal的访问权限
CREATE USER canal IDENTIFIED BY 'canal';
# 分配权限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
# 解除权限
# REVOKE SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* FROM 'canal'@'%';
# FLUSH PRIVILEGES;
3.3.部署并启动Canal
3.3.1.下载部署包

https://github.com/alibaba/canal/releases/

3.3.2.配置Canal

配置文件有两个:

  • canal/conf/example/instance.properties
  • canal/conf/canal.properties。

一般情况下,canal.properties 文件保持默认配置即可,所以我们仅对instance.properties 进行修改:

## mysql serverId
# Canal伪装成MySql Slave的id
canal.instance.mysql.slaveId = 1234

# position info
canal.instance.master.address = ***.***.***.***:3306 #改成自己的数据库地址 一般情况下只需要修改此处
canal.instance.master.journal.name =
canal.instance.master.position =
canal.instance.master.timestamp =

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =

# username/password
canal.instance.dbUsername = canal #改成自己的数据库信息 默认即可
canal.instance.dbPassword = canal #改成自己的数据库信息 默认即可
canal.instance.defaultDatabaseName =  #改成自己的数据库信息 默认即可
canal.instance.connectionCharset = UTF-8 #改成自己的数据库信息 默认即可

# table regex
canal.instance.filter.regex = .*\\..*
# table black regex
canal.instance.filter.black.regex =
3.3.3.启动canal
  • Linux:./canal/startup.sh
  • Windows:startup.bat
3.3.4.查看启动状态

通过查看logs/canal/canal.log 和logs/example/example.log日志来判断canal是否启动成功。

正常启动情况下,canal/logs/canal/canal.log的内容类似如下:

2017-07-24 16:43:43.983 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## start the canal server.
2017-07-24 16:43:44.336 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[130.10.7.37:11111]
2017-07-24 16:43:46.203 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## the canal server is running now ......

正常启动情况下,canal/logs/example/example.log的内容类似如下:

2017-07-24 16:43:44.976 [main] INFO  c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
2017-07-24 16:43:45.007 [main] INFO  c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
2017-07-24 16:43:45.236 [main] WARN  org.springframework.beans.TypeConverterDelegate - PropertyEditor [com.sun.beans.editors.EnumEditor] found through deprecated global PropertyEditorManager fallback - consider using a more isolated form of registration, e.g. on the BeanWrapper/BeanFactory!
2017-07-24 16:43:45.388 [main] INFO  c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example 
2017-07-24 16:43:45.800 [main] INFO  c.a.otter.canal.instance.core.AbstractCanalInstance - subscribe filter change to .*\..*
2017-07-24 16:43:45.801 [main] INFO  c.a.otter.canal.instance.core.AbstractCanalInstance - start successful....

如果出现报错信息,请根据报错信息进行调试。

3.3.5.关闭canal

./canal/stop.sh或者关闭startup.bat

4.编写Redis自动同步服务(RedisAutoSync)
4.1.服务说明

Canal服务的作用是:通过解析binlog读取MySql数据库的增量变化情况。

Canal服务本身并不能进行Redis与MySql的数据同步。

为了实现Redis与MySql的数据同步,我们还需要额外编写同步服务,这个服务的作用如下:

  • 连接Canal服务
  • 轮循的去获取Canal解析出来的MySql的增量数据
  • 根据MySql的增量数据,相应的做出Redis的更新和删除操作

为了便于称呼,我将这个服务命名为RedisAutoSync.

4.2.重点代码

下面分几个介绍RedisAutoSync的重点代码:

  • connectAndGetIncrementData():连接Canal服务,获取增量数据。
  • filterAndProcessIncrementData():根据表名进行筛选,并处理增量数据
  • dealIncrementDataStrategically():按策略处理增量数据
  • setRedis():更新缓存
  • delRedis():删除缓存
  • dataFormatAndTransCode():数据格式和编码转换

RedisAutoSync代码如下:

/**
 * <p>Title: Redis自动同步:读取Canal提供的MySql增量数据,进行Redis同步</p>
 * @author 韩超 2018/4/2 17:18
 */
public class RedisAutoSync {

    //日志
    private Logger logger = LoggerFactory.getLogger(RedisAutoSync.class);

    /**
     * <p>Title: 连接Canal服务并获取增量数据</p>
     *
     * @author 韩超 2018/4/2 16:31
     */
    public void connectAndGetIncrementData() throws UnsupportedEncodingException {
        //连接Canal服务--默认参数,无需配置
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
                11111), "example", "", "");
        //一个批次获取的最大数据量
        int batchSize = 1000;
        //如果连接有效,则进行增量数据获取
        if (connector.checkValid()) {
            logger.info("CanalConnector is valid");
            try {
                //连接
                connector.connect();
                //订购
                connector.subscribe(".*\\..*");
                //回滚
                connector.rollback();
                logger.info("开始获取增量日志,bachSize:{}", batchSize);
                while (true) {
                    // 获取指定数量的数据
                    Message message = connector.getWithoutAck(batchSize);
                    //获取批次id
                    long batchId = message.getId();
                    //获取数据大小
                    int size = message.getEntries().size();
                    if (batchId == -1 || size == 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //进行数据表筛选和增量数据处理
                        filterAndProcessIncrementData(message.getEntries());
                    }
                    connector.ack(batchId); // 提交确认
                    // connector.rollback(batchId); // 处理失败, 回滚数据
                }
            } finally {
                connector.disconnect();
            }
        } else {
            logger.info("CanalConnector is invalid!");
        }
    }

    /**
     * <p>Title:进行数据表筛选和增量数据处理 </p>
     *
     * @author 韩超 2018/4/2 16:44
     */
    private void filterAndProcessIncrementData(List<Entry> entrys) throws UnsupportedEncodingException {
        //并不是数据库的每个表的增量变化都需要进行相应的处理
        //自定义一个工具类,用于存储需要监测的数据表
        String allowdb = CanalDbConfig.getAllowDb();
        //获取数据库名与数据表名的MAP
        Map<String, String> allowTable = CanalDbConfig.getAllowTable();
        //对增量数据进行循环处理
        for (Entry entry : entrys) {
            //如果是事务开始或、事务结束或者查询,则跳过本次循环
            if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
                continue;
            }
            //定义一行数据变化
            RowChange rowChage;
            try {
                //获取一行数据
                rowChage = RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e);
            }
            //获取数据库事件类型
            EventType eventType = rowChage.getEventType();
            //从增量数据中获取数据库名
            String dbname = entry.getHeader().getSchemaName();
            //如果存在[运行监测的库和表]相关配置,则先判断数据库名和表名是否正确,然后处理缓存
            if (!"".equals(allowdb)) {
                //如果是允许监控的数据库
                if (dbname.matches(allowdb)) {
                    //如果是运行监控的表
                    if (entry.getHeader().getTableName().matches(allowTable.get(dbname))) {
                        //按策略处理增量数据
                        dealIncrementDataStrategically(entry, rowChage, eventType);
                    }
                }
            } else {
                //如果不存在[运行监测的库和表]相关配置,则表示要监测所有库和表
                //按策略处理增量数据
                dealIncrementDataStrategically(entry, rowChage, eventType);
            }
        }
    }

    /**
     * <p>Title: 按策略处理增量数据</p>
     *
     * @author 韩超 2018/4/2 16:52
     */
    private void dealIncrementDataStrategically(Entry entry, RowChange rowChage, EventType eventType) throws UnsupportedEncodingException {
        String templog = String.format("binlog[%s:%s] , name[%s,%s] , eventType : %s",
                entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                eventType);
        logger.info(templog);
        //按照行进行遍历
        for (RowData rowData : rowChage.getRowDatasList()) {
            //如果是Delete操作,则删除(del)缓存
            if (eventType == EventType.DELETE) {
                delRedis(entry.getHeader().getTableName(), rowData.getBeforeColumnsList());
            } else {//其他操作,则更新(set)缓存
                setRedis(entry.getHeader().getTableName(), rowData.getAfterColumnsList());
            }
        }
    }

    /**
     * <p>Title: 更新缓存</p>
     *
     * @author 韩超 2018/4/2 16:55
     */
    private void setRedis(String tableName, List<Column> columns) throws UnsupportedEncodingException {
        //定义json串用于存储增量数据的值
        JSONObject json = new JSONObject();
        //获取增量列和json串
        columns = dataFormatAndTransCode(columns, json);
        //定义 键 值
        String key, value;
        //如果存在数据,则继续处理
        if (columns.size() > 0) {
            //获取key值
            key = columns.get(0).getValue();
            //获取value值
            value = json.toJSONString();
            //根据表名决定操作方式
            switch (tableName) {
                //如果是特殊表,则进行特殊处理
                case "special_table01":
                    //进行特殊处理,如key值格式化等等
                    //...
                    //设置缓存:以gyrlzyw-rmzw为值的缓存
                    RedisUtils.set(key, value);
                    //打印日志信息
                    logger.info("设置缓存:key  = " + key + ",value = " + value);
                    break;
                case "special_table02":
                    //...
                    break;

                //如果是普通的缓存中间表,则不需要额外处理,直接更新对应缓存即可
                default:
                    //设置缓存
                    RedisUtils.set(key, value);
                    //打印日志信息
                    logger.info("设置缓存:key  = " + key + ",value = " + value);
                    break;
            }
        }
    }

    /**
     * <p>Title: 删除缓存</p>
     *
     * @author 韩超 2018/4/2 16:56
     */
    private void delRedis(String tableName, List<Column> columns) throws UnsupportedEncodingException {
        //定义
        JSONObject json = new JSONObject();
        //获取增量列
        dataFormatAndTransCode(columns, json);
        //定义 键
        String key;
        //如果存在数据,则继续处理
        if (columns.size() > 0) {
            //获取key值
            key = columns.get(0).getValue();
            //根据表名决定操作方式
            switch (tableName) {
                //如果是特殊表,则进行特殊处理
                case "special_table01":
                    //删除缓存
                    RedisUtils.del(key);
                    //打印日志信息
                    logger.info("删除缓存:key = " + key);
                    break;
                case "special_table02":
                    //...
                    break;

                //如果是普通的缓存中间表,则不需要额外处理,直接删除对应缓存即可
                default:
                    //删除缓存
                    RedisUtils.del(key);
                    //打印日志信息
                    logger.info("删除缓存:key = " + key);
                    break;
            }
        }
    }

    /**
     * <p>Title: 对增量数据进行格式化与编码转换</p>
     * <p>
     * <p>可以按照项目编码和项目字段进行自定义</p>
     *
     * @author 韩超 2018/4/2 16:56
     */
    private List<Column> dataFormatAndTransCode(List<Column> columns, JSONObject json) throws UnsupportedEncodingException {
        //按列遍历(其实就是按字段遍历)
        for (Column column : columns) {
            //如果字段类型是blob,则进行转码
            if (column.getMysqlType().contains("blob")) {
                json.put(column.getName(), new String(column.getValue().getBytes("ISO-8859-1"), "gbk"));
            } else {//如果是其他字段,不用处理
                json.put(column.getName(), column.getValue());
            }
        }
        return columns;
    }
}

备注:

5.注意事项
5.1.服务启动顺序

系统中关于MySql和Redis的服务有很多:MySql、Redis、Canal、RedisAutoSync以及主体系统WebApp。

这些服务的启动顺序(不包括其他系统如Solr等等)如下:

  1. 启动MySql服务 (如 service mysql start)
  2. 启动Redis服务 (如 redis-server /opt/redis/redis.conf)
  3. 启动Canal服务 (如 ./canal/startup.sh)
  4. 启动RedisAutoSync服务 (如 ./startup.sh)
  5. 启动WebApp服务 (如 ./startup.sh)
5.2.Canal解析进度与binlog日志不匹配问题解决

在开发和调试的过程中,需要多次启动服务、关闭服务。经常这么做会导致canal解析进度与binlog日志不匹配的问题发生。

此时,再启动Canal服务,canal.log和example.log就会报错。

可以通过以下方式移除canal的解析进度记录文件,从而解决问题:

  • 删除\canal.deployer-1.0.24\conf\example\meta.dat文件
  • 重启Canal服务

注意:此操作会导致部分增量数据的丢失,但是考虑到此问题只产生于开发和测试过程中,这些损失可以接受。

如果您有更好的解决方法,请多多指教。

5.3.MySql日志激增

通过Canal进行Redis与MySql的自动同步,会导致MySql的日志激增。

导致日志激增来源于2个方面:

  • 开启了binlog,binary log用于记录数据库增量变化,必然会很大。
  • binlog使用的是ROW模式,此模式虽然能够有效避免主从复制的主从不一致问题,但是相对于其他主从模式,产生的日志更多。

为了应对MySql日志激增问题,我们采取的是MySql日志自动清除配置。

您可以参考如下的文章:《MySql自动清除binary logs日志》

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值