转载自Ryan是菜鸟 | LNMP技术栈笔记

以太坊中除了基于运算能力的POW(Ethash)外,还有基于权利证明的POA共识机制,Clique是以太坊的POA共识算法的实现,这里主要对POA的Clique相关源码做一个解读分析。

Clique的初始化在 Ethereum.StartMining中,如果Ethereum.engine配置为clique.Clique, 根据当前节点的矿工地址(默认是acounts[0]), 配置clique的 签名者 : clique.Authorize(eb, wallet.SignHash) ,其中签名函数是SignHash,对给定的hash进行签名。

func (s *Ethereum) StartMining(local bool) error {eb, err := s.Etherbase()//用户地址if err != nil {log.Error("Cannot start mining without etherbase", "err", err)return fmt.Errorf("etherbase missing: %v", err)}if clique, ok := s.engine.(*clique.Clique); ok {//如果是clique共识算法wallet, err := s.accountManager.Find(accounts.Account{Address: eb})    // 根据用它胡地址获取wallet对象if wallet == nil || err != nil {log.Error("Etherbase account unavailable locally", "err", err)return fmt.Errorf("signer missing: %v", err)}clique.Authorize(eb, wallet.SignHash) // 注入签名者以及wallet对象获取签名方法}if local {// 如果本地CPU已开始挖矿,我们可以禁用引入的交易拒绝机制来加速同步时间。CPU挖矿在主网是荒诞的,所以没有人能碰到这个路径,然而一旦CPU挖矿同步标志完成以后,将保证私网工作也在一个独立矿工结点。atomic.StoreUint32(&s.protocolManager.acceptTxs, 1)}go s.miner.Start(eb)return nil
}

这个StartMining会在miner.start前调用,然后通过woker -> agent -> CPUAgent -> update -> seal 挖掘区块和组装(后面会写单独的文章来对挖矿过程做源码分析)。

Clique的代码块在go-ethereum/consensus/clique路径下。和ethash一样,在clique.go 中实现了consensus的接口, consensus 定义了下面这些接口:

type Engine interface {Author(header *types.Header) (common.Address, error)VerifyHeader(chain ChainReader, header *types.Header, seal bool) errorVerifyHeaders(chain ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error)VerifyUncles(chain ChainReader, block *types.Block) errorVerifySeal(chain ChainReader, header *types.Header) errorPrepare(chain ChainReader, header *types.Header) errorFinalize(chain ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error)Seal(chain ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error)CalcDifficulty(chain ChainReader, time uint64, parent *types.Header) *big.IntAPIs(chain ChainReader) []rpc.API
}

Engine.Seal()函数可对一个调用过 Finalize()的区块进行授权或封印,成功时返回的区块全部成员齐整,可视为一个正常区块,可被广播到整个网络中,也可以被插入区块链等。对于挖掘一个新区块来说,所有相关代码里 Engine.Seal()是其中最重要最复杂的一步,所以这里我们首先来看下Clique 结构体:

type Clique struct {config *params.CliqueConfig // 共识引擎配置参数db     ethdb.Database       // 数据库,用来存储和获取快照检查点recents    *lru.ARCCache // 最近区块快照,加速快照重组signatures *lru.ARCCache // 最近区块签名,加速挖矿proposals map[common.Address]bool // 目前正在推送的提案signer common.Address // 签名者的以太坊地址signFn SignerFn       // 授权哈希的签名方法lock   sync.RWMutex   // 用锁来保护签名字段
}

顺便来看下CliqueConfig共识引擎的配置参数结构体:

type CliqueConfig struct {Period uint64 `json:"period"` // 在区块之间执行的秒数(比如出块秒数15s)Epoch  uint64 `json:"epoch"`  // Epoch长度,重置投票和检查点(比如Epoch长度是30000个block, 每次进入新的epoch,前面的投票都被清空, 重新开始记录)
}

在上面的 StartMining中,通过Clique. Authorize来注入签名者和签名方法,先来看下Authorize:

func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {c.lock.Lock()defer c.lock.Unlock()// 这个方法就是为clique共识注入一个签名者的私钥地址已经签名函数用来挖出新块c.signer = signerc.signFn = signFn
}

再来看Clique的Seal()函数的具体实现:

//通过本地签名认证创建已密封的区块
func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {header := block.Header()// 不密封创世块number := header.Number.Uint64()if number == 0 {return nil, errUnknownBlock}// 不支持0-period的链,不支持空块密封,没有奖励但是能够密封if c.config.Period == 0 && len(block.Transactions()) == 0 {return nil, errWaitTransactions}// 在整个密封区块的过程中不要持有signer签名者字段c.lock.RLock()signer, signFn := c.signer, c.signFn //获取签名者和签名方法c.lock.RUnlock()snap, err := c.snapshot(chain, number-1, header.ParentHash, nil) //调用获取快照if err != nil {return nil, err}//检查我们是否被授权去签名一个区块if _, authorized := snap.Signers[signer]; !authorized {return nil, errUnauthorized}// 如果我们是在‘最近签名者’中则等待下一个区块for seen, recent := range snap.Recents {if recent == signer {// 当前签名者在‘最近签名者’中,如果当前区块没有剔除他的话只能等待(这里涉及到机会均等)if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit {log.Info("Signed recently, must wait for others")<-stopreturn nil, nil}}}// 好了,走到这说明协议已经允许我们来签名这个区块,等待我们的时间delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now()) // nolint: gosimpleif header.Difficulty.Cmp(diffNoTurn) == 0 {// 这不是我们的轮次来签名,延迟一点,随机延迟,这样对于每一个签签名者来说来允许并发签名wiggle := time.Duration(len(snap.Signers)/2+1) * wiggleTimedelay += time.Duration(rand.Int63n(int64(wiggle)))log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))}log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay))select {case <-stop:return nil, nilcase <-time.After(delay):}// 通过signFn签名函数开始签名sighash, err := signFn(accounts.Account{Address: signer}, sigHash(header).Bytes())if err != nil {return nil, err}//将签名结果替换保存在区块头的Extra字段中copy(header.Extra[len(header.Extra)-extraSeal:], sighash)//通过区块头重新组装生成一个区块return block.WithSeal(header), nil
}

Seal是共识引擎的入口之一,该函数通过clique.signer对区块签名

  • signer不在snapshot的signer中不允许签名
  • signer不是本区块的签名者需要延时随机一段时候后再签名,是本区块的签名者则直接签名
  • 签名存放在Extra的extraSeal的65个字节中

关于机会均等
为了使得出块的负载(或者说是机会)对于每个认证节点尽量均等,同时避免某些恶意节点持续出块,clique中规定每一个认证节点在连续SIGNER_LIMIT个区块中,最多只能签发一个区块,也就是说,每一轮中,最多只有SIGNER_COUNT - SIGNER_LIMIT个认证节点可以参与区块签发。
其中SIGNER_LIMIT = floor(SIGNER_COUNT / 2) + 1,SIGNER_COUNT表示认证节点的个数。

//snap.Signers是所有的认证节点
for seen, recent := range snap.Recents {if recent == signer {if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit {log.Info("Signed recently, must wait for others")<-stopreturn nil, nil}}
}

在保证好节点的个数大于坏节点的前提下,好节点最少的个数为SIGNER_LIMIT(大于50%),坏节点最多的个数为SIGNER_COUNT - SIGNER_LIMIT(小于50%)。一个节点在SIGNER_LIMIT这个时间窗口内最多只能签发一个区块,这就使得恶意节点在不超过50%的情况下,从理论上无法一直掌握区块的签发权。

关于难度计算
为了让每个认证节点都有均等的机会去签发一个区块,每个节点在签发时都会判断本节点是不是本轮的inturn节点,若是inturn节点,则该节点产生的区块难度为2,否则为1。每一轮仅有一个节点为inturn节点。

diffInTurn = big.NewInt(2)
diffNoTurn = big.NewInt(1) 

当inturn的结点离线时,其他结点会来竞争,难度值降为1。然而正常出块时,limit中的所有认证结点包括一个inturn和其他noturn的结点,clique是采用了给noturn加延迟时间的方式来支持inturn首先出块,避免noturn的结点无谓生成区块,上面的延时代码段已经有提现了。
判断是否为inturn的节点,将本地维护的认证节点按照字典序排序,若当前区块号除以认证节点个数的余数等于该节点的下标,则该节点为inturn节点。代码实现在 snapshot.go中:


// 通过给定的区块高度和签发者返回该签发者是否在轮次内
func (s *Snapshot) inturn(number uint64, signer common.Address) bool {signers, offset := s.signers(), 0for offset < len(signers) && signers[offset] != signer {offset++}return (number % uint64(len(signers))) == uint64(offset)
}

Seal()代码中有获取快照,然后从快照中来检查授权区块签名者的逻辑,那么我们继续来看下Snapshot,首先看下Snapshot的结构体:

// Snapshot对象是在给定时间点的一个认证投票的状态
type Snapshot struct {config   *params.CliqueConfig // 共识引擎配置参数sigcache *lru.ARCCache        // 签名缓存,最近的区块签名加速恢复。Number  uint64                      `json:"number"`  // 快照建立的区块号Hash    common.Hash                 `json:"hash"`    // 快照建立的区块哈希Signers map[common.Address]struct{} `json:"signers"` // 当下认证签名者的列表Recents map[uint64]common.Address   `json:"recents"` // 最近担当过数字签名算法的signer 的地址Votes   []*Vote                     `json:"votes"`   // 按时间顺序排列的投票名单。Tally   map[common.Address]Tally    `json:"tally"`   // 当前的投票结果,避免重新计算。
}

快照Snapshot对象中存在投票的Votes和记票的Tally对象:

// Vote代表了一个独立的投票,这个投票可以授权一个签名者,更改授权列表。
type Vote struct {Signer    common.Address `json:"signer"`    // 已授权的签名者(通过投票)Block     uint64         `json:"block"`     // 投票区块号Address   common.Address `json:"address"`   // 被投票的账户,修改它的授权Authorize bool           `json:"authorize"` // 对一个被投票账户是否授权或解授权
}// Tally是一个简单的用来保存当前投票分数的计分器
type Tally struct {Authorize bool `json:"authorize"` // 授权true或移除falseVotes     int  `json:"votes"`     // 该提案已获票数
}

Snapshot是一个快照,不仅是一个缓存,而且存储了最近签名者的map
loadSnapshot用来从数据库中加载一个已存在的快照:

func loadSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, db ethdb.Database, hash common.Hash) (*Snapshot, error) {//使用Database接口的Get方法通过Key来查询缓存内容blob, err := db.Get(append([]byte("clique-"), hash[:]...))if err != nil {return nil, err}snap := new(Snapshot)if err := json.Unmarshal(blob, snap); err != nil {return nil, err}snap.config = configsnap.sigcache = sigcachereturn snap, nil
}

newSnapshot函数用于创建快照,这个方法没有初始化最近的签名者集合,所以只使用创世块:

func newSnapshot(config *params.CliqueConfig, sigcache *lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot {//组装一个Snapshot对象snap := &Snapshot{config:   config,sigcache: sigcache,Number:   number,Hash:     hash,Signers:  make(map[common.Address]struct{}),Recents:  make(map[uint64]common.Address),Tally:    make(map[common.Address]Tally),}for _, signer := range signers {snap.Signers[signer] = struct{}{}}return snap
}

继续看下snapshot函数的具体实现:

// 快照会在给定的时间点检索授权快照
func (c *Clique) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*Snapshot, error) {// 在内存或者磁盘上查找一个快照来检查检查点checkpointsvar (headers []*types.Header    //区块头snap    *Snapshot    //快照对象)for snap == nil {// 如果在内存中找到快照时,快照对象从内存中取if s, ok := c.recents.Get(hash); ok {snap = s.(*Snapshot)break}// 如果在磁盘检查点找到快照时if number%checkpointInterval == 0 { //checkpointInterval = 1024 表示投票快照保存到数据库的区块的区块号if s, err := loadSnapshot(c.config, c.signatures, c.db, hash); err == nil {log.Trace("Loaded voting snapshot form disk", "number", number, "hash", hash)snap = sbreak}}// 如果在创世块,则新建一个快照if number == 0 {genesis := chain.GetHeaderByNumber(0)if err := c.VerifyHeader(chain, genesis, false); err != nil {return nil, err}signers := make([]common.Address, (len(genesis.Extra)-extraVanity-extraSeal)/common.AddressLength)for i := 0; i < len(signers); i++ {copy(signers[i][:], genesis.Extra[extraVanity+i*common.AddressLength:])}snap = newSnapshot(c.config, c.signatures, 0, genesis.Hash(), signers)if err := snap.store(c.db); err != nil {return nil, err}log.Trace("Stored genesis voting snapshot to disk")break}// 没有对于这个区块头的快照,收集区块头并向后移var header *types.Headerif len(parents) > 0 {// 如果我们有明确的父,从那里挑选(强制执行)header = parents[len(parents)-1]if header.Hash() != hash || header.Number.Uint64() != number {return nil, consensus.ErrUnknownAncestor}parents = parents[:len(parents)-1]} else {// 没有明确的父(或者没有更多的父)转到数据库获取header = chain.GetHeader(hash, number)if header == nil {return nil, consensus.ErrUnknownAncestor}}headers = append(headers, header)number, hash = number-1, header.ParentHash}// 找到了之前的快照,将所有的pedding块头放在它上面for i := 0; i < len(headers)/2; i++ {headers[i], headers[len(headers)-1-i] = headers[len(headers)-1-i], headers[i]}snap, err := snap.apply(headers) //通过区块头生成一个新的快照if err != nil {return nil, err}c.recents.Add(snap.Hash, snap) //将当前区块的区块hash保存到最近区块快照,加速快照重组// 如果我们已经生成一个新的检查点快照,保存在磁盘上if snap.Number%checkpointInterval == 0 && len(headers) > 0 {if err = snap.store(c.db); err != nil {return nil, err}log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash)}return snap, err
}

在snapshot中,snap.apply通过区块头来创建一个新的快照,这个apply中主要做什么操作?

//apply将给定的区块头应用于原始头来创建新的授权快照。
func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) {//可以传空区块头if len(headers) == 0 {return s, nil}//完整性检查区块头可用性for i := 0; i < len(headers)-1; i++ {if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 {return nil, errInvalidVotingChain}}if headers[0].Number.Uint64() != s.Number+1 {return nil, errInvalidVotingChain}//迭代区块头,创建一个新的快照snap := s.copy()// 投票的处理核心代码for _, header := range headers {// 删除检查点区块的所有投票 number := header.Number.Uint64()// 如果区块高度正好在Epoch结束,则清空投票和计分器,避免了维护统计信息无限增大的内存开销;if number%s.config.Epoch == 0 {snap.Votes = nilsnap.Tally = make(map[common.Address]Tally)}//从最近的签名者列表中删除最旧的签名者以允许它再次签名if limit := uint64(len(snap.Signers)/2 + 1); number >= limit {delete(snap.Recents, number-limit)}// 从区块头中解密出来签名者地址signer, err := ecrecover(header, s.sigcache)if err != nil {return nil, err}if _, ok := snap.Signers[signer]; !ok {return nil, errUnauthorized}for _, recent := range snap.Recents {if recent == signer {return nil, errUnauthorized}}snap.Recents[number] = signer// 区块头认证,不管该签名者之前的任何投票for i, vote := range snap.Votes {if vote.Signer == signer && vote.Address == header.Coinbase {// 从缓存计数器中移除该投票snap.uncast(vote.Address, vote.Authorize)// 从按时间排序的列表中移除投票snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)break // 只允许一票}}// 从签名者中计数新的投票var authorize boolswitch {case bytes.Equal(header.Nonce[:], nonceAuthVote):authorize = truecase bytes.Equal(header.Nonce[:], nonceDropVote):authorize = falsedefault:return nil, errInvalidVote}if snap.cast(header.Coinbase, authorize) {snap.Votes = append(snap.Votes, &Vote{Signer:    signer,Block:     number,Address:   header.Coinbase,Authorize: authorize,})}// 判断票数是否超过一半的投票者,如果投票通过,更新签名者列表if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 {if tally.Authorize {snap.Signers[header.Coinbase] = struct{}{}} else {delete(snap.Signers, header.Coinbase)// 签名者列表缩减,删除最近剩余的缓存if limit := uint64(len(snap.Signers)/2 + 1); number >= limit {delete(snap.Recents, number-limit)}for i := 0; i < len(snap.Votes); i++ {if snap.Votes[i].Signer == header.Coinbase {                                    // 从缓存计数器中移除该投票snap.uncast(snap.Votes[i].Address, snap.Votes[i].Authorize)// 从按时间排序的列表中移除投票    snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)i--}}}// 不管之前的任何投票,直接改变账户for i := 0; i < len(snap.Votes); i++ {if snap.Votes[i].Address == header.Coinbase {snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...)i--}}delete(snap.Tally, header.Coinbase)}}snap.Number += uint64(len(headers))snap.Hash = headers[len(headers)-1].Hash()return snap, nil
}

Snapshot.apply()方法的主要部分是迭代处理每个header对象,首先从数字签名中恢复出签名所用公钥,转化为common.Address类型,作为signer地址。数字签名(signagure)长度65 bytes,存放在Header.Extra[]的末尾。如果signer地址是尚未认证的,则直接退出本次迭代;如果是已认证的,则投票+1。所以一个父区块可添加一张记名投票,signer作为投票方地址,Header.Coinbase作为被投票地址,投票内容authorized可由Header.Nonce取值确定。更新投票统计信息。如果被投票地址的总投票次数达到已认证地址个数的一半,则通过之。该被投票地址的认证状态立即被更改,根据是何种更改,相应的更新缓存数据,并删除过时的投票信息。在所有Header对象都被处理完后,Snapshot内部的Number,Hash值会被更新,表明当前Snapshot快照结构已经更新到哪个区块了。

区块验证的过程是普通节点在收到一个新区块时,会从区块头的extraData字段中取出认证节点的签名,利用标准的spec256k1椭圆曲线进行反解公钥信息,并且从公钥中截取出签发节点的地址,若该节点是认证节点,且该节点本轮拥有签名的权限,则认为该区块为合法区块。verifySeal是被SubmitWork(miner/remote_agent.go) 来调用,SubmitWork函数尝试注入一个pow解决方案(共识引擎)到远程代理,返回这个解决方案是否被接受。(不能同时是一个坏的pow也不能有其他任何错误,例如没有工作被pending)解决方案有效时,返回到矿工并且通知接受结果。

// 检查包头中包含的签名是否满足共识协议要求。该方法接受一个可选的父头的列表,这些父头还不是本地区块链的一部分,用于生成快照
func (c *Clique) verifySeal(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error {// 不支持校检创世块number := header.Number.Uint64()if number == 0 {return errUnknownBlock}// 检索出所需的区块对象来校检去开头和将其缓存snap, err := c.snapshot(chain, number-1, header.ParentHash, parents)if err != nil {return err}//解析授权密钥并检查签署者,ecrecover方法从区块头中反解出Extra字段中签名字符串来获取签名者地址signer, err := ecrecover(header, c.signatures)if err != nil {return err}if _, ok := snap.Signers[signer]; !ok {return errUnauthorized}for seen, recent := range snap.Recents {if recent == signer {// 签署者是最近的,只有当前块没有移出时才会失败,参见seal中的机会均等if limit := uint64(len(snap.Signers)/2 + 1); seen > number-limit {return errUnauthorized}}}// 设置区块难度,参见上面的区块难度部分inturn := snap.inturn(header.Number.Uint64(), signer)if inturn && header.Difficulty.Cmp(diffInTurn) != 0 {return errInvalidDifficulty}if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 {return errInvalidDifficulty}return nil
}

前面已经分析了Clique的认证节点的出块和校检的过程,那么如何来区分一个节点是认证节点还是一个普通节点?以及一个授权者列表是如何产生并如何全网同步的?

Clique通过投票机制来确认一个认证节点,投票的范围在委员会中,委员会就是所有节点矿工集合,普通节点没有区块生成权利。矿工的投票流程如下:

  • 委员会节点通过RPC调用Propose,对某节点状态变更,从普通节点变成认证阶段,或者相反,写入到Clique.purposal集合中
// Propose注入一个新的授权提案,可以授权一个签名者或者移除一个。
func (api *API) Propose(address common.Address, auth bool) {api.clique.lock.Lock()defer api.clique.lock.Unlock()api.clique.proposals[address] = auth// true:授权,false:移除
}
  • 本地认证节点在一次区块打包的过程中,从purposal池中随机挑选一条还未被应用的purposal,并将信息填入区块头,将区块广播给其他节点;
//Clique.Prepare// 抓取所有有意义投票的提案addresses := make([]common.Address, 0, len(c.proposals))for address, authorize := range c.proposals {if snap.validVote(address, authorize) {addresses = append(addresses, address)}}// If there's pending proposals, cast a vote on themif len(addresses) > 0 {header.Coinbase = addresses[rand.Intn(len(addresses))] //随机挑选一条投票节点的地址赋值给区块头的Coinbase字段。// 通过提案内容来组装区块头的随机数字段。if c.proposals[header.Coinbase] {copy(header.Nonce[:], nonceAuthVote)} else {copy(header.Nonce[:], nonceDropVote)}}

在挖矿开始以后,会在miner.start()中提交一个commitNewWork,其中调用上面Prepare

    if err := self.engine.Prepare(self.chain, header); err != nil {log.Error("Failed to prepare header for mining", "err", err)return}
  • 其他节点在接收到区块后,取出其中的信息,封装成一个vote进行存储,并将投票结果应用到本地,若关于目标节点的状态更改获得的一致投票超过1/2,则更改目标节点的状态:若为新增认证节点,将目标节点的地址添加到本地的认证节点的列表中;若为删除认证节点,将目标节点的地址从本地的认证节点列表中删除。具体实现可以查看上面的Snapshot.apply()方法

以太坊POA共识机制Clique源码分析 1相关推荐

  1. 以太坊POA共识机制Clique源码分析

    以太坊中除了基于运算能力的POW(Ethash)外,还有基于权利证明的POA共识机制,Clique是以太坊的POA共识算法的实现,这里主要对POA的Clique相关源码做一个解读分析. Clique的 ...

  2. 【区块链 | 智能合约】Ethereum源代码(8)- Ethereum服务和以太坊P2P协议发送广播源码分析

    在"[区块链 | 智能合约]Ethereum源代码(2)- go-ethereum 客户端入口代码和Node分析"一文中,我们提到Ethereum作为一个service,被Node ...

  3. android 开发零起步学习笔记(二十二):ANDROID应用ACTIVITY、DIALOG、POPWINDOW、TOAST窗口添加机制及源码分析(一)

    原文:http://www.cnblogs.com/shanzei/p/4654817.html 第一部分: ANDROID应用ACTIVITY.DIALOG.POPWINDOW.TOAST窗口添加机 ...

  4. View事件分发机制(源码分析篇)

    01.Android中事件分发顺序 1.1 事件分发的对象是谁 事件分发的对象是事件.注意,事件分发是向下传递的,也就是父到子的顺序. 当用户触摸屏幕时(View或ViewGroup派生的控件),将产 ...

  5. Apache Storm 实时流处理系统ACK机制以及源码分析

    1.ACK机制简介 Storm的可靠性是指Storm会告知用户每一个消息单元是否在一个指定的时间(timeout)内被完全处理.完全处理的意思是该MessageId绑定的源Tuple以及由该源Tupl ...

  6. Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析

    1  背景 之所以写这一篇博客的原因是因为之前有写过一篇<Android应用setContentView与LayoutInflater加载解析机制源码分析>, 然后有人在文章下面评论和微博 ...

  7. (转) Android应用Activity、Dialog、PopWindow、Toast窗口添加机制及源码分析

    转载[工匠若水 http://blog.csdn.net/yanbober ] 1 背景 之所以写这一篇博客的原因是因为之前有写过一篇<Android应用setContentView与Layou ...

  8. Handler机制的源码分析

    2019独角兽企业重金招聘Python工程师标准>>> Handler,MessageQueue,Looper的关系 Looper的作用是在线程中处理消息的 MessageQueue ...

  9. 以太坊PoA共识引擎算法介绍(1)

    1. 以太坊中PoA产生的背景 如果你想用以太坊搭建一个联盟/私有链, 并要求该链交易成本更低甚至没有, 交易延时更低,并发更高, 还拥有完全的控制权(意味着被攻击概率更低). 目前以太坊采用PoW或 ...

最新文章

  1. Logger对象父子关系
  2. MSDN 教程短片 WPF 20(绑定3-ObjectDataProvider)
  3. 解决重启VCSA 6.0提示:503 Service Unavailable错误
  4. mybatis工作笔记003---Mybatis批量删除deleteByIds的用法
  5. 让一个DIV对齐到底部
  6. JavaScript DOM操作总结
  7. SAS学习笔记之《SAS编程与数据挖掘商业案例》(2)数据获取与数据集操作
  8. linux和嵌入式开发区别,嵌入式开发与普通编程开发的区别
  9. (转) latch 入门
  10. Python爬虫入门之查询ip地址
  11. 前端-埋点-理念-通识-浅谈
  12. 非对称加密-区块链核心技术之一
  13. 【微软算法面试高频题】可怜的小猪
  14. STM32F407配置pca9685驱动
  15. 全加器高进位和低进位的理解
  16. 【深挖字符串操作】·万字总结,这些知识点你真的懂了吗?
  17. 计算机应用基础 题库,计算机应用基础题库
  18. 摄像头寻找斑马线上拐点和摄像头图像压缩
  19. 简历应该怎么写?HR看一篇简历仅需要5秒吗?简历模板大全分享
  20. Virtualbox使用NAT模式, 并用端口转发实现主机访问虚拟机

热门文章

  1. 三思近10000㎡天幕屏耀显上海“八万人”体育场
  2. oracle lms进程 内存,Oracle进程:LMS 进程与Oracle RAC
  3. 2022最新Emlog新版导航源码 带用户中心
  4. 阿里云oss——OSSObject
  5. java IO的学习记录
  6. crash:overview
  7. [Place 30-876] Port ‘txclk‘ is assigned to PACKAGE_PIN * which can
  8. 6-3 统计专业人数 (15 分)
  9. 深圳软件测试培训:DOM节点
  10. 一场关于开源芯片生态之语言与工具链的讨论