使用canal進行mysql數據同步到Redis

業務場景描述

在我們的項目中有些配置信息持久化在數據庫中,這些配置信息又是在系統啓動後自動加載並緩存在local或者Redis中,但如果後臺運營系統進行了相應更新配置操作,我們需要實現“熱部署”或“熱插拔”等功能的話,我們有哪些方案可以實現呢?單機項目就非常簡單了,但分佈式集羣的項目怎麼辦呢?

分佈式集羣的項目之間同步數據,我們來講一下有哪些方案解決。

方案一:項目程序中對DB進行操作後,留一個後門接口進行刷新操作,針對不同機器的IP進行輪詢或點對點刷新同步操作。其簡單,就是再次進行一次DB查詢操作,該接口的安全性也需要留神,比較low。

方案二:項目程序中對DB進行操作時,通過切面或者自帶調用一個異步方法將數據發送給MQ或者緩存Redis等中間件,再由另一個程序去監聽並讀入到本地緩存或者公共緩存。其簡單但不穩定,如果忘了通知或者通知MQ失敗了或者事務回滾了,要謹慎和想完全其處理邏輯。

方案三:使用阿里的Canal等成熟的基於數據庫日誌增量訂閱&消費的中間件進行獨立有效的數據同步。其簡單,可獨立立項,更解耦更成熟,但缺點是屬於Java方向,暫時支持的數據庫爲MySQL,後文也將重點介紹這個方案。

方案四:使用成熟的分佈式系統的協調中間件Zookeeper等的環境變量進行簡單配置信息管理。

方案五使用mysql的udf去做,大體的思想是通過數據庫中的Trigger調用自定義的函數庫來觸發對Redis的相應操作,比較麻煩的一點是:自定義的函數庫需要我們基於mysql的API進行開發(C++),想想自己的Java程序要去調用這麼一堆玩意,本人很不情願。據瞭解,該方法也是阿里早起的解決方案,具體的步驟可參照:《【菜鳥玩Linux開發】通過MySQL自動同步刷新Redis》

方案六通過Gearman去同步


1. 可行方案

canal主要是基於數據庫的日誌解析,獲取增量變更進行同步,由此衍生出了增量訂閱&消費的業務,核心基本就是模擬mysql中slave節點請求。具體的原理在這裏不進行介紹,可以移步《阿里巴巴開源項目: canal 基於mysql數據庫binlog的增量訂閱&消費》 進行學習。

2. mysql的配置

  • 開啓mysql的binlog模塊

切換到mysql的安裝路徑,找到my.cnf(Linux)/my.ini (windows),加入如下內容:

[mysqld]
log-bin=mysql-bin #添加這一行就ok
binlog-format=ROW #選擇row模式
server_id=1 #配置mysql replaction需要定義,不能和canal的slaveId重複

配置完成後,需要重啓數據庫。當重啓數據庫遇到問題時,耐心解決,但需要警告的是,千萬別動data文件夾下的文件。當然如果你覺得你比較有“資本”,同時遇到了“mysql 1067 無法啓動”的錯誤,你可以試着備份一下data文件夾下的內容,刪除logfile文件,重啓數據庫即可,但本人極不推薦這樣進行操作。就是由於本人之前的無知,根據一個無良博客,誤刪了ibdata1文件,使得本人造成了很大的損失,mysql下的所有數據庫瞬間毀滅。

  • 配置mysql數據庫

創建canal用戶,用來管理canal的訪問權限。我們可以通過對canal用戶訪問權限的控制,進而控制canal能夠獲取的內容。

CREATE USER canal IDENTIFIED BY 'canal';    
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON 數據庫名.表名 TO 'canal'@'%';  
-- GRANT ALL PRIVILEGES ON 數據庫名.表名 TO 'canal'@'%' ;  
FLUSH PRIVILEGES;  

3. canal配置與部署

  • 下載部署包

下載,解壓,我使用的是最新版本1.0.22

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

  • 配置canal

主要配置的文件有兩處,canal/conf/example/instance.properties 和 canal/conf/canal.properties . 而canal.properties 文件我們一般保持默認配置,所以我們僅對instance.properties 進行修改。如果需要對canal進行復雜的配置,可以參考《Canal AdminGuide》

## mysql serverId
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 = 
  • 啓動canal
./canal/startup.sh
  • 查看啓動狀態

我們可以通過查看logs/canal/canal.log 和logs/example/example.log日誌來判斷canal是否啓動成功。

canal/logs/canal/canal.log

2016-12-29 14:03:00.956 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## start the canal server.
2016-12-29 14:03:01.071 [main] INFO  com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.1.99:11111]
2016-12-29 14:03:01.628 [main] INFO  com.alibaba.otter.canal.deployer.CanalLauncher - ## the canal server is running now ......

canal/logs/example/example.log

2016-12-29 14:03:01.357 [main] INFO  c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
2016-12-29 14:03:01.362 [main] INFO  c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
2016-12-29 14:03:01.535 [main] INFO  c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example 
2016-12-29 14:03:01.555 [main] INFO  c.a.otter.canal.instance.core.AbstractCanalInstance - start successful....

4. Java連接canal執行同步操作

在maven項目中中加載canal和redis依賴包.

<dependency>    
    <groupId>redis.clients</groupId>    
    <artifactId>jedis</artifactId>    
    <version>2.4.2</version>    
</dependency> 
<dependency>    
    <groupId>com.alibaba.otter</groupId>    
    <artifactId>canal.client</artifactId>    
    <version>1.0.22</version>    
</dependency>   

建立canal客戶端,從canal中獲取數據,並將數據更新至Redis.

import java.net.InetSocketAddress;  
import java.util.List;  

import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;  
import com.alibaba.otter.canal.common.utils.AddressUtils;  
import com.alibaba.otter.canal.protocol.Message;  
import com.alibaba.otter.canal.protocol.CanalEntry.Column;  
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;  
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;  
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;  
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;  
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;  
import com.alibaba.otter.canal.client.*;  

public class CanalClient{  
   public static void main(String args[]) {  
       CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),  
               11111), "example", "", "");  
       int batchSize = 1000;  
       try {  
           connector.connect();  
           connector.subscribe(".*\\..*");  
           connector.rollback();    
           while (true) {  
               Message message = connector.getWithoutAck(batchSize); // 獲取指定數量的數據  
               long batchId = message.getId();  
               int size = message.getEntries().size();  
               if (batchId == -1 || size == 0) {  
                   try {  
                       Thread.sleep(1000);  
                   } catch (InterruptedException e) {  
                       e.printStackTrace();  
                   }  
               } else {  
                   printEntry(message.getEntries());  
               }  
               connector.ack(batchId); // 提交確認  
               // connector.rollback(batchId); // 處理失敗, 回滾數據  
           }  
       } finally {  
           connector.disconnect();  
       }  
   }  

   private static void printEntry( List<Entry> entrys) {  
       for (Entry entry : entrys) {  
           if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {  
               continue;  
           }  
           RowChange rowChage = null;  
           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();  
           System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",  
                   entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),  
                   entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),  
                   eventType));  

           for (RowData rowData : rowChage.getRowDatasList()) {  
               if (eventType == EventType.DELETE) {  
                   redisDelete(rowData.getBeforeColumnsList());  
               } else if (eventType == EventType.INSERT) {  
                   redisInsert(rowData.getAfterColumnsList());  
               } else {  
                   System.out.println("-------> before");  
                   printColumn(rowData.getBeforeColumnsList());  
                   System.out.println("-------> after");  
                   redisUpdate(rowData.getAfterColumnsList());  
               }  
           }  
       }  
   }  

   private static void printColumn( List<Column> columns) {  
       for (Column column : columns) {  
           System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());  
       }  
   }  

  private static void redisInsert( List<Column> columns){
      JSONObject json=new JSONObject();
      for (Column column : columns) {  
          json.put(column.getName(), column.getValue());  
       }  
      if(columns.size()>0){
          RedisUtil.stringSet("user:"+ columns.get(0).getValue(),json.toJSONString());
      }
   }

  private static  void redisUpdate( List<Column> columns){
      JSONObject json=new JSONObject();
      for (Column column : columns) {  
          json.put(column.getName(), column.getValue());  
       }  
      if(columns.size()>0){
          RedisUtil.stringSet("user:"+ columns.get(0).getValue(),json.toJSONString());
      }
  }

   private static  void redisDelete( List<Column> columns){
       JSONObject json=new JSONObject();
          for (Column column : columns) {  
              json.put(column.getName(), column.getValue());  
           }  
          if(columns.size()>0){
              RedisUtil.delKey("user:"+ columns.get(0).getValue());
          }
   }   
}  

RedisUtil 工具類

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisUtil {

    // Redis服務器IP
    private static String ADDR = "0.0.0.0";

    // Redis的端口號
    private static int PORT = 6379;

    // 訪問密碼
    //private static String AUTH = "admin";

    // 可用連接實例的最大數目,默認值爲8;
    // 如果賦值爲-1,則表示不限制;如果pool已經分配了maxActive個jedis實例,則此時pool的狀態爲exhausted(耗盡)。
    private static int MAX_ACTIVE = 1024;

    // 控制一個pool最多有多少個狀態爲idle(空閒的)的jedis實例,默認值也是8。
    private static int MAX_IDLE = 200;

    // 等待可用連接的最大時間,單位毫秒,默認值爲-1,表示永不超時。如果超過等待時間,則直接拋出JedisConnectionException;
    private static int MAX_WAIT = 10000;

    // 過期時間
    protected static int  expireTime = 60 * 60 *24;

    // 連接池
    protected static JedisPool pool;

    /**
     * 靜態代碼,只在初次調用一次
     */
    static {
        JedisPoolConfig config = new JedisPoolConfig();
        //最大連接數
        config.setMaxTotal(MAX_ACTIVE);
        //最多空閒實例
        config.setMaxIdle(MAX_IDLE);
        //超時時間
        config.setMaxWaitMillis(MAX_WAIT);
        //
        config.setTestOnBorrow(false);
        pool = new JedisPool(config, ADDR, PORT, 1000);
    }

    /**
     * 獲取jedis實例
     */
    protected static synchronized Jedis getJedis() {
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
        } catch (Exception e) {
            e.printStackTrace();
            if (jedis != null) {
                pool.returnBrokenResource(jedis);
            }
        }
        return jedis;
    }

    /**
     * 釋放jedis資源
     * @param jedis
     * @param isBroken
     */
    protected static void closeResource(Jedis jedis, boolean isBroken) {
        try {
            if (isBroken) {
                pool.returnBrokenResource(jedis);
            } else {
                pool.returnResource(jedis);
            }
        } catch (Exception e) {

        }
    }

    /**
     * 是否存在key
     * @param key
     */
    public static boolean existKey(String key) {
        Jedis jedis = null;
        boolean isBroken = false;
        try {
            jedis = getJedis();
            jedis.select(0);
            return jedis.exists(key);
        } catch (Exception e) {
            isBroken = true;
        } finally {
            closeResource(jedis, isBroken);
        }
        return false;
    }

    /**
     * 刪除key
     * @param key
     */
    public static void delKey(String key) {
        Jedis jedis = null;
        boolean isBroken = false;
        try {
            jedis = getJedis();
            jedis.select(0);
            jedis.del(key);
        } catch (Exception e) {
            isBroken = true;
        } finally {
            closeResource(jedis, isBroken);
        }
    }

    /**
     * 取得key的值
     * @param key
     */
    public static String stringGet(String key) {
        Jedis jedis = null;
        boolean isBroken = false;
        String lastVal = null;
        try {
            jedis = getJedis();
            jedis.select(0);
            lastVal = jedis.get(key);
            jedis.expire(key, expireTime);
        } catch (Exception e) {
            isBroken = true;
        } finally {
            closeResource(jedis, isBroken);
        }
        return lastVal;
    }

    /**
     * 添加string數據
     * @param key
     * @param value
     */
    public static String stringSet(String key, String value) {
        Jedis jedis = null;
        boolean isBroken = false;
        String lastVal = null;
        try {
            jedis = getJedis();
            jedis.select(0);
            lastVal = jedis.set(key, value);
            jedis.expire(key, expireTime);
        } catch (Exception e) {
            e.printStackTrace();
            isBroken = true;
        } finally {
            closeResource(jedis, isBroken);
        }
        return lastVal;
    }

    /**
     *  添加hash數據
     * @param key
     * @param field
     * @param value
     */
    public static void hashSet(String key, String field, String value) {
        boolean isBroken = false;
        Jedis jedis = null;
        try {
            jedis = getJedis();
            if (jedis != null) {
                jedis.select(0);
                jedis.hset(key, field, value);
                jedis.expire(key, expireTime);
            }
        } catch (Exception e) {
            isBroken = true;
        } finally {
            closeResource(jedis, isBroken);
        }
    }
}

至此,我們利用canal進行了mysql數據同步到Redis的任務,可以根據不同的需求將代碼進行修改置於需要的位置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章