├── .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 | 
4 | [](https://travis-ci.com/stateis0/lu-raft-kv)
5 | [](https://codecov.io/gh/stateis0/lu-raft-kv)
6 | 
7 | [](http://isitmaintained.com/project/stateis0/lu-raft-kv "average time to resolve an issue")
8 | [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------