業務場景描述
在我們的項目中有些配置信息持久化在數據庫中,這些配置信息又是在系統啓動後自動加載並緩存在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的任務,可以根據不同的需求將代碼進行修改置於需要的位置。