├── .gitignore ├── HEADER ├── LICENSE ├── client ├── pom.xml └── src │ └── main │ ├── java │ └── cn │ │ └── think │ │ └── in │ │ └── java │ │ └── raft │ │ └── client │ │ ├── RaftClient1.java │ │ ├── RaftClient2.java │ │ ├── RaftClient3.java │ │ ├── RaftClientRPC.java │ │ ├── RaftClientWithCommandLine.java │ │ └── util │ │ └── SleepHelper.java │ └── resources │ └── log4j.xml ├── common ├── pom.xml └── src │ └── main │ └── java │ └── cn │ └── think │ └── in │ └── java │ └── raft │ └── common │ ├── LifeCycle.java │ ├── RaftRemotingException.java │ ├── entity │ ├── AentryParam.java │ ├── AentryResult.java │ ├── ClientKVAck.java │ ├── ClientKVReq.java │ ├── Command.java │ ├── LogEntry.java │ ├── NodeConfig.java │ ├── NodeStatus.java │ ├── Peer.java │ ├── PeerSet.java │ ├── ReplicationFailModel.java │ ├── RvoteParam.java │ └── RvoteResult.java │ └── rpc │ ├── DefaultRpcClient.java │ ├── Request.java │ ├── Response.java │ └── RpcClient.java ├── pom.xml ├── readme.md └── server ├── pom.xml └── src ├── main ├── java │ └── cn │ │ └── think │ │ └── in │ │ └── java │ │ └── raft │ │ └── server │ │ ├── Consensus.java │ │ ├── LogModule.java │ │ ├── Node.java │ │ ├── RaftNodeBootStrap.java │ │ ├── StateMachine.java │ │ ├── changes │ │ ├── ClusterMembershipChanges.java │ │ ├── Result.java │ │ └── Server.java │ │ ├── config │ │ └── Constant.java │ │ ├── constant │ │ └── StateMachineSaveType.java │ │ ├── current │ │ ├── RaftThread.java │ │ ├── RaftThreadPool.java │ │ └── RaftThreadPoolExecutor.java │ │ ├── exception │ │ └── RaftNotSupportException.java │ │ ├── impl │ │ ├── ClusterMembershipChangesImpl.java │ │ ├── DefaultConsensus.java │ │ ├── DefaultLogModule.java │ │ ├── DefaultNode.java │ │ ├── DefaultStateMachine.java │ │ └── RedisStateMachine.java │ │ ├── rpc │ │ ├── DefaultRpcServiceImpl.java │ │ ├── RaftUserProcessor.java │ │ └── RpcService.java │ │ └── util │ │ └── LongConvert.java └── resources │ └── log4j.xml └── test └── java └── cn └── think └── in └── java └── raft └── server └── impl ├── DefaultLogModuleTest.java ├── DefaultStateMachineTest.java └── RocksDBTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /target 3 | lu-raft/.idea 4 | *.iml 5 | /rocksDB-raft 6 | .idea 7 | target/ 8 | -------------------------------------------------------------------------------- /HEADER: -------------------------------------------------------------------------------- 1 | Licensed to the Apache Software Foundation (ASF) under one or more 2 | contributor license agreements. See the NOTICE file distributed with 3 | this work for additional information regarding copyright ownership. 4 | The ASF licenses this file to You under the Apache License, Version 2.0 5 | (the "License"); you may not use this file except in compliance with 6 | the License. You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.think.in.java 8 | lu-raft-parent 9 | 1.0-RELEASE 10 | 11 | 12 | org.example 13 | client 14 | 15 | 16 | 8 17 | 8 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.slf4j 24 | slf4j-api 25 | 26 | 27 | 28 | org.slf4j 29 | slf4j-log4j12 30 | 31 | 32 | 33 | com.alipay.sofa 34 | bolt 35 | 36 | 37 | slf4j-api 38 | org.slf4j 39 | 40 | 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | 46 | 47 | cn.think.in.java 48 | common 49 | 50 | 51 | com.google.guava 52 | guava 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /client/src/main/java/cn/think/in/java/raft/client/RaftClient1.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.client; 18 | 19 | import cn.think.in.java.raft.common.entity.LogEntry; 20 | import cn.think.in.java.raft.client.util.SleepHelper; 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | /** 24 | * @author 莫那·鲁道 25 | */ 26 | @Slf4j 27 | public class RaftClient1 { 28 | 29 | public static void main(String[] args) throws Throwable { 30 | 31 | RaftClientRPC rpc = new RaftClientRPC(); 32 | 33 | for (int i = 3; i > -1; i++) { 34 | try { 35 | String key = "hello:" + i; 36 | String value = "world:" + i; 37 | 38 | String putResult = rpc.put(key, value); 39 | 40 | log.info("key = {}, value = {}, put response : {}", key, value, putResult); 41 | 42 | SleepHelper.sleep(1000); 43 | 44 | LogEntry logEntry = rpc.get(key); 45 | 46 | log.info("key = {}, value = {}, get response : {}", key, value, logEntry); 47 | } catch (Exception e) { 48 | log.error(e.getMessage(), e); 49 | i = i - 1; 50 | } 51 | 52 | SleepHelper.sleep(5000); 53 | } 54 | 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /client/src/main/java/cn/think/in/java/raft/client/RaftClient2.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.client; 18 | 19 | import cn.think.in.java.raft.common.entity.LogEntry; 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | /** 23 | * @author 莫那·鲁道 24 | */ 25 | @Slf4j 26 | public class RaftClient2 { 27 | 28 | public static void main(String[] args) throws Throwable { 29 | 30 | RaftClientRPC rpc = new RaftClientRPC(); 31 | 32 | for (int i = 3; ; i++) { 33 | try { 34 | String key = "hello:" + i; 35 | 36 | LogEntry logEntry = rpc.get(key); 37 | 38 | log.info("key={}, get response : {}", key, logEntry); 39 | } catch (Exception e) { 40 | // ignore 41 | e.printStackTrace(); 42 | } finally { 43 | Thread.sleep(1000); 44 | } 45 | 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /client/src/main/java/cn/think/in/java/raft/client/RaftClient3.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.client; 18 | 19 | import cn.think.in.java.raft.common.entity.LogEntry; 20 | import cn.think.in.java.raft.client.util.SleepHelper; 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | /** 24 | * @author 莫那·鲁道 25 | */ 26 | @Slf4j 27 | public class RaftClient3 { 28 | 29 | public static void main(String[] args) throws Throwable { 30 | 31 | RaftClientRPC rpc = new RaftClientRPC(); 32 | 33 | int keyNum = 4; 34 | try { 35 | 36 | String key = "hello:" + keyNum; 37 | String value = "world:" + keyNum; 38 | 39 | String putResult = rpc.put(key, value); 40 | 41 | log.info("put response : {}, key={}, value={}", putResult, key, value); 42 | 43 | SleepHelper.sleep(1000); 44 | 45 | LogEntry logEntry = rpc.get(key); 46 | 47 | if (logEntry == null) { 48 | log.error("get logEntry : null, key={}", key); 49 | System.exit(1); 50 | return; 51 | } 52 | log.info("get logEntry : {}, key = {}", logEntry, key); 53 | } catch (Exception e) { 54 | e.printStackTrace(); 55 | } 56 | 57 | System.exit(1); 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /client/src/main/java/cn/think/in/java/raft/client/RaftClientRPC.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.client; 18 | 19 | //import raft.server.entity.LogEntry; 20 | //import raft.server.rpc.DefaultRpcClient; 21 | //import raft.server.rpc.Request; 22 | //import raft.server.rpc.RpcClient; 23 | //import com.google.common.collect.Lists; 24 | 25 | import cn.think.in.java.raft.common.entity.ClientKVAck; 26 | import cn.think.in.java.raft.common.entity.ClientKVReq; 27 | import cn.think.in.java.raft.common.rpc.DefaultRpcClient; 28 | import cn.think.in.java.raft.common.entity.LogEntry; 29 | import cn.think.in.java.raft.common.rpc.Request; 30 | import cn.think.in.java.raft.common.rpc.RpcClient; 31 | import com.google.common.collect.Lists; 32 | 33 | import java.util.List; 34 | import java.util.concurrent.atomic.AtomicLong; 35 | 36 | /** 37 | * @author gwk_2 38 | * @date 2022/4/19 15:50 39 | */ 40 | public class RaftClientRPC { 41 | 42 | private static List list = Lists.newArrayList("localhost:8777", "localhost:8778", "localhost:8779"); 43 | 44 | private final static RpcClient CLIENT = new DefaultRpcClient(); 45 | 46 | private AtomicLong count = new AtomicLong(3); 47 | 48 | public RaftClientRPC() throws Throwable { 49 | CLIENT.init(); 50 | } 51 | 52 | /** 53 | * @param key 54 | * @return 55 | */ 56 | public LogEntry get(String key) { 57 | ClientKVReq obj = ClientKVReq.builder().key(key).type(ClientKVReq.GET).build(); 58 | 59 | int index = (int) (count.incrementAndGet() % list.size()); 60 | 61 | String addr = list.get(index); 62 | 63 | ClientKVAck response; 64 | Request r = Request.builder().obj(obj).url(addr).cmd(Request.CLIENT_REQ).build(); 65 | try { 66 | response = CLIENT.send(r); 67 | } catch (Exception e) { 68 | r.setUrl(list.get((int) ((count.incrementAndGet()) % list.size()))); 69 | response = CLIENT.send(r); 70 | } 71 | 72 | return (LogEntry)response.getResult(); 73 | } 74 | 75 | /** 76 | * @param key 77 | * @param value 78 | * @return 79 | */ 80 | public String put(String key, String value) { 81 | int index = (int) (count.incrementAndGet() % list.size()); 82 | 83 | String addr = list.get(index); 84 | ClientKVReq obj = ClientKVReq.builder().key(key).value(value).type(ClientKVReq.PUT).build(); 85 | 86 | Request r = Request.builder().obj(obj).url(addr).cmd(Request.CLIENT_REQ).build(); 87 | ClientKVAck response; 88 | try { 89 | response = CLIENT.send(r); 90 | } catch (Exception e) { 91 | r.setUrl(list.get((int) ((count.incrementAndGet()) % list.size()))); 92 | response = CLIENT.send(r); 93 | } 94 | 95 | return response.getResult().toString(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /client/src/main/java/cn/think/in/java/raft/client/RaftClientWithCommandLine.java: -------------------------------------------------------------------------------- 1 | package cn.think.in.java.raft.client; 2 | 3 | import cn.think.in.java.raft.common.entity.LogEntry; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.util.Scanner; 7 | 8 | /** 9 | * Created by 大东 on 2023/2/25. 10 | */ 11 | @Slf4j 12 | public class RaftClientWithCommandLine { 13 | 14 | public static void main(String[] args) throws Throwable { 15 | 16 | RaftClientRPC rpc = new RaftClientRPC(); 17 | 18 | // 从键盘接收数据 19 | Scanner scan = new Scanner(System.in); 20 | 21 | // nextLine方式接收字符串 22 | System.out.println("Raft client is running, please input command:"); 23 | 24 | while (scan.hasNextLine()){ 25 | String input = scan.nextLine(); 26 | if (input.equals("exit")){ 27 | scan.close(); 28 | return; 29 | } 30 | String[] raftArgs = input.split(" "); 31 | int n = raftArgs.length; 32 | if (n != 2 && n != 3){ 33 | System.out.println("invalid input"); 34 | continue; 35 | } 36 | 37 | // get [key] 38 | if (n == 2){ 39 | if (!raftArgs[0].equals("get")){ 40 | System.out.println("invalid input"); 41 | continue; 42 | } 43 | LogEntry logEntry = rpc.get(raftArgs[1]); 44 | if (logEntry == null || logEntry.getCommand() == null){ 45 | System.out.println("null"); 46 | } else { 47 | System.out.println(logEntry.getCommand().getValue()); 48 | } 49 | 50 | } 51 | 52 | // [op] [key] [value] 53 | if (n == 3){ 54 | if (raftArgs[0].equals("put")){ 55 | System.out.println(rpc.put(raftArgs[1], raftArgs[2])); 56 | } else { 57 | System.out.println("invalid input"); 58 | } 59 | } 60 | } 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /client/src/main/java/cn/think/in/java/raft/client/util/SleepHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.client.util; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.util.concurrent.TimeUnit; 23 | 24 | /** 25 | * 26 | * @author 莫那·鲁道 27 | */ 28 | public class SleepHelper { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(SleepHelper.class); 31 | 32 | 33 | public static void sleep(int ms) { 34 | try { 35 | Thread.sleep(ms); 36 | } catch (InterruptedException e) { 37 | LOGGER.warn(e.getMessage()); 38 | } 39 | 40 | } 41 | 42 | public static void sleep2(int seconds) { 43 | try { 44 | TimeUnit.SECONDS.sleep(seconds); 45 | } catch (InterruptedException e) { 46 | LOGGER.warn(e.getMessage()); 47 | } 48 | 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /client/src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.think.in.java 8 | lu-raft-parent 9 | 1.0-RELEASE 10 | 11 | 12 | common 13 | 14 | 15 | 8 16 | 8 17 | UTF-8 18 | 19 | 20 | 21 | 22 | 23 | org.slf4j 24 | slf4j-api 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 31 | 32 | 33 | com.alipay.sofa 34 | bolt 35 | 36 | 37 | slf4j-api 38 | org.slf4j 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/LifeCycle.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common; 18 | 19 | /** 20 | * 21 | * @author 莫那·鲁道 22 | */ 23 | public interface LifeCycle { 24 | 25 | /** 26 | * 初始化. 27 | * @throws Throwable 28 | */ 29 | void init() throws Throwable; 30 | 31 | /** 32 | * 关闭资源. 33 | * @throws Throwable 34 | */ 35 | void destroy() throws Throwable; 36 | } 37 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/RaftRemotingException.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common; 18 | 19 | /** 20 | * 21 | */ 22 | public class RaftRemotingException extends RuntimeException { 23 | 24 | public RaftRemotingException() { 25 | super(); 26 | } 27 | 28 | public RaftRemotingException(String message) { 29 | super(message); 30 | } 31 | 32 | public RaftRemotingException(Throwable cause) { 33 | super(cause); 34 | } 35 | 36 | public RaftRemotingException(String message, Throwable cause) { 37 | super(message, cause); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/AentryParam.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | import lombok.ToString; 23 | 24 | import java.io.Serializable; 25 | 26 | /** 27 | * 28 | * 附加日志 RPC 参数. handlerAppendEntries 29 | * 30 | * @author 莫那·鲁道 31 | */ 32 | @Getter 33 | @Setter 34 | @ToString 35 | @Builder 36 | public class AentryParam implements Serializable { 37 | 38 | /** 候选人的任期号 */ 39 | private long term; 40 | 41 | /** 被请求者 ID(ip:selfPort) */ 42 | private String serverId; 43 | 44 | /** 领导人的 Id,以便于跟随者重定向请求 */ 45 | private String leaderId; 46 | 47 | /**新的日志条目紧随之前的索引值 */ 48 | private long prevLogIndex; 49 | 50 | /** prevLogIndex 条目的任期号 */ 51 | private long preLogTerm; 52 | 53 | /** 准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提高效率) */ 54 | private LogEntry[] entries; 55 | 56 | /** 领导人已经提交的日志的索引值 */ 57 | private long leaderCommit; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/AentryResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | import lombok.ToString; 22 | 23 | import java.io.Serializable; 24 | 25 | /** 26 | * 27 | * 附加 RPC 日志返回值. 28 | * 29 | * @author 莫那·鲁道 30 | */ 31 | @Setter 32 | @Getter 33 | @ToString 34 | public class AentryResult implements Serializable { 35 | 36 | /** 当前的任期号,用于领导人去更新自己 */ 37 | long term; 38 | 39 | /** 跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真 */ 40 | boolean success; 41 | 42 | public AentryResult(boolean success) { 43 | this.success = success; 44 | } 45 | 46 | private AentryResult(Builder builder) { 47 | setTerm(builder.term); 48 | setSuccess(builder.success); 49 | } 50 | 51 | public static AentryResult fail() { 52 | return new AentryResult(false); 53 | } 54 | 55 | public static AentryResult ok() { 56 | return new AentryResult(true); 57 | } 58 | 59 | public static Builder newBuilder() { 60 | return new Builder(); 61 | } 62 | 63 | 64 | public static final class Builder { 65 | 66 | private long term; 67 | private boolean success; 68 | 69 | private Builder() { 70 | } 71 | 72 | public Builder term(long val) { 73 | term = val; 74 | return this; 75 | } 76 | 77 | public Builder success(boolean val) { 78 | success = val; 79 | return this; 80 | } 81 | 82 | public AentryResult build() { 83 | return new AentryResult(this); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/ClientKVAck.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | import lombok.ToString; 22 | 23 | import java.io.Serializable; 24 | 25 | /** 26 | * 27 | * @author 莫那·鲁道 28 | */ 29 | @Getter 30 | @Setter 31 | @ToString 32 | public class ClientKVAck implements Serializable { 33 | 34 | Object result; 35 | 36 | public ClientKVAck(Object result) { 37 | this.result = result; 38 | } 39 | 40 | private ClientKVAck(Builder builder) { 41 | setResult(builder.result); 42 | } 43 | 44 | public static ClientKVAck ok() { 45 | return new ClientKVAck("ok"); 46 | } 47 | 48 | public static ClientKVAck fail() { 49 | return new ClientKVAck("fail"); 50 | } 51 | 52 | public static Builder newBuilder() { 53 | return new Builder(); 54 | } 55 | 56 | 57 | public static final class Builder { 58 | 59 | private Object result; 60 | 61 | private Builder() { 62 | } 63 | 64 | public Builder result(Object val) { 65 | result = val; 66 | return this; 67 | } 68 | 69 | public ClientKVAck build() { 70 | return new ClientKVAck(this); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/ClientKVReq.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Builder; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | import lombok.ToString; 23 | 24 | import java.io.Serializable; 25 | 26 | /** 27 | * 28 | * @author 莫那·鲁道 29 | */ 30 | @Getter 31 | @Setter 32 | @ToString 33 | @Builder 34 | public class ClientKVReq implements Serializable { 35 | 36 | public static int PUT = 0; 37 | public static int GET = 1; 38 | 39 | int type; 40 | 41 | String key; 42 | 43 | String value; 44 | 45 | public enum Type { 46 | /** 1111 */ 47 | PUT(0), GET(1); 48 | int code; 49 | 50 | Type(int code) { 51 | this.code = code; 52 | } 53 | 54 | public static Type value(int code ) { 55 | for (Type type : values()) { 56 | if (type.code == code) { 57 | return type; 58 | } 59 | } 60 | return null; 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/Command.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.*; 20 | 21 | import java.io.Serializable; 22 | 23 | /** 24 | * 25 | * @author 莫那·鲁道 26 | */ 27 | @Getter 28 | @Setter 29 | @ToString 30 | @Builder 31 | @AllArgsConstructor 32 | @NoArgsConstructor 33 | public class Command implements Serializable { 34 | 35 | String key; 36 | 37 | String value; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/LogEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.AllArgsConstructor; 20 | import lombok.Builder; 21 | import lombok.Data; 22 | import lombok.NoArgsConstructor; 23 | 24 | import java.io.Serializable; 25 | 26 | /** 27 | * 日志条目 28 | * 29 | * @author 莫那·鲁道 30 | */ 31 | @Data 32 | @Builder 33 | @NoArgsConstructor 34 | @AllArgsConstructor 35 | public class LogEntry implements Serializable, Comparable { 36 | 37 | private Long index; 38 | 39 | private long term; 40 | 41 | private Command command; 42 | 43 | @Override 44 | public int compareTo(Object o) { 45 | if (o == null) { 46 | return -1; 47 | } 48 | if (this.getIndex() > ((LogEntry) o).getIndex()) { 49 | return 1; 50 | } 51 | return -1; 52 | } 53 | 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/NodeConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | import lombok.ToString; 22 | 23 | import java.util.List; 24 | 25 | /** 26 | * 27 | * 节点配置 28 | * 29 | * @author 莫那·鲁道 30 | */ 31 | @Getter 32 | @Setter 33 | @ToString 34 | public class NodeConfig { 35 | 36 | /** 自身 selfPort */ 37 | public int selfPort; 38 | 39 | /** 所有节点地址. */ 40 | public List peerAddrs; 41 | /** 42 | * 状态快照存储类型 43 | */ 44 | public String stateMachineSaveType; 45 | } 46 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/NodeStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Getter; 20 | 21 | /** 22 | * 23 | * @author 莫那·鲁道 24 | */ 25 | public interface NodeStatus { 26 | 27 | int FOLLOWER = 0; 28 | int CANDIDATE = 1; 29 | int LEADER = 2; 30 | 31 | @Getter 32 | enum Enum { 33 | FOLLOWER(0), CANDIDATE(1), LEADER(2); 34 | 35 | Enum(int code) { 36 | this.code = code; 37 | } 38 | 39 | final int code; 40 | 41 | public static Enum value(int i) { 42 | for (Enum value : Enum.values()) { 43 | if (value.code == i) { 44 | return value; 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/Peer.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | 22 | import java.util.Objects; 23 | 24 | /** 25 | * 当前节点的 同伴. 26 | * 27 | * @author 莫那·鲁道 28 | */ 29 | @Getter 30 | @Setter 31 | public class Peer { 32 | 33 | /** ip:selfPort */ 34 | private final String addr; 35 | 36 | 37 | public Peer(String addr) { 38 | this.addr = addr; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) { 44 | return true; 45 | } 46 | if (o == null || getClass() != o.getClass()) { 47 | return false; 48 | } 49 | Peer peer = (Peer) o; 50 | return Objects.equals(addr, peer.addr); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | 56 | return Objects.hash(addr); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "Peer{" + 62 | "addr='" + addr + '\'' + 63 | '}'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/PeerSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import java.io.Serializable; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * 25 | * 节点集合. 去重. 26 | * 27 | * @author 莫那·鲁道 28 | */ 29 | public class PeerSet implements Serializable { 30 | 31 | private List list = new ArrayList<>(); 32 | 33 | private volatile Peer leader; 34 | 35 | /** final */ 36 | private volatile Peer self; 37 | 38 | private PeerSet() { 39 | } 40 | 41 | public static PeerSet getInstance() { 42 | return PeerSetLazyHolder.INSTANCE; 43 | } 44 | 45 | private static class PeerSetLazyHolder { 46 | 47 | private static final PeerSet INSTANCE = new PeerSet(); 48 | } 49 | 50 | public void setSelf(Peer peer) { 51 | self = peer; 52 | } 53 | 54 | public Peer getSelf() { 55 | return self; 56 | } 57 | 58 | public void addPeer(Peer peer) { 59 | list.add(peer); 60 | } 61 | 62 | public void removePeer(Peer peer) { 63 | list.remove(peer); 64 | } 65 | 66 | public List getPeers() { 67 | return list; 68 | } 69 | 70 | public List getPeersWithOutSelf() { 71 | List list2 = new ArrayList<>(list); 72 | list2.remove(self); 73 | return list2; 74 | } 75 | 76 | 77 | public Peer getLeader() { 78 | return leader; 79 | } 80 | 81 | public void setLeader(Peer peer) { 82 | leader = peer; 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return "PeerSet{" + 88 | "list=" + list + 89 | ", leader=" + leader + 90 | ", self=" + self + 91 | '}'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/ReplicationFailModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Builder; 20 | 21 | import java.util.concurrent.Callable; 22 | 23 | /** 24 | * 25 | * @author 莫那·鲁道 26 | */ 27 | @Builder 28 | public class ReplicationFailModel { 29 | static String count = "_count"; 30 | static String success = "_success"; 31 | 32 | public String countKey; 33 | public String successKey; 34 | public Callable callable; 35 | public LogEntry logEntry; 36 | public Peer peer; 37 | public Long offerTime; 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/RvoteParam.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Builder; 20 | import lombok.Data; 21 | import lombok.Getter; 22 | import lombok.Setter; 23 | 24 | import java.io.Serializable; 25 | 26 | /** 27 | * 请求投票 RPC 参数. 28 | * 29 | * @author 莫那·鲁道 30 | */ 31 | @Getter 32 | @Setter 33 | @Builder 34 | @Data 35 | public class RvoteParam implements Serializable { 36 | /** 候选人的任期号 */ 37 | private long term; 38 | 39 | /** 被请求者 ID(ip:selfPort) */ 40 | private String serverId; 41 | 42 | /** 请求选票的候选人的 Id(ip:selfPort) */ 43 | private String candidateId; 44 | 45 | /** 候选人的最后日志条目的索引值 */ 46 | private long lastLogIndex; 47 | 48 | /** 候选人最后日志条目的任期号 */ 49 | private long lastLogTerm; 50 | } 51 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/entity/RvoteResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.entity; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | 22 | import java.io.Serializable; 23 | 24 | /** 25 | * 26 | * 请求投票 RPC 返回值. 27 | * 28 | */ 29 | @Getter 30 | @Setter 31 | public class RvoteResult implements Serializable { 32 | 33 | /** 当前任期号,以便于候选人去更新自己的任期 */ 34 | long term; 35 | 36 | /** 候选人赢得了此张选票时为真 */ 37 | boolean voteGranted; 38 | 39 | public RvoteResult(boolean voteGranted) { 40 | this.voteGranted = voteGranted; 41 | } 42 | 43 | private RvoteResult(Builder builder) { 44 | setTerm(builder.term); 45 | setVoteGranted(builder.voteGranted); 46 | } 47 | 48 | public static RvoteResult fail() { 49 | return new RvoteResult(false); 50 | } 51 | 52 | public static RvoteResult ok() { 53 | return new RvoteResult(true); 54 | } 55 | 56 | public static Builder newBuilder() { 57 | return new Builder(); 58 | } 59 | 60 | 61 | public static final class Builder { 62 | 63 | private long term; 64 | private boolean voteGranted; 65 | 66 | private Builder() { 67 | } 68 | 69 | public Builder term(long term) { 70 | this.term = term; 71 | return this; 72 | } 73 | 74 | public Builder voteGranted(boolean voteGranted) { 75 | this.voteGranted = voteGranted; 76 | return this; 77 | } 78 | 79 | public RvoteResult build() { 80 | return new RvoteResult(this); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/rpc/DefaultRpcClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.rpc; 18 | 19 | import cn.think.in.java.raft.common.RaftRemotingException; 20 | import com.alipay.remoting.exception.RemotingException; 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * @author 莫那·鲁道 27 | */ 28 | @Slf4j 29 | public class DefaultRpcClient implements RpcClient { 30 | 31 | private final static com.alipay.remoting.rpc.RpcClient CLIENT = new com.alipay.remoting.rpc.RpcClient(); 32 | 33 | @Override 34 | public R send(Request request) { 35 | return send(request, (int) TimeUnit.SECONDS.toMillis(5)); 36 | } 37 | 38 | @Override 39 | public R send(Request request, int timeout) { 40 | Response result; 41 | try { 42 | result = (Response) CLIENT.invokeSync(request.getUrl(), request, timeout); 43 | return result.getResult(); 44 | } catch (RemotingException e) { 45 | throw new RaftRemotingException("rpc RaftRemotingException ", e); 46 | } catch (InterruptedException e) { 47 | // ignore 48 | } 49 | return null; 50 | } 51 | 52 | @Override 53 | public void init() { 54 | CLIENT.init(); 55 | } 56 | 57 | @Override 58 | public void destroy() { 59 | CLIENT.shutdown(); 60 | log.info("destroy success"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/rpc/Request.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.rpc; 18 | 19 | import lombok.Builder; 20 | import lombok.Data; 21 | 22 | import java.io.Serializable; 23 | 24 | /** 25 | * @author 莫那·鲁道 26 | */ 27 | @Builder 28 | @Data 29 | public class Request implements Serializable { 30 | 31 | /** 请求投票 */ 32 | public static final int R_VOTE = 0; 33 | /** 附加日志 */ 34 | public static final int A_ENTRIES = 1; 35 | /** 客户端 */ 36 | public static final int CLIENT_REQ = 2; 37 | /** 配置变更. add */ 38 | public static final int CHANGE_CONFIG_ADD = 3; 39 | /** 配置变更. remove */ 40 | public static final int CHANGE_CONFIG_REMOVE = 4; 41 | /** 请求类型 */ 42 | private int cmd = -1; 43 | 44 | /** 45 | * param 46 | * 47 | */ 48 | private Object obj; 49 | 50 | private String url; 51 | 52 | public Request() { 53 | } 54 | 55 | public Request(int cmd, Object obj, String url) { 56 | this.cmd = cmd; 57 | this.obj = obj; 58 | this.url = url; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/rpc/Response.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.rpc; 18 | 19 | 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | import java.io.Serializable; 24 | 25 | /** 26 | * 27 | * @author 莫那·鲁道 28 | */ 29 | @Getter 30 | @Setter 31 | public class Response implements Serializable { 32 | 33 | 34 | private T result; 35 | 36 | public Response(T result) { 37 | this.result = result; 38 | } 39 | 40 | private Response(Builder builder) { 41 | setResult((T) builder.result); 42 | } 43 | 44 | public static Response ok() { 45 | return new Response<>("ok"); 46 | } 47 | 48 | public static Response fail() { 49 | return new Response<>("fail"); 50 | } 51 | 52 | public static Builder newBuilder() { 53 | return new Builder(); 54 | } 55 | 56 | 57 | @Override 58 | public String toString() { 59 | return "Response{" + 60 | "result=" + result + 61 | '}'; 62 | } 63 | 64 | public static final class Builder { 65 | 66 | private Object result; 67 | 68 | private Builder() { 69 | } 70 | 71 | public Builder result(Object val) { 72 | result = val; 73 | return this; 74 | } 75 | 76 | public Response build() { 77 | return new Response(this); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /common/src/main/java/cn/think/in/java/raft/common/rpc/RpcClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.common.rpc; 18 | 19 | import cn.think.in.java.raft.common.LifeCycle; 20 | 21 | /** 22 | * @author 莫那·鲁道 23 | */ 24 | public interface RpcClient extends LifeCycle { 25 | 26 | /** 27 | * 发送请求, 并同步等待返回值. 28 | * @param request 参数 29 | * @param 返回值泛型 30 | * @return 31 | */ 32 | R send(Request request); 33 | 34 | R send(Request request, int timeout); 35 | } 36 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.think.in.java 8 | lu-raft-parent 9 | pom 10 | 1.0-RELEASE 11 | 12 | 13 | common 14 | server 15 | client 16 | 17 | 18 | 19 | 20 | junit 21 | junit 22 | 4.13.2 23 | test 24 | 25 | 26 | org.projectlombok 27 | lombok 28 | 1.18.26 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | cn.think.in.java 37 | server 38 | ${project.version} 39 | 40 | 41 | 42 | cn.think.in.java 43 | client 44 | ${project.version} 45 | 46 | 47 | 48 | cn.think.in.java 49 | common 50 | ${project.version} 51 | 52 | 53 | 54 | org.slf4j 55 | slf4j-api 56 | 1.7.25 57 | 58 | 59 | 60 | org.slf4j 61 | slf4j-log4j12 62 | 1.7.21 63 | 64 | 65 | 66 | 67 | com.alipay.sofa 68 | bolt 69 | 1.6.4 70 | 71 | 72 | slf4j-api 73 | org.slf4j 74 | 75 | 76 | 77 | 78 | 79 | 80 | redis.clients 81 | jedis 82 | 2.9.0 83 | 84 | 85 | 86 | 87 | com.alibaba 88 | fastjson 89 | 1.2.83 90 | 91 | 92 | 93 | 94 | org.projectlombok 95 | lombok 96 | 1.18.2 97 | 98 | 99 | 100 | 101 | com.google.guava 102 | guava 103 | 32.0.0-jre 104 | 105 | 106 | 107 | com.alipay.sofa 108 | hessian 109 | 3.3.6 110 | 111 | 112 | 115 | 116 | org.rocksdb 117 | rocksdbjni 118 | 5.14.3 119 | 120 | 121 | 122 | 123 | 124 | 125 | lu-raft 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-compiler-plugin 130 | 131 | 8 132 | 8 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![visitors](https://visitor-badge.glitch.me/badge?page_id=stateIs0.lu-raft-kv&left_color=green&right_color=red) 4 | [![build status](https://travis-ci.com/stateis0/lu-raft-kv.svg?branch=master)](https://travis-ci.com/stateis0/lu-raft-kv) 5 | [![codecov](https://codecov.io/gh/stateis0/lu-raft-kv/branch/master/graph/badge.svg)](https://codecov.io/gh/stateis0/lu-raft-kv) 6 | ![license](https://img.shields.io/github/license/stateis0/lu-raft-kv.svg) 7 | [![average time to resolve an issue](http://isitmaintained.com/badge/resolution/stateis0/lu-raft-kv.svg)](http://isitmaintained.com/project/stateis0/lu-raft-kv "average time to resolve an issue") 8 | [![percentage of issues still open](http://isitmaintained.com/badge/open/stateis0/lu-raft-kv.svg)](http://isitmaintained.com/project/stateis0/lu-raft-kv "percentage of issues still open") 9 | 10 | ## Lu-Raft-KV-Storage 11 | 12 | 这是一个 Java 版本的 Raft(CP) KV 分布式存储实现. 可用于 Raft 初学者深入学习 Raft 协议. 13 | 14 | 相关文章 [ Java 版 Raft 分布式 KV 存储](http://thinkinjava.cn/2019/01/12/2019/2019-01-12-lu-raft-kv/) 需要🪜 15 | 16 | 为了尽可能的保证数据一致性,该实现的"性能"没有基于 AP 的实现好。 17 | 18 | 目前实现了 Raft 4 大核心功能的其中 2 个功能. 19 | 20 | 1. leader 选举 21 | 2. 日志复制 22 | 3. 成员变更(未测试) 23 | 4. 快照压缩(未实现) 24 | 25 | ## Design 26 | 27 | 完全是参照 RAFT 论文来写的. 没有任何妥协. 28 | 29 | ![image](https://user-images.githubusercontent.com/24973360/50371851-b13de880-05fd-11e9-958a-5813b3b6d761.png) 30 | 31 | 32 | 33 | ## quick start 34 | 35 | 🔥🔥🔥🔥🔥 注意:该项目仅支持 oracle jdk8 启动。 36 | 37 | 🔴🔴🔴🔴🔴 注意:idea 需要安装 lombok 插件。 38 | 39 | #### 验证 "leader 选举" 40 | 41 | 1. 在 idea 中配置 5 个 application 启动项,配置 main 类为 RaftNodeBootStrap 类, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779 42 | 系统配置, 表示分布式环境下的 5 个机器节点. 43 | 2. 依次启动 5 个 RaftNodeBootStrap 节点, 端口分别是 8775,8776, 8777, 8778, 8779. 44 | 3. 观察控制台, 约 6 秒后, 会发生选举事件,此时,会产生一个 leader. 而 leader 会立刻发送心跳维持自己的地位. 45 | 4. 如果leader 的端口是 8775, 使用 idea 关闭 8775 端口,模拟节点挂掉, 大约 15 秒后, 会重新开始选举, 并且会在剩余的 4 个节点中,产生一个新的 leader. 并开始发送心跳日志。 46 | 47 | #### 验证"日志复制" 48 | 49 | ##### 正常状态下 50 | 51 | 52 | 53 | 1. 在 idea 中配置 5 个 application 启动项,配置 main 类为 RaftNodeBootStrap 类, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779 54 | 2. 依次启动 5 个 RaftNodeBootStrap 节点, 端口分别是 8775,8776, 8777, 8778, 8779. 55 | 3. 使用客户端写入 kv 数据. 56 | 4. 杀掉所有节点, 使用 junit test 读取每个 rocksDB 的值, 验证每个节点的数据是否一致. 57 | 58 | ##### 非正常状态下 59 | 60 | 1. 在 idea 中配置 5 个 application 启动项,配置 main 类为 RaftNodeBootStrap 类, 加入 -DserverPort=8775 -DserverPort=8776 -DserverPort=8777 -DserverPort=8778 -DserverPort=8779 61 | 2. 依次启动 5 个 RaftNodeBootStrap 节点, 端口分别是 8775,8776, 8777, 8778, 8779. 62 | 3. 使用客户端写入 kv 数据. 63 | 4. 杀掉 leader (假设是 8775). 64 | 5. 再次写入数据. 65 | 6. 重启 8775. 66 | 7. 关闭所有节点, 读取 RocksDB 验证数据一致性. 67 | 68 | 69 | ## Acknowledgments 70 | 71 | 感谢 SOFA-Bolt 提供 RPC 网络框架 https://github.com/alipay/sofa-bolt 72 | 73 | 感谢 rocksDB 提供 KV 存储 https://github.com/facebook/rocksdb 74 | 75 | ## License 76 | 77 | [Apache 2.0 License.](https://github.com/stateIs0/lu-raft-kv/blob/master/LICENSE) 78 | 79 | 80 | ## Star History 81 | 82 | ![Star History Chart](https://starchart.cc/stateis0/lu-raft-kv.svg) 83 | 84 | 85 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.think.in.java 8 | lu-raft-parent 9 | 1.0-RELEASE 10 | 11 | 12 | server 13 | 14 | 15 | 8 16 | 8 17 | UTF-8 18 | 19 | 20 | 21 | 22 | 23 | cn.think.in.java 24 | common 25 | 26 | 27 | 28 | 29 | org.slf4j 30 | slf4j-api 31 | 32 | 33 | 34 | org.slf4j 35 | slf4j-log4j12 36 | 37 | 38 | 39 | 40 | 41 | com.alipay.sofa 42 | bolt 43 | 44 | 45 | slf4j-api 46 | org.slf4j 47 | 48 | 49 | 50 | 51 | 52 | 53 | redis.clients 54 | jedis 55 | 56 | 57 | 58 | 59 | com.alibaba 60 | fastjson 61 | 62 | 63 | 64 | 65 | org.projectlombok 66 | lombok 67 | 68 | 69 | 70 | 71 | com.google.guava 72 | guava 73 | 74 | 75 | 76 | com.alipay.sofa 77 | hessian 78 | 79 | 80 | 81 | redis.clients 82 | jedis 83 | 84 | 85 | 88 | 89 | org.rocksdb 90 | rocksdbjni 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/Consensus.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server; 18 | 19 | 20 | import cn.think.in.java.raft.common.entity.AentryParam; 21 | import cn.think.in.java.raft.common.entity.AentryResult; 22 | import cn.think.in.java.raft.common.entity.RvoteParam; 23 | import cn.think.in.java.raft.common.entity.RvoteResult; 24 | 25 | /** 26 | * 27 | * Raft 一致性模块. 28 | * @author 莫那·鲁道 29 | */ 30 | public interface Consensus { 31 | 32 | /** 33 | * 请求投票 RPC 34 | * 35 | * 接收者实现: 36 | * 37 | * 如果term < currentTerm返回 false (5.2 节) 38 | * 如果 votedFor 为空或者就是 candidateId,并且候选人的日志至少和自己一样新,那么就投票给他(5.2 节,5.4 节) 39 | * @return 40 | */ 41 | RvoteResult requestVote(RvoteParam param); 42 | 43 | /** 44 | * 附加日志(多个日志,为了提高效率) RPC 45 | * 46 | * 接收者实现: 47 | * 48 | * 如果 term < currentTerm 就返回 false (5.1 节) 49 | * 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节) 50 | * 如果已经存在的日志条目和新的产生冲突(索引值相同但是任期号不同),删除这一条和之后所有的 (5.3 节) 51 | * 附加任何在已有的日志中不存在的条目 52 | * 如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个 53 | * @return 54 | */ 55 | AentryResult appendEntries(AentryParam param); 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/LogModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server; 18 | 19 | import cn.think.in.java.raft.common.LifeCycle; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | 22 | /** 23 | * 24 | * @author 莫那·鲁道 25 | */ 26 | public interface LogModule extends LifeCycle { 27 | 28 | void write(LogEntry logEntry); 29 | 30 | LogEntry read(Long index); 31 | 32 | void removeOnStartIndex(Long startIndex); 33 | 34 | LogEntry getLast(); 35 | 36 | Long getLastIndex(); 37 | } 38 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/Node.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server; 18 | 19 | import cn.think.in.java.raft.common.LifeCycle; 20 | import cn.think.in.java.raft.common.entity.*; 21 | 22 | /** 23 | * 24 | * @author 莫那·鲁道 25 | */ 26 | public interface Node extends LifeCycle { 27 | 28 | /** 29 | * 设置配置文件. 30 | * 31 | * @param config 32 | */ 33 | void setConfig(NodeConfig config); 34 | 35 | /** 36 | * 处理请求投票 RPC. 37 | * 38 | * @param param 39 | * @return 40 | */ 41 | RvoteResult handlerRequestVote(RvoteParam param); 42 | 43 | /** 44 | * 处理附加日志请求. 45 | * 46 | * @param param 47 | * @return 48 | */ 49 | AentryResult handlerAppendEntries(AentryParam param); 50 | 51 | /** 52 | * 处理客户端请求. 53 | * 54 | * @param request 55 | * @return 56 | */ 57 | ClientKVAck handlerClientRequest(ClientKVReq request); 58 | 59 | /** 60 | * 转发给 leader 节点. 61 | * @param request 62 | * @return 63 | */ 64 | ClientKVAck redirect(ClientKVReq request); 65 | 66 | } 67 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/RaftNodeBootStrap.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server; 18 | 19 | import cn.think.in.java.raft.common.entity.NodeConfig; 20 | import cn.think.in.java.raft.server.constant.StateMachineSaveType; 21 | import cn.think.in.java.raft.server.impl.DefaultNode; 22 | import io.netty.util.internal.StringUtil; 23 | import lombok.extern.slf4j.Slf4j; 24 | 25 | import java.util.Arrays; 26 | 27 | import static cn.think.in.java.raft.server.config.Constant.*; 28 | 29 | /** 30 | * -DserverPort=8775 31 | * -DserverPort=8776 32 | * -DserverPort=8777 33 | * -DserverPort=8778 34 | * -DserverPort=8779 35 | */ 36 | @Slf4j 37 | public class RaftNodeBootStrap { 38 | 39 | public static final String[] DEFAULT_PROCESS = new String[]{"localhost:8775", "localhost:8776", "localhost:8777", "localhost:8778", "localhost:8779"}; 40 | 41 | public static void main(String[] args) throws Throwable { 42 | boot(); 43 | } 44 | 45 | public static void boot() throws Throwable { 46 | String property = System.getProperty(CLUSTER_ADDR_LIST); 47 | String[] peerAddr; 48 | 49 | if (StringUtil.isNullOrEmpty(property)) { 50 | peerAddr = DEFAULT_PROCESS; 51 | } else { 52 | peerAddr = property.split(SPLIT); 53 | } 54 | 55 | NodeConfig config = new NodeConfig(); 56 | 57 | // 自身节点 58 | config.setSelfPort(Integer.parseInt(System.getProperty(SERVER_PORT, "8779"))); 59 | 60 | // 其他节点地址 61 | config.setPeerAddrs(Arrays.asList(peerAddr)); 62 | config.setStateMachineSaveType(StateMachineSaveType.ROCKS_DB.getTypeName()); 63 | 64 | Node node = DefaultNode.getInstance(); 65 | node.setConfig(config); 66 | 67 | node.init(); 68 | 69 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 70 | synchronized (node) { 71 | node.notifyAll(); 72 | } 73 | })); 74 | 75 | log.info("gracefully wait"); 76 | 77 | synchronized (node) { 78 | node.wait(); 79 | } 80 | 81 | log.info("gracefully stop"); 82 | node.destroy(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/StateMachine.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server; 18 | 19 | import cn.think.in.java.raft.common.LifeCycle; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | 22 | /** 23 | * 状态机接口. 24 | * @author 莫那·鲁道 25 | */ 26 | public interface StateMachine extends LifeCycle { 27 | 28 | /** 29 | * 将数据应用到状态机. 30 | * 31 | * 原则上,只需这一个方法(apply). 其他的方法是为了更方便的使用状态机. 32 | * @param logEntry 日志中的数据. 33 | */ 34 | void apply(LogEntry logEntry); 35 | 36 | LogEntry get(String key); 37 | 38 | String getString(String key); 39 | 40 | void setString(String key, String value); 41 | 42 | void delString(String... key); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/changes/ClusterMembershipChanges.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.changes; 18 | 19 | import cn.think.in.java.raft.common.entity.Peer; 20 | 21 | /** 22 | * 23 | * 集群配置变更接口. 24 | * 25 | * @author 莫那·鲁道 26 | */ 27 | public interface ClusterMembershipChanges { 28 | 29 | /** 30 | * 添加节点. 31 | * @param newPeer 32 | * @return 33 | */ 34 | Result addPeer(Peer newPeer); 35 | 36 | /** 37 | * 删除节点. 38 | * @param oldPeer 39 | * @return 40 | */ 41 | Result removePeer(Peer oldPeer); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/changes/Result.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.changes; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | 22 | /** 23 | * 24 | * @author 莫那·鲁道 25 | */ 26 | @Getter 27 | @Setter 28 | public class Result { 29 | 30 | public static final int FAIL = 0; 31 | public static final int SUCCESS = 1; 32 | 33 | int status; 34 | 35 | String leaderHint; 36 | 37 | public Result() { 38 | } 39 | 40 | public Result(Builder builder) { 41 | setStatus(builder.status); 42 | setLeaderHint(builder.leaderHint); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "Result{" + 48 | "status=" + status + 49 | ", leaderHint='" + leaderHint + '\'' + 50 | '}'; 51 | } 52 | 53 | public static Builder newBuilder() { 54 | return new Builder(); 55 | } 56 | 57 | @Getter 58 | public enum Status { 59 | FAIL(0), SUCCESS(1); 60 | 61 | int code; 62 | 63 | Status(int code) { 64 | this.code = code; 65 | } 66 | 67 | public static Status value(int v) { 68 | for (Status i : values()) { 69 | if (i.code == v) { 70 | return i; 71 | } 72 | } 73 | return null; 74 | } 75 | } 76 | 77 | public static final class Builder { 78 | 79 | private int status; 80 | private String leaderHint; 81 | 82 | private Builder() { 83 | } 84 | 85 | public Builder status(int val) { 86 | status = val; 87 | return this; 88 | } 89 | 90 | public Builder leaderHint(String val) { 91 | leaderHint = val; 92 | return this; 93 | } 94 | 95 | public Result build() { 96 | return new Result(this); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/changes/Server.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.changes; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | 22 | /** 23 | * @author 莫那·鲁道 24 | */ 25 | @Getter 26 | @Setter 27 | public class Server { 28 | 29 | String address; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/config/Constant.java: -------------------------------------------------------------------------------- 1 | package cn.think.in.java.raft.server.config; 2 | 3 | /** 4 | * @version 1.0 5 | * @Author cxs 6 | * @Description 7 | * @date 2023/5/31 8 | **/ 9 | public class Constant { 10 | 11 | public static final String CLUSTER_ADDR_LIST = "cluster.addr.list"; 12 | 13 | public static final String SERVER_PORT = "serverPort"; 14 | 15 | public static final String SPLIT = ","; 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/constant/StateMachineSaveType.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.constant; 18 | 19 | import cn.think.in.java.raft.server.StateMachine; 20 | import cn.think.in.java.raft.server.impl.DefaultStateMachine; 21 | import cn.think.in.java.raft.server.impl.RedisStateMachine; 22 | import lombok.Getter; 23 | 24 | /** 25 | * 26 | * 快照存储类型 27 | * 28 | * @author rensailong 29 | */ 30 | @Getter 31 | public enum StateMachineSaveType { 32 | /** sy */ 33 | REDIS("redis", "redis存储", RedisStateMachine.getInstance()), 34 | ROCKS_DB("RocksDB", "RocksDB本地存储", DefaultStateMachine.getInstance()) 35 | ; 36 | 37 | public StateMachine getStateMachine() { 38 | return this.stateMachine; 39 | } 40 | 41 | private String typeName; 42 | 43 | private String desc; 44 | 45 | private StateMachine stateMachine; 46 | 47 | StateMachineSaveType(String typeName, String desc, StateMachine stateMachine) { 48 | this.typeName = typeName; 49 | this.desc = desc; 50 | this.stateMachine = stateMachine; 51 | } 52 | 53 | public static StateMachineSaveType getForType(String typeName) { 54 | for (StateMachineSaveType value : values()) { 55 | if (value.getTypeName().equals(typeName)) { 56 | return value; 57 | } 58 | } 59 | 60 | return null; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/current/RaftThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.current; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | /** 22 | * @author 莫那·鲁道 23 | */ 24 | @Slf4j 25 | public class RaftThread extends Thread { 26 | 27 | private static final UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER = (t, e) 28 | -> log.warn("Exception occurred from thread {}", t.getName(), e); 29 | 30 | public RaftThread(String threadName, Runnable r) { 31 | super(r, threadName); 32 | setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/current/RaftThreadPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.current; 18 | 19 | import java.util.concurrent.*; 20 | 21 | /** 22 | * @author 莫那·鲁道 23 | */ 24 | public class RaftThreadPool { 25 | 26 | private static final int CUP = Runtime.getRuntime().availableProcessors(); 27 | private static final int MAX_POOL_SIZE = CUP * 2; 28 | private static final int QUEUE_SIZE = 1024; 29 | private static final long KEEP_TIME = 1000 * 60; 30 | private static final TimeUnit KEEP_TIME_UNIT = TimeUnit.MILLISECONDS; 31 | 32 | private static ScheduledExecutorService ss = getScheduled(); 33 | private static ThreadPoolExecutor te = getThreadPool(); 34 | 35 | private static ThreadPoolExecutor getThreadPool() { 36 | return new RaftThreadPoolExecutor( 37 | CUP, 38 | MAX_POOL_SIZE, 39 | KEEP_TIME, 40 | KEEP_TIME_UNIT, 41 | new LinkedBlockingQueue<>(QUEUE_SIZE), 42 | new NameThreadFactory()); 43 | } 44 | 45 | private static ScheduledExecutorService getScheduled() { 46 | return new ScheduledThreadPoolExecutor(CUP, new NameThreadFactory()); 47 | } 48 | 49 | 50 | public static void scheduleAtFixedRate(Runnable r, long initDelay, long delay) { 51 | ss.scheduleAtFixedRate(r, initDelay, delay, TimeUnit.MILLISECONDS); 52 | } 53 | 54 | 55 | public static void scheduleWithFixedDelay(Runnable r, long delay) { 56 | ss.scheduleWithFixedDelay(r, 0, delay, TimeUnit.MILLISECONDS); 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | public static Future submit(Callable r) { 61 | return te.submit(r); 62 | } 63 | 64 | public static void execute(Runnable r) { 65 | te.execute(r); 66 | } 67 | 68 | public static void execute(Runnable r, boolean sync) { 69 | if (sync) { 70 | r.run(); 71 | } else { 72 | te.execute(r); 73 | } 74 | } 75 | 76 | static class NameThreadFactory implements ThreadFactory { 77 | 78 | @Override 79 | public Thread newThread(Runnable r) { 80 | Thread t = new RaftThread("Raft thread", r); 81 | t.setDaemon(true); 82 | t.setPriority(5); 83 | return t; 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/current/RaftThreadPoolExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.current; 18 | 19 | import lombok.extern.slf4j.Slf4j; 20 | 21 | import java.util.concurrent.BlockingQueue; 22 | import java.util.concurrent.ThreadPoolExecutor; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * @author 莫那·鲁道 27 | */ 28 | @Slf4j 29 | public class RaftThreadPoolExecutor extends ThreadPoolExecutor { 30 | 31 | private static final ThreadLocal COST_TIME_WATCH = ThreadLocal.withInitial(System::currentTimeMillis); 32 | 33 | public RaftThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 34 | BlockingQueue workQueue, RaftThreadPool.NameThreadFactory nameThreadFactory) { 35 | super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, nameThreadFactory); 36 | } 37 | 38 | @Override 39 | protected void beforeExecute(Thread t, Runnable r) { 40 | COST_TIME_WATCH.get(); 41 | log.debug("raft thread pool before Execute"); 42 | } 43 | 44 | @Override 45 | protected void afterExecute(Runnable r, Throwable t) { 46 | log.debug("raft thread pool after Execute, cost time : {}", System.currentTimeMillis() - COST_TIME_WATCH.get()); 47 | COST_TIME_WATCH.remove(); 48 | } 49 | 50 | @Override 51 | protected void terminated() { 52 | log.info("active count : {}, queueSize : {}, poolSize : {}", getActiveCount(), getQueue().size(), getPoolSize()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/exception/RaftNotSupportException.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.exception; 18 | 19 | /** 20 | * 21 | */ 22 | public class RaftNotSupportException extends RuntimeException { 23 | 24 | public RaftNotSupportException() { 25 | } 26 | 27 | public RaftNotSupportException(String message) { 28 | super(message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/impl/ClusterMembershipChangesImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.server.changes.ClusterMembershipChanges; 20 | import cn.think.in.java.raft.server.changes.Result; 21 | import cn.think.in.java.raft.common.entity.LogEntry; 22 | import cn.think.in.java.raft.common.entity.NodeStatus; 23 | import cn.think.in.java.raft.common.entity.Peer; 24 | import cn.think.in.java.raft.common.rpc.Request; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | /** 29 | * 集群配置变更接口默认实现. 30 | * 31 | * @author 莫那·鲁道 32 | */ 33 | public class ClusterMembershipChangesImpl implements ClusterMembershipChanges { 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(ClusterMembershipChangesImpl.class); 36 | 37 | 38 | private final DefaultNode node; 39 | 40 | public ClusterMembershipChangesImpl(DefaultNode node) { 41 | this.node = node; 42 | } 43 | 44 | /** 45 | * 必须是同步的,一次只能添加一个节点 46 | * 47 | * @param newPeer 48 | */ 49 | @Override 50 | public synchronized Result addPeer(Peer newPeer) { 51 | // 已经存在 52 | if (node.peerSet.getPeersWithOutSelf().contains(newPeer)) { 53 | return new Result(); 54 | } 55 | 56 | node.peerSet.getPeersWithOutSelf().add(newPeer); 57 | 58 | if (node.status == NodeStatus.LEADER) { 59 | node.nextIndexs.put(newPeer, 0L); 60 | node.matchIndexs.put(newPeer, 0L); 61 | 62 | for (long i = 0; i < node.logModule.getLastIndex(); i++) { 63 | LogEntry entry = node.logModule.read(i); 64 | if (entry != null) { 65 | node.replication(newPeer, entry); 66 | } 67 | } 68 | 69 | for (Peer ignore : node.peerSet.getPeersWithOutSelf()) { 70 | // TODO 同步到其他节点. 71 | Request request = Request.builder() 72 | .cmd(Request.CHANGE_CONFIG_ADD) 73 | .url(newPeer.getAddr()) 74 | .obj(newPeer) 75 | .build(); 76 | 77 | Result result = node.rpcClient.send(request); 78 | if (result != null && result.getStatus() == Result.Status.SUCCESS.getCode()) { 79 | LOGGER.info("replication config success, peer : {}, newServer : {}", newPeer, newPeer); 80 | } else { 81 | LOGGER.warn("replication config fail, peer : {}, newServer : {}", newPeer, newPeer); 82 | } 83 | } 84 | 85 | } 86 | 87 | return new Result(); 88 | } 89 | 90 | 91 | /** 92 | * 必须是同步的,一次只能删除一个节点 93 | * 94 | * @param oldPeer 95 | */ 96 | @Override 97 | public synchronized Result removePeer(Peer oldPeer) { 98 | node.peerSet.getPeersWithOutSelf().remove(oldPeer); 99 | node.nextIndexs.remove(oldPeer); 100 | node.matchIndexs.remove(oldPeer); 101 | 102 | return new Result(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/impl/DefaultConsensus.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.server.Consensus; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | import cn.think.in.java.raft.common.entity.NodeStatus; 22 | import cn.think.in.java.raft.common.entity.Peer; 23 | import cn.think.in.java.raft.common.entity.AentryParam; 24 | import cn.think.in.java.raft.common.entity.AentryResult; 25 | import cn.think.in.java.raft.common.entity.RvoteParam; 26 | import cn.think.in.java.raft.common.entity.RvoteResult; 27 | import io.netty.util.internal.StringUtil; 28 | import lombok.Getter; 29 | import lombok.Setter; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import java.util.concurrent.locks.ReentrantLock; 34 | 35 | /** 36 | * 37 | * 默认的一致性模块实现. 38 | * 39 | * @author 莫那·鲁道 40 | */ 41 | @Setter 42 | @Getter 43 | public class DefaultConsensus implements Consensus { 44 | 45 | 46 | private static final Logger LOGGER = LoggerFactory.getLogger(DefaultConsensus.class); 47 | 48 | 49 | public final DefaultNode node; 50 | 51 | public final ReentrantLock voteLock = new ReentrantLock(); 52 | public final ReentrantLock appendLock = new ReentrantLock(); 53 | 54 | public DefaultConsensus(DefaultNode node) { 55 | this.node = node; 56 | } 57 | 58 | /** 59 | * 请求投票 RPC 60 | * 61 | * 接收者实现: 62 | * 如果term < currentTerm返回 false (5.2 节) 63 | * 如果 votedFor 为空或者就是 candidateId,并且候选人的日志至少和自己一样新,那么就投票给他(5.2 节,5.4 节) 64 | */ 65 | @Override 66 | public RvoteResult requestVote(RvoteParam param) { 67 | try { 68 | RvoteResult.Builder builder = RvoteResult.newBuilder(); 69 | if (!voteLock.tryLock()) { 70 | return builder.term(node.getCurrentTerm()).voteGranted(false).build(); 71 | } 72 | 73 | // 对方任期没有自己新 74 | if (param.getTerm() < node.getCurrentTerm()) { 75 | return builder.term(node.getCurrentTerm()).voteGranted(false).build(); 76 | } 77 | 78 | // (当前节点并没有投票 或者 已经投票过了且是对方节点) && 对方日志和自己一样新 79 | LOGGER.info("node {} current vote for [{}], param candidateId : {}", node.peerSet.getSelf(), node.getVotedFor(), param.getCandidateId()); 80 | LOGGER.info("node {} current term {}, peer term : {}", node.peerSet.getSelf(), node.getCurrentTerm(), param.getTerm()); 81 | 82 | if ((StringUtil.isNullOrEmpty(node.getVotedFor()) || node.getVotedFor().equals(param.getCandidateId()))) { 83 | 84 | if (node.getLogModule().getLast() != null) { 85 | // 对方没有自己新 86 | if (node.getLogModule().getLast().getTerm() > param.getLastLogTerm()) { 87 | return RvoteResult.fail(); 88 | } 89 | // 对方没有自己新 90 | if (node.getLogModule().getLastIndex() > param.getLastLogIndex()) { 91 | return RvoteResult.fail(); 92 | } 93 | } 94 | 95 | // 切换状态 96 | node.status = NodeStatus.FOLLOWER; 97 | // 更新 98 | node.peerSet.setLeader(new Peer(param.getCandidateId())); 99 | node.setCurrentTerm(param.getTerm()); 100 | node.setVotedFor(param.getServerId()); 101 | // 返回成功 102 | return builder.term(node.currentTerm).voteGranted(true).build(); 103 | } 104 | 105 | return builder.term(node.currentTerm).voteGranted(false).build(); 106 | 107 | } finally { 108 | voteLock.unlock(); 109 | } 110 | } 111 | 112 | 113 | /** 114 | * 附加日志(多个日志,为了提高效率) RPC 115 | * 116 | * 接收者实现: 117 | * 如果 term < currentTerm 就返回 false (5.1 节) 118 | * 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节) 119 | * 如果已经存在的日志条目和新的产生冲突(索引值相同但是任期号不同),删除这一条和之后所有的 (5.3 节) 120 | * 附加任何在已有的日志中不存在的条目 121 | * 如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个 122 | */ 123 | @Override 124 | public AentryResult appendEntries(AentryParam param) { 125 | AentryResult result = AentryResult.fail(); 126 | try { 127 | if (!appendLock.tryLock()) { 128 | return result; 129 | } 130 | 131 | result.setTerm(node.getCurrentTerm()); 132 | // 不够格 133 | if (param.getTerm() < node.getCurrentTerm()) { 134 | return result; 135 | } 136 | 137 | node.preHeartBeatTime = System.currentTimeMillis(); 138 | node.preElectionTime = System.currentTimeMillis(); 139 | node.peerSet.setLeader(new Peer(param.getLeaderId())); 140 | 141 | // 够格 142 | if (param.getTerm() >= node.getCurrentTerm()) { 143 | LOGGER.debug("node {} become FOLLOWER, currentTerm : {}, param Term : {}, param serverId = {}", 144 | node.peerSet.getSelf(), node.currentTerm, param.getTerm(), param.getServerId()); 145 | // 认怂 146 | node.status = NodeStatus.FOLLOWER; 147 | } 148 | // 使用对方的 term. 149 | node.setCurrentTerm(param.getTerm()); 150 | 151 | //心跳 152 | if (param.getEntries() == null || param.getEntries().length == 0) { 153 | LOGGER.info("node {} append heartbeat success , he's term : {}, my term : {}", 154 | param.getLeaderId(), param.getTerm(), node.getCurrentTerm()); 155 | 156 | // 处理 leader 已提交但未应用到状态机的日志 157 | 158 | // 下一个需要提交的日志的索引(如有) 159 | long nextCommit = node.getCommitIndex() + 1; 160 | 161 | //如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个 162 | if (param.getLeaderCommit() > node.getCommitIndex()) { 163 | int commitIndex = (int) Math.min(param.getLeaderCommit(), node.getLogModule().getLastIndex()); 164 | node.setCommitIndex(commitIndex); 165 | node.setLastApplied(commitIndex); 166 | } 167 | 168 | while (nextCommit <= node.getCommitIndex()){ 169 | // 提交之前的日志 170 | node.stateMachine.apply(node.logModule.read(nextCommit)); 171 | nextCommit++; 172 | } 173 | return AentryResult.newBuilder().term(node.getCurrentTerm()).success(true).build(); 174 | } 175 | 176 | // 真实日志 177 | // 第一次 178 | if (node.getLogModule().getLastIndex() != 0 && param.getPrevLogIndex() != 0) { 179 | LogEntry logEntry; 180 | if ((logEntry = node.getLogModule().read(param.getPrevLogIndex())) != null) { 181 | // 如果日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false 182 | // 需要减小 nextIndex 重试. 183 | if (logEntry.getTerm() != param.getPreLogTerm()) { 184 | return result; 185 | } 186 | } else { 187 | // index 不对, 需要递减 nextIndex 重试. 188 | return result; 189 | } 190 | 191 | } 192 | 193 | // 如果已经存在的日志条目和新的产生冲突(索引值相同但是任期号不同),删除这一条和之后所有的 194 | LogEntry existLog = node.getLogModule().read(((param.getPrevLogIndex() + 1))); 195 | if (existLog != null && existLog.getTerm() != param.getEntries()[0].getTerm()) { 196 | // 删除这一条和之后所有的, 然后写入日志和状态机. 197 | node.getLogModule().removeOnStartIndex(param.getPrevLogIndex() + 1); 198 | } else if (existLog != null) { 199 | // 已经有日志了, 不能重复写入. 200 | result.setSuccess(true); 201 | return result; 202 | } 203 | 204 | // 写进日志 205 | for (LogEntry entry : param.getEntries()) { 206 | node.getLogModule().write(entry); 207 | result.setSuccess(true); 208 | } 209 | 210 | // 下一个需要提交的日志的索引(如有) 211 | long nextCommit = node.getCommitIndex() + 1; 212 | 213 | //如果 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个 214 | if (param.getLeaderCommit() > node.getCommitIndex()) { 215 | int commitIndex = (int) Math.min(param.getLeaderCommit(), node.getLogModule().getLastIndex()); 216 | node.setCommitIndex(commitIndex); 217 | node.setLastApplied(commitIndex); 218 | } 219 | 220 | while (nextCommit <= node.getCommitIndex()){ 221 | // 提交之前的日志 222 | node.stateMachine.apply(node.logModule.read(nextCommit)); 223 | nextCommit++; 224 | } 225 | 226 | result.setTerm(node.getCurrentTerm()); 227 | 228 | node.status = NodeStatus.FOLLOWER; 229 | // TODO, 是否应当在成功回复之后, 才正式提交? 防止 leader "等待回复"过程中 挂掉. 230 | return result; 231 | } finally { 232 | appendLock.unlock(); 233 | } 234 | } 235 | 236 | 237 | } 238 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/impl/DefaultLogModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.server.LogModule; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | import com.alibaba.fastjson.JSON; 22 | import lombok.Getter; 23 | import lombok.Setter; 24 | import lombok.extern.slf4j.Slf4j; 25 | import org.rocksdb.Options; 26 | import org.rocksdb.RocksDB; 27 | import org.rocksdb.RocksDBException; 28 | 29 | import java.io.File; 30 | import java.util.concurrent.locks.ReentrantLock; 31 | 32 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 33 | 34 | /** 35 | * 36 | * 默认的日志实现. 日志模块不关心 key, 只关心 index. 37 | * 38 | * @author 莫那·鲁道 39 | */ 40 | @Setter 41 | @Getter 42 | @Slf4j 43 | public class DefaultLogModule implements LogModule { 44 | 45 | /** public just for test */ 46 | public String dbDir; 47 | public String logsDir; 48 | 49 | private RocksDB logDb; 50 | 51 | public final static byte[] LAST_INDEX_KEY = "LAST_INDEX_KEY".getBytes(); 52 | 53 | private ReentrantLock lock = new ReentrantLock(); 54 | 55 | private DefaultLogModule() { 56 | if (dbDir == null) { 57 | dbDir = "./rocksDB-raft/" + System.getProperty("serverPort"); 58 | } 59 | if (logsDir == null) { 60 | logsDir = dbDir + "/logModule"; 61 | } 62 | RocksDB.loadLibrary(); 63 | Options options = new Options(); 64 | options.setCreateIfMissing(true); 65 | 66 | File file = new File(logsDir); 67 | boolean success = false; 68 | if (!file.exists()) { 69 | success = file.mkdirs(); 70 | } 71 | if (success) { 72 | log.warn("make a new dir : " + logsDir); 73 | } 74 | try { 75 | logDb = RocksDB.open(options, logsDir); 76 | } catch (RocksDBException e) { 77 | log.warn(e.getMessage()); 78 | } 79 | } 80 | 81 | public static DefaultLogModule getInstance() { 82 | return DefaultLogsLazyHolder.INSTANCE; 83 | } 84 | 85 | @Override 86 | public void init() throws Throwable { 87 | 88 | } 89 | 90 | @Override 91 | public void destroy() throws Throwable { 92 | logDb.close(); 93 | log.info("destroy success"); 94 | } 95 | 96 | private static class DefaultLogsLazyHolder { 97 | 98 | private static final DefaultLogModule INSTANCE = new DefaultLogModule(); 99 | } 100 | 101 | /** 102 | * logEntry 的 index 就是 key. 严格保证递增. 103 | * 104 | * @param logEntry 105 | */ 106 | @Override 107 | public void write(LogEntry logEntry) { 108 | 109 | boolean success = false; 110 | boolean result; 111 | try { 112 | result = lock.tryLock(3000, MILLISECONDS); 113 | if (!result) { 114 | throw new RuntimeException("write fail, tryLock fail."); 115 | } 116 | logEntry.setIndex(getLastIndex() + 1); 117 | logDb.put(logEntry.getIndex().toString().getBytes(), JSON.toJSONBytes(logEntry)); 118 | success = true; 119 | log.info("DefaultLogModule write rocksDB success, logEntry info : [{}]", logEntry); 120 | } catch (RocksDBException | InterruptedException e) { 121 | throw new RuntimeException(e); 122 | } finally { 123 | if (success) { 124 | updateLastIndex(logEntry.getIndex()); 125 | } 126 | lock.unlock(); 127 | } 128 | } 129 | 130 | 131 | @Override 132 | public LogEntry read(Long index) { 133 | try { 134 | byte[] result = logDb.get(convert(index)); 135 | if (result == null) { 136 | return null; 137 | } 138 | return JSON.parseObject(result, LogEntry.class); 139 | } catch (RocksDBException e) { 140 | throw new RuntimeException(e); 141 | } 142 | } 143 | 144 | @Override 145 | public void removeOnStartIndex(Long startIndex) { 146 | boolean success = false; 147 | int count = 0; 148 | boolean tryLock; 149 | try { 150 | tryLock = lock.tryLock(3000, MILLISECONDS); 151 | if (!tryLock) { 152 | throw new RuntimeException("tryLock fail, removeOnStartIndex fail"); 153 | } 154 | for (long i = startIndex; i <= getLastIndex(); i++) { 155 | logDb.delete(String.valueOf(i).getBytes()); 156 | ++count; 157 | } 158 | success = true; 159 | log.warn("rocksDB removeOnStartIndex success, count={} startIndex={}, lastIndex={}", count, startIndex, getLastIndex()); 160 | } catch (InterruptedException | RocksDBException e) { 161 | throw new RuntimeException(e); 162 | } finally { 163 | if (success) { 164 | updateLastIndex(getLastIndex() - count); 165 | } 166 | lock.unlock(); 167 | } 168 | } 169 | 170 | 171 | @Override 172 | public LogEntry getLast() { 173 | try { 174 | byte[] result = logDb.get(convert(getLastIndex())); 175 | if (result == null) { 176 | return null; 177 | } 178 | return JSON.parseObject(result, LogEntry.class); 179 | } catch (RocksDBException e) { 180 | throw new RuntimeException(e); 181 | } 182 | } 183 | 184 | @Override 185 | public Long getLastIndex() { 186 | byte[] lastIndex; 187 | try { 188 | lastIndex = logDb.get(LAST_INDEX_KEY); 189 | if (lastIndex == null) { 190 | lastIndex = "-1".getBytes(); 191 | } 192 | } catch (RocksDBException e) { 193 | throw new RuntimeException(e); 194 | } 195 | return Long.valueOf(new String(lastIndex)); 196 | } 197 | 198 | private byte[] convert(Long key) { 199 | return key.toString().getBytes(); 200 | } 201 | 202 | // on lock 203 | private void updateLastIndex(Long index) { 204 | try { 205 | // overWrite 206 | logDb.put(LAST_INDEX_KEY, index.toString().getBytes()); 207 | } catch (RocksDBException e) { 208 | throw new RuntimeException(e); 209 | } 210 | } 211 | 212 | 213 | } 214 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/impl/DefaultNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.common.entity.*; 20 | import cn.think.in.java.raft.server.Consensus; 21 | import cn.think.in.java.raft.server.LogModule; 22 | import cn.think.in.java.raft.server.Node; 23 | import cn.think.in.java.raft.server.StateMachine; 24 | import cn.think.in.java.raft.server.changes.ClusterMembershipChanges; 25 | import cn.think.in.java.raft.server.changes.Result; 26 | import cn.think.in.java.raft.common.RaftRemotingException; 27 | import cn.think.in.java.raft.server.constant.StateMachineSaveType; 28 | import cn.think.in.java.raft.server.current.RaftThreadPool; 29 | import cn.think.in.java.raft.server.rpc.DefaultRpcServiceImpl; 30 | import cn.think.in.java.raft.server.rpc.RpcService; 31 | import cn.think.in.java.raft.server.util.LongConvert; 32 | import cn.think.in.java.raft.common.rpc.DefaultRpcClient; 33 | import cn.think.in.java.raft.common.rpc.Request; 34 | import cn.think.in.java.raft.common.rpc.RpcClient; 35 | import lombok.Getter; 36 | import lombok.Setter; 37 | import lombok.extern.slf4j.Slf4j; 38 | 39 | import java.util.*; 40 | import java.util.concurrent.*; 41 | import java.util.concurrent.atomic.AtomicInteger; 42 | 43 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 44 | 45 | /** 46 | * 抽象机器节点, 初始为 follower, 角色随时变化. 47 | * 48 | * @author 莫那·鲁道 49 | */ 50 | @Getter 51 | @Setter 52 | @Slf4j 53 | public class DefaultNode implements Node, ClusterMembershipChanges { 54 | 55 | /** 选举时间间隔基数 */ 56 | public volatile long electionTime = 15 * 1000; 57 | /** 上一次选举时间 */ 58 | public volatile long preElectionTime = 0; 59 | 60 | /** 上次一心跳时间戳 */ 61 | public volatile long preHeartBeatTime = 0; 62 | /** 心跳间隔基数 */ 63 | public final long heartBeatTick = 5 * 100; 64 | 65 | 66 | private HeartBeatTask heartBeatTask = new HeartBeatTask(); 67 | private ElectionTask electionTask = new ElectionTask(); 68 | private ReplicationFailQueueConsumer replicationFailQueueConsumer = new ReplicationFailQueueConsumer(); 69 | 70 | private LinkedBlockingQueue replicationFailQueue = new LinkedBlockingQueue<>(2048); 71 | 72 | 73 | /** 74 | * 节点当前状态 75 | * 76 | * @see NodeStatus 77 | */ 78 | public volatile int status = NodeStatus.FOLLOWER; 79 | 80 | public PeerSet peerSet; 81 | 82 | volatile boolean running = false; 83 | 84 | /* ============ 所有服务器上持久存在的 ============= */ 85 | 86 | /** 服务器最后一次知道的任期号(初始化为 0,持续递增) */ 87 | volatile long currentTerm = 0; 88 | /** 在当前获得选票的候选人的 Id */ 89 | volatile String votedFor; 90 | /** 日志条目集;每一个条目包含一个用户状态机执行的指令,和收到时的任期号 */ 91 | LogModule logModule; 92 | 93 | 94 | 95 | /* ============ 所有服务器上经常变的 ============= */ 96 | 97 | /** 已知的最大的已经被提交的日志条目的索引值 */ 98 | volatile long commitIndex; 99 | 100 | /** 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增) */ 101 | volatile long lastApplied = 0; 102 | 103 | /* ========== 在领导人里经常改变的(选举后重新初始化) ================== */ 104 | 105 | /** 对于每一个服务器,需要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一) */ 106 | Map nextIndexs; 107 | 108 | /** 对于每一个服务器,已经复制给他的日志的最高索引值 */ 109 | Map matchIndexs; 110 | 111 | 112 | 113 | /* ============================== */ 114 | 115 | public NodeConfig config; 116 | 117 | public RpcService rpcServer; 118 | 119 | public RpcClient rpcClient = new DefaultRpcClient(); 120 | 121 | public StateMachine stateMachine; 122 | 123 | /* ============================== */ 124 | 125 | /** 一致性模块实现 */ 126 | Consensus consensus; 127 | 128 | ClusterMembershipChanges delegate; 129 | 130 | 131 | /* ============================== */ 132 | 133 | private DefaultNode() { 134 | } 135 | 136 | public static DefaultNode getInstance() { 137 | return DefaultNodeLazyHolder.INSTANCE; 138 | } 139 | 140 | 141 | private static class DefaultNodeLazyHolder { 142 | 143 | private static final DefaultNode INSTANCE = new DefaultNode(); 144 | } 145 | 146 | @Override 147 | public void init() throws Throwable { 148 | running = true; 149 | rpcServer.init(); 150 | rpcClient.init(); 151 | 152 | consensus = new DefaultConsensus(this); 153 | delegate = new ClusterMembershipChangesImpl(this); 154 | 155 | RaftThreadPool.scheduleWithFixedDelay(heartBeatTask, 500); 156 | RaftThreadPool.scheduleAtFixedRate(electionTask, 6000, 500); 157 | RaftThreadPool.execute(replicationFailQueueConsumer); 158 | 159 | LogEntry logEntry = logModule.getLast(); 160 | if (logEntry != null) { 161 | currentTerm = logEntry.getTerm(); 162 | } 163 | 164 | log.info("start success, selfId : {} ", peerSet.getSelf()); 165 | } 166 | 167 | @Override 168 | public void setConfig(NodeConfig config) { 169 | this.config = config; 170 | stateMachine = StateMachineSaveType.getForType(config.getStateMachineSaveType()).getStateMachine(); 171 | logModule = DefaultLogModule.getInstance(); 172 | 173 | peerSet = PeerSet.getInstance(); 174 | for (String s : config.getPeerAddrs()) { 175 | Peer peer = new Peer(s); 176 | peerSet.addPeer(peer); 177 | if (s.equals("localhost:" + config.getSelfPort())) { 178 | peerSet.setSelf(peer); 179 | } 180 | } 181 | 182 | rpcServer = new DefaultRpcServiceImpl(config.selfPort, this); 183 | } 184 | 185 | 186 | @Override 187 | public RvoteResult handlerRequestVote(RvoteParam param) { 188 | log.warn("handlerRequestVote will be invoke, param info : {}", param); 189 | return consensus.requestVote(param); 190 | } 191 | 192 | @Override 193 | public AentryResult handlerAppendEntries(AentryParam param) { 194 | if (param.getEntries() != null) { 195 | log.warn("node receive node {} append entry, entry content = {}", param.getLeaderId(), param.getEntries()); 196 | } 197 | 198 | return consensus.appendEntries(param); 199 | } 200 | 201 | 202 | @Override 203 | public ClientKVAck redirect(ClientKVReq request) { 204 | Request r = Request.builder() 205 | .obj(request) 206 | .url(peerSet.getLeader().getAddr()) 207 | .cmd(Request.CLIENT_REQ).build(); 208 | 209 | return rpcClient.send(r); 210 | } 211 | 212 | /** 213 | * 客户端的每一个请求都包含一条被复制状态机执行的指令。 214 | * 领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行的发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。 215 | * 当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中然后把执行的结果返回给客户端。 216 | * 如果跟随者崩溃或者运行缓慢,再或者网络丢包, 217 | * 领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。 218 | * 219 | * @param request 220 | * @return 221 | */ 222 | @Override 223 | public synchronized ClientKVAck handlerClientRequest(ClientKVReq request) { 224 | 225 | log.warn("handlerClientRequest handler {} operation, and key : [{}], value : [{}]", 226 | ClientKVReq.Type.value(request.getType()), request.getKey(), request.getValue()); 227 | 228 | if (status != NodeStatus.LEADER) { 229 | log.warn("I not am leader , only invoke redirect method, leader addr : {}, my addr : {}", 230 | peerSet.getLeader(), peerSet.getSelf().getAddr()); 231 | return redirect(request); 232 | } 233 | 234 | if (request.getType() == ClientKVReq.GET) { 235 | LogEntry logEntry = stateMachine.get(request.getKey()); 236 | if (logEntry != null) { 237 | return new ClientKVAck(logEntry); 238 | } 239 | return new ClientKVAck(null); 240 | } 241 | 242 | LogEntry logEntry = LogEntry.builder() 243 | .command(Command.builder(). 244 | key(request.getKey()). 245 | value(request.getValue()). 246 | build()) 247 | .term(currentTerm) 248 | .build(); 249 | 250 | // 预提交到本地日志, TODO 预提交 251 | logModule.write(logEntry); 252 | log.info("write logModule success, logEntry info : {}, log index : {}", logEntry, logEntry.getIndex()); 253 | 254 | final AtomicInteger success = new AtomicInteger(0); 255 | 256 | List> futureList = new ArrayList<>(); 257 | 258 | int count = 0; 259 | // 复制到其他机器 260 | for (Peer peer : peerSet.getPeersWithOutSelf()) { 261 | // TODO check self and RaftThreadPool 262 | count++; 263 | // 并行发起 RPC 复制. 264 | futureList.add(replication(peer, logEntry)); 265 | } 266 | 267 | CountDownLatch latch = new CountDownLatch(futureList.size()); 268 | List resultList = new CopyOnWriteArrayList<>(); 269 | 270 | getRPCAppendResult(futureList, latch, resultList); 271 | 272 | try { 273 | latch.await(4000, MILLISECONDS); 274 | } catch (InterruptedException e) { 275 | log.error(e.getMessage(), e); 276 | } 277 | 278 | for (Boolean aBoolean : resultList) { 279 | if (aBoolean) { 280 | success.incrementAndGet(); 281 | } 282 | } 283 | 284 | // 如果存在一个满足N > commitIndex的 N,并且大多数的matchIndex[i] ≥ N成立, 285 | // 并且log[N].term == currentTerm成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节) 286 | List matchIndexList = new ArrayList<>(matchIndexs.values()); 287 | // 小于 2, 没有意义 288 | int median = 0; 289 | if (matchIndexList.size() >= 2) { 290 | Collections.sort(matchIndexList); 291 | median = matchIndexList.size() / 2; 292 | } 293 | Long N = matchIndexList.get(median); 294 | if (N > commitIndex) { 295 | LogEntry entry = logModule.read(N); 296 | if (entry != null && entry.getTerm() == currentTerm) { 297 | commitIndex = N; 298 | } 299 | } 300 | 301 | // 响应客户端(成功一半) 302 | if (success.get() >= (count / 2)) { 303 | // 更新 304 | commitIndex = logEntry.getIndex(); 305 | // 应用到状态机 306 | getStateMachine().apply(logEntry); 307 | lastApplied = commitIndex; 308 | 309 | log.info("success apply local state machine, logEntry info : {}", logEntry); 310 | // 返回成功. 311 | return ClientKVAck.ok(); 312 | } else { 313 | // 回滚已经提交的日志. 314 | logModule.removeOnStartIndex(logEntry.getIndex()); 315 | log.warn("fail apply local state machine, logEntry info : {}", logEntry); 316 | // TODO 不应用到状态机,但已经记录到日志中.由定时任务从重试队列取出,然后重复尝试,当达到条件时,应用到状态机. 317 | // 这里应该返回错误, 因为没有成功复制过半机器. 318 | return ClientKVAck.fail(); 319 | } 320 | } 321 | 322 | private void getRPCAppendResult(List> futureList, CountDownLatch latch, List resultList) { 323 | for (Future future : futureList) { 324 | RaftThreadPool.execute(() -> { 325 | try { 326 | resultList.add(future.get(3000, MILLISECONDS)); 327 | } catch (Exception e) { 328 | log.error(e.getMessage(), e); 329 | resultList.add(false); 330 | } finally { 331 | latch.countDown(); 332 | } 333 | }); 334 | } 335 | } 336 | 337 | 338 | /** 复制到其他机器 */ 339 | public Future replication(Peer peer, LogEntry entry) { 340 | 341 | return RaftThreadPool.submit(() -> { 342 | 343 | long start = System.currentTimeMillis(), end = start; 344 | 345 | // 20 秒重试时间 346 | while (end - start < 20 * 1000L) { 347 | 348 | AentryParam aentryParam = AentryParam.builder().build(); 349 | aentryParam.setTerm(currentTerm); 350 | aentryParam.setServerId(peer.getAddr()); 351 | aentryParam.setLeaderId(peerSet.getSelf().getAddr()); 352 | 353 | aentryParam.setLeaderCommit(commitIndex); 354 | 355 | // 以我这边为准, 这个行为通常是成为 leader 后,首次进行 RPC 才有意义. 356 | Long nextIndex = nextIndexs.get(peer); 357 | LinkedList logEntries = new LinkedList<>(); 358 | if (entry.getIndex() >= nextIndex) { 359 | for (long i = nextIndex; i <= entry.getIndex(); i++) { 360 | LogEntry l = logModule.read(i); 361 | if (l != null) { 362 | logEntries.add(l); 363 | } 364 | } 365 | } else { 366 | logEntries.add(entry); 367 | } 368 | // 最小的那个日志. 369 | LogEntry preLog = getPreLog(logEntries.getFirst()); 370 | aentryParam.setPreLogTerm(preLog.getTerm()); 371 | aentryParam.setPrevLogIndex(preLog.getIndex()); 372 | 373 | aentryParam.setEntries(logEntries.toArray(new LogEntry[0])); 374 | 375 | Request request = Request.builder() 376 | .cmd(Request.A_ENTRIES) 377 | .obj(aentryParam) 378 | .url(peer.getAddr()) 379 | .build(); 380 | 381 | try { 382 | AentryResult result = getRpcClient().send(request); 383 | if (result == null) { 384 | return false; 385 | } 386 | if (result.isSuccess()) { 387 | log.info("append follower entry success , follower=[{}], entry=[{}]", peer, aentryParam.getEntries()); 388 | // update 这两个追踪值 389 | nextIndexs.put(peer, entry.getIndex() + 1); 390 | matchIndexs.put(peer, entry.getIndex()); 391 | return true; 392 | } else if (!result.isSuccess()) { 393 | // 对方比我大 394 | if (result.getTerm() > currentTerm) { 395 | log.warn("follower [{}] term [{}] than more self, and my term = [{}], so, I will become follower", 396 | peer, result.getTerm(), currentTerm); 397 | currentTerm = result.getTerm(); 398 | // 认怂, 变成跟随者 399 | status = NodeStatus.FOLLOWER; 400 | return false; 401 | } // 没我大, 却失败了,说明 index 不对.或者 term 不对. 402 | else { 403 | // 递减 404 | if (nextIndex == 0) { 405 | nextIndex = 1L; 406 | } 407 | nextIndexs.put(peer, nextIndex - 1); 408 | log.warn("follower {} nextIndex not match, will reduce nextIndex and retry RPC append, nextIndex : [{}]", peer.getAddr(), 409 | nextIndex); 410 | // 重来, 直到成功. 411 | } 412 | } 413 | 414 | end = System.currentTimeMillis(); 415 | 416 | } catch (Exception e) { 417 | log.warn(e.getMessage(), e); 418 | // TODO 到底要不要放队列重试? 419 | // ReplicationFailModel model = ReplicationFailModel.newBuilder() 420 | // .callable(this) 421 | // .logEntry(entry) 422 | // .peer(peer) 423 | // .offerTime(System.currentTimeMillis()) 424 | // .build(); 425 | // replicationFailQueue.offer(model); 426 | return false; 427 | } 428 | } 429 | // 超时了,没办法了 430 | return false; 431 | }); 432 | 433 | } 434 | 435 | private LogEntry getPreLog(LogEntry logEntry) { 436 | LogEntry entry = logModule.read(logEntry.getIndex() - 1); 437 | 438 | if (entry == null) { 439 | log.warn("get perLog is null , parameter logEntry : {}", logEntry); 440 | entry = LogEntry.builder().index(0L).term(0).command(null).build(); 441 | } 442 | return entry; 443 | } 444 | 445 | 446 | class ReplicationFailQueueConsumer implements Runnable { 447 | 448 | /** 一分钟 */ 449 | long intervalTime = 1000 * 60; 450 | 451 | @Override 452 | public void run() { 453 | while (running) { 454 | try { 455 | ReplicationFailModel model = replicationFailQueue.poll(1000, MILLISECONDS); 456 | if (model == null) { 457 | continue; 458 | } 459 | if (status != NodeStatus.LEADER) { 460 | // 应该清空? 461 | replicationFailQueue.clear(); 462 | continue; 463 | } 464 | log.warn("replication Fail Queue Consumer take a task, will be retry replication, content detail : [{}]", model.logEntry); 465 | long offerTime = model.offerTime; 466 | if (System.currentTimeMillis() - offerTime > intervalTime) { 467 | log.warn("replication Fail event Queue maybe full or handler slow"); 468 | } 469 | 470 | Callable callable = model.callable; 471 | Future future = RaftThreadPool.submit(callable); 472 | Boolean r = future.get(3000, MILLISECONDS); 473 | // 重试成功. 474 | if (r) { 475 | // 可能有资格应用到状态机. 476 | tryApplyStateMachine(model); 477 | } 478 | 479 | } catch (InterruptedException e) { 480 | // ignore 481 | } catch (ExecutionException | TimeoutException e) { 482 | log.warn(e.getMessage()); 483 | } 484 | } 485 | } 486 | } 487 | 488 | private void tryApplyStateMachine(ReplicationFailModel model) { 489 | 490 | String success = stateMachine.getString(model.successKey); 491 | stateMachine.setString(model.successKey, String.valueOf(Integer.parseInt(success) + 1)); 492 | 493 | String count = stateMachine.getString(model.countKey); 494 | 495 | if (Integer.parseInt(success) >= Integer.parseInt(count) / 2) { 496 | stateMachine.apply(model.logEntry); 497 | stateMachine.delString(model.countKey, model.successKey); 498 | } 499 | } 500 | 501 | 502 | @Override 503 | public void destroy() throws Throwable { 504 | rpcServer.destroy(); 505 | stateMachine.destroy(); 506 | rpcClient.destroy(); 507 | running = false; 508 | log.info("destroy success"); 509 | } 510 | 511 | 512 | /** 513 | * 1. 在转变成候选人后就立即开始选举过程 514 | * 自增当前的任期号(currentTerm) 515 | * 给自己投票 516 | * 重置选举超时计时器 517 | * 发送请求投票的 RPC 给其他所有服务器 518 | * 2. 如果接收到大多数服务器的选票,那么就变成领导人 519 | * 3. 如果接收到来自新的领导人的附加日志 RPC,转变成跟随者 520 | * 4. 如果选举过程超时,再次发起一轮选举 521 | */ 522 | class ElectionTask implements Runnable { 523 | 524 | @Override 525 | public void run() { 526 | 527 | if (status == NodeStatus.LEADER) { 528 | return; 529 | } 530 | 531 | long current = System.currentTimeMillis(); 532 | // 基于 RAFT 的随机时间,解决冲突. 533 | electionTime = electionTime + ThreadLocalRandom.current().nextInt(50); 534 | if (current - preElectionTime < electionTime) { 535 | return; 536 | } 537 | status = NodeStatus.CANDIDATE; 538 | log.error("node {} will become CANDIDATE and start election leader, current term : [{}], LastEntry : [{}]", 539 | peerSet.getSelf(), currentTerm, logModule.getLast()); 540 | 541 | preElectionTime = System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(200) + 150; 542 | 543 | currentTerm = currentTerm + 1; 544 | // 推荐自己. 545 | votedFor = peerSet.getSelf().getAddr(); 546 | 547 | List peers = peerSet.getPeersWithOutSelf(); 548 | 549 | ArrayList> futureArrayList = new ArrayList<>(); 550 | 551 | log.info("peerList size : {}, peer list content : {}", peers.size(), peers); 552 | 553 | // 发送请求 554 | for (Peer peer : peers) { 555 | 556 | futureArrayList.add(RaftThreadPool.submit(() -> { 557 | long lastTerm = 0L; 558 | LogEntry last = logModule.getLast(); 559 | if (last != null) { 560 | lastTerm = last.getTerm(); 561 | } 562 | 563 | RvoteParam param = RvoteParam.builder(). 564 | term(currentTerm). 565 | candidateId(peerSet.getSelf().getAddr()). 566 | lastLogIndex(LongConvert.convert(logModule.getLastIndex())). 567 | lastLogTerm(lastTerm). 568 | build(); 569 | 570 | Request request = Request.builder() 571 | .cmd(Request.R_VOTE) 572 | .obj(param) 573 | .url(peer.getAddr()) 574 | .build(); 575 | 576 | try { 577 | return getRpcClient().send(request); 578 | } catch (RaftRemotingException e) { 579 | log.error("ElectionTask RPC Fail , URL : " + request.getUrl()); 580 | return null; 581 | } 582 | })); 583 | } 584 | 585 | AtomicInteger success2 = new AtomicInteger(0); 586 | CountDownLatch latch = new CountDownLatch(futureArrayList.size()); 587 | 588 | log.info("futureArrayList.size() : {}", futureArrayList.size()); 589 | // 等待结果. 590 | for (Future future : futureArrayList) { 591 | RaftThreadPool.submit(() -> { 592 | try { 593 | RvoteResult result = future.get(3000, MILLISECONDS); 594 | if (result == null) { 595 | return -1; 596 | } 597 | boolean isVoteGranted = result.isVoteGranted(); 598 | 599 | if (isVoteGranted) { 600 | success2.incrementAndGet(); 601 | } else { 602 | // 更新自己的任期. 603 | long resTerm = result.getTerm(); 604 | if (resTerm >= currentTerm) { 605 | currentTerm = resTerm; 606 | } 607 | } 608 | return 0; 609 | } catch (Exception e) { 610 | log.error("future.get exception , e : ", e); 611 | return -1; 612 | } finally { 613 | latch.countDown(); 614 | } 615 | }); 616 | } 617 | 618 | try { 619 | // 稍等片刻 620 | latch.await(3500, MILLISECONDS); 621 | } catch (InterruptedException e) { 622 | log.warn("InterruptedException By Master election Task"); 623 | } 624 | 625 | int success = success2.get(); 626 | log.info("node {} maybe become leader , success count = {} , status : {}", peerSet.getSelf(), success, NodeStatus.Enum.value(status)); 627 | // 如果投票期间,有其他服务器发送 appendEntry , 就可能变成 follower ,这时,应该停止. 628 | if (status == NodeStatus.FOLLOWER) { 629 | return; 630 | } 631 | // 加上自身. 632 | if (success >= peers.size() / 2) { 633 | log.warn("node {} become leader ", peerSet.getSelf()); 634 | status = NodeStatus.LEADER; 635 | peerSet.setLeader(peerSet.getSelf()); 636 | votedFor = ""; 637 | becomeLeaderToDoThing(); 638 | } else { 639 | // else 重新选举 640 | votedFor = ""; 641 | } 642 | // 再次更新选举时间 643 | preElectionTime = System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(200) + 150; 644 | 645 | } 646 | } 647 | 648 | /** 649 | * 初始化所有的 nextIndex 值为自己的最后一条日志的 index + 1. 如果下次 RPC 时, 跟随者和leader 不一致,就会失败. 650 | * 那么 leader 尝试递减 nextIndex 并进行重试.最终将达成一致. 651 | */ 652 | private void becomeLeaderToDoThing() { 653 | nextIndexs = new ConcurrentHashMap<>(); 654 | matchIndexs = new ConcurrentHashMap<>(); 655 | for (Peer peer : peerSet.getPeersWithOutSelf()) { 656 | nextIndexs.put(peer, logModule.getLastIndex() + 1); 657 | matchIndexs.put(peer, 0L); 658 | } 659 | 660 | // 创建[空日志]并提交,用于处理前任领导者未提交的日志 661 | LogEntry logEntry = LogEntry.builder() 662 | .command(null) 663 | .term(currentTerm) 664 | .build(); 665 | 666 | // 预提交到本地日志, TODO 预提交 667 | logModule.write(logEntry); 668 | log.info("write logModule success, logEntry info : {}, log index : {}", logEntry, logEntry.getIndex()); 669 | 670 | final AtomicInteger success = new AtomicInteger(0); 671 | 672 | List> futureList = new ArrayList<>(); 673 | 674 | int count = 0; 675 | // 复制到其他机器 676 | for (Peer peer : peerSet.getPeersWithOutSelf()) { 677 | // TODO check self and RaftThreadPool 678 | count++; 679 | // 并行发起 RPC 复制. 680 | futureList.add(replication(peer, logEntry)); 681 | } 682 | 683 | CountDownLatch latch = new CountDownLatch(futureList.size()); 684 | List resultList = new CopyOnWriteArrayList<>(); 685 | 686 | getRPCAppendResult(futureList, latch, resultList); 687 | 688 | try { 689 | latch.await(4000, MILLISECONDS); 690 | } catch (InterruptedException e) { 691 | log.error(e.getMessage(), e); 692 | } 693 | 694 | for (Boolean aBoolean : resultList) { 695 | if (aBoolean) { 696 | success.incrementAndGet(); 697 | } 698 | } 699 | 700 | // 如果存在一个满足N > commitIndex的 N,并且大多数的matchIndex[i] ≥ N成立, 701 | // 并且log[N].term == currentTerm成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节) 702 | List matchIndexList = new ArrayList<>(matchIndexs.values()); 703 | // 小于 2, 没有意义 704 | int median = 0; 705 | if (matchIndexList.size() >= 2) { 706 | Collections.sort(matchIndexList); 707 | median = matchIndexList.size() / 2; 708 | } 709 | Long N = matchIndexList.get(median); 710 | if (N > commitIndex) { 711 | LogEntry entry = logModule.read(N); 712 | if (entry != null && entry.getTerm() == currentTerm) { 713 | commitIndex = N; 714 | } 715 | } 716 | 717 | // 响应客户端(成功一半) 718 | if (success.get() >= (count / 2)) { 719 | // 更新 720 | commitIndex = logEntry.getIndex(); 721 | // 应用到状态机 722 | getStateMachine().apply(logEntry); 723 | lastApplied = commitIndex; 724 | 725 | log.info("success apply local state machine, logEntry info : {}", logEntry); 726 | } else { 727 | // 回滚已经提交的日志 728 | logModule.removeOnStartIndex(logEntry.getIndex()); 729 | log.warn("fail apply local state machine, logEntry info : {}", logEntry); 730 | 731 | // 无法提交空日志,让出领导者位置 732 | log.warn("node {} becomeLeaderToDoThing fail ", peerSet.getSelf()); 733 | status = NodeStatus.FOLLOWER; 734 | peerSet.setLeader(null); 735 | votedFor = ""; 736 | } 737 | 738 | } 739 | 740 | 741 | class HeartBeatTask implements Runnable { 742 | 743 | @Override 744 | public void run() { 745 | 746 | if (status != NodeStatus.LEADER) { 747 | return; 748 | } 749 | 750 | long current = System.currentTimeMillis(); 751 | if (current - preHeartBeatTime < heartBeatTick) { 752 | return; 753 | } 754 | log.info("=========== NextIndex ============="); 755 | for (Peer peer : peerSet.getPeersWithOutSelf()) { 756 | log.info("Peer {} nextIndex={}", peer.getAddr(), nextIndexs.get(peer)); 757 | } 758 | 759 | preHeartBeatTime = System.currentTimeMillis(); 760 | 761 | // 心跳只关心 term 和 leaderID 762 | for (Peer peer : peerSet.getPeersWithOutSelf()) { 763 | 764 | AentryParam param = AentryParam.builder() 765 | .entries(null)// 心跳,空日志. 766 | .leaderId(peerSet.getSelf().getAddr()) 767 | .serverId(peer.getAddr()) 768 | .term(currentTerm) 769 | .leaderCommit(commitIndex) // 心跳时与跟随者同步 commit index 770 | .build(); 771 | 772 | Request request = new Request( 773 | Request.A_ENTRIES, 774 | param, 775 | peer.getAddr()); 776 | 777 | RaftThreadPool.execute(() -> { 778 | try { 779 | AentryResult aentryResult = getRpcClient().send(request); 780 | long term = aentryResult.getTerm(); 781 | 782 | if (term > currentTerm) { 783 | log.error("self will become follower, he's term : {}, my term : {}", term, currentTerm); 784 | currentTerm = term; 785 | votedFor = ""; 786 | status = NodeStatus.FOLLOWER; 787 | } 788 | } catch (Exception e) { 789 | log.error("HeartBeatTask RPC Fail, request URL : {} ", request.getUrl()); 790 | } 791 | }, false); 792 | } 793 | } 794 | } 795 | 796 | @Override 797 | public Result addPeer(Peer newPeer) { 798 | return delegate.addPeer(newPeer); 799 | } 800 | 801 | @Override 802 | public Result removePeer(Peer oldPeer) { 803 | return delegate.removePeer(oldPeer); 804 | } 805 | 806 | } 807 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/impl/DefaultStateMachine.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.common.entity.Command; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | import cn.think.in.java.raft.server.StateMachine; 22 | import com.alibaba.fastjson.JSON; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.rocksdb.Options; 25 | import org.rocksdb.RocksDB; 26 | import org.rocksdb.RocksDBException; 27 | 28 | import java.io.File; 29 | 30 | /** 31 | * 默认的状态机实现. 32 | * 33 | * @author 莫那·鲁道 34 | */ 35 | @Slf4j 36 | public class DefaultStateMachine implements StateMachine { 37 | 38 | /** public just for test */ 39 | public String dbDir; 40 | public String stateMachineDir; 41 | 42 | public RocksDB machineDb; 43 | 44 | 45 | private DefaultStateMachine() { 46 | dbDir = "./rocksDB-raft/" + System.getProperty("serverPort"); 47 | 48 | stateMachineDir = dbDir + "/stateMachine"; 49 | RocksDB.loadLibrary(); 50 | 51 | File file = new File(stateMachineDir); 52 | boolean success = false; 53 | 54 | if (!file.exists()) { 55 | success = file.mkdirs(); 56 | } 57 | if (success) { 58 | log.warn("make a new dir : " + stateMachineDir); 59 | } 60 | Options options = new Options(); 61 | options.setCreateIfMissing(true); 62 | try { 63 | machineDb = RocksDB.open(options, stateMachineDir); 64 | } catch (RocksDBException e) { 65 | throw new RuntimeException(e); 66 | } 67 | } 68 | 69 | public static DefaultStateMachine getInstance() { 70 | return DefaultStateMachineLazyHolder.INSTANCE; 71 | } 72 | 73 | @Override 74 | public void init() throws Throwable { 75 | 76 | } 77 | 78 | @Override 79 | public void destroy() throws Throwable { 80 | machineDb.close(); 81 | log.info("destroy success"); 82 | } 83 | 84 | private static class DefaultStateMachineLazyHolder { 85 | 86 | private static final DefaultStateMachine INSTANCE = new DefaultStateMachine(); 87 | } 88 | 89 | @Override 90 | public LogEntry get(String key) { 91 | try { 92 | byte[] result = machineDb.get(key.getBytes()); 93 | if (result == null) { 94 | return null; 95 | } 96 | return JSON.parseObject(result, LogEntry.class); 97 | } catch (RocksDBException e) { 98 | throw new RuntimeException(e); 99 | } 100 | } 101 | 102 | @Override 103 | public String getString(String key) { 104 | try { 105 | byte[] bytes = machineDb.get(key.getBytes()); 106 | if (bytes != null) { 107 | return new String(bytes); 108 | } 109 | } catch (RocksDBException e) { 110 | throw new RuntimeException(e); 111 | } 112 | return ""; 113 | } 114 | 115 | @Override 116 | public void setString(String key, String value) { 117 | try { 118 | machineDb.put(key.getBytes(), value.getBytes()); 119 | } catch (RocksDBException e) { 120 | throw new RuntimeException(e); 121 | } 122 | } 123 | 124 | @Override 125 | public void delString(String... key) { 126 | try { 127 | for (String s : key) { 128 | machineDb.delete(s.getBytes()); 129 | } 130 | } catch (RocksDBException e) { 131 | throw new RuntimeException(e); 132 | } 133 | } 134 | 135 | @Override 136 | public synchronized void apply(LogEntry logEntry) { 137 | 138 | try { 139 | Command command = logEntry.getCommand(); 140 | 141 | if (command == null) { 142 | // 忽略空日志 143 | log.warn("insert no-op log, logEntry={}", logEntry); 144 | return; 145 | } 146 | String key = command.getKey(); 147 | machineDb.put(key.getBytes(), JSON.toJSONBytes(logEntry)); 148 | } catch (RocksDBException e) { 149 | throw new RuntimeException(e); 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/impl/RedisStateMachine.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.server.StateMachine; 20 | import cn.think.in.java.raft.common.entity.Command; 21 | import cn.think.in.java.raft.common.entity.LogEntry; 22 | import com.alibaba.fastjson.JSON; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig; 25 | import redis.clients.jedis.Jedis; 26 | import redis.clients.jedis.JedisPool; 27 | 28 | /** 29 | * redis实现状态机存储 30 | * 31 | * @author rensailong 32 | */ 33 | @Slf4j 34 | public class RedisStateMachine implements StateMachine { 35 | 36 | private JedisPool jedisPool; 37 | 38 | private RedisStateMachine() { 39 | init(); 40 | } 41 | 42 | public static RedisStateMachine getInstance() { 43 | return RedisStateMachineLazyHolder.INSTANCE; 44 | } 45 | 46 | private static class RedisStateMachineLazyHolder { 47 | 48 | private static final RedisStateMachine INSTANCE = new RedisStateMachine(); 49 | } 50 | 51 | @Override 52 | public void init() { 53 | GenericObjectPoolConfig redisConfig = new GenericObjectPoolConfig(); 54 | redisConfig.setMaxTotal(100); 55 | redisConfig.setMaxWaitMillis(10 * 1000); 56 | redisConfig.setMaxIdle(100); 57 | redisConfig.setTestOnBorrow(true); 58 | // todo config 59 | jedisPool = new JedisPool(redisConfig, System.getProperty("redis.host", "127.0.0.1"), 6379); 60 | } 61 | 62 | @Override 63 | public void destroy() throws Throwable { 64 | jedisPool.close(); 65 | log.info("destroy success"); 66 | } 67 | 68 | @Override 69 | public void apply(LogEntry logEntry) { 70 | try (Jedis jedis = jedisPool.getResource()) { 71 | Command command = logEntry.getCommand(); 72 | if (command == null) { 73 | // 忽略空日志 74 | log.warn("insert no-op log, logEntry={}", logEntry); 75 | return; 76 | } 77 | String key = command.getKey(); 78 | jedis.set(key.getBytes(), JSON.toJSONBytes(logEntry)); 79 | } catch (Exception e) { 80 | throw new RuntimeException(e); 81 | } 82 | } 83 | 84 | @Override 85 | public LogEntry get(String key) { 86 | LogEntry result = null; 87 | try (Jedis jedis = jedisPool.getResource()) { 88 | result = JSON.parseObject(jedis.get(key), LogEntry.class); 89 | } catch (Exception e) { 90 | throw new RuntimeException(e); 91 | } 92 | return result; 93 | } 94 | 95 | @Override 96 | public String getString(String key) { 97 | String result = null; 98 | try (Jedis jedis = jedisPool.getResource()) { 99 | result = jedis.get(key); 100 | } catch (Exception e) { 101 | throw new RuntimeException(e); 102 | } 103 | return result; 104 | } 105 | 106 | @Override 107 | public void setString(String key, String value) { 108 | try (Jedis jedis = jedisPool.getResource()) { 109 | jedis.set(key, value); 110 | } catch (Exception e) { 111 | throw new RuntimeException(e); 112 | } 113 | } 114 | 115 | @Override 116 | public void delString(String... keys) { 117 | try (Jedis jedis = jedisPool.getResource()) { 118 | jedis.del(keys); 119 | } catch (Exception e) { 120 | throw new RuntimeException(e); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/rpc/DefaultRpcServiceImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.rpc; 18 | 19 | import cn.think.in.java.raft.common.entity.ClientKVReq; 20 | import cn.think.in.java.raft.server.changes.ClusterMembershipChanges; 21 | import cn.think.in.java.raft.common.entity.AentryParam; 22 | import cn.think.in.java.raft.common.entity.Peer; 23 | import cn.think.in.java.raft.common.entity.RvoteParam; 24 | import cn.think.in.java.raft.server.impl.DefaultNode; 25 | import cn.think.in.java.raft.common.rpc.Request; 26 | import cn.think.in.java.raft.common.rpc.Response; 27 | import com.alipay.remoting.BizContext; 28 | import com.alipay.remoting.rpc.RpcServer; 29 | import lombok.extern.slf4j.Slf4j; 30 | 31 | /** 32 | * Raft Server 33 | * 34 | * @author 莫那·鲁道 35 | */ 36 | @Slf4j 37 | public class DefaultRpcServiceImpl implements RpcService { 38 | 39 | private final DefaultNode node; 40 | 41 | private final RpcServer rpcServer; 42 | 43 | public DefaultRpcServiceImpl(int port, DefaultNode node) { 44 | rpcServer = new RpcServer(port, false, false); 45 | rpcServer.registerUserProcessor(new RaftUserProcessor() { 46 | 47 | @Override 48 | public Object handleRequest(BizContext bizCtx, Request request) { 49 | return handlerRequest(request); 50 | } 51 | }); 52 | 53 | this.node = node; 54 | } 55 | 56 | 57 | @Override 58 | public Response handlerRequest(Request request) { 59 | if (request.getCmd() == Request.R_VOTE) { 60 | return new Response<>(node.handlerRequestVote((RvoteParam) request.getObj())); 61 | } else if (request.getCmd() == Request.A_ENTRIES) { 62 | return new Response<>(node.handlerAppendEntries((AentryParam) request.getObj())); 63 | } else if (request.getCmd() == Request.CLIENT_REQ) { 64 | return new Response<>(node.handlerClientRequest((ClientKVReq) request.getObj())); 65 | } else if (request.getCmd() == Request.CHANGE_CONFIG_REMOVE) { 66 | return new Response<>(((ClusterMembershipChanges) node).removePeer((Peer) request.getObj())); 67 | } else if (request.getCmd() == Request.CHANGE_CONFIG_ADD) { 68 | return new Response<>(((ClusterMembershipChanges) node).addPeer((Peer) request.getObj())); 69 | } 70 | return null; 71 | } 72 | 73 | 74 | @Override 75 | public void init() { 76 | rpcServer.start(); 77 | } 78 | 79 | @Override 80 | public void destroy() { 81 | rpcServer.stop(); 82 | log.info("destroy success"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/rpc/RaftUserProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.rpc; 18 | 19 | import cn.think.in.java.raft.common.rpc.Request; 20 | import cn.think.in.java.raft.server.exception.RaftNotSupportException; 21 | import com.alipay.remoting.AsyncContext; 22 | import com.alipay.remoting.BizContext; 23 | import com.alipay.remoting.rpc.protocol.AbstractUserProcessor; 24 | 25 | /** 26 | * 27 | * @author 莫那·鲁道 28 | */ 29 | public abstract class RaftUserProcessor extends AbstractUserProcessor { 30 | 31 | @Override 32 | public void handleRequest(BizContext bizCtx, AsyncContext asyncCtx, T request) { 33 | throw new RaftNotSupportException( 34 | "Raft Server not support handleRequest(BizContext bizCtx, AsyncContext asyncCtx, T request) "); 35 | } 36 | 37 | 38 | @Override 39 | public String interest() { 40 | return Request.class.getName(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/rpc/RpcService.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.rpc; 18 | 19 | import cn.think.in.java.raft.common.LifeCycle; 20 | import cn.think.in.java.raft.common.rpc.Request; 21 | import cn.think.in.java.raft.common.rpc.Response; 22 | 23 | /** 24 | * @author 莫那·鲁道 25 | */ 26 | public interface RpcService extends LifeCycle { 27 | 28 | /** 29 | * 处理请求. 30 | * @param request 请求参数. 31 | * @return 返回值. 32 | */ 33 | Response handlerRequest(Request request); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /server/src/main/java/cn/think/in/java/raft/server/util/LongConvert.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.util; 18 | 19 | /** 20 | * 21 | * @author 莫那·鲁道 22 | */ 23 | public class LongConvert { 24 | 25 | public static long convert(Long l) { 26 | if (l == null) { 27 | return 0; 28 | } 29 | return l; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /server/src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /server/src/test/java/cn/think/in/java/raft/server/impl/DefaultLogModuleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.common.entity.Command; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | import org.junit.After; 22 | import org.junit.Assert; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | 26 | 27 | 28 | /** 29 | * @author 莫那·鲁道 30 | */ 31 | public class DefaultLogModuleTest { 32 | 33 | static DefaultLogModule defaultLogs = DefaultLogModule.getInstance(); 34 | 35 | static { 36 | System.setProperty("serverPort", "8779"); 37 | defaultLogs.dbDir = "/Users/cxs/code/lu-raft-revert/rocksDB-raft/" + System.getProperty("serverPort"); 38 | defaultLogs.logsDir = defaultLogs.dbDir + "/logModule"; 39 | } 40 | 41 | @Before 42 | public void setUp() throws Exception { 43 | System.setProperty("serverPort", "8777"); 44 | } 45 | 46 | @After 47 | public void tearDown() throws Exception { 48 | 49 | } 50 | 51 | @Test 52 | public void write() { 53 | LogEntry entry = LogEntry.builder(). 54 | term(1). 55 | command(Command.builder().key("hello").value("world").build()). 56 | build(); 57 | defaultLogs.write(entry); 58 | 59 | Assert.assertEquals(entry, defaultLogs.read(entry.getIndex())); 60 | } 61 | 62 | @Test 63 | public void read() { 64 | System.out.println(defaultLogs.getLastIndex()); 65 | } 66 | 67 | @Test 68 | public void remove() { 69 | defaultLogs.removeOnStartIndex(3L); 70 | } 71 | 72 | @Test 73 | public void getLast() { 74 | 75 | } 76 | 77 | @Test 78 | public void getLastIndex() { 79 | } 80 | 81 | @Test 82 | public void getDbDir() { 83 | } 84 | 85 | @Test 86 | public void getLogsDir() { 87 | } 88 | 89 | @Test 90 | public void setDbDir() { 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server/src/test/java/cn/think/in/java/raft/server/impl/DefaultStateMachineTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import cn.think.in.java.raft.common.entity.Command; 20 | import cn.think.in.java.raft.common.entity.LogEntry; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import org.rocksdb.RocksDBException; 24 | 25 | /** 26 | * 27 | * @author 莫那·鲁道 28 | */ 29 | public class DefaultStateMachineTest { 30 | static DefaultStateMachine machine = DefaultStateMachine.getInstance(); 31 | 32 | static { 33 | System.setProperty("serverPort", "8777"); 34 | machine.dbDir = "/Users/cxs/code/lu-raft-revert/rocksDB-raft/" + System.getProperty("serverPort"); 35 | machine.stateMachineDir = machine.dbDir + "/stateMachine"; 36 | } 37 | 38 | @Before 39 | public void before() { 40 | machine = DefaultStateMachine.getInstance(); 41 | } 42 | 43 | @Test 44 | public void apply() { 45 | LogEntry logEntry = LogEntry.builder().term(1).command(Command.builder().key("hello").value("value1").build()).build(); 46 | machine.apply(logEntry); 47 | } 48 | 49 | 50 | @Test 51 | public void applyRead() throws RocksDBException { 52 | 53 | System.out.println(machine.get("hello:7")); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/test/java/cn/think/in/java/raft/server/impl/RocksDBTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one or more 3 | contributor license agreements. See the NOTICE file distributed with 4 | this work for additional information regarding copyright ownership. 5 | The ASF licenses this file to You under the Apache License, Version 2.0 6 | (the "License"); you may not use this file except in compliance with 7 | the License. You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | package cn.think.in.java.raft.server.impl; 18 | 19 | import java.io.File; 20 | 21 | import com.alibaba.fastjson.JSON; 22 | 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.rocksdb.Options; 26 | import org.rocksdb.RocksDB; 27 | import org.rocksdb.RocksDBException; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import lombok.Getter; 32 | import lombok.Setter; 33 | import lombok.ToString; 34 | 35 | /** 36 | * 37 | * @author 莫那·鲁道 38 | */ 39 | public class RocksDBTest { 40 | 41 | private static final Logger LOGGER = LoggerFactory.getLogger(DefaultStateMachine.class); 42 | 43 | private String dbDir = "./rocksDB-raft/" + System.getProperty("serverPort"); 44 | private String stateMachineDir = dbDir + "/test"; 45 | 46 | public RocksDB machineDb; 47 | 48 | static { 49 | RocksDB.loadLibrary(); 50 | } 51 | 52 | public byte[] lastIndexKey = "LAST_INDEX_KEY".getBytes(); 53 | 54 | public RocksDBTest() { 55 | try { 56 | System.setProperty("serverPort", "8078"); 57 | File file = new File(stateMachineDir); 58 | if (!file.exists()) { 59 | file.mkdirs(); 60 | } 61 | Options options = new Options(); 62 | options.setCreateIfMissing(true); 63 | machineDb = RocksDB.open(options, stateMachineDir); 64 | 65 | } catch (RocksDBException e) { 66 | e.printStackTrace(); 67 | } 68 | } 69 | 70 | 71 | public static RocksDBTest getInstance() { 72 | return RocksDBTestLazyHolder.INSTANCE; 73 | } 74 | 75 | private static class RocksDBTestLazyHolder { 76 | 77 | private static final RocksDBTest INSTANCE = new RocksDBTest(); 78 | } 79 | 80 | RocksDBTest instance; 81 | 82 | @Before 83 | public void before() { 84 | instance = getInstance(); 85 | } 86 | 87 | @Test 88 | public void test() throws RocksDBException { 89 | System.out.println(getLastIndex()); 90 | System.out.println(get(getLastIndex())); 91 | 92 | write(new Cmd("hello", "value")); 93 | 94 | System.out.println(getLastIndex()); 95 | 96 | System.out.println(get(getLastIndex())); 97 | 98 | deleteOnStartIndex(getLastIndex()); 99 | 100 | write(new Cmd("hello", "value")); 101 | 102 | deleteOnStartIndex(1L); 103 | 104 | System.out.println(getLastIndex()); 105 | 106 | System.out.println(get(getLastIndex())); 107 | 108 | 109 | } 110 | 111 | public synchronized void write(Cmd cmd) { 112 | try { 113 | cmd.setIndex(getLastIndex() + 1); 114 | machineDb.put(cmd.getIndex().toString().getBytes(), JSON.toJSONBytes(cmd)); 115 | } catch (RocksDBException e) { 116 | e.printStackTrace(); 117 | } finally { 118 | updateLastIndex(cmd.getIndex()); 119 | } 120 | } 121 | 122 | public synchronized void deleteOnStartIndex(Long index) { 123 | try { 124 | for (long i = index; i <= getLastIndex(); i++) { 125 | try { 126 | machineDb.delete((i + "").getBytes()); 127 | } catch (RocksDBException e) { 128 | e.printStackTrace(); 129 | } 130 | } 131 | 132 | } finally { 133 | updateLastIndex(index - 1); 134 | } 135 | } 136 | 137 | public Cmd get(Long index) { 138 | try { 139 | if (index == null) { 140 | throw new IllegalArgumentException(); 141 | } 142 | byte[] cmd = machineDb.get(index.toString().getBytes()); 143 | if (cmd != null) { 144 | return JSON.parseObject(machineDb.get(index.toString().getBytes()), Cmd.class); 145 | } 146 | } catch (RocksDBException e) { 147 | e.printStackTrace(); 148 | } 149 | return null; 150 | } 151 | 152 | 153 | public void updateLastIndex(Long index) { 154 | try { 155 | // overWrite 156 | machineDb.put(this.lastIndexKey, index.toString().getBytes()); 157 | } catch (RocksDBException e) { 158 | e.printStackTrace(); 159 | } 160 | } 161 | 162 | public Long getLastIndex() { 163 | byte[] lastIndex = new byte[0]; 164 | try { 165 | lastIndex = machineDb.get(this.lastIndexKey); 166 | if (lastIndex == null) { 167 | lastIndex = "0".getBytes(); 168 | } 169 | } catch (RocksDBException e) { 170 | e.printStackTrace(); 171 | } 172 | return Long.valueOf(new String(lastIndex)); 173 | } 174 | 175 | @Setter 176 | @Getter 177 | @ToString 178 | static class Cmd { 179 | 180 | Long index; 181 | String key; 182 | String value; 183 | 184 | public Cmd() { 185 | } 186 | 187 | public Cmd(String key, String value) { 188 | this.key = key; 189 | this.value = value; 190 | } 191 | } 192 | } 193 | --------------------------------------------------------------------------------