流程
Canal的原理是模拟Slave向Master发送请求,Canal解析binlog,但不将解析结果持久化,而是保存在内存中,每次有客户端读取一次消息,就删除该消息。这里所说的客户端,就需要我们写一个连接Canal的程序,持续从Canal获取数据。
程序写MySQL, 解析binlog,数据放入队列写Redis
读Redis
步骤
一、配置Canal
参考https://github.com/alibaba/canal
【mysql配置】
1,配置参数
1. [mysqld]
2. log-bin=mysql-bin #添加这一行就ok
3. binlog-format=ROW #选择row模式
4. server_id=1 #配置mysql replaction需要定义,不能和canal的slaveId重复
2,在mysql中 配置canal数据库管理用户,配置相应权限(repication权限)
1. CREATE USER canal IDENTIFIED BY 'canal';
2. GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
3. -- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
4. FLUSH PRIVILEGES;
【canal下载和配置】
1,下载canal https://github.com/alibaba/canal/releases
2,解压
1. mkdir /tmp/canal
2. tar zxvf canal.deployer-$version.tar.gz -C /tmp/canal
3,修改配置文件
1. vi conf/example/instance.properties
1. #################################################
2. ## mysql serverId
3. canal.instance.mysql.slaveId = 1234
4.
5. # position info,需要改成自己的数据库信息
6. canal.instance.master.address = 127.0.0.1:3306
7. canal.instance.master.journal.name =
8. canal.instance.master.position =
9. canal.instance.master.timestamp =
10.
11. #canal.instance.standby.address =
12. #canal.instance.standby.journal.name =
13. #canal.instance.standby.position =
14. #canal.instance.standby.timestamp =
15.
16. # username/password,需要改成自己的数据库信息
17. canal.instance.dbUsername = canal
18. canal.instance.dbPassword = canal
19. canal.instance.defaultDatabaseName =
20. canal.instance.connectionCharset = UTF-8
21.
22. # table regex
23. canal.instance.filter.regex = .*\\..*
24.
25. #################################################
【canal启动和关闭】
1,启动
1. sh bin/startup.sh
2,查看日志
1. vi logs/canal/canal.log
1. 2013-02-05 22:45:27.967 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## start the canal server.
2. <pre name="user-content-code">2013-02-05 22:45:28.113 [main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[10.1.29.120:11111]
3. 2013-02-05 22:45:28.210 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## the canal server is running now ......
具体instance的日志:
1. vi logs/example/example.log
1. 2013-02-05 22:50:45.636 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
2. 2013-02-05 22:50:45.641 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
3. 2013-02-05 22:50:45.803 [main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example
4. 2013-02-05 22:50:45.810 [main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start successful....
3,关闭
1. sh bin/stop.sh
注意:
1,这里只需要配置好参数后,就可以直接运行
2,Canal没有解析后的文件,不会持久化
二、创建客户端
参考https://github.com/alibaba/canal/wiki/ClientExample
其中一个是连接canal并操作的类,一个是redis的工具类,使用maven主要是依赖包的下载很方便。
pom.xml
1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2. 4.0.0</modelVersion>
3. .alibaba.otter</groupId>
4. .sample</artifactId>
5. 0.0.1-SNAPSHOT</version>
6. <dependencies>
7. <dependency>
8. .alibaba.otter</groupId>
9. .client</artifactId>
10. 1.0.12</version>
11. </dependency>
12.
13. <dependency>
14. .springframework</groupId>
15. <artifactId>spring-test</artifactId>
16. 3.1.2.RELEASE</version>
17. <scope>test</scope>
18. </dependency>
19.
20. <dependency>
21. .clients</groupId>
22. <artifactId>jedis</artifactId>
23. 2.4.2</version>
24. </dependency>
25.
26. </dependencies>
27. <build/>
28. </project>
2,ClientSample代码
这里主要做两个工作,一个是循环从Canal上取数据,一个是将数据更新至Redis
1. package canal.sample;
2.
3. import java.net.InetSocketAddress;
4. import java.util.List;
5.
6. import com.alibaba.fastjson.JSONObject;
7. import com.alibaba.otter.canal.client.CanalConnector;
8. import com.alibaba.otter.canal.common.utils.AddressUtils;
9. import com.alibaba.otter.canal.protocol.Message;
10. import com.alibaba.otter.canal.protocol.CanalEntry.Column;
11. import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
12. import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
13. import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
14. import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
15. import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
16. import com.alibaba.otter.canal.client.*;
17.
18. public class ClientSample {
19.
20. public static void main(String args[]) {
21.
22. // 创建链接
23. new InetSocketAddress(AddressUtils.getHostIp(),
24. "example", "", "");
25. int batchSize = 1000;
26. try {
27. connector.connect();
28. ".*\\..*");
29. connector.rollback();
30. while (true) {
31. // 获取指定数量的数据
32. long batchId = message.getId();
33. int size = message.getEntries().size();
34. if (batchId == -1 || size == 0) {
35. try {
36. Thread.sleep(1000);
37. catch (InterruptedException e) {
38. e.printStackTrace();
39. }
40. else {
41. printEntry(message.getEntries());
42. }
43.
44. // 提交确认
45. // connector.rollback(batchId); // 处理失败, 回滚数据
46. }
47.
48. } finally {
49. connector.disconnect();
50. }
51. }
52.
53. private static void printEntry( List<Entry> entrys) {
54. for (Entry entry : entrys) {
55. if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
56. continue;
57. }
58.
59. RowChange rowChage = null;
60. try {
61. rowChage = RowChange.parseFrom(entry.getStoreValue());
62. catch (Exception e) {
63. throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
64. e);
65. }
66.
67. EventType eventType = rowChage.getEventType();
68. "================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
69. entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
70. entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
71. eventType));
72.
73. for (RowData rowData : rowChage.getRowDatasList()) {
74. if (eventType == EventType.DELETE) {
75. redisDelete(rowData.getBeforeColumnsList());
76. else if (eventType == EventType.INSERT) {
77. redisInsert(rowData.getAfterColumnsList());
78. else {
79. "-------> before");
80. printColumn(rowData.getBeforeColumnsList());
81. "-------> after");
82. redisUpdate(rowData.getAfterColumnsList());
83. }
84. }
85. }
86. }
87.
88. private static void printColumn( List<Column> columns) {
89. for (Column column : columns) {
90. " : " + column.getValue() + " update=" + column.getUpdated());
91. }
92. }
93.
94. private static void redisInsert( List<Column> columns){
95. new JSONObject();
96. for (Column column : columns) {
97. json.put(column.getName(), column.getValue());
98. }
99. if(columns.size()>0){
100. "user:"+ columns.get(0).getValue(),json.toJSONString());
101. }
102. }
103.
104. private static void redisUpdate( List<Column> columns){
105. new JSONObject();
106. for (Column column : columns) {
107. json.put(column.getName(), column.getValue());
108. }
109. if(columns.size()>0){
110. "user:"+ columns.get(0).getValue(),json.toJSONString());
111. }
112. }
113.
114. private static void redisDelete( List<Column> columns){
115. new JSONObject();
116. for (Column column : columns) {
117. json.put(column.getName(), column.getValue());
118. }
119. if(columns.size()>0){
120. "user:"+ columns.get(0).getValue());
121. }
122. }
123.
124.
125. }
3,RedisUtil代码
1. package canal.sample;
2.
3. import redis.clients.jedis.Jedis;
4. import redis.clients.jedis.JedisPool;
5. import redis.clients.jedis.JedisPoolConfig;
6.
7. public class RedisUtil {
8.
9. // Redis服务器IP
10. private static String ADDR = "10.1.2.190";
11.
12. // Redis的端口号
13. private static int PORT = 6379;
14.
15. // 访问密码
16. private static String AUTH = "admin";
17.
18. // 可用连接实例的最大数目,默认值为8;
19. // 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
20. private static int MAX_ACTIVE = 1024;
21.
22. // 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
23. private static int MAX_IDLE = 200;
24.
25. // 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
26. private static int MAX_WAIT = 10000;
27.
28. // 过期时间
29. protected static int expireTime = 660 * 660 *24;
30.
31. // 连接池
32. protected static JedisPool pool;
33.
34. /**
35. * 静态代码,只在初次调用一次
36. */
37. static {
38. new JedisPoolConfig();
39. //最大连接数
40. .setMaxTotal(MAX_ACTIVE);
41. //最多空闲实例
42. .setMaxIdle(MAX_IDLE);
43. //超时时间
44. .setMaxWaitMillis(MAX_WAIT);
45. //
46. .setTestOnBorrow(false);
47. new JedisPool(config, ADDR, PORT, 1000);
48. }
49.
50. /**
51. * 获取jedis实例
52. */
53. protected static synchronized Jedis getJedis() {
54. Jedis jedis = null;
55. try {
56. .getResource();
57. catch (Exception e) {
58. .printStackTrace();
59. if (jedis != null) {
60. .returnBrokenResource(jedis);
61. }
62. }
63. return jedis;
64. }
65.
66. /**
67. * 释放jedis资源
68. *
69. * @param jedis
70. * @param isBroken
71. */
72. protected static void closeResource(Jedis jedis, boolean isBroken) {
73. try {
74. if (isBroken) {
75. .returnBrokenResource(jedis);
76. else {
77. .returnResource(jedis);
78. }
79. catch (Exception e) {
80.
81. }
82. }
83.
84. /**
85. * 是否存在key
86. *
87. * @param key
88. */
89. public static boolean existKey(String key) {
90. Jedis jedis = null;
91. false;
92. try {
93. jedis = getJedis();
94. .select(0);
95. return jedis.exists(key);
96. catch (Exception e) {
97. true;
98. } finally {
99. closeResource(jedis, isBroken);
100. }
101. return false;
102. }
103.
104. /**
105. * 删除key
106. *
107. * @param key
108. */
109. public static void delKey(String key) {
110. Jedis jedis = null;
111. false;
112. try {
113. jedis = getJedis();
114. .select(0);
115. .del(key);
116. catch (Exception e) {
117. true;
118. } finally {
119. closeResource(jedis, isBroken);
120. }
121. }
122.
123. /**
124. * 取得key的值
125. *
126. * @param key
127. */
128. public static String stringGet(String key) {
129. Jedis jedis = null;
130. false;
131. String lastVal = null;
132. try {
133. jedis = getJedis();
134. .select(0);
135. .get(key);
136. .expire(key, expireTime);
137. catch (Exception e) {
138. true;
139. } finally {
140. closeResource(jedis, isBroken);
141. }
142. return lastVal;
143. }
144.
145. /**
146. * 添加string数据
147. *
148. * @param key
149. * @param value
150. */
151. public static String stringSet(String key, String value) {
152. Jedis jedis = null;
153. false;
154. String lastVal = null;
155. try {
156. jedis = getJedis();
157. .select(0);
158. .set(key, value);
159. .expire(key, expireTime);
160. catch (Exception e) {
161. .printStackTrace();
162. true;
163. } finally {
164. closeResource(jedis, isBroken);
165. }
166. return lastVal;
167. }
168.
169. /**
170. * 添加hash数据
171. *
172. * @param key
173. * @param field
174. * @param value
175. */
176. public static void hashSet(String key, String field, String value) {
177. false;
178. Jedis jedis = null;
179. try {
180. jedis = getJedis();
181. if (jedis != null) {
182. .select(0);
183. .hset(key, field, value);
184. .expire(key, expireTime);
185. }
186. catch (Exception e) {
187. true;
188. } finally {
189. closeResource(jedis, isBroken);
190. }
191. }
192.
193. }
注意:
1,客户端的Jedis连接不同于项目里的Jedis连接需要Spring注解,直接使用静态方法就可以。
运行
1,运行canal服务端startup.bat / startup.sh
2,运行客户端程序
注意
1,虽然canal服务端解析binlog后不会把数据持久化,但canal服务端会记录每次客户端消费的位置(客户端每次ack时服务端会记录pos点)。如果数据正在更新时,canal服务端挂掉,客户端也会跟着挂掉,mysql依然在插入数据,而redis则因为客户端的关闭而停止更新,造成mysql和redis的数据不一致。解决办法是,只要重启canal服务端和客户端就可以了,虽然canal服务端因为重启之前解析数据清空,但因为canal服务端记录的是客户端最后一次获取的pos点,canal服务端再从这个pos点开始解析,客户端更新至redis,以达到数据的一致。
2,如果只有一个canal服务端和一个客户端,肯定存在可用性低的问题,一种做法是用程序来监控canal服务端和客户端,如果挂掉,再重启;一种做法是多个canal服务端+zk,将canal服务端的配置文件放在zk,任何一个canal服务端挂掉后,切换到其他canal服务端,读到的配置文件的内容就是一致的(还有记录的消费pos点),保证业务的高可用,客户端可使用相同的做法。