Paxos算法分析和实现

背景

Paxos算法,是用来解决分布式环境中一致性问题的算法,其在zookeeper等框架中均有体现。
提到一致性,这是一个特别常见的问题,缓存和数据库的一致性,事务的一致性,再到现在探讨的分布式一致性的问题。要弄清楚Paxos算法,必须首先明白其到底能解决什么问题?什么是分布式的一致性问题。
首先搭建一个场景:
在分布式环境下,为了实现高可靠,我们往往会通过冗余或者备份的方式,将数据保存在多个不同的节点上(节点可以简单理解为服务器),这样当其中一个节点挂掉后数据不会无法访问。但是,既然有了冗余,当我们修改某个节点 的数据时就需要考虑一致性的问题:必须将保存这个数据的其他节点也同步更新,否则会出现数据不一致的情况,导致之后通过另一个节点访问该数据时出现错误。
当然,这只是自己构建的一个场景,网上对于Paxos可解决的问题,描述如下:
机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)
而对Paxos的定义,则是“基于消息传递且具有高度容错特性的一致性算法
我理解的这个消息传递可以是简单的socket通信,也可以是消息队列的形式,还可以是rpc远程调用的方式传递消息。
高度容错特性,就是Paxos算法可以在某个节点宕机,或者网络异常,消息的延迟等问题的情况下,快速且正确在集群内部对某个数据达成一致。

内容

提案:是用来描述Paxos算法的形式化的内容,其由提案号(id)提案内容(value) 组成,id就是虚的没有实际意义的东西,单纯用来实现Paxos算法,而value对应在实际的分布式系统中为所需要修改数据的命令或log信息。
角色:是Paxos算法中抽象出来的概念,其对应着实际分布式环境中的不同分工,如proposer和客户端直接交互,接收用户的需求,并将该需求/提案发送给acceptor,由于是分布式环境,因此并发情况下可能同时有多个用户对节点数据修改,即不同的proposer向共同的acceptor同时发出提案。acceptor就是保存数据的节点,由于数据是存放在多个节点上的,因此acceptor也是不止一个的,Paxos算法的核心就是要让不同的acceptor在接收到不同proposer提案的情况下,最终依然能达成共识,即所有acceptor最终接受的提案内容相同,因此Paxos算法又叫分布式共识算法。还有最后一个角色是learner,这是用来记录信息的角色,当acceptor确定接受的提案后,就通知learner进程,让其记录该提案内容。
三种角色:

  • proposer:提案的发出者。提案由(id,value)构成
  • acceptor:提案的决议者。内部维护了(maxID, acceptID, acceptValue)三元组,分别表示当前记录的最大的提案号(只要经历过第一阶段回复即记录),当前接受响应的最大提案号(必须经历过第二阶段),当前接收响应的最大提案号对应的提案内容。
  • learner:提案的记录者。

算法过程

Paxos算法主要由两个阶段组成。

第一阶段(prepare):

  1. proposer接受用户的提案需求,生成一个id和value,并发送给半数以上的acceptor。
  2. acceptor接收到提案后,判断id是否大于自己维护的maxID,如果不大于maxID,则不回复这个提案或者回复error;如果大于maxID,则修改maxID为当前id,并回复(ok, acceptID, acceptValue)或者(ok, null, null)(之前没有提案)

第二阶段(accept):

  1. proposer如果没有收到半数以上的回复,则增大提案号,并重新开始第一阶段;如果收到半数以上的回复,则将value设为收到的回复中最大的acceptID对应的acceptValue,并将(id, value)发送给acceptor。
  2. acceptor接收到accept请求后,依然需要判断id是否大于自己维护的maxID,如果不大于maxID,则不回复这个提案或者回复error;如果大于maxID,则修改maxID,以及acceptID和acceptValue。并回复(ok)。同时将提案发送给learner,通知其记录。
  3. proposer接收到的ok数过半,可以确定对应提案被选中,如果没过半,则重新开始第一阶段,发送prepare请求。

代码实现

/*
 * @Author: gll
 * @Date: 2021-06-01 09:46:27
 * @Description: 模拟Paxos算法
 */
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

//--------------------------------Proposer.go---------------------------------//
type Data struct {
	id    int //提案ID
	value int //提案内容
}

type Proposer struct {
	m_acceptorCount int //acceptor数量
	m_proposerCount int //proposer数量

	m_value           Data //提案
	m_proposeFinished bool //完成prepared
	m_isAgree         bool //完成accept
	m_maxAcceptedNum  int
	// m_start
	m_okCount     int //acceptor的成功回复
	m_refuseCount int //acceptor的拒绝回复
}

//设置接受者和提议者的数量
func (p *Proposer) SetProposerCount(pc, ac int) {
	p.m_acceptorCount = ac
	p.m_proposerCount = pc
}

//开启propose阶段
func (p *Proposer) StartPropose(value *Data) {
	p.m_value = *value
	p.m_proposeFinished = false
	p.m_isAgree = false
	p.m_maxAcceptedNum = 0
	p.m_okCount = 0
	p.m_refuseCount = 0
}

//获取提案内容
func (p *Proposer) GetProposal() Data {
	return p.m_value
}

//判断第一阶段是否回复超过半数
func (p *Proposer) Proposed(ok bool, lastAcceptValue Data) bool {
	if p.m_proposeFinished {
		return true
	}
	if !ok {
		p.m_refuseCount++
		if p.m_refuseCount > p.m_acceptorCount/2 {
			p.m_value.id += p.m_proposerCount
			p.StartPropose(&p.m_value)
			return false
		}

		return true
	}

	p.m_okCount++
	//记录所有的回复中最大的编号对应的提案
	if lastAcceptValue.id > p.m_maxAcceptedNum {
		p.m_maxAcceptedNum = lastAcceptValue.id
		p.m_value.value = lastAcceptValue.value
	}

	if p.m_okCount > p.m_acceptorCount/2 {
		p.m_okCount = 0
		p.m_proposeFinished = true
	}
	return true
}

func (p *Proposer) StartAccept() bool {
	return p.m_proposeFinished
}

func (p *Proposer) Accepted(ok bool) bool {
	if !p.m_proposeFinished {
		return true
	}
	if !ok {
		p.m_refuseCount++
		if p.m_refuseCount > p.m_acceptorCount/2 {
			p.m_value.id += p.m_proposerCount
			p.StartPropose(&p.m_value)
			return false
		}

		return true
	}
	p.m_okCount++
	if p.m_okCount > p.m_acceptorCount/2 {
		p.m_isAgree = true
	}
	return true
}

func (p *Proposer) IsAgree() bool {
	return p.m_isAgree
}

//------------------------------------Acceptor.go-----------------------------//
type Acceptor struct {
	m_lastAcceptValue Data //最后接受的提议
	m_maxID           int  //Propose提交的最大id
}

func (a *Acceptor) accept(d *Data) bool {
	if d.id == 0 {
		return false
	}
	//注意:这里和propose阶段的不同是没有等于号,因为即使和记录的maxID相同也可以接受
	if d.id < a.m_maxID {
		return false
	}

	a.m_lastAcceptValue = *d
	//fmt.Printf("lastValue.id:%d, lastValue.value:%d\n", a.m_lastAcceptValue.id, a.m_lastAcceptValue.value)
	return true
}

//acceptor第一阶段对提案的处理
func (a *Acceptor) propose(numID int) (bool, Data) {
	if numID == 0 {
		return false, a.m_lastAcceptValue
	}
	if numID <= a.m_maxID {
		return false, a.m_lastAcceptValue
	}

	a.m_maxID = numID
	//fmt.Printf("id:%d, value:%d\n", a.m_lastAcceptValue.id, a.m_lastAcceptValue.value)
	//d = &a.m_lastAcceptValue
	return true, a.m_lastAcceptValue

}

//---------------------------------------------paxos.go-------------------------//
//设置接受者和提议者的人数
const (
	PROPOSERNUM = 2
	ACCEPTNUM   = 3
)

var (
	p           [2]Proposer    //提议者
	a           [3]Acceptor    //接受者
	mu          [3]sync.Mutex  //每一个接受者对应一个mu
	wg          sync.WaitGroup //goroutine中同步使用
	isFinished  bool           = false
	finalValue  int
	finishcount int32
)

func mypropose(id int) {
	fmt.Printf("the Proposor %d is beginning\n", id)
	mypro := p[id]                  //当前是第几个proposer
	var value = mypro.GetProposal() //获取对应的提案内容
	finalValue = -1
	//var lastValue Data              //记录acceptor保存的最新的提案

	var acceptorId [3]int //acceptor
	var count int = 0     //cceptor序号

	for {
		//开始prepare(第一阶段)
		//首先需要将提案发送给所有acceptor,并判断所有的回复是否超过半数
		value = mypro.GetProposal()
		fmt.Printf("Proposer %d 开始Propose阶段:提议 = [编号:%d, 提案内容:%d]\n", id, value.id, value.value)
		count = 0
		for i := 0; i < ACCEPTNUM; i++ {
			time.Sleep(1)
			mu[i].Lock()
			//向acceptor发出提案,并得到其返回值
			ok, lastValue := a[i].propose(value.id)
			//fmt.Printf("lastValue.id:%d, lastValue.value:%d\n", lastValue.id, lastValue.value)
			mu[i].Unlock()
			time.Sleep(1)
			if !mypro.Proposed(ok, lastValue) {
				time.Sleep(1)
				break
			}
			curValue := mypro.GetProposal()
			//判断提案内容是否被修改过
			if curValue.value != value.value {
				fmt.Printf("Proposer%d号修改了提议: 提议 = [编号:%d,提案内容:%d]\n", id, curValue.id, curValue.value)
				break
			}
			acceptorId[count] = i
			count++
			//是否超过半数,可以进行accept阶段
			if mypro.StartAccept() {
				break
			}
		}

		if !mypro.StartAccept() {
			continue
		}

		//开始Accept(第二阶段)
		value = mypro.GetProposal()
		fmt.Printf("Proposer%d号开始Accept阶段: 提议 = [编号:%d,提案内容:%d]\n", id, value.id, value.value)
		for i := 0; i < count; i++ {
			time.Sleep(1)
			mu[i].Lock()
			ok := a[acceptorId[i]].accept(&value)
			mu[i].Unlock()
			time.Sleep(1)
			if !mypro.Accepted(ok) {
				time.Sleep(1)
				break
			}

			if mypro.IsAgree() {
				fmt.Printf("%d号提议被批准, 最终提议 = [编号:%d,提案内容:%d]\n", id, value.id, value.value)
				//fmt.Printf("finalValue:%d\n", finalValue)
				if finalValue == -1 {
					finalValue = value.value
				} else if finalValue != value.value {
					finalValue = 0
				}

				atomic.AddInt32(&finishcount, 1)
				if PROPOSERNUM == atomic.LoadInt32(&finishcount) {
					isFinished = true
					if finalValue > 0 {
						fmt.Printf("Paxos完成,最终提案内容为:%d\n", finalValue)
					} else {
						fmt.Printf("Paxos完成,最终提案内容不一致\n")
					}
				}
				wg.Done()
				return
			}
		}
	}
	// return
	// wg.Done()
}

func main() {
	fmt.Println("Paxos开始")
	var d Data //提案变量
	// atomic.StoreInt32(finishcount, 0)
	for i := 0; i < PROPOSERNUM; i++ {
		p[i].SetProposerCount(PROPOSERNUM, ACCEPTNUM)
		d.id = i + 1          //设置提案id
		d.value = i + 1       //设置提案内容
		p[i].StartPropose(&d) // 将提案赋给对应的proposer
	}
	for num := 0; num < PROPOSERNUM; num++ {
		wg.Add(1)
		go mypropose(num) //开启一个新的proposer线程,使其给acceptor发送提案
	}
	wg.Wait()
	fmt.Println("Paxos结束")

}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值