BFT共识协议Java简单实现
关于拜占庭容错
拜占庭容错指的是若干节点之间要达成信息一致,但是其中节点可能会作恶,比如发送不一致的假消息,这一点与Raft等以前熟知的非拜占庭容错协议不一样,非拜占庭容错场景里的节点可能会故障,但不会故意作恶。
BFT协议里要求有3N+1个节点,那么如果诚实节点达到2N+1,则整个节点网络可以达到共识。
分为3个阶段:PRE-PREPARE, PREPARE, COMMIT
代码
public class Message {
enum Type {PRE_PREPARE, PREPARE, COMMIT};
Type type; //消息类型
int viewNumber; //任期,主节点不换任期不变
int seqNumber; //消息在某任期下的序列号
String digest; //消息摘要
int senderId; //发送者ID,消息是谁发的
public Message(Type type, int viewNumber, int seqNumber, String digest, int senderId){
this.type = type;
this.viewNumber = viewNumber;
this.seqNumber = seqNumber;
this.digest = digest;
this.senderId = senderId;
}
import com.alibaba.fastjson2.JSON;
import java.util.HashMap;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 模拟节点
* */
public class Node implements Runnable{
int id;
public boolean primary;
//List<Node> allNodes; // 模拟广播
Queue<Message> preQueue = new LinkedBlockingQueue<Message>();
Queue<Message> prepareQueue = new LinkedBlockingQueue<Message>();
Queue<Message> commitQueue = new LinkedBlockingQueue<Message>();
HashMap<String, Integer> msgMap = new HashMap<>(); //key viewnumber+seqNubmer+type+digest value count
HashMap<String, Boolean> msgBroadMap = new HashMap<>(); //key viewnumber+seqNubmer+type+digest value bool
public Node (int id, boolean primary){
this.id = id;
this.primary = primary;
}
//接收消息
void receive(Message message) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String key = getMessageKey(message);
if(null==msgMap.get(key)){
msgMap.put(key, 1);
}else {
msgMap.put(key, msgMap.get(key)+1);
}
msgBroadMap.put(key, false);
//System.out.println(msgMap);
//System.out.println(msgBroadMap);
switch (message.type){
case PRE_PREPARE: preQueue.add(message); break;
case PREPARE: prepareQueue.add(message); break;
case COMMIT: commitQueue.add(message); break;
}
}
private boolean isMessageTrue(Message msg){
String key = getMessageKey(msg);
if(null!=msgMap.get(key)){
if(msgMap.get(key) >= 3) return true;
}
return false;
}
// 节点收PRE_PREPARE消息,到网络中广播 prepare消息
void handlePrePrepare(Message msg) {
System.out.println(this.id + "节点收到PRE-PREPARE消息" + JSON.toJSONString(msg));
NetwokContext.broadcast(msg, Message.Type.PREPARE, this.id);
String key = getMessageKey(msg);
msgBroadMap.put(key, true);
}
// 节点收到prepare消息,判断一致的消息数量是否达到2n+1,是则广播commit消息
void handlePrepare(Message msg) {
String key = getMessageKey(msg);
if(isMessageTrue(msg) && !msgBroadMap.get(key)){
System.out.println(this.id + "节点收到PREPARE消息" + JSON.toJSONString(msg) + "后,已收到满足数量的一致消息,广播COMMIT消息");
NetwokContext.broadcast(msg, Message.Type.COMMIT, this.id);
msgBroadMap.put(key, true);
}
}
//节点收到commit消息,判断一致的消息数量是否达到2n+1,是则最终提交
void handleCommit(Message msg){
String key = getMessageKey(msg);
if(isMessageTrue(msg) && !msgBroadMap.get(key)) {
System.out.println(this.id + "节点收到COMMIT消息" + JSON.toJSONString(msg) + "后,已收到满足数量的一致消息,进行本地提交执行");
msgBroadMap.put(key, true);
}
}
@Override
public void run() {
while (true){
if(!preQueue.isEmpty()) handlePrePrepare(preQueue.poll());
if(!prepareQueue.isEmpty()) handlePrepare(prepareQueue.poll());
if(!commitQueue.isEmpty()) handleCommit(commitQueue.poll());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private String getMessageKey(Message msg){
return msg.viewNumber+""+ msg.seqNumber + "" + msg.type + msg.digest;
}
}
上面是消息类和节点类,接下来是这个4节点网络环境的模拟上下文,以及测试程序。
/**
* 模拟节点中保存的网络上下文
* */
public class NetwokContext {
public static List<Node> allNodes = new ArrayList<>(); // 模拟广播
public static void broadcast(Message msg, Message.Type type,int senderId){
for(Node node : allNodes){
Message amsg = new Message(type, msg.viewNumber, msg.seqNumber, msg.digest, senderId);
node.receive(amsg);
}
}
}
public class PBFTdemo {
public static void main(String[] args) throws Exception{
// 模拟 4 个节点,其中 Node0 是主节点
Node node0 = new Node(0, true);
Node node1 = new Node(1, false);
Node node2 = new Node(2, false);
Node node3 = new Node(3, false);
NetwokContext.allNodes.add(node0);
NetwokContext.allNodes.add(node1);
NetwokContext.allNodes.add(node2);
NetwokContext.allNodes.add(node3);
Thread thread0 = new Thread(node0);
Thread thread1 = new Thread(node1);
Thread thread2 = new Thread(node2);
Thread thread3 = new Thread(node3);
thread0.setDaemon(true);
thread1.setDaemon(true);
thread2.setDaemon(true);
thread3.setDaemon(true);
thread0.start();
thread1.start();
thread2.start();
thread3.start();
case1();
//case2();
TimeUnit.SECONDS.sleep(10);
}
//正常情况
private static void case1(){
String digest = "提刀上洛"; // 简化
Message prePrepare = new Message(Message.Type.PRE_PREPARE, 0, 1, digest, 0);
for (Node node : NetwokContext.allNodes) {
node.receive(prePrepare);
}
}
//主节点作恶
private static void case2(){
NetwokContext.allNodes.get(0).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "提刀上洛", 0));
NetwokContext.allNodes.get(1).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "提刀上洛", 0));
NetwokContext.allNodes.get(2).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "跑路", 0));
NetwokContext.allNodes.get(3).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "跑路", 0));
}
}
case1运行结果:
1节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
0节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
2节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
3节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
0节点收到PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
1节点收到PREPARE消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
2节点收到PREPARE消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
3节点收到PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
0节点收到COMMIT消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
1节点收到COMMIT消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
2节点收到COMMIT消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
3节点收到COMMIT消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
case2运行结果:
0节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
1节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
2节点收到PRE-PREPARE消息{"digest":"跑路","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
3节点收到PRE-PREPARE消息{"digest":"跑路","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
浙公网安备 33010602011771号