├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── cn │ └── thinkingdata │ └── kafka │ ├── cache │ └── KafkaCache.java │ ├── close │ ├── DaemonCloseThread.java │ ├── ScanTermMethod.java │ ├── SignalTermMethod.java │ └── TermMethod.java │ ├── constant │ └── KafkaMysqlOffsetParameter.java │ ├── consumer │ ├── KafkaConsumerRebalancerListener.java │ ├── KafkaSubscribeConsumeThread.java │ ├── KafkaSubscribeConsumer.java │ ├── KafkaSubscribeConsumerManager.java │ ├── NewIDataLineProcessor.java │ ├── dao │ │ └── KafkaConsumerOffset.java │ ├── exception │ │ ├── ExceptionHandler.java │ │ └── TaKafkaCommonException.java │ ├── offset │ │ ├── MysqlOffsetManager.java │ │ └── OffsetManager.java │ └── persist │ │ ├── DBPoolConnection.java │ │ ├── DefaultStorePersist.java │ │ ├── MysqlOffsetPersist.java │ │ ├── OffsetPersist.java │ │ ├── RedisStorePersistService.java │ │ └── StorePersist.java │ ├── test │ ├── ProcessDataThread.java │ └── TestMain.java │ └── util │ ├── CommonUtils.java │ └── RetryerUtil.java └── resources └── log4j.properties /.gitignore: -------------------------------------------------------------------------------- 1 | ################### 2 | ## Folders ## 3 | ############################## 4 | .settings 5 | target 6 | .idea/ 7 | 8 | ############################## 9 | ## Eclipse conf ## 10 | ############################## 11 | .classpath 12 | .project -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kafka-common 2 | 3 | This is the code for the exactly-once consumer persists the offsets in mysql for kafka 0.10.0. 4 | 5 | You can create a table in mysql to store the offsets in mysql. 6 | 7 | Example for the offset table: 8 | 9 | CREATE TABLE `kafka_consumer_offset` ( 10 | `oid` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', 11 | `topic` varchar(100) NOT NULL COMMENT 'kafka主题', 12 | `kafka_partition` int(11) NOT NULL COMMENT 'kafka主题下的leader分区号', 13 | `consumer_group` varchar(100) NOT NULL COMMENT '消费组', 14 | `offset` bigint(100) NOT NULL DEFAULT '0' COMMENT '偏移量', 15 | `last_flush_offset` bigint(100) NOT NULL DEFAULT '0' COMMENT '上一次的偏移量', 16 | `kafka_cluster_name` varchar(100) NOT NULL DEFAULT '' COMMENT 'kafka集群唯一标识键', 17 | `owner` varchar(255) NOT NULL DEFAULT '' COMMENT 'kafka消费者名称', 18 | `update_time` timestamp NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '更新时间', 19 | `create_time` timestamp NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '入库时间', 20 | PRIMARY KEY (`oid`), 21 | UNIQUE KEY `topic_partition_consumer` (`kafka_cluster_name`,`topic`,`kafka_partition`,`consumer_group`) USING BTREE 22 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 23 | 24 | 25 | If you have any problems or you find any bugs, please do not hesitate to contact me(yangruochen@thinkingdata.cn). 26 | 27 | ## maven central repo: 28 | ```xml 29 | 30 | cn.thinkingdata 31 | kafka-common 32 | 0.3.1 33 | 34 | ``` 35 | ## 2021/03/22 36 | version 0.3.1 fix bug 37 | 38 | ## 2021/03/19 39 | version 0.3.0 reform code and public to central repo 40 | 41 | ## 2020/11/06 42 | version 0.2.1 fix bug, replace stop(timeout) to stopWithException, optimize the time complexity 43 | 44 | ## 2020/03/19 45 | version 0.2.0 fix bug, change KafkaSubscribeConsumer constructor, kafka-client change to 2.3.0 46 | 47 | ## 2019/08/21 48 | version 0.1.3 change name to kafka-common 49 | 50 | ## 2019/08/21 51 | version 0.1.2 change new interface 52 | 53 | ## 2019/07/24 54 | version 0.1.1 fix bug 55 | 56 | ## 2019/07/19 57 | version 0.1.0 this version is used in our product (thinking analytics, see: https://www.thinkingdata.cn/manual.html), and it is served for more than 50 companies. 58 | 59 | ## 2018/09/05 60 | version 0.0.2 fix the problems when the kafka coordinate dead 61 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | cn.thinkingdata 5 | kafka-common 6 | 0.4.0 7 | thinkingdata-kafka-common 8 | kafka-common 9 | http://www.thinkinggame.cn 10 | 11 | org.sonatype.oss 12 | oss-parent 13 | 7 14 | 15 | 16 | 17 | The Apache Software License, Version 2.0 18 | http://www.apache.org/licenses/LICENSE-2.0.txt 19 | 20 | 21 | 22 | 23 | https://github.com/ThinkingDataAnalytics/kafka-common 24 | https://github.com/ThinkingDataAnalytics/kafka-common 25 | https://github.com/ThinkingDataAnalytics/kafka-common 26 | 27 | 28 | 29 | thinkingdata 30 | yangruochen@thinkingdata.cn 31 | http://www.thinkinggame.cn 32 | 33 | 34 | 35 | 36 | UTF-8 37 | 38 | 39 | 40 | 41 | commons-codec 42 | commons-codec 43 | 1.10 44 | 45 | 46 | commons-logging 47 | commons-logging 48 | 1.2 49 | 50 | 51 | org.slf4j 52 | slf4j-api 53 | 1.6.1 54 | jar 55 | compile 56 | 57 | 58 | org.slf4j 59 | slf4j-log4j12 60 | 1.6.1 61 | jar 62 | compile 63 | 64 | 65 | org.apache.commons 66 | commons-lang3 67 | 3.4 68 | 69 | 70 | com.google.guava 71 | guava 72 | 18.0 73 | 74 | 75 | org.apache.kafka 76 | kafka-clients 77 | 2.3.0 78 | 79 | 80 | org.apache.kafka 81 | kafka_2.12 82 | 2.3.0 83 | 84 | 85 | com.alibaba 86 | fastjson 87 | 1.2.71 88 | 89 | 90 | mysql 91 | mysql-connector-java 92 | 5.1.46 93 | 94 | 95 | com.github.rholder 96 | guava-retrying 97 | 2.0.0 98 | 99 | 100 | com.google.guava 101 | guava 102 | 103 | 104 | 105 | 106 | joda-time 107 | joda-time 108 | 2.9.9 109 | 110 | 111 | 112 | com.alibaba 113 | druid 114 | 1.1.14 115 | 116 | 117 | commons-collections 118 | commons-collections 119 | 3.2.2 120 | 121 | 122 | 123 | 124 | 125 | kafka-mysql-offset 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-compiler-plugin 130 | 3.5.1 131 | 132 | 1.8 133 | 1.8 134 | UTF-8 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-javadoc-plugin 140 | 2.10.4 141 | 142 | true 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | ossrh 173 | 174 | 175 | releases 176 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 177 | 178 | 179 | snapshots 180 | https://oss.sonatype.org/content/repositories/snapshots/ 181 | 182 | 183 | 184 | 185 | compile 186 | 187 | 188 | 189 | org.apache.maven.plugins 190 | maven-source-plugin 191 | 3.0.1 192 | 193 | 194 | package 195 | 196 | jar-no-fork 197 | 198 | 199 | 200 | 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-javadoc-plugin 205 | 2.10.4 206 | 207 | 208 | package 209 | 210 | jar 211 | 212 | 213 | 214 | 215 | 216 | 217 | org.apache.maven.plugins 218 | maven-gpg-plugin 219 | 1.6 220 | 221 | 222 | sign-artifacts 223 | verify 224 | 225 | sign 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/cache/KafkaCache.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.cache; 2 | 3 | import cn.thinkingdata.kafka.consumer.KafkaConsumerRebalancerListener; 4 | import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumeThread; 5 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 6 | import org.apache.kafka.common.TopicPartition; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.CopyOnWriteArrayList; 12 | 13 | public class KafkaCache { 14 | 15 | public static Map kafkaConsumerOffsetMaps = new ConcurrentHashMap(); 16 | public static List consumeThreadList = new CopyOnWriteArrayList(); 17 | public static List rebalancerListenerList = new CopyOnWriteArrayList(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/close/DaemonCloseThread.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.close; 2 | 3 | import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumer; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | public class DaemonCloseThread extends Thread { 8 | 9 | private static final Logger logger = LoggerFactory 10 | .getLogger(DaemonCloseThread.class); 11 | 12 | KafkaSubscribeConsumer consumers; 13 | 14 | TermMethod closeMethod; 15 | 16 | Boolean flag = true; 17 | 18 | public DaemonCloseThread(KafkaSubscribeConsumer consumers, TermMethod closeMethod) { 19 | this.consumers = consumers; 20 | this.closeMethod = closeMethod; 21 | } 22 | 23 | public static void destroy(KafkaSubscribeConsumer consumers) { 24 | consumers.destroy(); 25 | } 26 | 27 | @Override 28 | public void run() { 29 | close(); 30 | } 31 | 32 | public void shutdown() { 33 | flag = false; 34 | } 35 | 36 | public void afterDestroyConsumer() { 37 | closeMethod.afterDestroyConsumer(); 38 | } 39 | 40 | 41 | private void close() { 42 | logger.info("start DaemonCloseThread!"); 43 | while (flag) { 44 | Boolean receiveTermSignal = closeMethod.receiveTermSignal(); 45 | if (receiveTermSignal) { 46 | logger.info("start to destroy consumers"); 47 | destroy(consumers); 48 | break; 49 | } else { 50 | try { 51 | Thread.sleep(50); 52 | } catch (InterruptedException e) { 53 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 54 | } 55 | } 56 | } 57 | logger.info("DaemonCloseThread stop!"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/close/ScanTermMethod.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.close; 2 | 3 | import java.util.Scanner; 4 | 5 | public class ScanTermMethod implements TermMethod { 6 | 7 | Scanner scan = new Scanner(System.in); 8 | 9 | @Override 10 | public Boolean receiveTermSignal() { 11 | System.out.println("Stop it now?(yes/no):"); 12 | String result = scan.nextLine(); 13 | return result.equals("yes"); 14 | 15 | } 16 | 17 | @Override 18 | public void afterDestroyConsumer() { 19 | scan.close(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/close/SignalTermMethod.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.close; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import sun.misc.Signal; 6 | import sun.misc.SignalHandler; 7 | 8 | public class SignalTermMethod implements TermMethod, SignalHandler { 9 | private static final Logger logger = LoggerFactory.getLogger(SignalTermMethod.class); 10 | 11 | Signal termSignal = new Signal("TERM"); 12 | 13 | public Boolean termSignalFlag = false; 14 | 15 | public SignalTermMethod() { 16 | init(); 17 | } 18 | 19 | public void init() { 20 | Signal.handle(termSignal, this); 21 | } 22 | 23 | @Override 24 | public Boolean receiveTermSignal() { 25 | return termSignalFlag; 26 | } 27 | 28 | @Override 29 | public void afterDestroyConsumer() { 30 | } 31 | 32 | @Override 33 | public void handle(Signal signal) { 34 | if (signal.getName().equals("TERM")) { 35 | logger.info("signal term received"); 36 | termSignalFlag = true; 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/close/TermMethod.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.close; 2 | 3 | public interface TermMethod { 4 | 5 | Boolean receiveTermSignal(); 6 | 7 | void afterDestroyConsumer(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/constant/KafkaMysqlOffsetParameter.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.constant; 2 | 3 | import cn.thinkingdata.kafka.util.CommonUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.Map; 9 | import java.util.Properties; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | public class KafkaMysqlOffsetParameter { 13 | 14 | private static final Logger logger = LoggerFactory 15 | .getLogger(KafkaMysqlOffsetParameter.class); 16 | 17 | public static final AtomicBoolean kafkaSubscribeConsumerClosed = new AtomicBoolean(true); 18 | public static final AtomicBoolean mysqlAndBackupStoreConnState = new AtomicBoolean(true); 19 | public static final String hostname = CommonUtils.getHostName(); 20 | 21 | public static String jdbcUrl; 22 | public static String username; 23 | public static String password; 24 | public static String tableName; 25 | public static String brokerList; 26 | public static String kafkaClusterName; 27 | public static String topic; 28 | public static String consumerGroup; 29 | public static Integer processThreadNum; 30 | public static Integer flushOffsetSize; 31 | public static Integer flushInterval; 32 | private static String maxPartitionFetchBytes = "524288"; 33 | private static String heartbeatInterval = "10000"; 34 | public static String sessionTimeout = "30000"; 35 | private static String requestTimeout = "40000"; 36 | public static String autoOffsetReset = "latest"; 37 | public static Integer pollInterval = 50; 38 | public static Integer maxPollRecords = 1000; 39 | public static String partitionAssignmentStrategy; 40 | public static Properties kafkaConf; 41 | public static Properties kafkaConfExtraProperties = new Properties(); 42 | 43 | // public static void setMaxPartitionFetchBytes(Long maxPartitionFetchBytes) 44 | // { 45 | // KafkaMysqlOffsetParameter.maxPartitionFetchBytes = 46 | // maxPartitionFetchBytes.toString(); 47 | // } 48 | 49 | public static void createKafkaConfProp() { 50 | kafkaConf = new Properties(); 51 | kafkaConf.put("bootstrap.servers", KafkaMysqlOffsetParameter.brokerList); 52 | kafkaConf.put("group.id", KafkaMysqlOffsetParameter.consumerGroup); 53 | // Below is a key setting to turn off the auto commit. 54 | kafkaConf.put("enable.auto.commit", "false"); 55 | kafkaConf.put("heartbeat.interval.ms", KafkaMysqlOffsetParameter.heartbeatInterval); 56 | kafkaConf.put("session.timeout.ms", KafkaMysqlOffsetParameter.sessionTimeout); 57 | kafkaConf.put("max.poll.records", maxPollRecords); 58 | kafkaConf.put("request.timeout.ms", KafkaMysqlOffsetParameter.requestTimeout); 59 | // Control maximum data on each poll, make sure this value is bigger 60 | // than the maximum single record size 61 | kafkaConf.put("max.partition.fetch.bytes", KafkaMysqlOffsetParameter.maxPartitionFetchBytes); 62 | kafkaConf.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); 63 | kafkaConf.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); 64 | kafkaConf.put("auto.offset.reset", KafkaMysqlOffsetParameter.autoOffsetReset); 65 | if (StringUtils.isNotBlank(partitionAssignmentStrategy)) { 66 | kafkaConf.put("partition.assignment.strategy", KafkaMysqlOffsetParameter.partitionAssignmentStrategy); 67 | } 68 | kafkaConf.putAll(kafkaConfExtraProperties); 69 | } 70 | 71 | public static void createKafkaConfProp(Map prop) { 72 | kafkaConfExtraProperties.putAll(prop); 73 | jdbcUrl = prop.get("jdbc.url"); 74 | kafkaConfExtraProperties.remove("jdbc.url"); 75 | username = prop.get("username"); 76 | kafkaConfExtraProperties.remove("username"); 77 | password = prop.get("password"); 78 | kafkaConfExtraProperties.remove("password"); 79 | tableName = prop.get("table.name"); 80 | kafkaConfExtraProperties.remove("table.name"); 81 | brokerList = prop.get("broker.list"); 82 | kafkaConfExtraProperties.remove("broker.list"); 83 | kafkaClusterName = prop.get("kafka.cluster.name"); 84 | kafkaConfExtraProperties.remove("kafka.cluster.name"); 85 | topic = prop.get("topic"); 86 | kafkaConfExtraProperties.remove("topic"); 87 | consumerGroup = prop.get("consumer.group"); 88 | kafkaConfExtraProperties.remove("consumer.group"); 89 | processThreadNum = Integer.parseInt(prop.get("process.thread.num")); 90 | kafkaConfExtraProperties.remove("process.thread.num"); 91 | flushOffsetSize = Integer.parseInt(prop.get("flush.offset.size")); 92 | kafkaConfExtraProperties.remove("flush.offset.size"); 93 | flushInterval = Integer.parseInt(prop.get("flush.interval")); 94 | kafkaConfExtraProperties.remove("flush.interval"); 95 | 96 | assert null != jdbcUrl; 97 | assert null != username; 98 | assert null != password; 99 | assert null != tableName; 100 | assert null != brokerList; 101 | assert null != kafkaClusterName; 102 | assert null != topic; 103 | assert null != consumerGroup; 104 | assert null != processThreadNum; 105 | assert null != flushOffsetSize; 106 | assert null != flushInterval; 107 | 108 | 109 | if (prop.get("heartbeat.interval") != null) { 110 | heartbeatInterval = String.valueOf(Integer.parseInt(prop.get("heartbeat.interval")) * 1000); 111 | kafkaConfExtraProperties.remove("heartbeat.interval"); 112 | } 113 | if (prop.get("session.timeout") != null) { 114 | sessionTimeout = String.valueOf(Integer.parseInt(prop.get("session.timeout")) * 1000); 115 | kafkaConfExtraProperties.remove("session.timeout"); 116 | } 117 | if (prop.get("request.timeout") != null) { 118 | requestTimeout = String.valueOf(Integer.parseInt(prop.get("request.timeout")) * 1000); 119 | kafkaConfExtraProperties.remove("request.timeout"); 120 | } 121 | if (prop.get("max.partition.fetch.bytes") != null) { 122 | maxPartitionFetchBytes = prop.get("max.partition.fetch.bytes"); 123 | kafkaConfExtraProperties.remove("max.partition.fetch.bytes"); 124 | } 125 | if (prop.get("auto.offset.reset") != null) { 126 | autoOffsetReset = prop.get("auto.offset.reset"); 127 | assert (autoOffsetReset.equals("latest") || autoOffsetReset.equals("earliest") || autoOffsetReset.equals("none")); 128 | kafkaConfExtraProperties.remove("auto.offset.reset"); 129 | } 130 | if (prop.get("poll.interval") != null) { 131 | pollInterval = Integer.parseInt(prop.get("poll.interval")); 132 | kafkaConfExtraProperties.remove("poll.interval"); 133 | } 134 | if (prop.get("max.poll.records") != null) { 135 | maxPollRecords = Integer.parseInt(prop.get("max.poll.records")); 136 | kafkaConfExtraProperties.remove("max.poll.records"); 137 | } 138 | if (prop.get("partition.assignment.strategy") != null) { 139 | partitionAssignmentStrategy = prop.get("partition.assignment.strategy"); 140 | kafkaConfExtraProperties.remove("partition.assignment.strategy"); 141 | } 142 | createKafkaConfProp(); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/KafkaConsumerRebalancerListener.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer; 2 | 3 | import cn.thinkingdata.kafka.cache.KafkaCache; 4 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 5 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 6 | import cn.thinkingdata.kafka.consumer.offset.MysqlOffsetManager; 7 | import cn.thinkingdata.kafka.consumer.offset.OffsetManager; 8 | import cn.thinkingdata.kafka.consumer.persist.MysqlOffsetPersist; 9 | import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; 10 | import org.apache.kafka.clients.consumer.KafkaConsumer; 11 | import org.apache.kafka.common.TopicPartition; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.Collection; 16 | import java.util.Date; 17 | 18 | /** 19 | * Re-balancer for any subscription changes. 20 | */ 21 | public class KafkaConsumerRebalancerListener implements ConsumerRebalanceListener { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(KafkaConsumerRebalancerListener.class); 24 | 25 | private final OffsetManager offsetManager = MysqlOffsetManager.getInstance(); 26 | 27 | private final KafkaConsumer consumer; 28 | 29 | public KafkaConsumerRebalancerListener(KafkaConsumer consumer) { 30 | this.consumer = consumer; 31 | } 32 | 33 | 34 | // onPartitionsRevoked 这个方法会在 Consumer 停止拉取数据之后、group 进行 rebalance 35 | // 操作之前调用,作用是对已经 ack 的 msg 进行 commit; 36 | @Override 37 | public void onPartitionsRevoked(Collection partitions) { 38 | logger.info("start onPartitionsRevoked!"); 39 | for (TopicPartition partition : partitions) { 40 | KafkaConsumerOffset kafkaConsumerOffset = KafkaCache.kafkaConsumerOffsetMaps.get(partition); 41 | if (kafkaConsumerOffset != null) { 42 | kafkaConsumerOffset.setOffset(consumer.position(partition)); 43 | KafkaCache.kafkaConsumerOffsetMaps.put(partition, kafkaConsumerOffset); 44 | MysqlOffsetPersist.getInstance().flush(kafkaConsumerOffset); 45 | //删除kafkaConsumerOffsetSet里的kafkaConsumerOffset 46 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 47 | if (consumeThread.consumer.equals(consumer)) { 48 | logger.debug("consumeThread.kafkaConsumerOffsetSet remove kafkaConsumerOffset, the kafkaConsumerOffset is " + kafkaConsumerOffset); 49 | consumeThread.kafkaConsumerOffsetSet.remove(kafkaConsumerOffset); 50 | } 51 | } 52 | } 53 | } 54 | //有特殊的情况,就是两个线程同时拥有一个partition,这时,需要手动清空kafkaConsumerOffsetSet 55 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 56 | if (consumeThread.consumer.equals(consumer)) { 57 | consumeThread.kafkaConsumerOffsetSet.clear(); 58 | if (consumeThread.assignedPartitions != null) { 59 | consumeThread.assignedPartitions = null; 60 | } 61 | } 62 | } 63 | logger.info("finish onPartitionsRevoked!"); 64 | } 65 | 66 | // onPartitionsAssigned 这个方法 group 已经进行 reassignment 67 | // 之后,开始拉取数据之前调用,作用是清理内存中不属于这个线程的 msg、获取 partition 的 last committed offset。 68 | @Override 69 | public void onPartitionsAssigned(Collection partitions) { 70 | logger.info("start onPartitionsAssigned!"); 71 | Date now = new Date(); 72 | for (TopicPartition partition : partitions) { 73 | consumer.seek(partition, offsetManager.readOffsetFromCache(partition.topic(), partition.partition()).getOffset()); 74 | TopicPartition topicPartition = new TopicPartition(partition.topic(), partition.partition()); 75 | KafkaConsumerOffset kafkaConsumerOffset = KafkaCache.kafkaConsumerOffsetMaps.get(topicPartition); 76 | // 设定owner 77 | kafkaConsumerOffset.setOwner(KafkaMysqlOffsetParameter.kafkaClusterName 78 | + "-" 79 | + partition.topic() 80 | + "-" 81 | + partition.partition() 82 | + "-" 83 | + KafkaMysqlOffsetParameter.consumerGroup 84 | + "-" 85 | + now.getTime() 86 | + "-" 87 | + KafkaMysqlOffsetParameter.hostname 88 | + "-" 89 | + consumer.toString().substring( 90 | consumer.toString().lastIndexOf("@") + 1)); 91 | MysqlOffsetPersist.getInstance().updateOwner(kafkaConsumerOffset); 92 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 93 | if (consumeThread.consumer.equals(consumer)) { 94 | consumeThread.assignedPartitions = partitions; 95 | } 96 | } 97 | } 98 | logger.info("finish onPartitionsAssigned!"); 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/KafkaSubscribeConsumeThread.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer; 2 | 3 | import cn.thinkingdata.kafka.cache.KafkaCache; 4 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 5 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 6 | import cn.thinkingdata.kafka.consumer.offset.MysqlOffsetManager; 7 | import cn.thinkingdata.kafka.consumer.offset.OffsetManager; 8 | import cn.thinkingdata.kafka.consumer.persist.MysqlOffsetPersist; 9 | import cn.thinkingdata.kafka.util.CommonUtils; 10 | import org.apache.commons.collections.CollectionUtils; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.apache.kafka.clients.consumer.ConsumerRecord; 13 | import org.apache.kafka.clients.consumer.ConsumerRecords; 14 | import org.apache.kafka.clients.consumer.KafkaConsumer; 15 | import org.apache.kafka.clients.consumer.OffsetOutOfRangeException; 16 | import org.apache.kafka.common.TopicPartition; 17 | import org.apache.kafka.common.errors.WakeupException; 18 | import org.joda.time.DateTime; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.util.Arrays; 23 | import java.util.Collection; 24 | import java.util.Date; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Set; 28 | import java.util.concurrent.BlockingQueue; 29 | import java.util.concurrent.CountDownLatch; 30 | import java.util.concurrent.CyclicBarrier; 31 | import java.util.concurrent.LinkedBlockingQueue; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.stream.Collectors; 34 | 35 | public class KafkaSubscribeConsumeThread implements Runnable { 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(KafkaSubscribeConsumeThread.class); 38 | private final NewIDataLineProcessor dataProcessor; 39 | public KafkaConsumer consumer; 40 | private final OffsetManager offsetManager = MysqlOffsetManager.getInstance(); 41 | public volatile Boolean kafkaPollFlag = false; 42 | public volatile Boolean kafkaConsumerFlag = false; 43 | // public volatile Boolean offsetFlushFlag = false; 44 | private final CyclicBarrier offsetFlushBarrier; 45 | public volatile Collection assignedPartitions = null; 46 | private final BlockingQueue> unsent = new LinkedBlockingQueue<>(); 47 | public Set kafkaConsumerOffsetSet = new HashSet(); 48 | // records的初始值是1000,所以这边capacity的值设为3000 49 | private final BlockingQueue> processDataQueue = 50 | new LinkedBlockingQueue<>(3000); 51 | private volatile Thread consumerThread; 52 | public ProcessDataWorker processDataWorker = new ProcessDataWorker(); 53 | 54 | 55 | /** 56 | * The consumer is currently paused due to a slow execution data. The consumer will be 57 | * resumed when the current batch of records has been processed but will continue 58 | * to be polled. 59 | */ 60 | private volatile Boolean paused = false; 61 | 62 | public KafkaSubscribeConsumeThread(KafkaConsumer consumer, NewIDataLineProcessor dataProcessor, CyclicBarrier offsetFlushBarrier) { 63 | this.consumer = consumer; 64 | this.dataProcessor = dataProcessor; 65 | this.offsetFlushBarrier = offsetFlushBarrier; 66 | } 67 | 68 | @Override 69 | public void run() { 70 | consumerThread = Thread.currentThread(); 71 | kafkaConsumerFlag = true; 72 | //启动processDataWorker 73 | new Thread(processDataWorker, consumerThread.getName() + "-" + "working thread").start(); 74 | Set> lastConsumerRecordSet = new HashSet>(); 75 | Long count = 0L; 76 | DateTime sessionTimeoutDataTime = new DateTime().plusSeconds(Integer.parseInt(KafkaMysqlOffsetParameter.sessionTimeout)); 77 | try { 78 | while (!KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.get()) { 79 | if (KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.get()) { 80 | kafkaPollFlag = true; 81 | count = 0L; 82 | // 如果执行dataExecute的时间超过了sessionTimeout 83 | if (sessionTimeoutDataTime.isAfterNow()) { 84 | // 如果有新的consumer,则调用rebalance,并阻塞线程 85 | ConsumerRecords records = null; 86 | try { 87 | records = consumer.poll(KafkaMysqlOffsetParameter.pollInterval); 88 | } catch (OffsetOutOfRangeException e) { 89 | logger.error("consumer poll out of range, the error is " + CommonUtils.getStackTraceAsString(e)); 90 | for (TopicPartition topicPartition : consumer.assignment()) { 91 | logger.error("the topicPartition is " + topicPartition.toString() + ",the offset is " + consumer.position(topicPartition)); 92 | } 93 | synchronized (OffsetManager.class) { 94 | MysqlOffsetManager.getInstance().getExternalStorePersist().executeWhenOffsetReset(this); 95 | } 96 | } 97 | // 计算开始时间 98 | sessionTimeoutDataTime = new DateTime().plusSeconds(Integer.parseInt(KafkaMysqlOffsetParameter.sessionTimeout) / 1000); 99 | logger.debug("sessionTimeoutDataTime is " + sessionTimeoutDataTime.toString()); 100 | if (records != null) { 101 | if (records.count() > 0) { 102 | logger.debug("poll records size: " + records.count() 103 | + ", partition is " + records.partitions() 104 | + ", thread is " 105 | + Thread.currentThread().getName()); 106 | } 107 | //先暂停 108 | pause(); 109 | //放到队列 110 | sendToQueue(records); 111 | //恢复 112 | if (isResume()) { 113 | resume(); 114 | } else { 115 | // 休息30豪秒 116 | Thread.sleep(30); 117 | } 118 | if (CollectionUtils.isNotEmpty(assignedPartitions)) { 119 | for (TopicPartition assignedPartition : assignedPartitions) { 120 | List> recordsListPerPartition = records.records(assignedPartition); 121 | if (CollectionUtils.isNotEmpty(recordsListPerPartition)) { 122 | ConsumerRecord lastRecord = recordsListPerPartition.get(recordsListPerPartition.size() - 1); 123 | lastConsumerRecordSet.add(lastRecord); 124 | } 125 | } 126 | } 127 | // 更新offset 128 | saveLastConsumerRecordSet(this, lastConsumerRecordSet, count, false); 129 | lastConsumerRecordSet.clear(); 130 | } 131 | } else { 132 | // sessionTimeOut了,进行异常处理 133 | logger.info("kafka session time out, the consumer is " + consumer.toString()); 134 | MysqlOffsetManager.getInstance() 135 | .getExternalStorePersist() 136 | .executeWhenExecuteDataSessionTimeout(this); 137 | break; 138 | } 139 | } else { 140 | logger.info("mysql and backup store connect error, the mysqlAndBackupStoreConnState is " + KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.get()); 141 | kafkaPollFlag = false; 142 | try { 143 | Thread.sleep(50); 144 | } catch (InterruptedException e) { 145 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 146 | } 147 | } 148 | } 149 | kafkaPollFlag = false; 150 | logger.info("kafka consumer close, the kafkaSubscribeConsumerClosed is " 151 | + KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.get()); 152 | } catch (WakeupException | InterruptedException e) { 153 | // 外部thread中断kafka的poll操作 154 | logger.info("stop consumer with wakeup or interupted, the kafkaSubscribeConsumerClosed is " 155 | + KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.get() 156 | + ", the thread is " 157 | + Thread.currentThread().getName() 158 | + ", the consumeThreadList is " 159 | + StringUtils.join(KafkaCache.consumeThreadList.stream().map(o -> o.consumerThread.getName()).collect(Collectors.toList()), ",") 160 | + " the kafka cluster name is " 161 | + KafkaMysqlOffsetParameter.kafkaClusterName 162 | + ", the Exception is " + CommonUtils.getStackTraceAsString(e)); 163 | // 更新offset 164 | saveLastConsumerRecordSet(this, lastConsumerRecordSet, count, true); 165 | kafkaPollFlag = false; 166 | logger.info("stop consumer with wakeup finished"); 167 | } catch (Exception e) { 168 | // 更新offset 169 | saveLastConsumerRecordSet(this, lastConsumerRecordSet, count, false); 170 | logger.error("stop consumer with exception, the kafkaSubscribeConsumerClosed is " 171 | + KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.get() 172 | + ", the thread is " 173 | + Thread.currentThread().getName() 174 | + ", the consumeThreadList is " 175 | + StringUtils.join(KafkaCache.consumeThreadList.stream().map(o -> o.consumerThread.getName()).collect(Collectors.toList()), ",") 176 | + " the kafka cluster name is " 177 | + KafkaMysqlOffsetParameter.kafkaClusterName 178 | + ", the kafkaConsumerOffsetMaps In cache is" + Arrays.toString(KafkaCache.kafkaConsumerOffsetMaps.entrySet().toArray()) + ", the Exception is " + CommonUtils.getStackTraceAsString(e)); 179 | kafkaPollFlag = false; 180 | logger.info("stop consumer finished"); 181 | synchronized (OffsetManager.class) { 182 | MysqlOffsetManager.getInstance().getExternalStorePersist().executeWhenException(); 183 | } 184 | } finally { 185 | try { 186 | closeKafkaSubscribeConsumeThread(); 187 | } catch (Exception e) { 188 | logger.error("closeKafkaSubscribeConsumeThread error, the thread is " + Thread.currentThread().getName() + ", the Exception is " + CommonUtils.getStackTraceAsString(e)); 189 | } 190 | } 191 | } 192 | 193 | private void saveLastConsumerRecordSet(KafkaSubscribeConsumeThread consumeThread, Set> lastConsumerRecordSet, Long count, Boolean cleanOwner) { 194 | for (ConsumerRecord lastConsumerRecord : lastConsumerRecordSet) { 195 | Date now = new Date(); 196 | TopicPartition topicPartition = new TopicPartition(lastConsumerRecord.topic(), lastConsumerRecord.partition()); 197 | KafkaConsumerOffset kafkaConsumerOffset = KafkaCache.kafkaConsumerOffsetMaps.get(topicPartition); 198 | if (kafkaConsumerOffset == null) { 199 | logger.error("kafkaConsumerOffset is null in cache, the lastConsumerRecord is " 200 | + lastConsumerRecord 201 | + ", the kafkaConsumerOffsetMaps is " 202 | + Arrays.toString(KafkaCache.kafkaConsumerOffsetMaps.entrySet().toArray())); 203 | kafkaConsumerOffset = offsetManager.readOffsetFromCache( 204 | lastConsumerRecord.topic(), 205 | lastConsumerRecord.partition()); 206 | // 设定owner 207 | kafkaConsumerOffset 208 | .setOwner(KafkaMysqlOffsetParameter.kafkaClusterName 209 | + "-" 210 | + lastConsumerRecord.topic() 211 | + "-" 212 | + lastConsumerRecord.partition() 213 | + "-" 214 | + KafkaMysqlOffsetParameter.consumerGroup 215 | + "-" 216 | + now.getTime() 217 | + "-" 218 | + KafkaMysqlOffsetParameter.hostname 219 | + "-" 220 | + consumer.toString().substring( 221 | consumer.toString().lastIndexOf("@") + 1)); 222 | } 223 | kafkaConsumerOffset.setTopic(lastConsumerRecord.topic()); 224 | kafkaConsumerOffset.setPartition(lastConsumerRecord.partition()); 225 | kafkaConsumerOffset.setConsumer_group(KafkaMysqlOffsetParameter.consumerGroup); 226 | kafkaConsumerOffset.setOffset(lastConsumerRecord.offset() + 1L); 227 | kafkaConsumerOffset.setKafka_cluster_name(KafkaMysqlOffsetParameter.kafkaClusterName); 228 | kafkaConsumerOffset.setCount(count); 229 | if (cleanOwner) { 230 | //退出的时候清除Owner 231 | kafkaConsumerOffset.setOwner(""); 232 | logger.info("clean owner, the thread is " + Thread.currentThread().getName() + ", the kafkaConsumerOffset is " + kafkaConsumerOffset.toString()); 233 | } 234 | kafkaConsumerOffsetSet.add(kafkaConsumerOffset); 235 | offsetManager.saveOffsetInCache(consumeThread, kafkaConsumerOffset); 236 | } 237 | } 238 | 239 | public void closeKafkaSubscribeConsumeThread() throws InterruptedException { 240 | logger.info("start to stop processDataWorker " + processDataWorker.executingThread.getName()); 241 | processDataWorker.stop(); 242 | logger.info("wait for the mysql persist finish"); 243 | // 等待MysqlOffsetPersist的persist动作完成 244 | for (; ; ) { 245 | if (!MysqlOffsetPersist.runFlag && KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.get()) { 246 | break; 247 | } else { 248 | try { 249 | Thread.sleep(10); 250 | } catch (InterruptedException e) { 251 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 252 | } 253 | } 254 | } 255 | logger.info("flush before kafka consumer close"); 256 | logger.debug("kafkaConsumerOffsetMaps is " 257 | + Arrays.toString(KafkaCache.kafkaConsumerOffsetMaps.entrySet().toArray())); 258 | logger.debug("kafkaConsumerOffsetSet is " + kafkaConsumerOffsetSet + " ,the thread is " + Thread.currentThread().getName()); 259 | try { 260 | for (KafkaConsumerOffset kafkaConsumerOffset : kafkaConsumerOffsetSet) { 261 | TopicPartition topicPartition = new TopicPartition(kafkaConsumerOffset.getTopic(), kafkaConsumerOffset.getPartition()); 262 | KafkaConsumerOffset kafkaConsumerOffsetInCache = KafkaCache.kafkaConsumerOffsetMaps.get(topicPartition); 263 | // 因为有Marking the coordinator 264 | // dead的情况,所以可能kafkaConsumerOffsetSet里有该partition, 265 | // 而另一个线程的kafkaConsumerOffsetSet也有该partition,前一个已经在KafkaCache.kafkaConsumerOffsets中remove了, 266 | // 所以有可能查出来是null 267 | if (kafkaConsumerOffsetInCache != null) { 268 | // 因为有可能mysql里的kafka_consumer_offset为空,consumer拿lastest,这时候的offset不是0,是lastest,是需要保存的 269 | Long consumerPosition = null; 270 | try { 271 | consumerPosition = consumer.position(topicPartition); 272 | } catch (Exception e) { 273 | logger.info("the consumer get position error, the error is " 274 | + e.toString() 275 | + ", the topicPartition is " 276 | + topicPartition); 277 | } 278 | logger.debug("consumer position is " + consumerPosition); 279 | if (consumerPosition != null 280 | && consumerPosition != 0L 281 | && kafkaConsumerOffsetInCache != null 282 | && consumerPosition > kafkaConsumerOffsetInCache.getOffset()) { 283 | logger.info("consumer position " 284 | + consumerPosition 285 | + "is bigger than the offset in kafkaConsumerOffsetInCache " 286 | + kafkaConsumerOffsetInCache); 287 | kafkaConsumerOffsetInCache.setOffset(consumerPosition); 288 | } 289 | MysqlOffsetPersist.getInstance().flush( 290 | kafkaConsumerOffsetInCache); 291 | } else { 292 | logger.error("kafkaConsumerOffsetInCache is null, kafkaConsumerOffset is " 293 | + kafkaConsumerOffset 294 | + ", kafkaConsumerOffsetSet is " 295 | + kafkaConsumerOffsetSet 296 | + ", kafkaConsumerOffsetMaps is " 297 | + Arrays.toString(KafkaCache.kafkaConsumerOffsetMaps.entrySet().toArray())); 298 | } 299 | } 300 | offsetFlushBarrier.await(); 301 | logger.info("start to flush the rest KafkaCache.kafkaConsumerOffsetMaps " 302 | + Arrays.toString(KafkaCache.kafkaConsumerOffsetMaps.entrySet().toArray()) 303 | + ", the thread is " 304 | + Thread.currentThread().getName()); 305 | flushKafkaConsumerOffsetsInKafkaCache(); 306 | consumer.close(); 307 | } catch (Exception e) { 308 | logger.error("close consumer error, the exception is " + CommonUtils.getStackTraceAsString(e)); 309 | consumer.close(); 310 | } 311 | kafkaConsumerFlag = false; 312 | sendUnsentToProcessDataQueue(true); 313 | logger.info("kafka consumer finally close"); 314 | } 315 | 316 | private synchronized void flushKafkaConsumerOffsetsInKafkaCache() { 317 | for (KafkaConsumerOffset kafkaConsumerOffset : KafkaCache.kafkaConsumerOffsetMaps.values()) { 318 | logger.info("kafkaConsumerOffset in cache is not be consumed, kafkaConsumerOffset is " 319 | + kafkaConsumerOffset); 320 | // 因为有可能mysql里的kafka_consumer_offset为空,consumer拿lastest,这时候的offset不是0,是lastest,是需要保存的 321 | TopicPartition topicPartition = new TopicPartition( 322 | kafkaConsumerOffset.getTopic(), 323 | kafkaConsumerOffset.getPartition()); 324 | Long consumerPosition = null; 325 | try { 326 | consumerPosition = consumer.position(topicPartition); 327 | } catch (IllegalArgumentException | IllegalStateException e) { 328 | logger.info("flushKafkaConsumerOffsetsInKafkaCache, the consumer get position error, the error is " 329 | + e.toString() 330 | + ", the topicPartition is " 331 | + topicPartition); 332 | } 333 | if (kafkaConsumerOffset != null) { 334 | if (consumerPosition != null && consumerPosition != 0L 335 | && consumerPosition > kafkaConsumerOffset.getOffset()) { 336 | logger.debug("consumer position is " + consumerPosition); 337 | logger.info("consumer position " + consumerPosition 338 | + " is bigger than the offset in kafkaConsumerOffset " 339 | + kafkaConsumerOffset); 340 | kafkaConsumerOffset.setOffset(consumerPosition); 341 | } 342 | MysqlOffsetPersist.getInstance().flush(kafkaConsumerOffset); 343 | } 344 | } 345 | } 346 | 347 | // Shutdown hook which can be called from a separate thread 348 | public void shutdown() { 349 | KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.set(true); 350 | if (consumer != null) { 351 | consumer.wakeup(); 352 | } 353 | } 354 | 355 | public void pause() { 356 | if (this.assignedPartitions != null) { 357 | // avoid group management rebalance due to a slow 358 | // consumer 359 | this.consumer.pause(this.assignedPartitions); 360 | this.paused = true; 361 | } 362 | } 363 | 364 | public Boolean isResume() { 365 | // 如果unsent为空,则恢复消费 366 | return CollectionUtils.isEmpty(unsent); 367 | } 368 | 369 | public void resume() { 370 | if (this.assignedPartitions != null) { 371 | // avoid group management rebalance due to a slow 372 | // consumer 373 | this.consumer.resume(this.assignedPartitions); 374 | this.paused = false; 375 | } 376 | } 377 | 378 | private void sendToQueue(ConsumerRecords records) throws InterruptedException { 379 | Boolean flag = true; 380 | if (CollectionUtils.isEmpty(unsent)) { 381 | for (ConsumerRecord record : records) { 382 | if (flag) { 383 | flag = this.processDataQueue.offer(record, 200, TimeUnit.MILLISECONDS); 384 | //如果没有放入成功说明队列已满 385 | if (!flag) { 386 | logger.info("the processDataQueue is full..."); 387 | unsent.put(record); 388 | } 389 | } else { 390 | unsent.put(record); 391 | } 392 | } 393 | } else { 394 | if (records.count() > 0) { 395 | logger.info("the unsent is not empty but the consummer still polling records, it can be only happed after rebalanced"); 396 | } 397 | for (ConsumerRecord record : records) { 398 | unsent.put(record); 399 | } 400 | try { 401 | Thread.sleep(100); 402 | } catch (InterruptedException e) { 403 | logger.error("------- thread can not sleep ---------------------" 404 | + e.toString()); 405 | } 406 | // 试着将unsent里的records放入processDataQueue 407 | sendUnsentToProcessDataQueue(false); 408 | } 409 | } 410 | 411 | private void sendUnsentToProcessDataQueue(Boolean shutdown) throws InterruptedException { 412 | while (CollectionUtils.isNotEmpty(unsent)) { 413 | //拿出队首元素但不出栈 414 | ConsumerRecord recordInUnsent = unsent.peek(); 415 | if (recordInUnsent != null) { 416 | Boolean flag = this.processDataQueue.offer(recordInUnsent, 200, TimeUnit.MILLISECONDS); 417 | if (!flag) { 418 | //如果没有放入processDataQueue成功说明队列已满 419 | logger.info("the processDataQueue is full... and the unsent is not empty"); 420 | //如果没有停止,则跳出,否则需要将unsent清空才能退出 421 | if (!shutdown) { 422 | break; 423 | } else { 424 | try { 425 | Thread.sleep(10); 426 | } catch (InterruptedException e) { 427 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 428 | } 429 | } 430 | } else { 431 | //如果放入processDataQueue成功则出栈 432 | unsent.poll(); 433 | } 434 | } else { 435 | logger.error("the unsent is not empty, but the recordInUnsent is null!!"); 436 | } 437 | } 438 | } 439 | 440 | //processData线程消费queue 441 | public final class ProcessDataWorker implements Runnable { 442 | 443 | private final CountDownLatch exitLatch = new CountDownLatch(1); 444 | private volatile Thread executingThread; 445 | private volatile Boolean workerStopFlag = false; 446 | public volatile Boolean workingFlag = false; 447 | private volatile ConsumerRecord consumerRecord; 448 | private static final long MAX_WAIT_MS = 1000; 449 | 450 | @Override 451 | public void run() { 452 | workingFlag = true; 453 | try { 454 | this.executingThread = Thread.currentThread(); 455 | while (true) { 456 | processOperationData(); 457 | // 如果queue是空,并且stop为true则退出 458 | if (processDataQueue.size() == 0 && workerStopFlag && unsent.size() == 0) { 459 | break; 460 | } 461 | } 462 | } catch (Exception e) { 463 | logger.error("processDataWorker thread is failed, the error is " + e.toString()); 464 | } finally { 465 | logger.info("processDataWorker " + Thread.currentThread().getName() + " is safely closed..."); 466 | exitLatch.countDown(); 467 | workingFlag = false; 468 | } 469 | } 470 | 471 | private void processOperationData() throws InterruptedException { 472 | // 如果出现除InterruptedException的错误,则必须catch住,要不然,线程会中断! 473 | try { 474 | consumerRecord = processDataQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS); 475 | if (consumerRecord != null) { 476 | dataProcessor.processData(consumerRecord); 477 | } 478 | } catch (InterruptedException e) { 479 | throw e; 480 | } catch (Exception e) { 481 | logger.error("processOperationData error, the error is " + CommonUtils.getStackTraceAsString(e)); 482 | } 483 | } 484 | 485 | public void stopWithException() { 486 | try { 487 | Thread.sleep(1000); 488 | } catch (InterruptedException e) { 489 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 490 | } 491 | workerStopFlag = true; 492 | } 493 | 494 | 495 | private void stop() { 496 | // 等待拉取动作结束 497 | for (; ; ) { 498 | if (kafkaPollFlag == false) { 499 | break; 500 | } else { 501 | try { 502 | Thread.sleep(10); 503 | } catch (InterruptedException e) { 504 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 505 | } 506 | } 507 | } 508 | workerStopFlag = true; 509 | } 510 | } 511 | } 512 | 513 | 514 | 515 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/KafkaSubscribeConsumer.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer; 2 | 3 | import cn.thinkingdata.kafka.cache.KafkaCache; 4 | import cn.thinkingdata.kafka.close.DaemonCloseThread; 5 | import cn.thinkingdata.kafka.close.TermMethod; 6 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 7 | import cn.thinkingdata.kafka.consumer.offset.MysqlOffsetManager; 8 | import cn.thinkingdata.kafka.consumer.persist.MysqlOffsetPersist; 9 | import cn.thinkingdata.kafka.consumer.persist.StorePersist; 10 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 11 | import org.apache.kafka.clients.consumer.KafkaConsumer; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.concurrent.CyclicBarrier; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.ThreadFactory; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | public class KafkaSubscribeConsumer { 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(KafkaSubscribeConsumer.class); 28 | 29 | protected NewIDataLineProcessor dataProcessor; 30 | protected volatile ExecutorService executorService; 31 | private final TermMethod closeMethod; 32 | private volatile DaemonCloseThread closeSignal; 33 | private static volatile Integer startCount = 0; 34 | 35 | public KafkaSubscribeConsumer(Map map, NewIDataLineProcessor dataProcessor, TermMethod closeMethod) { 36 | KafkaMysqlOffsetParameter.createKafkaConfProp(map); 37 | this.dataProcessor = dataProcessor; 38 | this.closeMethod = closeMethod; 39 | } 40 | 41 | public KafkaSubscribeConsumer(Map map, NewIDataLineProcessor dataProcessor, TermMethod closeMethod, StorePersist externalStorePersist){ 42 | this(map, dataProcessor, closeMethod); 43 | MysqlOffsetManager.getInstance().setExternalStorePersist(externalStorePersist); 44 | } 45 | 46 | public void run() { 47 | //判断mysql和redis是否通 48 | Boolean mysqlStateCheck = MysqlOffsetPersist.getInstance().mysqlStateCheckWithRetry(); 49 | if(!mysqlStateCheck){ 50 | logger.info("mysql is not connected!"); 51 | System.exit(-1); 52 | } 53 | Boolean backupStoreStateCheck = MysqlOffsetPersist.getInstance().backupStoreStateCheckWithRetry(); 54 | if(!backupStoreStateCheck){ 55 | logger.info("backup store is not connected!"); 56 | System.exit(-1); 57 | } 58 | KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.set(false); 59 | List topicList = new ArrayList(); 60 | topicList.addAll(Arrays.asList(KafkaMysqlOffsetParameter.topic.split(","))); 61 | ThreadFactory kafkaConsumeThreadNamedThreadFactory = new ThreadFactoryBuilder().setNameFormat("kafka-consume-thread-%d").build(); 62 | executorService = Executors.newFixedThreadPool(KafkaMysqlOffsetParameter.processThreadNum,kafkaConsumeThreadNamedThreadFactory); 63 | CyclicBarrier offsetFlushBarrier = new CyclicBarrier(KafkaMysqlOffsetParameter.processThreadNum); 64 | for (int i = 0; i < KafkaMysqlOffsetParameter.processThreadNum; i++) { 65 | KafkaSubscribeConsumerManager kafkaSubscribeConsumer = KafkaSubscribeConsumerManager.getInstance(); 66 | KafkaConsumer consumer = kafkaSubscribeConsumer.createKafkaConsumer(topicList, KafkaMysqlOffsetParameter.kafkaConf); 67 | KafkaSubscribeConsumeThread consumeThread = new KafkaSubscribeConsumeThread(consumer, dataProcessor, offsetFlushBarrier); 68 | KafkaCache.consumeThreadList.add(consumeThread); 69 | executorService.submit(consumeThread); 70 | } 71 | // 启动定时刷数据入mysql 72 | if(startCount.equals(0)){ 73 | MysqlOffsetPersist.getInstance().start(); 74 | startCount = startCount + 1; 75 | } 76 | closeSignal = new DaemonCloseThread(this, closeMethod); 77 | closeSignal.setDaemon(true); 78 | closeSignal.start(); 79 | } 80 | 81 | public void stop() { 82 | stop(120000); 83 | } 84 | 85 | public void stop(long stopTimeOut) { 86 | long startTime = System.currentTimeMillis(); 87 | logger.info("consumers start shutdown"); 88 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 89 | if(consumeThread != null){ 90 | consumeThread.shutdown(); 91 | } 92 | } 93 | Boolean stopExceptionFlag = false; 94 | // 等待所有拉取线程自动停止 95 | for (;;) { 96 | if(stopExceptionFlag){ 97 | break; 98 | } 99 | Boolean kafkaPollFlag = false; 100 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 101 | if (consumeThread != null && consumeThread.kafkaPollFlag) { 102 | kafkaPollFlag = true; 103 | } 104 | } 105 | if (!kafkaPollFlag) { 106 | break; 107 | } 108 | if(System.currentTimeMillis()-startTime > stopTimeOut){ 109 | stopWithTimeOUt(); 110 | stopExceptionFlag = true; 111 | } 112 | } 113 | logger.info("kafka polling closed"); 114 | // 等待所有consumer关闭 115 | for (;;) { 116 | if(stopExceptionFlag){ 117 | break; 118 | } 119 | Boolean kafkaConsumerFlag = false; 120 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 121 | if (consumeThread != null && consumeThread.kafkaConsumerFlag) { 122 | kafkaConsumerFlag = true; 123 | } 124 | } 125 | if (!kafkaConsumerFlag) { 126 | break; 127 | } 128 | if(System.currentTimeMillis()-startTime > stopTimeOut){ 129 | stopWithTimeOUt(); 130 | stopExceptionFlag = true; 131 | } 132 | } 133 | logger.info("kafka consumer closed"); 134 | // 等待所有consumer的working线程关闭 135 | for (;;) { 136 | if(stopExceptionFlag){ 137 | break; 138 | } 139 | Boolean processDataWorkingFlag = false; 140 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 141 | if (consumeThread != null && consumeThread.processDataWorker.workingFlag) { 142 | processDataWorkingFlag = true; 143 | } 144 | } 145 | if (!processDataWorkingFlag) { 146 | break; 147 | } 148 | if(System.currentTimeMillis()-startTime > stopTimeOut){ 149 | stopWithTimeOUt(); 150 | stopExceptionFlag = true; 151 | } 152 | } 153 | logger.info("process data worker closed"); 154 | // 关闭线程池 155 | if (executorService != null) 156 | executorService.shutdown(); 157 | try { 158 | if (!executorService.awaitTermination(120000, TimeUnit.MILLISECONDS)) { 159 | logger.error("Timed out waiting for consumer threads to shut down, exiting uncleanly"); 160 | } 161 | } catch (InterruptedException e) { 162 | logger.error("Interrupted during shutdown, exiting uncleanly"); 163 | } 164 | logger.info("dataProcessor start to shutdown"); 165 | dataProcessor.finishProcess(); 166 | KafkaCache.kafkaConsumerOffsetMaps.clear(); 167 | KafkaCache.consumeThreadList.clear(); 168 | KafkaCache.rebalancerListenerList.clear(); 169 | closeSignal.shutdown(); 170 | } 171 | 172 | private void stopWithTimeOUt() { 173 | logger.info("kafka polling/kafka consumer/process data worker closed with timeout"); 174 | for (KafkaSubscribeConsumeThread consumeThread : KafkaCache.consumeThreadList) { 175 | if(consumeThread != null && consumeThread.processDataWorker != null){ 176 | consumeThread.processDataWorker.stopWithException(); 177 | } 178 | } 179 | try { 180 | // 等待所有拉取线程自动停止 181 | Thread.sleep(5000); 182 | // 等待所有consumer关闭 183 | Thread.sleep(5000); 184 | // 等待所有consumer的working线程关闭 185 | Thread.sleep(5000); 186 | } catch (InterruptedException e2) { 187 | logger.error("------- thread can not sleep ---------------------" 188 | + e2.toString()); 189 | } 190 | } 191 | 192 | public void destroy() { 193 | MysqlOffsetPersist.destoryFlag = true; 194 | stop(); 195 | closeSignal.afterDestroyConsumer(); 196 | logger.info("mysql start to shutdown"); 197 | // 关闭mysql连接 198 | MysqlOffsetPersist.getInstance().shutdown(); 199 | } 200 | 201 | public void destroy(long stopTimeOut) { 202 | MysqlOffsetPersist.destoryFlag = true; 203 | stop(stopTimeOut); 204 | closeSignal.afterDestroyConsumer(); 205 | logger.info("mysql start to shutdown"); 206 | // 关闭mysql连接 207 | MysqlOffsetPersist.getInstance().shutdown(); 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/KafkaSubscribeConsumerManager.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer; 2 | 3 | import cn.thinkingdata.kafka.cache.KafkaCache; 4 | import org.apache.kafka.clients.consumer.KafkaConsumer; 5 | 6 | import java.util.List; 7 | import java.util.Properties; 8 | 9 | public class KafkaSubscribeConsumerManager { 10 | 11 | private static KafkaSubscribeConsumerManager instance; 12 | 13 | private KafkaSubscribeConsumerManager() { 14 | } 15 | 16 | public static synchronized KafkaSubscribeConsumerManager getInstance() { 17 | if (instance == null) { 18 | instance = new KafkaSubscribeConsumerManager(); 19 | } 20 | return instance; 21 | } 22 | 23 | public KafkaConsumer createKafkaConsumer(List topicList, Properties props) { 24 | KafkaConsumer consumer = new KafkaConsumer(props); 25 | KafkaConsumerRebalancerListener rebalancerListener = new KafkaConsumerRebalancerListener(consumer); 26 | KafkaCache.rebalancerListenerList.add(rebalancerListener); 27 | consumer.subscribe(topicList, rebalancerListener); 28 | return consumer; 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/NewIDataLineProcessor.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerRecord; 4 | 5 | public interface NewIDataLineProcessor { 6 | 7 | void processData(ConsumerRecord consumerRecord); 8 | 9 | void finishProcess(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/dao/KafkaConsumerOffset.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.dao; 2 | 3 | import java.util.Date; 4 | 5 | public class KafkaConsumerOffset { 6 | private Integer oid; 7 | private String topic; 8 | private Integer partition; 9 | private String consumer_group; 10 | private Long offset; 11 | // 上一次flush记录下的offset值 12 | private Long last_flush_offset; 13 | // 计数器 每消费一个记录,计数器加一,flush完,计数器清零 14 | private Long count; 15 | private String kafka_cluster_name; 16 | private String owner; 17 | private Date update_time; 18 | private Date create_time; 19 | 20 | public Integer getOid() { 21 | return oid; 22 | } 23 | 24 | public void setOid(Integer oid) { 25 | this.oid = oid; 26 | } 27 | 28 | public String getTopic() { 29 | return topic; 30 | } 31 | 32 | public void setTopic(String topic) { 33 | this.topic = topic; 34 | } 35 | 36 | public Integer getPartition() { 37 | return partition; 38 | } 39 | 40 | public void setPartition(Integer partition) { 41 | this.partition = partition; 42 | } 43 | 44 | public String getConsumer_group() { 45 | return consumer_group; 46 | } 47 | 48 | public void setConsumer_group(String consumer_group) { 49 | this.consumer_group = consumer_group; 50 | } 51 | 52 | public Long getOffset() { 53 | return offset; 54 | } 55 | 56 | public void setOffset(Long offset) { 57 | this.offset = offset; 58 | } 59 | 60 | public Long getLast_flush_offset() { 61 | return last_flush_offset; 62 | } 63 | 64 | public void setLast_flush_offset(Long last_flush_offset) { 65 | this.last_flush_offset = last_flush_offset; 66 | } 67 | 68 | public Long getCount() { 69 | return count; 70 | } 71 | 72 | public void setCount(Long count) { 73 | this.count = count; 74 | } 75 | 76 | public String getKafka_cluster_name() { 77 | return kafka_cluster_name; 78 | } 79 | 80 | public void setKafka_cluster_name(String kafka_cluster_name) { 81 | this.kafka_cluster_name = kafka_cluster_name; 82 | } 83 | 84 | public String getOwner() { 85 | return owner; 86 | } 87 | 88 | public void setOwner(String owner) { 89 | this.owner = owner; 90 | } 91 | 92 | public Date getUpdate_time() { 93 | return update_time; 94 | } 95 | 96 | public void setUpdate_time(Date update_time) { 97 | this.update_time = update_time; 98 | } 99 | 100 | public Date getCreate_time() { 101 | return create_time; 102 | } 103 | 104 | public void setCreate_time(Date create_time) { 105 | this.create_time = create_time; 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return "KafkaConsumerOffset [oid=" + oid + ", topic=" + topic 111 | + ", partition=" + partition + ", consumer_group=" 112 | + consumer_group + ", offset=" + offset 113 | + ", last_flush_offset=" + last_flush_offset + ", count=" 114 | + count + ", kafka_cluster_name=" + kafka_cluster_name 115 | + ", owner=" + owner + ", update_time=" + update_time 116 | + ", create_time=" + create_time + "]"; 117 | } 118 | 119 | @Override 120 | public int hashCode() { 121 | final int prime = 31; 122 | int result = 1; 123 | result = prime * result + ((consumer_group == null) ? 0 : consumer_group.hashCode()); 124 | result = prime * result + ((kafka_cluster_name == null) ? 0 : kafka_cluster_name.hashCode()); 125 | result = prime * result + ((partition == null) ? 0 : partition.hashCode()); 126 | result = prime * result + ((topic == null) ? 0 : topic.hashCode()); 127 | return result; 128 | } 129 | 130 | @Override 131 | public boolean equals(Object obj) { 132 | if (this == obj) 133 | return true; 134 | if (obj == null) 135 | return false; 136 | if (getClass() != obj.getClass()) 137 | return false; 138 | KafkaConsumerOffset other = (KafkaConsumerOffset) obj; 139 | if (consumer_group == null) { 140 | if (other.consumer_group != null) 141 | return false; 142 | } else if (!consumer_group.equals(other.consumer_group)) 143 | return false; 144 | if (kafka_cluster_name == null) { 145 | if (other.kafka_cluster_name != null) 146 | return false; 147 | } else if (!kafka_cluster_name.equals(other.kafka_cluster_name)) 148 | return false; 149 | if (partition == null) { 150 | if (other.partition != null) 151 | return false; 152 | } else if (!partition.equals(other.partition)) 153 | return false; 154 | if (topic == null) { 155 | return other.topic == null; 156 | } else return topic.equals(other.topic); 157 | } 158 | 159 | public boolean isNull() { 160 | if (this == null) 161 | return true; 162 | return this.topic == null || this.partition == null || this.kafka_cluster_name == null || this.consumer_group == null; 163 | 164 | } 165 | 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/exception/ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.exception; 2 | 3 | import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumeThread; 4 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 5 | 6 | public interface ExceptionHandler { 7 | 8 | KafkaConsumerOffset executeWhenReadNullFromBackupExternalStore(String topic, Integer partition); 9 | 10 | KafkaConsumerOffset executeWhenReadNullFromMysql(String topic, Integer partition); 11 | 12 | Boolean executeWhenSaveOffsetFailInMysqlAndExternalStore(KafkaConsumerOffset kafkaConsumerOffset); 13 | 14 | void executeWhenSessionTimeout(Integer count); 15 | 16 | void executeWhenExecuteDataSessionTimeout(KafkaSubscribeConsumeThread kafkaSubscribeConsumeThread); 17 | 18 | void executeWhenOffsetReset(KafkaSubscribeConsumeThread consumeThread); 19 | 20 | void executeWhenException(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/exception/TaKafkaCommonException.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.exception; 2 | 3 | public class TaKafkaCommonException extends RuntimeException { 4 | 5 | public TaKafkaCommonException(String msg) { 6 | super(msg); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/offset/MysqlOffsetManager.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.offset; 2 | 3 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 4 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 5 | import cn.thinkingdata.kafka.consumer.persist.DBPoolConnection; 6 | import cn.thinkingdata.kafka.util.CommonUtils; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.sql.Connection; 11 | import java.sql.PreparedStatement; 12 | import java.sql.ResultSet; 13 | import java.sql.SQLException; 14 | import java.sql.Statement; 15 | import java.sql.Timestamp; 16 | import java.util.Date; 17 | 18 | public class MysqlOffsetManager extends OffsetManager { 19 | 20 | private static MysqlOffsetManager instance; 21 | 22 | private static final Logger logger = LoggerFactory 23 | .getLogger(MysqlOffsetManager.class); 24 | 25 | public static synchronized MysqlOffsetManager getInstance() { 26 | if (instance == null) { 27 | instance = new MysqlOffsetManager(); 28 | } 29 | return instance; 30 | } 31 | 32 | DBPoolConnection dbp = DBPoolConnection.getInstance(); 33 | 34 | private MysqlOffsetManager() { 35 | } 36 | 37 | //去掉synchronized,因为MysqlOffsetPersist的flush和persist里有synchronized方法 38 | @Override 39 | protected Boolean saveOffsetInExternalStore(KafkaConsumerOffset kafkaConsumerOffset) { 40 | logger.debug("because of the muti-thread, the value is not exactly right, kafkaConsumerOffset is " + kafkaConsumerOffset.toString()); 41 | try (Connection conn = dbp.getConnection()) { 42 | String sql = "INSERT INTO " 43 | + KafkaMysqlOffsetParameter.tableName 44 | + " VALUES" 45 | + " (null,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY" 46 | + " UPDATE offset=?, last_flush_offset=?, kafka_cluster_name=?," 47 | + " owner=?, update_time=?;"; 48 | PreparedStatement ps = conn.prepareStatement(sql); 49 | ps.setString(1, kafkaConsumerOffset.getTopic()); 50 | ps.setInt(2, kafkaConsumerOffset.getPartition()); 51 | ps.setString(3, kafkaConsumerOffset.getConsumer_group()); 52 | ps.setLong(5, kafkaConsumerOffset.getLast_flush_offset()); 53 | ps.setLong(4, kafkaConsumerOffset.getOffset()); 54 | ps.setString(6, kafkaConsumerOffset.getKafka_cluster_name()); 55 | ps.setString(7, kafkaConsumerOffset.getOwner()); 56 | ps.setTimestamp(8, new Timestamp(kafkaConsumerOffset.getUpdate_time().getTime())); 57 | ps.setTimestamp(9, new Timestamp(kafkaConsumerOffset.getCreate_time().getTime())); 58 | ps.setLong(11, kafkaConsumerOffset.getLast_flush_offset()); 59 | ps.setLong(10, kafkaConsumerOffset.getOffset()); 60 | ps.setString(12, kafkaConsumerOffset.getKafka_cluster_name()); 61 | ps.setString(13, kafkaConsumerOffset.getOwner()); 62 | ps.setTimestamp(14, new Timestamp(kafkaConsumerOffset.getUpdate_time().getTime())); 63 | ps.execute(); 64 | return true; 65 | } catch (SQLException e) { 66 | logger.error("mysql save offset error, the error is " + CommonUtils.getStackTraceAsString(e)); 67 | return false; 68 | } 69 | } 70 | 71 | @Override 72 | protected KafkaConsumerOffset readOffsetFromExternalStore(String topic, 73 | int partition) { 74 | KafkaConsumerOffset kafkaConsumerOffset = new KafkaConsumerOffset(); 75 | Date now = new Date(); 76 | String sql = "select * from " + KafkaMysqlOffsetParameter.tableName 77 | + " where kafka_cluster_name = '" 78 | + KafkaMysqlOffsetParameter.kafkaClusterName 79 | + "' and topic = '" + topic + "' and kafka_partition = " 80 | + partition + " and consumer_group = '" 81 | + KafkaMysqlOffsetParameter.consumerGroup + "';"; 82 | try (Connection conn = dbp.getConnection(); Statement statement = conn.createStatement(); ResultSet rs = statement.executeQuery(sql)){ 83 | int count = 0; 84 | while (rs.next()) { 85 | count++; 86 | if (count > 1) { 87 | logger.error("DUPLICATE KEY in " 88 | + KafkaMysqlOffsetParameter.tableName 89 | + " , the kafka cluster name is " 90 | + KafkaMysqlOffsetParameter.kafkaClusterName 91 | + " , the topic is " + topic 92 | + ", the partition is " + partition 93 | + ", the consumerGroup is " 94 | + KafkaMysqlOffsetParameter.consumerGroup); 95 | return kafkaConsumerOffset; 96 | } 97 | kafkaConsumerOffset.setOid(rs.getInt("oid")); 98 | kafkaConsumerOffset.setTopic(topic); 99 | kafkaConsumerOffset.setPartition(partition); 100 | kafkaConsumerOffset.setConsumer_group(KafkaMysqlOffsetParameter.consumerGroup); 101 | kafkaConsumerOffset.setOffset(rs.getLong("offset")); 102 | kafkaConsumerOffset.setLast_flush_offset(rs.getLong("offset")); 103 | kafkaConsumerOffset.setKafka_cluster_name(KafkaMysqlOffsetParameter.kafkaClusterName); 104 | kafkaConsumerOffset.setOwner(rs.getString("owner")); 105 | kafkaConsumerOffset.setCount(0L); 106 | kafkaConsumerOffset.setUpdate_time(rs.getDate("update_time")); 107 | kafkaConsumerOffset.setCreate_time(rs.getDate("create_time")); 108 | } 109 | if (count == 0) { 110 | logger.info("offset is not in " 111 | + KafkaMysqlOffsetParameter.tableName 112 | + " , the kafka cluster name is " 113 | + KafkaMysqlOffsetParameter.kafkaClusterName 114 | + " , the topic is " + topic + ", the partition is " 115 | + partition + ", the consumerGroup is " 116 | + KafkaMysqlOffsetParameter.consumerGroup); 117 | kafkaConsumerOffset.setTopic(topic); 118 | kafkaConsumerOffset.setPartition(partition); 119 | kafkaConsumerOffset.setConsumer_group(KafkaMysqlOffsetParameter.consumerGroup); 120 | kafkaConsumerOffset.setOffset(0L); 121 | kafkaConsumerOffset.setLast_flush_offset(0L); 122 | kafkaConsumerOffset.setKafka_cluster_name(KafkaMysqlOffsetParameter.kafkaClusterName); 123 | kafkaConsumerOffset.setCount(0L); 124 | kafkaConsumerOffset.setUpdate_time(now); 125 | return kafkaConsumerOffset; 126 | } 127 | } catch (Exception e) { 128 | logger.error("mysql read offset error, the error is " + CommonUtils.getStackTraceAsString(e)); 129 | return null; 130 | } 131 | return kafkaConsumerOffset; 132 | } 133 | 134 | public void shutdown() { 135 | logger.info("mysql shutdown!"); 136 | try { 137 | dbp.close(); 138 | } catch (Exception e) { 139 | logger.error("can not close mysql connection pool, the error is " + CommonUtils.getStackTraceAsString(e)); 140 | } 141 | } 142 | 143 | //去掉synchronized,因为MysqlOffsetPersist的flush和persist里有synchronized方法 144 | public Boolean saveOffsetInCacheToMysql(KafkaConsumerOffset kafkaConsumerOffset) { 145 | Long lag = kafkaConsumerOffset.getOffset() - kafkaConsumerOffset.getLast_flush_offset(); 146 | if (!lag.equals(0L)) { 147 | logger.debug("because of the muti-thread, the value is not exactly right, the lag is " + lag); 148 | return saveOffsetInExternalStore(kafkaConsumerOffset); 149 | } 150 | return true; 151 | } 152 | 153 | public Boolean mysqlStateCheck() { 154 | String sql = "select * from " + KafkaMysqlOffsetParameter.tableName + " limit 10;"; 155 | try (Connection conn = dbp.getConnection(); Statement statement = conn.createStatement()) { 156 | logger.info("mysql reconnected!"); 157 | statement.execute(sql); 158 | return true; 159 | } catch (SQLException e) { 160 | return false; 161 | } 162 | } 163 | 164 | public Boolean updateOwner(KafkaConsumerOffset kafkaConsumerOffset) { 165 | logger.debug("update the owner for kafkaConsumerOffset, kafkaConsumerOffset is " 166 | + kafkaConsumerOffset.toString()); 167 | Date now = new Date(); 168 | Boolean flag = true; 169 | try (Connection conn = dbp.getConnection(); Statement statement = conn.createStatement()){ 170 | if (kafkaConsumerOffset.getOffset() == 0L) { 171 | kafkaConsumerOffset.setUpdate_time(now); 172 | if (kafkaConsumerOffset.getCreate_time() == null) 173 | kafkaConsumerOffset.setCreate_time(now); 174 | flag = saveOffsetInExternalStore(kafkaConsumerOffset); 175 | } else { 176 | String sql = "UPDATE " + KafkaMysqlOffsetParameter.tableName 177 | + " set owner='" + kafkaConsumerOffset.getOwner() + "', update_time = NOW()" 178 | + " where kafka_cluster_name = '" 179 | + KafkaMysqlOffsetParameter.kafkaClusterName 180 | + "' and topic = '" + kafkaConsumerOffset.getTopic() 181 | + "' and kafka_partition = " 182 | + kafkaConsumerOffset.getPartition() 183 | + " and consumer_group = '" 184 | + KafkaMysqlOffsetParameter.consumerGroup + "';"; 185 | statement.execute(sql); 186 | } 187 | if (flag) { 188 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(true); 189 | return true; 190 | } else { 191 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(false); 192 | logger.error("mysql update the owner error"); 193 | return false; 194 | } 195 | 196 | } catch (SQLException e) { 197 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(false); 198 | logger.error("mysql update the owner error, the error is " + CommonUtils.getStackTraceAsString(e)); 199 | return false; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/offset/OffsetManager.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.offset; 2 | 3 | import cn.thinkingdata.kafka.cache.KafkaCache; 4 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 5 | import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumeThread; 6 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 7 | import cn.thinkingdata.kafka.consumer.persist.DefaultStorePersist; 8 | import cn.thinkingdata.kafka.consumer.persist.StorePersist; 9 | import cn.thinkingdata.kafka.util.CommonUtils; 10 | import cn.thinkingdata.kafka.util.RetryerUtil; 11 | import com.github.rholder.retry.RetryException; 12 | import com.github.rholder.retry.Retryer; 13 | import com.google.common.base.Predicates; 14 | import org.apache.kafka.common.TopicPartition; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | import java.util.concurrent.ExecutionException; 19 | 20 | public abstract class OffsetManager { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(OffsetManager.class); 23 | 24 | private final Retryer retryerWithResultNull = RetryerUtil.initRetryerByTimesWithIfResult(3, 300, Predicates.isNull()); 25 | 26 | private StorePersist externalStorePersist = new DefaultStorePersist(); 27 | 28 | public StorePersist getExternalStorePersist() { 29 | return externalStorePersist; 30 | } 31 | 32 | public void setExternalStorePersist(StorePersist externalStorePersist) { 33 | this.externalStorePersist = externalStorePersist; 34 | } 35 | 36 | public void saveOffsetInCache(KafkaSubscribeConsumeThread consumeThread, KafkaConsumerOffset kafkaConsumerOffset) { 37 | TopicPartition topicPartition = new TopicPartition(kafkaConsumerOffset.getTopic(), kafkaConsumerOffset.getPartition()); 38 | KafkaConsumerOffset kafkaConsumerOffsetOld = KafkaCache.kafkaConsumerOffsetMaps.get(topicPartition); 39 | // compare kafkaConsumerOffsetOld and kafkaConsumerOffset, avoid reset 40 | if (kafkaConsumerOffsetOld != null && kafkaConsumerOffsetOld.getOffset() > kafkaConsumerOffset.getOffset()) { 41 | logger.info("kafka consumer offset reset, the old kafkaConsumerOffset is " + kafkaConsumerOffsetOld + ", the kafkaConsumerOffset is " + kafkaConsumerOffset); 42 | synchronized (OffsetManager.class) { 43 | externalStorePersist.executeWhenOffsetReset(consumeThread); 44 | } 45 | } else if (kafkaConsumerOffsetOld == null 46 | || !kafkaConsumerOffset.getCount().equals(0L)) { 47 | KafkaCache.kafkaConsumerOffsetMaps.put(topicPartition, kafkaConsumerOffset); 48 | kafkaConsumerOffset.setCount(0L); 49 | } 50 | } 51 | 52 | public KafkaConsumerOffset readOffsetFromMysql(final String topic, final Integer partition) { 53 | KafkaConsumerOffset kafkaConsumerOffset = null; 54 | try { 55 | kafkaConsumerOffset = retryerWithResultNull 56 | .call(() -> readOffsetFromExternalStore(topic, partition)); 57 | if (kafkaConsumerOffset == null) { 58 | logger.error("the kafkaConsumerOffset read from mysql is null , the topic is " + topic + "the partition is " + partition); 59 | } 60 | } catch (ExecutionException | RetryException e) { 61 | logger.error("retry to read kafkaConsumerOffset from mysql error, the error is " + CommonUtils.getStackTraceAsString(e)); 62 | return null; 63 | } 64 | return kafkaConsumerOffset; 65 | } 66 | 67 | public KafkaConsumerOffset readOffsetFromBackupExternalStore( 68 | final String topic, final Integer partition) { 69 | KafkaConsumerOffset kafkaConsumerOffset = null; 70 | try { 71 | kafkaConsumerOffset = retryerWithResultNull 72 | .call(() -> externalStorePersist.readOffsetFromBackupExternalStore(topic, partition)); 73 | if (kafkaConsumerOffset == null) { 74 | logger.error("the kafkaConsumerOffset read from backup external store is null , the topic is " + topic + "the partition is " + partition); 75 | } 76 | } catch (ExecutionException | RetryException e) { 77 | logger.error("retry to read kafkaConsumerOffset from backup external store error, the error is " + CommonUtils.getStackTraceAsString(e)); 78 | return null; 79 | } 80 | return kafkaConsumerOffset; 81 | } 82 | 83 | public synchronized KafkaConsumerOffset readOffsetFromCache(String topic, Integer partition) { 84 | TopicPartition topicPartition = new TopicPartition(topic, partition); 85 | KafkaConsumerOffset kafkaConsumerOffset = KafkaCache.kafkaConsumerOffsetMaps.get(topicPartition); 86 | if (kafkaConsumerOffset == null) { 87 | kafkaConsumerOffset = readOffsetFromMysql(topic, partition); 88 | if (kafkaConsumerOffset == null) { 89 | logger.error("can not read offset from mysql! the topic is " + topic + ",the partition is " + partition); 90 | kafkaConsumerOffset = externalStorePersist.executeWhenReadNullFromMysql(topic, partition); 91 | } 92 | // 从另一个备用存储读取的接口如果读取成功,默认是空 93 | KafkaConsumerOffset kafkaConsumerOffsetFromBackupExternalStore = readOffsetFromBackupExternalStore(topic, partition); 94 | if (kafkaConsumerOffsetFromBackupExternalStore == null) { 95 | logger.error("can not read offset from backup external store! the topic is " + topic + ",the partition is " + partition); 96 | kafkaConsumerOffsetFromBackupExternalStore = externalStorePersist.executeWhenReadNullFromBackupExternalStore(topic, partition); 97 | } 98 | // 判断两个存储中的数值,然后确定用offset更大的那个 99 | kafkaConsumerOffset = getKafkaConsumerOffsetFromMysqlAndBackupExternalStore(kafkaConsumerOffset, kafkaConsumerOffsetFromBackupExternalStore); 100 | if (kafkaConsumerOffset != null) { 101 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(true); 102 | KafkaCache.kafkaConsumerOffsetMaps.put(topicPartition, kafkaConsumerOffset); 103 | } else { 104 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(false); 105 | logger.error("the kafkaConsumerOffset read from external store is null , the topic is " + topic + ",the partition is " + partition); 106 | } 107 | } 108 | return kafkaConsumerOffset; 109 | } 110 | 111 | public KafkaConsumerOffset getKafkaConsumerOffsetFromMysqlAndBackupExternalStore( 112 | KafkaConsumerOffset kafkaConsumerOffset, 113 | KafkaConsumerOffset kafkaConsumerOffsetFromBackupExternalStore) { 114 | if (kafkaConsumerOffsetFromBackupExternalStore == null) { 115 | logger.error("getKafkaConsumerOffsetFromMysqlAndBackupExternalStore, the kafka consumer offset from backup external store is null!"); 116 | System.exit(-1); 117 | return kafkaConsumerOffset; 118 | } 119 | if (kafkaConsumerOffsetFromBackupExternalStore.isNull()) { 120 | logger.info("getKafkaConsumerOffsetFromMysqlAndBackupExternalStore, the kafka consumer offset from backup external store is null, the offset is " 121 | + kafkaConsumerOffsetFromBackupExternalStore); 122 | return kafkaConsumerOffset; 123 | } 124 | if (!kafkaConsumerOffsetFromBackupExternalStore 125 | .equals(kafkaConsumerOffset)) { 126 | logger.error("getKafkaConsumerOffsetFromMysqlAndBackupExternalStore error, the kafkaConsumerOffsetFromBackupExternalStore is " 127 | + kafkaConsumerOffsetFromBackupExternalStore 128 | + ", the kafkaConsumerOffset is " 129 | + kafkaConsumerOffset 130 | + ", they should be equal!"); 131 | System.exit(-1); 132 | return kafkaConsumerOffset; 133 | } 134 | if (kafkaConsumerOffsetFromBackupExternalStore.getOffset() > kafkaConsumerOffset.getOffset()) { 135 | kafkaConsumerOffset = kafkaConsumerOffsetFromBackupExternalStore; 136 | } 137 | return kafkaConsumerOffset; 138 | } 139 | 140 | abstract Boolean saveOffsetInExternalStore(KafkaConsumerOffset kafkaConsumerOffset); 141 | 142 | abstract KafkaConsumerOffset readOffsetFromExternalStore(String topic, int partition); 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/persist/DBPoolConnection.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.persist; 2 | 3 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 4 | import cn.thinkingdata.kafka.util.CommonUtils; 5 | import com.alibaba.druid.pool.DruidDataSource; 6 | import com.alibaba.druid.pool.DruidDataSourceFactory; 7 | import com.alibaba.druid.pool.DruidPooledConnection; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.sql.SQLException; 12 | import java.util.Properties; 13 | 14 | 15 | public class DBPoolConnection { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(MysqlOffsetPersist.class); 18 | private static DBPoolConnection dbPoolConnection = null; 19 | private static DruidDataSource druidDataSource = null; 20 | 21 | static { 22 | try { 23 | Properties properties = new Properties(); 24 | // properties.load(DBPoolConnection.class.getResourceAsStream("/db_server.properties")); 25 | properties.put("driverClassName", "com.mysql.jdbc.Driver"); 26 | properties.put("url", KafkaMysqlOffsetParameter.jdbcUrl); 27 | properties.put("username", KafkaMysqlOffsetParameter.username); 28 | properties.put("password", KafkaMysqlOffsetParameter.password); 29 | properties.put("filters", "stat"); 30 | properties.put("initialSize", "1"); 31 | properties.put("minIdle", "1"); 32 | properties.put("maxActive", "30"); 33 | properties.put("maxWait", "60000"); 34 | properties.put("timeBetweenEvictionRunsMillis", "60000"); 35 | properties.put("minEvictableIdleTimeMillis", "300000"); 36 | properties.put("validationQuery", "SELECT 1"); 37 | properties.put("testWhileIdle", "true"); 38 | properties.put("testOnBorrow", "false"); 39 | properties.put("testOnReturn", "false"); 40 | properties.put("poolPreparedStatements", "true"); 41 | properties.put("maxPoolPreparedStatementPerConnectionSize", "20"); 42 | druidDataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties); 43 | } catch (Exception e) { 44 | logger.error("get druidDataSource error, the error is " + CommonUtils.getStackTraceAsString(e)); 45 | System.exit(-1); 46 | } 47 | } 48 | 49 | /** 50 | * 数据库连接池单例 51 | * 52 | * @return 53 | */ 54 | public static synchronized DBPoolConnection getInstance() { 55 | if (null == dbPoolConnection) { 56 | dbPoolConnection = new DBPoolConnection(); 57 | } 58 | return dbPoolConnection; 59 | } 60 | 61 | private DBPoolConnection() { 62 | } 63 | 64 | /** 65 | * 返回druid数据库连接 66 | * 67 | * @return 68 | * @throws SQLException 69 | */ 70 | public DruidPooledConnection getConnection() throws SQLException { 71 | return druidDataSource.getConnection(); 72 | } 73 | 74 | public void close() { 75 | druidDataSource.close(); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/persist/DefaultStorePersist.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.persist; 2 | 3 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 4 | import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumeThread; 5 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 6 | import cn.thinkingdata.kafka.consumer.exception.TaKafkaCommonException; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class DefaultStorePersist implements StorePersist { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(DefaultStorePersist.class); 13 | 14 | @Override 15 | public KafkaConsumerOffset readOffsetFromBackupExternalStore(String topic, int partition) { 16 | return new KafkaConsumerOffset(); 17 | } 18 | 19 | @Override 20 | public Boolean saveOffsetInBackupExternalStore(KafkaConsumerOffset kafkaConsumerOffset) { 21 | return true; 22 | } 23 | 24 | @Override 25 | public KafkaConsumerOffset executeWhenReadNullFromBackupExternalStore(String topic, Integer partition) { 26 | logger.error("can not read offset from backup external store!"); 27 | throw new TaKafkaCommonException("executeWhenReadNullFromBackupExternalStore, can not read offset from backup external store!"); 28 | } 29 | 30 | @Override 31 | public KafkaConsumerOffset executeWhenReadNullFromMysql(String topic, Integer partition) { 32 | logger.error("can not read offset from mysql!"); 33 | throw new TaKafkaCommonException("executeWhenReadNullFromMysql, can not read offset from mysql!"); 34 | } 35 | 36 | @Override 37 | public Boolean executeWhenSaveOffsetFailInMysqlAndExternalStore( 38 | KafkaConsumerOffset kafkaConsumerOffset) { 39 | logger.error("save offset fail in mysql or external store!"); 40 | throw new TaKafkaCommonException("executeWhenSaveOffsetFailInMysqlAndExternalStore, save offset fail in mysql or external store!"); 41 | } 42 | 43 | @Override 44 | public Boolean backupStoreStateCheck() { 45 | return true; 46 | } 47 | 48 | @Override 49 | public Boolean updateOwner(KafkaConsumerOffset kafkaConsumerOffset) { 50 | return true; 51 | } 52 | 53 | @Override 54 | public void executeWhenSessionTimeout(Integer count) { 55 | logger.info("session will time out! the count is " + count 56 | + ", the session time out is " 57 | + KafkaMysqlOffsetParameter.sessionTimeout); 58 | throw new TaKafkaCommonException("executeWhenSessionTimeout, session will time out! the count is " + count 59 | + ", the session time out is " 60 | + KafkaMysqlOffsetParameter.sessionTimeout); 61 | } 62 | 63 | @Override 64 | public void executeWhenExecuteDataSessionTimeout(KafkaSubscribeConsumeThread kafkaSubscribeConsumeThread) { 65 | logger.info("session time out! the count is, the session time out is " 66 | + KafkaMysqlOffsetParameter.sessionTimeout); 67 | throw new TaKafkaCommonException("executeWhenExecuteDataSessionTimeout, session time out! the count is, the session time out is " 68 | + KafkaMysqlOffsetParameter.sessionTimeout); 69 | } 70 | 71 | @Override 72 | public void executeWhenOffsetReset(KafkaSubscribeConsumeThread consumeThread) { 73 | logger.info("offset reset!"); 74 | throw new TaKafkaCommonException("executeWhenOffsetReset, kafka offset reset!"); 75 | } 76 | 77 | @Override 78 | public void executeWhenException() { 79 | logger.info("exception!"); 80 | throw new TaKafkaCommonException("executeWhenException!"); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/persist/MysqlOffsetPersist.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.persist; 2 | 3 | import cn.thinkingdata.kafka.cache.KafkaCache; 4 | import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 5 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 6 | import cn.thinkingdata.kafka.consumer.offset.MysqlOffsetManager; 7 | import cn.thinkingdata.kafka.util.CommonUtils; 8 | import cn.thinkingdata.kafka.util.RetryerUtil; 9 | import com.github.rholder.retry.RetryException; 10 | import com.github.rholder.retry.Retryer; 11 | import com.google.common.base.Predicates; 12 | import org.apache.kafka.common.TopicPartition; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.util.Date; 17 | import java.util.concurrent.ExecutionException; 18 | 19 | public class MysqlOffsetPersist extends Thread implements OffsetPersist { 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(MysqlOffsetPersist.class); 22 | 23 | private static MysqlOffsetPersist instance; 24 | public static volatile Boolean destoryFlag = false; 25 | public static volatile Boolean runFlag = false; 26 | 27 | private final StorePersist externalStorePersist = MysqlOffsetManager.getInstance().getExternalStorePersist(); 28 | 29 | // public static Boolean mysqlOffsetPersistFlag = false; 30 | 31 | public static synchronized MysqlOffsetPersist getInstance() { 32 | if (instance == null) { 33 | instance = new MysqlOffsetPersist(); 34 | } 35 | return instance; 36 | } 37 | 38 | private final Retryer retryerWithResultFails = RetryerUtil.initRetryerByTimesWithIfResult(3, 300, Predicates.equalTo(false)); 39 | 40 | private MysqlOffsetPersist() { 41 | } 42 | 43 | // persist和flush互斥 44 | @Override 45 | public void persist(KafkaConsumerOffset kafkaConsumerOffset) { 46 | Boolean saveOffsetFlag = saveOffset(kafkaConsumerOffset); 47 | if (!saveOffsetFlag) { 48 | logger.error("can not persist in both mysql or backup store"); 49 | externalStorePersist.executeWhenSaveOffsetFailInMysqlAndExternalStore(kafkaConsumerOffset); 50 | } 51 | } 52 | 53 | @Override 54 | public void shutdown() { 55 | // 关闭定时线程 56 | logger.info("------- Shutting mysql offset thread Down ---------------------"); 57 | MysqlOffsetManager.getInstance().shutdown(); 58 | } 59 | 60 | public void mysqlAndBackupStoreStateCheckJob() { 61 | // 如果mysql和BackupStoreState连接不通,看看有没有恢复 62 | int count = -1; 63 | while (!KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.get()) { 64 | if (count == -1) { 65 | logger.info("------- mysql or backup store down, check mysql and backup store status ---------------------"); 66 | count = 0; 67 | } 68 | Boolean mysqlStateCheck = mysqlStateCheckWithRetry(); 69 | Boolean backupStoreStateCheck = backupStoreStateCheckWithRetry(); 70 | if (!mysqlStateCheck || !backupStoreStateCheck) { 71 | count++; 72 | // 如果mysql或者back up store超过sessionTimeout,就要停止 73 | if (count > Integer.parseInt(KafkaMysqlOffsetParameter.sessionTimeout) - 10) { 74 | externalStorePersist.executeWhenSessionTimeout(count); 75 | } 76 | } else { 77 | count = 0; 78 | } 79 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(mysqlStateCheck || backupStoreStateCheck); 80 | try { 81 | Thread.sleep(1000); 82 | } catch (InterruptedException e) { 83 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 84 | } 85 | } 86 | } 87 | 88 | public Boolean mysqlStateCheckWithRetry() { 89 | Boolean flag = false; 90 | try { 91 | flag = retryerWithResultFails.call(() -> MysqlOffsetManager.getInstance().mysqlStateCheck()); 92 | } catch (ExecutionException | RetryException e) { 93 | logger.error("retry mysqlStateCheck error, the error is " + CommonUtils.getStackTraceAsString(e)); 94 | flag = false; 95 | } 96 | return flag; 97 | } 98 | 99 | public Boolean backupStoreStateCheckWithRetry() { 100 | Boolean flag = false; 101 | try { 102 | flag = retryerWithResultFails.call(() -> externalStorePersist.backupStoreStateCheck()); 103 | } catch (ExecutionException | RetryException e) { 104 | logger.error("retry backupStoreStateCheck error, the error is " + CommonUtils.getStackTraceAsString(e)); 105 | flag = false; 106 | } 107 | return flag; 108 | } 109 | 110 | @Override 111 | public void run() { 112 | while (!destoryFlag) { 113 | if (!KafkaMysqlOffsetParameter.kafkaSubscribeConsumerClosed.get()) { 114 | runFlag = true; 115 | mysqlAndBackupStoreStateCheckJob(); 116 | // 如果通,并且没有进行rebalance,则定时刷数据进mysql 117 | if (KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.get()) { 118 | persisit(); 119 | } 120 | } 121 | runFlag = false; 122 | try { 123 | Thread.sleep(new Long(KafkaMysqlOffsetParameter.flushInterval) * 100); 124 | } catch (InterruptedException e) { 125 | logger.error("------- thread can not sleep ---------------------" + e.toString()); 126 | } 127 | } 128 | runFlag = false; 129 | logger.info("mysql persist stop, runFlag is " + runFlag); 130 | } 131 | 132 | private synchronized void persisit() { 133 | Date now = new Date(); 134 | for (KafkaConsumerOffset kafkaConsumerOffsetInCache : KafkaCache.kafkaConsumerOffsetMaps.values()) { 135 | // 根据同步offset的size,同步offset的时间 136 | Long lag = kafkaConsumerOffsetInCache.getOffset() - kafkaConsumerOffsetInCache.getLast_flush_offset(); 137 | Long updateInterval = now.getTime() - kafkaConsumerOffsetInCache.getUpdate_time().getTime(); 138 | if (lag >= KafkaMysqlOffsetParameter.flushOffsetSize 139 | || updateInterval >= new Long(KafkaMysqlOffsetParameter.flushInterval) * 1000) { 140 | persist(kafkaConsumerOffsetInCache); 141 | } 142 | } 143 | } 144 | 145 | public Boolean saveOffset(final KafkaConsumerOffset kafkaConsumerOffset) { 146 | Boolean flagMysqlStore = false; 147 | Date now = new Date(); 148 | // 更新的Update_time 149 | kafkaConsumerOffset.setUpdate_time(now); 150 | if (kafkaConsumerOffset.getCreate_time() == null) 151 | kafkaConsumerOffset.setCreate_time(now); 152 | // 得到Last_flush_offset防止consumeThread线程修改数据 153 | Long last_flush_offset = kafkaConsumerOffset.getOffset(); 154 | try { 155 | flagMysqlStore = retryerWithResultFails.call(() -> MysqlOffsetManager.getInstance().saveOffsetInCacheToMysql(kafkaConsumerOffset)); 156 | } catch (ExecutionException | RetryException e) { 157 | logger.error("retry to save kafkaConsumerOffset to mysql and backup external store error, the error is " + CommonUtils.getStackTraceAsString(e)); 158 | flagMysqlStore = false; 159 | } 160 | 161 | Boolean flagBackupStore = false; 162 | try { 163 | // 写一个存到备用存储的接口,默认是空 164 | flagBackupStore = retryerWithResultFails.call(() -> MysqlOffsetManager.getInstance().getExternalStorePersist().saveOffsetInBackupExternalStore(kafkaConsumerOffset)); 165 | } catch (ExecutionException | RetryException e) { 166 | logger.error("retry to save kafkaConsumerOffset to mysql and backup external store error, the error is " + CommonUtils.getStackTraceAsString(e)); 167 | flagBackupStore = false; 168 | } 169 | Boolean saveOffsetFlag = flagMysqlStore || flagBackupStore; 170 | if (saveOffsetFlag) { 171 | kafkaConsumerOffset.setLast_flush_offset(last_flush_offset); 172 | kafkaConsumerOffset.setCount(0L); 173 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(true); 174 | } else { 175 | KafkaMysqlOffsetParameter.mysqlAndBackupStoreConnState.set(false); 176 | } 177 | return saveOffsetFlag; 178 | } 179 | 180 | @Override 181 | public synchronized Boolean flush(KafkaConsumerOffset kafkaConsumerOffset) { 182 | logger.info("------- flush offset in cache to mysql ---------------------"); 183 | Boolean saveOffsetFlag = saveOffset(kafkaConsumerOffset); 184 | if (!saveOffsetFlag) { 185 | logger.error("can not flush in mysql or backup store"); 186 | externalStorePersist.executeWhenSaveOffsetFailInMysqlAndExternalStore(kafkaConsumerOffset); 187 | } 188 | TopicPartition topicPartition = new TopicPartition(kafkaConsumerOffset.getTopic(), kafkaConsumerOffset.getPartition()); 189 | KafkaConsumerOffset kafkaConsumerOffsetInMap = KafkaCache.kafkaConsumerOffsetMaps.get(topicPartition); 190 | if (kafkaConsumerOffsetInMap != null && kafkaConsumerOffsetInMap.equals(kafkaConsumerOffset)) { 191 | KafkaCache.kafkaConsumerOffsetMaps.remove(topicPartition); 192 | } 193 | kafkaConsumerOffset.setOwner(""); 194 | updateOwner(kafkaConsumerOffset); 195 | return saveOffsetFlag; 196 | 197 | } 198 | 199 | public synchronized Boolean updateOwner(final KafkaConsumerOffset kafkaConsumerOffset) { 200 | try { 201 | Boolean flagMysqlStore = false; 202 | flagMysqlStore = retryerWithResultFails.call(() -> MysqlOffsetManager.getInstance().updateOwner(kafkaConsumerOffset)); 203 | Boolean flagBackupStore = false; 204 | flagBackupStore = retryerWithResultFails.call(() -> MysqlOffsetManager.getInstance().getExternalStorePersist().updateOwner(kafkaConsumerOffset)); 205 | Boolean flag = flagMysqlStore || flagBackupStore; 206 | return flag; 207 | } catch (ExecutionException | RetryException e) { 208 | logger.error("retry to updateOwner from mysql error, the error is " + CommonUtils.getStackTraceAsString(e)); 209 | return false; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/persist/OffsetPersist.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.persist; 2 | 3 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 4 | 5 | public interface OffsetPersist { 6 | 7 | void persist(KafkaConsumerOffset kafkaConsumerOffsetInCache); 8 | 9 | void shutdown(); 10 | 11 | Boolean flush(KafkaConsumerOffset kafkaConsumerOffsetInCache); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/persist/RedisStorePersistService.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.persist; 2 | 3 | public class RedisStorePersistService{ 4 | 5 | } 6 | //import cn.thinkingdata.kafka.constant.KafkaMysqlOffsetParameter; 7 | //import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumeThread; 8 | //import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 9 | //import cn.thinkingdata.kafka.consumer.persist.MysqlOffsetPersist; 10 | //import cn.thinkingdata.kafka.consumer.persist.StorePersist; 11 | //import cn.thinkingdata.kafka.util.CommonUtils; 12 | //import cn.thinkingdata.kafka.util.RetryerUtil; 13 | //import cn.thinkingdata.ta.etl.domain.Do_ta_company_info; 14 | //import cn.thinkingdata.ta.etl.mapper.MapperTaMysql; 15 | //import cn.thinkingdata.ta.etl.service.TaCenteredAlertSendService; 16 | //import com.github.rholder.retry.RetryException; 17 | //import com.github.rholder.retry.Retryer; 18 | //import com.google.common.base.Predicates; 19 | //import com.google.common.base.Throwables; 20 | //import org.apache.commons.collections.MapUtils; 21 | //import org.apache.commons.lang.StringUtils; 22 | //import org.joda.time.DateTime; 23 | //import org.slf4j.Logger; 24 | //import org.slf4j.LoggerFactory; 25 | //import org.springframework.beans.factory.annotation.Autowired; 26 | //import org.springframework.beans.factory.annotation.Qualifier; 27 | //import org.springframework.data.redis.connection.RedisConnection; 28 | //import org.springframework.data.redis.core.RedisCallback; 29 | //import org.springframework.data.redis.core.RedisTemplate; 30 | //import org.springframework.data.redis.serializer.RedisSerializer; 31 | //import org.springframework.stereotype.Service; 32 | // 33 | //import java.util.Date; 34 | //import java.util.HashMap; 35 | //import java.util.Map; 36 | //import java.util.concurrent.Callable; 37 | //import java.util.concurrent.ExecutionException; 38 | // 39 | //@Service 40 | //public class RedisStorePersistService implements StorePersist { 41 | // 42 | // private static final Logger logger = LoggerFactory 43 | // .getLogger(RedisStorePersistService.class); 44 | // 45 | // public static volatile Boolean restartKafkaConsumerFlag = false; 46 | // public static volatile Boolean stopEtlWithExceptionFlag = false; 47 | // 48 | // @Autowired 49 | // @Qualifier("redisKafkaTemplate") 50 | // private RedisTemplate redisKafkaTemplate; 51 | // 52 | // @Autowired 53 | // private TaCenteredAlertSendService taCenteredAlertSendService; 54 | // 55 | // @Autowired 56 | // private MapperTaMysql mapperTaMysql; 57 | // 58 | // private DateTime lastMailForOffsetReset = null; 59 | // 60 | // 61 | // // private Retryer retryerWithResultNull = RetryerUtil 62 | // // .initRetryerWithIfResult(Predicates.isNull()); 63 | // 64 | // // 4分钟重试,不行就退出 65 | // private Retryer retryerWithResultFails = RetryerUtil 66 | // .initRetryerWithStopTimeIfResult(240L, Predicates.equalTo(false)); 67 | // 68 | // // 无限重试 69 | // private Retryer retryer = cn.thinkingdata.ta.etl.util.RetryerUtil.initRetryer(); 70 | // 71 | // 72 | // public KafkaConsumerOffset readOffset(String topic, Integer partition) { 73 | // return redisKafkaTemplate.execute(new RedisCallback() { 74 | // @Override 75 | // public KafkaConsumerOffset doInRedis(RedisConnection redisConnection) { 76 | // try { 77 | // if (partition == null || StringUtils.isBlank(topic)) { 78 | // logger.error("read kafkaConsumerOffset from redis error, the topic is " 79 | // + topic + ", the partition is " + partition); 80 | // return null; 81 | // } 82 | // KafkaConsumerOffset kafkaConsumerOffset = new KafkaConsumerOffset(); 83 | // RedisSerializer serializer = redisKafkaTemplate 84 | // .getStringSerializer(); 85 | //// KafkaMysqlOffsetParameter.kafkaClusterName = "tga"; 86 | //// KafkaMysqlOffsetParameter.consumerGroup = "taRealtimeEtlGroup"; 87 | // String redisKeystr = KafkaMysqlOffsetParameter.kafkaClusterName 88 | // + "-" 89 | // + topic 90 | // + "-" 91 | // + partition 92 | // + "-" 93 | // + KafkaMysqlOffsetParameter.consumerGroup; 94 | // byte[] redisKey = serializer.serialize(redisKeystr); 95 | // Map kafkaConsumerOffsetMapByte = redisConnection.hGetAll(redisKey); 96 | // if (MapUtils.isNotEmpty(kafkaConsumerOffsetMapByte)) { 97 | // for (byte[] key : kafkaConsumerOffsetMapByte.keySet()) { 98 | // if ("oid".equals(serializer.deserialize(key))) { 99 | // byte[] oidByte = kafkaConsumerOffsetMapByte.get(key); 100 | // if (oidByte != null) { 101 | //// Integer oid = Integer.parseInt(serializer 102 | //// .deserialize(oidByte)); 103 | // kafkaConsumerOffset.setOid(Integer.parseInt(serializer.deserialize(oidByte))); 104 | // } 105 | // } 106 | // if ("partition".equals(serializer.deserialize(key))) { 107 | // byte[] partitionByte = kafkaConsumerOffsetMapByte 108 | // .get(key); 109 | // if (partitionByte != null) { 110 | // kafkaConsumerOffset.setPartition(Integer.parseInt(serializer.deserialize(partitionByte))); 111 | // } 112 | // } 113 | // if ("topic".equals(serializer.deserialize(key))) { 114 | // byte[] topicByte = kafkaConsumerOffsetMapByte 115 | // .get(key); 116 | // if (topicByte != null) { 117 | // kafkaConsumerOffset.setTopic(serializer.deserialize(topicByte)); 118 | // } 119 | // } 120 | // if ("consumer_group".equals(serializer.deserialize(key))) { 121 | // byte[] consumer_groupByte = kafkaConsumerOffsetMapByte 122 | // .get(key); 123 | // if (consumer_groupByte != null) { 124 | // kafkaConsumerOffset.setConsumer_group(serializer.deserialize(consumer_groupByte)); 125 | // } 126 | // } 127 | // if ("offset".equals(serializer.deserialize(key))) { 128 | // byte[] offsetByte = kafkaConsumerOffsetMapByte 129 | // .get(key); 130 | // if (offsetByte != null) { 131 | // kafkaConsumerOffset.setOffset(Long.parseLong(serializer.deserialize(offsetByte))); 132 | // } 133 | // } 134 | // if ("last_flush_offset".equals(serializer.deserialize(key))) { 135 | // byte[] last_flush_offsetByte = kafkaConsumerOffsetMapByte 136 | // .get(key); 137 | // if (last_flush_offsetByte != null) { 138 | // kafkaConsumerOffset.setLast_flush_offset(Long.parseLong(serializer.deserialize(last_flush_offsetByte))); 139 | // } 140 | // } 141 | // if ("kafka_cluster_name".equals(serializer.deserialize(key))) { 142 | // byte[] kafka_cluster_nameByte = kafkaConsumerOffsetMapByte 143 | // .get(key); 144 | // if (kafka_cluster_nameByte != null) { 145 | // kafkaConsumerOffset.setKafka_cluster_name(serializer.deserialize(kafka_cluster_nameByte)); 146 | // } 147 | // } 148 | // if ("owner".equals(serializer.deserialize(key))) { 149 | // byte[] ownerByte = kafkaConsumerOffsetMapByte 150 | // .get(key); 151 | // if (ownerByte != null) { 152 | // kafkaConsumerOffset.setOwner(serializer.deserialize(ownerByte)); 153 | // } 154 | // } 155 | // if ("update_time".equals(serializer.deserialize(key))) { 156 | // byte[] update_timeByte = kafkaConsumerOffsetMapByte 157 | // .get(key); 158 | // if (update_timeByte != null) { 159 | // kafkaConsumerOffset.setUpdate_time(new Date(Long.parseLong(serializer.deserialize(update_timeByte)))); 160 | // } 161 | // } 162 | // if ("create_time".equals(serializer.deserialize(key))) { 163 | // byte[] create_timeByte = kafkaConsumerOffsetMapByte 164 | // .get(key); 165 | // if (create_timeByte != null) { 166 | // kafkaConsumerOffset.setCreate_time(new Date(Long.parseLong(serializer.deserialize(create_timeByte)))); 167 | // } 168 | // } 169 | // } 170 | // } 171 | // if (!kafkaConsumerOffset.isNull()) { 172 | // if (kafkaConsumerOffset.getCount() == null) { 173 | // kafkaConsumerOffset.setCount(0L); 174 | // } 175 | // } 176 | // logger.info("kafkaConsumerOffset in redis is " + kafkaConsumerOffset); 177 | // return kafkaConsumerOffset; 178 | // } catch (Exception e) { 179 | // logger.error("read kafkaConsumerOffset from redis error, the topic is " 180 | // + topic 181 | // + ", the partition is " 182 | // + partition 183 | // + ", the error is " 184 | // + CommonUtils.getStackTraceAsString(e)); 185 | // return null; 186 | // } 187 | // } 188 | // }); 189 | // } 190 | // 191 | // // private Boolean clearOffset(String topic, Integer partition) { 192 | // // return redisTemplate.execute(new RedisCallback() { 193 | // // @Override 194 | // // public Boolean doInRedis(RedisConnection redisConnection) { 195 | // // try { 196 | // // String redisKeystr = KafkaMysqlOffsetParameter.kafkaClusterName 197 | // // + "-" 198 | // // + topic 199 | // // + "-" 200 | // // + partition 201 | // // + "-" 202 | // // + KafkaMysqlOffsetParameter.consumerGroup; 203 | // // RedisSerializer serializer = redisTemplate 204 | // // .getStringSerializer(); 205 | // // byte[] redisKey = serializer.serialize(redisKeystr); 206 | // // Long del = redisConnection.del(redisKey); 207 | // // if (del != 1L) { 208 | // // logger.error("clear kafkaConsumerOffset from redis error, the topic is " 209 | // // + topic 210 | // // + ", the partition is " 211 | // // + partition 212 | // // + ", the del number is " + del.toString()); 213 | // // } 214 | // // return true; 215 | // // } catch (Exception e) { 216 | // // logger.error("clear kafkaConsumerOffset from redis error, the topic is " 217 | // // + topic 218 | // // + ", the partition is " 219 | // // + partition 220 | // // + ", the error is " 221 | // // + CommonUtils.getStackTraceAsString(e)); 222 | // // return false; 223 | // // } 224 | // // } 225 | // // }); 226 | // // } 227 | // 228 | // private Boolean saveOffset(KafkaConsumerOffset kafkaConsumerOffset) { 229 | // return redisKafkaTemplate.execute(new RedisCallback() { 230 | // @Override 231 | // public Boolean doInRedis(RedisConnection redisConnection) { 232 | // try { 233 | // if (kafkaConsumerOffset == null 234 | // || kafkaConsumerOffset.isNull()) { 235 | // logger.error("save offset fail in redis! kafkaConsumerOffset is " 236 | // + kafkaConsumerOffset); 237 | // return true; 238 | // } 239 | // RedisSerializer serializer = redisKafkaTemplate 240 | // .getStringSerializer(); 241 | // String redisKeystr = kafkaConsumerOffset 242 | // .getKafka_cluster_name() 243 | // + "-" 244 | // + kafkaConsumerOffset.getTopic() 245 | // + "-" 246 | // + kafkaConsumerOffset.getPartition() 247 | // + "-" 248 | // + kafkaConsumerOffset.getConsumer_group(); 249 | // byte[] redisKey = serializer.serialize(redisKeystr); 250 | // Map redisValue = new HashMap(); 251 | // if (kafkaConsumerOffset.getOid() != null) { 252 | // redisValue.put(serializer.serialize("oid"), serializer 253 | // .serialize(kafkaConsumerOffset.getOid() 254 | // .toString())); 255 | // } 256 | // redisValue.put(serializer.serialize("topic"), serializer 257 | // .serialize(kafkaConsumerOffset.getTopic())); 258 | // redisValue.put(serializer.serialize("partition"), 259 | // serializer.serialize(kafkaConsumerOffset 260 | // .getPartition().toString())); 261 | // redisValue.put(serializer.serialize("consumer_group"), 262 | // serializer.serialize(kafkaConsumerOffset 263 | // .getConsumer_group())); 264 | // redisValue.put(serializer.serialize("offset"), serializer 265 | // .serialize(kafkaConsumerOffset.getOffset() 266 | // .toString())); 267 | // redisValue.put(serializer.serialize("last_flush_offset"), 268 | // serializer.serialize(kafkaConsumerOffset 269 | // .getLast_flush_offset().toString())); 270 | // redisValue.put(serializer.serialize("kafka_cluster_name"), 271 | // serializer.serialize(kafkaConsumerOffset 272 | // .getKafka_cluster_name())); 273 | // redisValue.put(serializer.serialize("owner"), serializer 274 | // .serialize(kafkaConsumerOffset.getOwner())); 275 | // redisValue.put(serializer.serialize("update_time"), 276 | // serializer.serialize(Long 277 | // .toString(kafkaConsumerOffset 278 | // .getUpdate_time().getTime()))); 279 | // redisValue.put(serializer.serialize("create_time"), 280 | // serializer.serialize(Long 281 | // .toString(kafkaConsumerOffset 282 | // .getCreate_time().getTime()))); 283 | // redisConnection.hMSet(redisKey, redisValue); 284 | // return true; 285 | // } catch (Exception e) { 286 | // logger.error("write kafkaConsumerOffset" 287 | // + kafkaConsumerOffset.toString() 288 | // + " to redis error, the error is " 289 | // + CommonUtils.getStackTraceAsString(e)); 290 | // return false; 291 | // } 292 | // 293 | // } 294 | // }); 295 | // } 296 | // 297 | // // 去掉synchronized,因为MysqlOffsetPersist的flush和persist里有synchronized方法 298 | // @Override 299 | // public Boolean saveOffsetInBackupExternalStore( 300 | // KafkaConsumerOffset kafkaConsumerOffset) { 301 | // Long lag = kafkaConsumerOffset.getOffset() 302 | // - kafkaConsumerOffset.getLast_flush_offset(); 303 | // if (!lag.equals(0L)) { 304 | // logger.debug("because of the muti-thread, the value is not exactly right, the lag is " 305 | // + lag); 306 | // return saveOffset(kafkaConsumerOffset); 307 | // } 308 | // return true; 309 | // } 310 | // 311 | // @Override 312 | // public KafkaConsumerOffset readOffsetFromBackupExternalStore(String topic, 313 | // int partition) { 314 | // KafkaConsumerOffset kafkaConsumerOffsetFromBackupExternalStore = readOffset( 315 | // topic, partition); 316 | // return kafkaConsumerOffsetFromBackupExternalStore; 317 | // } 318 | // 319 | // // @Override 320 | // // public synchronized Boolean clearOffsetFromBackupExternalStore( 321 | // // String topic, int partition) { 322 | // // return clearOffset(topic, partition); 323 | // // } 324 | // 325 | // @Override 326 | // public KafkaConsumerOffset executeWhenReadNullFromBackupExternalStore( 327 | // String topic, Integer partition) { 328 | // // KafkaConsumerOffset kafkaConsumerOffsetFromBackupExternalStore = 329 | // // null; 330 | // // try { 331 | // // kafkaConsumerOffsetFromBackupExternalStore = (KafkaConsumerOffset) 332 | // // retryerWithResultNull 333 | // // .call(new Callable() { 334 | // // @Override 335 | // // public Object call() throws Exception { 336 | // // return readOffset(topic, partition); 337 | // // } 338 | // // }); 339 | // // } catch (ExecutionException | RetryException e) { 340 | // // logger.error("retry to read kafkaConsumerOffset from redis error, the error is " 341 | // // + CommonUtils.getStackTraceAsString(e)); 342 | // // } 343 | // // return kafkaConsumerOffsetFromBackupExternalStore; 344 | // logger.error("can not read offset from redis! exit the process!"); 345 | // stopEtlWithExceptionFlag = true; 346 | // try { 347 | // Thread.sleep(100000); 348 | // } catch (InterruptedException e) { 349 | // logger.error("------- thread can not sleep ---------------------" 350 | // + e.toString()); 351 | // } 352 | // return null; 353 | // } 354 | // 355 | // @Override 356 | // public KafkaConsumerOffset executeWhenReadNullFromMysql(String topic, 357 | // Integer partition) { 358 | // // KafkaConsumerOffset kafkaConsumerOffset = null; 359 | // // try { 360 | // // kafkaConsumerOffset = (KafkaConsumerOffset) retryerWithResultNull 361 | // // .call(new Callable() { 362 | // // @Override 363 | // // public Object call() throws Exception { 364 | // // return MysqlOffsetManager.getInstance() 365 | // // .readOffsetFromMysql(topic, partition); 366 | // // } 367 | // // }); 368 | // // if (kafkaConsumerOffset == null) { 369 | // // logger.error("the kafkaConsumerOffset read from mysql is null , the topic is " 370 | // // + topic 371 | // // + "the partition is " 372 | // // + partition 373 | // // + "exit the process unclear!"); 374 | // // System.exit(-1); 375 | // // } 376 | // // } catch (ExecutionException | RetryException e) { 377 | // // logger.error("the kafkaConsumerOffset read from mysql is null , the topic is " 378 | // // + topic 379 | // // + "the partition is " 380 | // // + partition 381 | // // + "exit the process unclear! the error is " 382 | // // + CommonUtils.getStackTraceAsString(e)); 383 | // // System.exit(-1); 384 | // // return null; 385 | // // } 386 | // // return kafkaConsumerOffset; 387 | // logger.error("can not read offset from mysql! exit the process!"); 388 | // stopEtlWithExceptionFlag = true; 389 | // try { 390 | // Thread.sleep(100000); 391 | // } catch (InterruptedException e) { 392 | // logger.error("------- thread can not sleep ---------------------" 393 | // + e.toString()); 394 | // } 395 | // return null; 396 | // } 397 | // 398 | // // @Override 399 | // // public Boolean executeWhenNotClearExternalStore(final String topic, 400 | // // final Integer partition) { 401 | // // Boolean flag = false; 402 | // // try { 403 | // // flag = (Boolean) retryerWithResultFails.call(new Callable() { 404 | // // @Override 405 | // // public Object call() throws Exception { 406 | // // return clearOffsetFromBackupExternalStore(topic, partition); 407 | // // } 408 | // // }); 409 | // // if (!flag) { 410 | // // logger.error("retry to clear kafkaConsumerOffset from backup external store error!"); 411 | // // } 412 | // // } catch (ExecutionException | RetryException e) { 413 | // // logger.error("retry to clear kafkaConsumerOffset from backup external store error, the error is " 414 | // // + CommonUtils.getStackTraceAsString(e)); 415 | // // return false; 416 | // // } 417 | // // return flag; 418 | // // } 419 | // 420 | // @Override 421 | // public Boolean executeWhenSaveOffsetFailInMysqlAndExternalStore( 422 | // final KafkaConsumerOffset kafkaConsumerOffset) { 423 | // logger.error("save offset fail in mysql and redis! retry!"); 424 | // Boolean flag = false; 425 | // try { 426 | // flag = (Boolean) retryerWithResultFails.call(new Callable() { 427 | // @Override 428 | // public Object call() throws Exception { 429 | // return MysqlOffsetPersist.getInstance().saveOffset( 430 | // kafkaConsumerOffset); 431 | // } 432 | // }); 433 | // if (!flag) { 434 | // logger.error("save offset fail in mysql and redis! exit the process unclear!"); 435 | // stopEtlWithExceptionFlag = true; 436 | // try { 437 | // Thread.sleep(100000); 438 | // } catch (InterruptedException e) { 439 | // logger.error("------- thread can not sleep ---------------------" 440 | // + e.toString()); 441 | // } 442 | // return null; 443 | // } 444 | // } catch (ExecutionException | RetryException e) { 445 | // logger.error("save offset fail in mysql and redis! exit the process unclear! the error is " 446 | // + CommonUtils.getStackTraceAsString(e)); 447 | // stopEtlWithExceptionFlag = true; 448 | // try { 449 | // Thread.sleep(100000); 450 | // } catch (InterruptedException e2) { 451 | // logger.error("------- thread can not sleep ---------------------" 452 | // + e2.toString()); 453 | // } 454 | // return null; 455 | // } 456 | // return flag; 457 | // } 458 | // 459 | // @Override 460 | // public Boolean backupStoreStateCheck() { 461 | // try { 462 | // return redisKafkaTemplate.execute(new RedisCallback() { 463 | // @Override 464 | // public Boolean doInRedis(RedisConnection redisConnection) { 465 | // try { 466 | // Boolean closed = redisConnection.isClosed(); 467 | // if (!closed) { 468 | // logger.info("redis reconnected!"); 469 | // } else { 470 | // logger.info("redis session closed!"); 471 | // } 472 | // return !closed; 473 | // } catch (Exception e) { 474 | // logger.info("redis connect error, the error is " + e); 475 | // return false; 476 | // } 477 | // } 478 | // }); 479 | // 480 | // } catch (Exception e) { 481 | // logger.error("redis connect error, the error is " + e); 482 | // return false; 483 | // } 484 | // 485 | // } 486 | // 487 | // @Override 488 | // public Boolean updateOwner(KafkaConsumerOffset kafkaConsumerOffset) { 489 | // Date now = new Date(); 490 | // if (kafkaConsumerOffset.getOffset() == 0L) { 491 | // kafkaConsumerOffset.setUpdate_time(now); 492 | // if (kafkaConsumerOffset.getCreate_time() == null) { 493 | // kafkaConsumerOffset.setCreate_time(now); 494 | // } 495 | // } 496 | // return updateOwnerInRedis(kafkaConsumerOffset); 497 | // } 498 | // 499 | // private Boolean updateOwnerInRedis(KafkaConsumerOffset kafkaConsumerOffset) { 500 | // try { 501 | // return redisKafkaTemplate.execute(new RedisCallback() { 502 | // @Override 503 | // public Boolean doInRedis(RedisConnection redisConnection) { 504 | // 505 | // if (kafkaConsumerOffset == null 506 | // || kafkaConsumerOffset.isNull()) { 507 | // logger.error("save offset fail in redis! kafkaConsumerOffset is " 508 | // + kafkaConsumerOffset); 509 | // return true; 510 | // } 511 | // RedisSerializer serializer = redisKafkaTemplate 512 | // .getStringSerializer(); 513 | // String redisKeystr = kafkaConsumerOffset 514 | // .getKafka_cluster_name() 515 | // + "-" 516 | // + kafkaConsumerOffset.getTopic() 517 | // + "-" 518 | // + kafkaConsumerOffset.getPartition() 519 | // + "-" 520 | // + kafkaConsumerOffset.getConsumer_group(); 521 | // byte[] redisKey = serializer.serialize(redisKeystr); 522 | // Map redisValue = new HashMap(); 523 | // if (kafkaConsumerOffset.getOid() != null) { 524 | // redisValue.put(serializer.serialize("oid"), serializer 525 | // .serialize(kafkaConsumerOffset.getOid() 526 | // .toString())); 527 | // } 528 | // redisValue.put(serializer.serialize("topic"), serializer 529 | // .serialize(kafkaConsumerOffset.getTopic())); 530 | // redisValue.put(serializer.serialize("partition"), 531 | // serializer.serialize(kafkaConsumerOffset 532 | // .getPartition().toString())); 533 | // redisValue.put(serializer.serialize("consumer_group"), 534 | // serializer.serialize(kafkaConsumerOffset 535 | // .getConsumer_group())); 536 | // redisValue.put(serializer.serialize("offset"), serializer 537 | // .serialize(kafkaConsumerOffset.getOffset() 538 | // .toString())); 539 | // redisValue.put(serializer.serialize("last_flush_offset"), 540 | // serializer.serialize(kafkaConsumerOffset 541 | // .getLast_flush_offset().toString())); 542 | // redisValue.put(serializer.serialize("kafka_cluster_name"), 543 | // serializer.serialize(kafkaConsumerOffset 544 | // .getKafka_cluster_name())); 545 | // redisValue.put(serializer.serialize("owner"), serializer 546 | // .serialize(kafkaConsumerOffset.getOwner())); 547 | // redisValue.put(serializer.serialize("update_time"), 548 | // serializer.serialize(Long 549 | // .toString(kafkaConsumerOffset 550 | // .getUpdate_time().getTime()))); 551 | // redisValue.put(serializer.serialize("create_time"), 552 | // serializer.serialize(Long 553 | // .toString(kafkaConsumerOffset 554 | // .getCreate_time().getTime()))); 555 | // redisConnection.hMSet(redisKey, redisValue); 556 | // return true; 557 | // 558 | // 559 | // } 560 | // }); 561 | // } catch (Exception e) { 562 | // logger.error("updateOwner in redis error, the error is " + CommonUtils.getStackTraceAsString(e)); 563 | // return false; 564 | // } 565 | // } 566 | // 567 | // @Override 568 | // public void executeWhenSessionTimeout(Integer count) { 569 | // logger.error("session will time out! the count is " + count 570 | // + ", the session time out is " 571 | // + KafkaMysqlOffsetParameter.sessionTimeout + ",exit unclear!"); 572 | // stopEtlWithExceptionFlag = true; 573 | // try { 574 | // Thread.sleep(100000); 575 | // } catch (InterruptedException e) { 576 | // logger.error("------- thread can not sleep ---------------------" 577 | // + e.toString()); 578 | // } 579 | // } 580 | // 581 | // @Override 582 | // public void executeWhenExecuteDataSessionTimeout(KafkaSubscribeConsumeThread kafkaSubscribeConsumeThread) { 583 | // logger.error("session time out!" + ", the session time out is " 584 | // + KafkaMysqlOffsetParameter.sessionTimeout); 585 | // restartKafkaConsumerFlag = true; 586 | // } 587 | // 588 | // @Override 589 | // public void executeWhenOffsetReset() { 590 | // logger.error("kafka consumer offset has no such offset!"); 591 | // try { 592 | // if(lastMailForOffsetReset == null || (lastMailForOffsetReset != null && lastMailForOffsetReset.plusMinutes(3).isBeforeNow())){ 593 | // Do_ta_company_info companyInfo = mapperTaMysql.getCompanyInfo(); 594 | // String mailContent = "公司名称:" + companyInfo.getCompany_name() + "
kafka Offset reset"; 595 | // taCenteredAlertSendService.alertByMail("ta_alert@thinkingdata.cn",null,"etl因为kafka的offset的reset而暂停60秒",mailContent); 596 | // lastMailForOffsetReset = new DateTime(); 597 | // } 598 | // Thread.sleep(3000); 599 | // } catch (Exception e) { 600 | // logger.error("send mail to ta_alert@thinkingdata.cn, " + Throwables.getStackTraceAsString(e)); 601 | // } 602 | // } 603 | // 604 | // @Override 605 | // public void executeWhenException() { 606 | // logger.error("kafka consumer offset reset, exit unclear"); 607 | // try { 608 | // Do_ta_company_info companyInfo = mapperTaMysql.getCompanyInfo(); 609 | // String mailContent = "公司名称:" + companyInfo.getCompany_name() + "
kafka读取异常,请人工介入"; 610 | // taCenteredAlertSendService.alertByMail("ta_alert@thinkingdata.cn",null,"etl因为kafka的offset异常退出",mailContent); 611 | // } catch (Exception e) { 612 | // logger.error("send mail to ta_alert@thinkingdata.cn, " + Throwables.getStackTraceAsString(e)); 613 | // } 614 | // // do not let monitor restart 615 | //// try { 616 | //// setKeyVal(TaConstants.ETL_RESET_MARK, "1"); 617 | //// } catch (ExecutionException | RetryException e) { 618 | //// logger.error("set etl reset mark error, " + e.toString()); 619 | //// } 620 | // stopEtlWithExceptionFlag = true; 621 | // try { 622 | // Thread.sleep(80000); 623 | // } catch (InterruptedException e) { 624 | // logger.error("------- thread can not sleep ---------------------" + e.toString()); 625 | // } 626 | // } 627 | // 628 | //// public Object setKeyVal(String key, String value) throws ExecutionException, RetryException { 629 | //// return retryer.call(new Callable() { 630 | //// @Override 631 | //// public Boolean call() throws Exception { 632 | //// return redisKafkaTemplate.execute((RedisCallback) redisConnection -> { 633 | //// RedisSerializer serializer = redisKafkaTemplate.getStringSerializer(); 634 | //// return redisConnection.set(serializer.serialize(key), serializer.serialize(value)); 635 | //// }); 636 | //// } 637 | //// }); 638 | //// } 639 | // 640 | // 641 | //} 642 | // 643 | // 644 | // 645 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/consumer/persist/StorePersist.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.consumer.persist; 2 | 3 | import cn.thinkingdata.kafka.consumer.dao.KafkaConsumerOffset; 4 | import cn.thinkingdata.kafka.consumer.exception.ExceptionHandler; 5 | 6 | public interface StorePersist extends ExceptionHandler { 7 | 8 | // 从另一个备用存储读取的接口如果读取成功,则把它删除,默认是空 9 | KafkaConsumerOffset readOffsetFromBackupExternalStore(String topic, int partition); 10 | 11 | // 写一个存到备用存储的接口,默认是空 12 | Boolean saveOffsetInBackupExternalStore(KafkaConsumerOffset kafkaConsumerOffset); 13 | 14 | Boolean backupStoreStateCheck(); 15 | 16 | Boolean updateOwner(KafkaConsumerOffset kafkaConsumerOffset); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/test/ProcessDataThread.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.test; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.concurrent.atomic.AtomicLong; 7 | 8 | public class ProcessDataThread implements Runnable { 9 | 10 | private static final Logger logger = LoggerFactory 11 | .getLogger(ProcessDataThread.class); 12 | 13 | public static AtomicLong messageCount = new AtomicLong(0L); 14 | 15 | private final String key; 16 | private final String value; 17 | 18 | public ProcessDataThread(String key, String value) { 19 | this.key = key; 20 | this.value = value; 21 | } 22 | 23 | @Override 24 | public void run() { 25 | logger.info(key + "-----" + value); 26 | long count = messageCount.incrementAndGet(); 27 | logger.info(Thread.currentThread().getName() + "-------" + count); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/test/TestMain.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.test; 2 | 3 | 4 | import cn.thinkingdata.kafka.close.ScanTermMethod; 5 | import cn.thinkingdata.kafka.consumer.KafkaSubscribeConsumer; 6 | import cn.thinkingdata.kafka.consumer.NewIDataLineProcessor; 7 | import org.apache.kafka.clients.consumer.ConsumerRecord; 8 | import org.apache.log4j.PropertyConfigurator; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.io.IOException; 13 | import java.net.URL; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.concurrent.LinkedBlockingQueue; 17 | import java.util.concurrent.ThreadPoolExecutor; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | public class TestMain { 21 | 22 | private static final Logger logger = LoggerFactory 23 | .getLogger(TestMain.class); 24 | 25 | static String jdbcUrl = "jdbc:mysql://ta1:3306/ta?autoReconnect=true&useUnicode=true"; 26 | // static String dataProcessNum = "3"; 27 | static KafkaSubscribeConsumer consumers; 28 | 29 | 30 | public static void main(String[] args) { 31 | 32 | String brokerList = args[0]; 33 | String kafkaClusterName = args[1]; 34 | String topic = args[2]; 35 | String consumerGroup = args[3]; 36 | String processThreadNum = args[4]; 37 | String flushOffsetSize = args[5]; 38 | String flushInterval = args[6]; 39 | final String dataProcessNum = args[7]; 40 | String maxPartitionFetchBytes = args[8]; 41 | 42 | URL url = TestMain.class.getResource("/log4j.properties"); 43 | PropertyConfigurator.configure(url); 44 | 45 | 46 | NewIDataLineProcessor dataProcessor = new NewIDataLineProcessor() { 47 | 48 | ThreadPoolExecutor executorService = new ThreadPoolExecutor(Integer.parseInt(dataProcessNum), Integer.parseInt(dataProcessNum), 49 | 0L, TimeUnit.MILLISECONDS, 50 | new LinkedBlockingQueue(500));{ 51 | executorService.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 52 | } 53 | 54 | @Override 55 | public void processData(ConsumerRecord consumerRecord) { 56 | executorService.submit(new ProcessDataThread(consumerRecord.key(), consumerRecord.value())); 57 | } 58 | 59 | @Override 60 | public void finishProcess() { 61 | // 关闭线程池 62 | if (executorService != null) 63 | executorService.shutdown(); 64 | try { 65 | if (!executorService.awaitTermination(5000, TimeUnit.MILLISECONDS)) { 66 | logger.warn("Timed out waiting for data process threads to shut down, exiting uncleanly"); 67 | } 68 | } catch (InterruptedException e) { 69 | logger.warn("Interrupted during shutdown, exiting uncleanly"); 70 | } 71 | } 72 | }; 73 | 74 | Map map = new HashMap<>(); 75 | map.put("jdbc.url", jdbcUrl); 76 | map.put("username", "ta"); 77 | map.put("password", "ThinkingData2018"); 78 | map.put("table.name", "kafka_consumer_offset"); 79 | map.put("broker.list", brokerList); 80 | map.put("kafka.cluster.name", kafkaClusterName); 81 | map.put("topic", topic); 82 | map.put("consumer.group", consumerGroup); 83 | map.put("process.thread.num", processThreadNum); 84 | map.put("flush.offset.size", flushOffsetSize); 85 | map.put("flush.interval", flushInterval); 86 | if (maxPartitionFetchBytes != null) { 87 | map.put("max.partition.fetch.bytes", maxPartitionFetchBytes); 88 | } 89 | consumers = new KafkaSubscribeConsumer(map, dataProcessor, new ScanTermMethod()); 90 | 91 | consumers.run(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/util/CommonUtils.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.util; 2 | 3 | 4 | import java.io.PrintWriter; 5 | import java.io.StringWriter; 6 | import java.net.InetAddress; 7 | import java.net.UnknownHostException; 8 | 9 | public class CommonUtils { 10 | 11 | public static String getHostNameForLiunx() { 12 | try { 13 | return (InetAddress.getLocalHost()).getHostName(); 14 | } catch (UnknownHostException uhe) { 15 | String host = uhe.getMessage(); // host = "hostname: hostname" 16 | if (host != null) { 17 | int colon = host.indexOf(':'); 18 | if (colon > 0) { 19 | return host.substring(0, colon); 20 | } 21 | } 22 | return "UnknownHost"; 23 | } 24 | } 25 | 26 | public static String getHostName() { 27 | if (System.getenv("COMPUTERNAME") != null) { 28 | return System.getenv("COMPUTERNAME"); 29 | } else { 30 | return getHostNameForLiunx(); 31 | } 32 | } 33 | 34 | public static String getStackTraceAsString(Throwable throwable) { 35 | StringWriter stringWriter = new StringWriter(); 36 | throwable.printStackTrace(new PrintWriter(stringWriter)); 37 | return stringWriter.toString(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/cn/thinkingdata/kafka/util/RetryerUtil.java: -------------------------------------------------------------------------------- 1 | package cn.thinkingdata.kafka.util; 2 | 3 | import com.github.rholder.retry.Attempt; 4 | import com.github.rholder.retry.RetryListener; 5 | import com.github.rholder.retry.Retryer; 6 | import com.github.rholder.retry.RetryerBuilder; 7 | import com.github.rholder.retry.StopStrategies; 8 | import com.github.rholder.retry.WaitStrategies; 9 | import com.google.common.base.Predicate; 10 | import com.google.common.base.Throwables; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.concurrent.TimeUnit; 15 | 16 | /** 17 | * Created by yangruchen on 2019/1/10. 18 | */ 19 | public class RetryerUtil { 20 | private static final Logger logger = LoggerFactory.getLogger(RetryerUtil.class); 21 | 22 | public static Retryer initRetryerByTimesWithIfResult(int retryTimes,long sleepMilliseconds, Predicate predicate){ 23 | Retryer retryer = RetryerBuilder.newBuilder().retryIfException().retryIfResult(predicate) 24 | .withWaitStrategy(WaitStrategies.fixedWait(sleepMilliseconds,TimeUnit.MILLISECONDS)) 25 | .withStopStrategy(StopStrategies.stopAfterAttempt(retryTimes)) 26 | .withRetryListener(new RetryListener() { 27 | @Override 28 | public void onRetry(Attempt attempt) { 29 | if (attempt.hasException()){ 30 | logger.error(Throwables.getStackTraceAsString(attempt.getExceptionCause())); 31 | } 32 | if(attempt.getAttemptNumber() > 1L){ 33 | logger.info("开始进行失败重试,重试次数:" + attempt.getAttemptNumber() + ", 距离第一次失败时间:" + attempt.getDelaySinceFirstAttempt() + "毫秒"); 34 | } 35 | } 36 | }) 37 | .build(); 38 | return retryer; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | ### set log levels ### 2 | log4j.rootLogger=INFO,infoFile 3 | #log4j.Logger=search,Test 4 | 5 | ### console ### 6 | log4j.appender.stdout = org.apache.log4j.ConsoleAppender 7 | log4j.appender.stdout.Threshold = INFO 8 | log4j.appender.stdout.Target = System.out 9 | log4j.appender.stdout.layout = org.apache.log4j.PatternLayout 10 | log4j.appender.stdout.layout.ConversionPattern =[%p] [%-d{yyyy-MM-dd HH:mm:ss}] %C.%M(%L) | %m%n 11 | 12 | log4j.appender.infoFile = org.apache.log4j.DailyRollingFileAppender 13 | log4j.appender.infoFile.Threshold = INFO 14 | log4j.appender.infoFile.File = ./kafka-consumer.log 15 | log4j.appender.infoFile.DatePattern = '.'yyyy-MM-dd'.log' 16 | log4j.appender.infoFile.Append=true 17 | log4j.appender.infoFile.layout = org.apache.log4j.PatternLayout 18 | log4j.appender.infoFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m %x %n 19 | --------------------------------------------------------------------------------