拜占庭將軍問題和 Raft 共識演算法講解

来源:https://www.cnblogs.com/Jcloud/archive/2023/02/17/17129961.html
-Advertisement-
Play Games

在分散式系統中, 什麼是拜占庭將軍問題?產生的場景和解決方案是什麼?什麼是 Raft 共識演算法?Raft 演算法是如何解決拜占庭將軍問題的?其核心原理和演算法邏輯是什麼?除了 Raft,還有哪些共識演算法?共識問題作為分散式系統的一大難點和痛點,本文主要介紹了其產生的背景、原因,以及通用的 Raft 演算法... ...


作者: 京東物流 郭益如

導讀

在分散式系統中, 什麼是拜占庭將軍問題?產生的場景和解決方案是什麼?什麼是 Raft 共識演算法?Raft 演算法是如何解決拜占庭將軍問題的?其核心原理和演算法邏輯是什麼?除了 Raft,還有哪些共識演算法?共識問題作為分散式系統的一大難點和痛點,本文主要介紹了其產生的背景、原因,以及通用的 Raft 演算法解決方案。

01 拜占庭將軍問題

【分散式對等網路中的通信容錯問題。

在分散式計算中,不同的電腦通過通訊交換信息達成共識按照一套協作策略行動。有時候,系統中的成員電腦可能出錯而發送錯誤的信息,用於傳遞信息的通訊網路也可能導致信息損壞,使得網路中不同的成員關於全體協作的策略得出不同結論,從而破壞系統一致性,這就是拜占庭將軍問題。

拜占庭將軍問題被認為是容錯性問題中最難的問題類型之一。】

9 位將軍兵分 9 路去打仗,他們各自有權力觀測敵情並做出行動判斷 —— 進攻或撤退,他們必須行動一致,即所有軍隊一起進攻或者一起撤退,否則部分進攻部分撤退會造成災難性後果。

前提:

  1. 將軍之間只能通過信使互相聯繫,每位將軍將自己的判斷發送給其他將軍,並接收其他將軍發送的判斷;

  2. 收到信息的將軍綜合所有的判斷,當超過半數都選擇進攻時,就決定進攻,當超過半數都選擇撤退時就決定撤退;

問題是,將軍中間可能出現叛徒,他可能會選擇相反的結果進行通信(投票),也可能選擇性的發送信息,叛徒要達成的目標是:

  1. 選擇性的發送信息,欺騙某些將軍採取進攻的行動;

  2. 促成一個錯誤的決定,比如將軍們不希望進攻時進攻;

  3. 迷惑某些將軍,使得他們無法做出決定;

如果叛徒達成了其中之一,任何的攻擊結果都是註定要失敗的,只有完全達成一致的努力才能獲得勝利。

比如,可能 9 位將軍中有 8 位忠誠的將軍和一名叛徒,8 位將軍中 4 位選擇進攻,4 位選擇撤退,叛徒分別給選擇進攻的將軍發送進攻的信息,給選擇撤退的將軍發送撤退信息。這樣一來,在4 位選擇進攻的將軍看,共 5 位將軍選擇進攻,從而發起進攻;而在 4 位選擇撤退的將軍看,共 5 位將軍選擇撤退,從而發起撤退,這樣各個將軍的一致性就遭到了破壞。

並且,叛徒將軍可能會偽造其他將軍的身份發送信件;

拜占庭將軍問題描述的是,在存在信息丟失的不可靠通道上試圖通過消息傳遞的方式達到一致性是不可能的,在系統中除了存在的消息延遲或不可送達故障外,還可能包括消息篡改、節點處理異常等潛在性異常。

1.1 拜占庭容錯

在早期的解決方案中,一種是 “拜占庭容錯” ,它遵循“少數服從多數”的共識機制,即使出現了錯誤或偽造的信息,只要有問題的將軍數量不到 1/3,仍可以達到“拜占庭容錯”,使整個系統便可以正常運作。

為什麼是 1/3呢?

其原理是這樣的,假設將軍總數是 N,其中正直的將軍數量是 S,反叛的將軍數量是 T, 那麼 N=S+T;

為了保證即使反叛的將軍都不去投票也能產生最終的結果,那麼 S 必須要超過半數,這種情況下,S 都做出相同的選擇,依然可以達成共識,即 S>T;

如果叛徒給一半支持進攻的將軍發送進攻信息,給一半支持撤退的將軍發送撤退信息,這種情況要保證也能產生最終的投票結果,則 X > S/2 + E;

綜合以上關係,可以得到:

N = S + T

X < S

X > S/2 + T

求解以上不等式,可以得到:

(N-T)/2 > T,即 N > 3T

所以要保證正直的將軍至少占所有將軍總數的 2/3,才有可能達成共識。

1.2 拜占庭演算法

拜占庭演算法是一種共識演算法,確定共識的原則,各個節點通過這個共識原則既可以保證一致性,又能保證基本的分區容錯性。

共識是可容錯系統中的一個基本問題: 即使面對故障,伺服器如何在共用狀態上達成一致?

02 Raft演算法

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕。

【Raft 演算法解決的是簡化版的拜占庭將軍問題,即在不考慮數據丟失、篡改的情況下的拜占庭將軍問題。】

假設現在有 3 位將軍 A、B、C,將軍中沒有叛徒,信使的信息可靠,但有可能被暗殺,此時將軍們如何達成進攻的一致性決定?

方案: Raft的方案是,在所有的將軍中選出一個大將軍,用來做出所有的決定。大將軍派信使給其他將軍,如果過一段時間沒有回覆(可能被暗殺)就再派一個信使,直到收到回覆。

如果大將軍的信使,派出去一個被幹掉一個,其他將軍們總也收不到大將軍的信息,他們如何達成一致性決定?

方案: 每位將軍都有一個隨機時間的計時器,時間一到,他就把自己當成大將軍的候選人,派信使將選舉結果給將軍 B、C。 如果將軍 B、C 還沒有把選舉大將軍結果投給其他人(包括自己)時,他們就會把選舉票投給 A。A 將軍的信使返回 A 時,A 將軍就知道自己收到了足夠的票數,成為了新的大將軍。

Raft 演算法是一種簡單易懂的共識演算法,它通過首先選舉一個 Leader 主節點,然後讓Leader 完全負責數據同步,依靠狀態機和主從同步的方式,在各個節點之間實現數據的一致性。

通過這種主節點進行數據同步的方式,Raft 將一致性問題拆分成了三個相對獨立的子問題:

1. 主節點選取 Leader Election: 啟動集群時,或者現有主節點失敗時,會啟動新的投票,獲得大多數選票(N/2+1)的節點會成為新的主節點;

2. 複製日誌 Log Replication: 主節點從客戶端接收日誌信息,再把信息複製到其他從節點上,使得日誌信息都能保持數據一致;

3. 安全性: Raft 定義了一系列規範來保證數據安全性。

2.1 Raft節點

Raft 演算法為節點定義了三種角色: Leader(主節點)Follower(從節點)Candidate(參與投票競爭的節點) ,節點的角色是可以轉換的,在任意的時間,每個伺服器一定處於三種狀態中的一個。

每個節點上都有一個倒計時器(Election Timeout),隨機值在 150ms ~ 300ms 之間,當節點收到選舉請求,或收到 Leader 的 Heartbeat 時,就會重置倒計時。

2.1.1 主節點 Leader

通常情況下,系統中只有一個主節點,用來發起心跳,處理所有的客戶端請求,創建日誌和同步日誌。

2.1.2 從節點 Follower

除主節點外,其他的節點都是從節點,用於接收主節點的心跳和日誌數據,保證其數據狀態與主節點一致,以及在 Leader 選舉時,投票給 Candidate。

如果有客戶端跟Follower 聯繫,那麼 Follower 會把請求重定向給 Leader。

2.1.3 候選人 Candidate

候選人 Candidate是在 Leader 選舉過程中的臨時角色,由 Follower 轉換而來,用於發起投票參與競選。

Raft 節點狀態圖:

圖1 Raft 節點狀態圖

啟動時,或 Follower 接收不到 Leader 信息時,它就會變成 Candidate 併發起一次選舉。獲得集群中大多數選票的Candidate 就成為新的 Leader。

2.1.4 任期 Term

Raft 把時間分割成任意長度的任期 Term,用連續的整數標記。

圖2 各任期 Term 下的狀態演變

每一個任期都會開始一次新的選舉,一個或多個 Candidate 會嘗試成為 Leader。如果一個 Candidate 贏得了選舉,它就會在該任期內擔任 Leader,直到任期結束或者伺服器宕機。在某些情況下,沒有選出 Leader(如選票瓜分等),則會開啟下一個任期並立刻開始新的選舉。

任期在 Raft 演算法中充當邏輯時鐘的作用,每一個節點都會存儲當前的 Term 號,這一編號在整個集群時期內單調增長,伺服器之間通信的時候也會交換當前的 Term 號:

  • 如果一個節點發現其 Term 號比其他伺服器小,那麼它會更新自己的 Term 號到較大的值;
  • 如果一個 Candidate 或者 Leader 發現其 Term 號過期了,那麼它會立即恢覆成 Follower 狀態;
  • 如果一個節點接收到的請求中 Term 號過期了,那麼它會直接拒絕此次請求。

Raft 演算法中伺服器節點之間通信使用遠程過程調用(RPCs),並且基本的一致性演算法只需要兩種類型的 RPCs。請求投票(RequestVote) RPCs 由候選人在選舉期間發起,然後附加條目(AppendEntries)RPCs 由領導者發起,用來複制日誌和提供一種心跳機制。如果未及時收到響應,則請求者有責任重試 RPC。

2.1.5 事件 Entry

每一個事件是一個 Entry,只有 Leader 可以創建 Entry,結構為 <term, index, cmd>其中 cmd 是可以應用到狀態機的操作。

2.1.6 日誌 Log

日誌是 Raft 的核心概念,是一個由 Entry 構成的數組。只有 Leader 才可以改變其他節點的 Log。Leader 先把 Entry 添加到自己的 Log 數組中,發起共識請求,獲得同意後,才會將 Entry 提交給狀態機。Follower 只能從 Leader 中獲取新日誌和當前的 CommitIndex,然後把對應的 Entry 應用到自己的狀態機中。

2.2 選取主節點 Leader Election

2.2.1 選舉機制

  • raft 通過心跳機制來觸發 Leader 的選舉;
  • Leader 會向所有的 Follower 周期性發送心跳來保證自己的 Leader 地位。
  • 如果伺服器能夠收到來自 Leader 或者 Candidate 的有效信息,那麼它會一直保持為 Follower 狀態,並且刷新自己的 electionElapsed,重新計時。
  • 如果一個 Follower 在一個周期內沒有收到任何信息,也就是選舉超時,它就會認為此時沒有可用的 Leader,開始進行一次選舉以選出一個新的 Leader。
  • 當伺服器啟動時,所有的節點都是 Follower。

2.2.2 選舉過程

Follower 自增的 term 號並且轉換狀態為 Candidate。然後他會向所有節點發起 RequestVoteRPC 請求, Candidate 的狀態會持續到以下情況發生:

  • 獲得大多數選票(N/2 +1),贏得選舉,成為 Leader
  • 其他節點贏得選舉
  • 一輪選舉結束,無人勝出

在 Candidate 等待選票的時候,它可能收到其他節點聲明其是 Leader 的心跳:

  • 該 Leader 的 term 號大於等於自己的 term 號,說明對方已經成為 Leader,則自己回退為 Follower。
  • 該 Leader 的 term 號小於自己的 term 號,那麼會拒絕該請求並讓該節點更新 term。

為了避免出現“腦裂”,即同一時刻出現多個 Candidate,導致沒有 Candidate 獲得大多數選票的狀況,Raft 增加了隨機選舉超時時間的方法。每一個Candidate 在發起選舉後,都會隨機化一個超時時間( 150-300 毫秒),使得各個伺服器分散開來,在大多數情況下只有一個伺服器會率先超時,贏得選舉。

相關代碼實現:

【plain】
func (rf *Raft) RequestVote(request *RequestVoteRequest, response *RequestVoteResponse) {
  rf.mu.Lock()
  defer rf.mu.Unlock()
  defer rf.persist()
  defer DPrintf("{Node %v}'s state is {state %v,term %v,commitIndex %v,lastApplied %v,firstLog %v,lastLog %v} before processing requestVoteRequest %v and reply requestVoteResponse %v", rf.me, rf.state, rf.currentTerm, rf.commitIndex, rf.lastApplied, rf.getFirstLog(), rf.getLastLog(), request, response)




  if request.Term < rf.currentTerm || (request.Term == rf.currentTerm && rf.votedFor != -1 && rf.votedFor != request.CandidateId) {
    response.Term, response.VoteGranted = rf.currentTerm, false
    return
  }
  if request.Term > rf.currentTerm {
    rf.ChangeState(StateFollower)
    rf.currentTerm, rf.votedFor = request.Term, -1
  }
  if !rf.isLogUpToDate(request.LastLogTerm, request.LastLogIndex) {
    response.Term, response.VoteGranted = rf.currentTerm, false
    return
  }
  rf.votedFor = request.CandidateId
  rf.electionTimer.Reset(RandomizedElectionTimeout())
  response.Term, response.VoteGranted = rf.currentTerm, true
}


func (rf *Raft) StartElection() {
  request := rf.genRequestVoteRequest()
  DPrintf("{Node %v} starts election with RequestVoteRequest %v", rf.me, request)
  // use Closure
  grantedVotes := 1
  rf.votedFor = rf.me
  rf.persist()
  for peer := range rf.peers {
    if peer == rf.me {
      continue
    }
    go func(peer int) {
      response := new(RequestVoteResponse)
      if rf.sendRequestVote(peer, request, response) {
        rf.mu.Lock()
        defer rf.mu.Unlock()
        DPrintf("{Node %v} receives RequestVoteResponse %v from {Node %v} after sending RequestVoteRequest %v in term %v", rf.me, response, peer, request, rf.currentTerm)
        if rf.currentTerm == request.Term && rf.state == StateCandidate {
          if response.VoteGranted {
            grantedVotes += 1
            if grantedVotes > len(rf.peers)/2 {
              DPrintf("{Node %v} receives majority votes in term %v", rf.me, rf.currentTerm)
              rf.ChangeState(StateLeader)
              rf.BroadcastHeartbeat(true)
            }
          } else if response.Term > rf.currentTerm {
            DPrintf("{Node %v} finds a new leader {Node %v} with term %v and steps down in term %v", rf.me, peer, response.Term, rf.currentTerm)
            rf.ChangeState(StateFollower)
            rf.currentTerm, rf.votedFor = response.Term, -1
            rf.persist()
          }
        }
      }
    }(peer)
  }
}

2.3 日誌同步 Log Replication

Raft 通過Leader 向集群中所有 Follower 進行日誌同步來保證整個集群數據的最終一致性。

只有 Leader 有許可權接受客戶端的請求並且同步數據給集群中其他節點。每一個客戶端的請求都包含一條需要被覆制狀態機 RSM(Replicated State Mechine)執行的命令,Leader 收到客戶端請求後,會生成一個 Entry,包含,再將這個 entry 添加到自己的日誌末尾後,向所有的節點廣播該 Entry,要求其他伺服器複製這條 Entry。

圖3 主從節點進行 Entry 複製

如圖所示,Logs 日誌是一個順序存儲的 Entry 數組,方框內是任期 Term 號。

2.3.1 日誌同步流程

例如,在 Term3 中,Leader 最後一個 Entry 的Index 為 7,x 值為 5,收到請求 set x=4時:

圖4 日誌同步流程

  1. Leader 收到客戶端請求 x←4 時,Leader 會生成一條新的 Entry<8, 3, set x=4>,並將該 Entry 添加到自己的 Log 數組最後

  2. Leader 通過 AppendEntries RPC 廣播該 Entry;

  3. 如果 Follower 接受該 Entry,則會將 Entry 添加到其日誌後面,同時返回給 Leader 同意。

  4. 如果 Leader 收到了多數的成功響應,Leader 認為這個 Entry 是 committed,應用到自己的狀態機 RSM 中,並且向客戶端返回執行結果。之後,該 commited 信息會隨著之後的 AppendEntryRPC 傳達到其他節點。

  5. committed 表示被 Leader 創建的 Entry 已經複製到了大多數的伺服器上,Leader 會跟蹤它記錄的最大索引值 Index,併在之後的 AppendEntries RPC(包括心跳)中,包含該索引值,以此確保其他伺服器同步這個 Entry 已經提交,Follower 接收到該信息後,也會按順序同步更新到本地的狀態機中。

Raft 通過這種日誌機制來保證不同伺服器上日誌的一致性和安全性:

  • 在兩個日誌里,有兩個 entry 擁有相同的 index 和 term,那麼它們一定有相同的 cmd;
  • 在兩個日誌里,有兩個 entry 擁有相同的 index 和 term,那麼它們前面的 entry 也一定相同。

2.3.2 Leader Crash

一般情況下,Leader 和 Follower 的日誌保持一致,AppendEntries 的一致性檢查通常不會失敗。然後,Leader Crash 可能會導致數據丟失:

圖5 Leader Crash時的數據狀況

當最上面的 Leader 掌權後,Follower 日誌可能有 a~f 幾種情況:

1. 日誌丟失(a,b);

2. Follower 含有未提交數據(c、d);

3. 日誌丟失 + Follower 含有未提交數據(e、f);

場景 f 可能出現的情況為:

如果一臺伺服器在 Term2 時是 Leader,並且向它的日誌中添加了一些數據條目,然後在數據提交前宕機了;接著該 Leader 很快重啟後,又稱為了任期 3 的 Leader,接著又向它的日誌中添加了一些數據,然後在 Term2,Term3 數據條目提交前,又宕機了,之後一直處於宕機狀態,直到有新的 Leader 產生。

當遇到這種一致性檢查失敗的情況時,Leader 通過強制 Follower 複製自己的日誌來處理日誌的不一致。這就意味著,在 Follower 上的衝突日誌會被領導者的日誌覆蓋

Leader 找到 Follower 與它日誌一致的地方(Index=3),然後刪除 Follower 在該位置之後的日誌,接著把這之後的日誌發送給 Follower:

Leader 給每一個Follower 維護了一個 nextIndex,它表示 Leader 將要發送給該追隨者的下一條日誌條目的索引。當一個 Leader 開始掌權時,它會將 nextIndex 初始化為它的最新的日誌條目索引數+1。如果一個 Follower 的日誌和 Leader 的不一致,AppendEntries 一致性檢查會在下一次 AppendEntries RPC 時返回失敗。在失敗之後,Leader 會將 nextIndex 遞減然後重試 AppendEntries RPC。最終 nextIndex 會達到一個 Leader 和 Follower 日誌一致的地方。這時,AppendEntries 會返回成功,Follower 中衝突的日誌條目都被移除了,並且添加所缺少的上了 Leader 的日誌條目。一旦 AppendEntries 返回成功,Follower 和 Leader 的日誌就一致了,這樣的狀態會保持到該任期結束。

相關實現代碼:

【plain】
func (rf *Raft) replicateOneRound(peer int) {
  rf.mu.RLock()
  if rf.state != StateLeader {
    rf.mu.RUnlock()
    return
  }
  prevLogIndex := rf.nextIndex[peer] - 1
  if prevLogIndex < rf.getFirstLog().Index {
    // only snapshot can catch up
    request := rf.genInstallSnapshotRequest()
    rf.mu.RUnlock()
    response := new(InstallSnapshotResponse)
    if rf.sendInstallSnapshot(peer, request, response) {
      rf.mu.Lock()
      rf.handleInstallSnapshotResponse(peer, request, response)
      rf.mu.Unlock()
    }
  } else {
    // just entries can catch up
    request := rf.genAppendEntriesRequest(prevLogIndex)
    rf.mu.RUnlock()
    response := new(AppendEntriesResponse)
    if rf.sendAppendEntries(peer, request, response) {
      rf.mu.Lock()
      rf.handleAppendEntriesResponse(peer, request, response)
      rf.mu.Unlock()
    }
  }
}


func (rf *Raft) AppendEntries(request *AppendEntriesRequest, response *AppendEntriesResponse) {
  rf.mu.Lock()
  defer rf.mu.Unlock()
  defer rf.persist()
  defer DPrintf("{Node %v}'s state is {state %v,term %v,commitIndex %v,lastApplied %v,firstLog %v,lastLog %v} before processing AppendEntriesRequest %v and reply AppendEntriesResponse %v", rf.me, rf.state, rf.currentTerm, rf.commitIndex, rf.lastApplied, rf.getFirstLog(), rf.getLastLog(), request, response)


  if request.Term < rf.currentTerm {
    response.Term, response.Success = rf.currentTerm, false
    return
  }


  if request.Term > rf.currentTerm {
    rf.currentTerm, rf.votedFor = request.Term, -1
  }


  rf.ChangeState(StateFollower)
  rf.electionTimer.Reset(RandomizedElectionTimeout())


  if request.PrevLogIndex < rf.getFirstLog().Index {
    response.Term, response.Success = 0, false
    DPrintf("{Node %v} receives unexpected AppendEntriesRequest %v from {Node %v} because prevLogIndex %v < firstLogIndex %v", rf.me, request, request.LeaderId, request.PrevLogIndex, rf.getFirstLog().Index)
    return
  }


  if !rf.matchLog(request.PrevLogTerm, request.PrevLogIndex) {
    response.Term, response.Success = rf.currentTerm, false
    lastIndex := rf.getLastLog().Index
    if lastIndex < request.PrevLogIndex {
      response.ConflictTerm, response.ConflictIndex = -1, lastIndex+1
    } else {
      firstIndex := rf.getFirstLog().Index
      response.ConflictTerm = rf.logs[request.PrevLogIndex-firstIndex].Term
      index := request.PrevLogIndex - 1
      for index >= firstIndex && rf.logs[index-firstIndex].Term == response.ConflictTerm {
        index--
      }
      response.ConflictIndex = index
    }
    return
  }


  firstIndex := rf.getFirstLog().Index
  for index, entry := range request.Entries {
    if entry.Index-firstIndex >= len(rf.logs) || rf.logs[entry.Index-firstIndex].Term != entry.Term {
      rf.logs = shrinkEntriesArray(append(rf.logs[:entry.Index-firstIndex], request.Entries[index:]...))
      break
    }
  }


  rf.advanceCommitIndexForFollower(request.LeaderCommit)


  response.Term, response.Success = rf.currentTerm,True
}

2.3.3 安全性

Leader 需要保證其存儲全部已經提交的日誌條目,保證日誌條目只能從 Leader 流向 Follower,且 Leader 永遠不會覆蓋已經存在的日誌條目。

每個 Candidate 發送 RequestVoteRPC 時,都會帶上最後一個 Entry 的信息。所有節點收到投票信息時,會對該 Entry 進行比較,如果發現自己的更新,則拒絕投票給該 Candidate。

03 其他一致性演算法

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕。

3.1 Paxos 演算法

早期的共識演算法,由拜占庭將軍問題的提出者 Leslie Lamport 所發明。谷歌的分散式鎖服務 Chubby 就是以 Paxos 演算法為基礎。

3.2 ZAB 演算法

Zookeeper 所使用的一致性演算法,在流程上和 Raft 演算法比較接近。

3.3 PBFT 演算法

區塊鏈技術所使用的共識演算法之一,適用於私有鏈的共識。

04 總結

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網路獲取最新模板,當獲取到模板後進行模板載入,載入階段會將產物轉換為視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕。

Raft 演算法是很廣泛的強一致性、去中心化和高可用的分散式協議,是一種 leader-based 的共識演算法。通過將共識問題拆分成主節點選舉和主從日誌同步,以及安全流程,來提高分散式系統的數據一致性、可靠性和容錯性;首先選舉主節點,然後主節點負責接收外部請求、數據複製、提交,保證系統中數據都是一致的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、日期和時間的格式化 1、原生方法 1.1、使用 toLocaleString 方法 Date 對象有一個 toLocaleString 方法,該方法可以根據本地時間和地區設置格式化日期時間。例如: const date = new Date(); console.log(date.toLocal ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 最簡單的代碼,最極致的享受,主打的就是一個炫酷~ 滾動視差 滾動視差效果(Parallax Scrolling)是指讓多層背景以不同的速度位移,形成立體的運動效果的視覺體驗,在前端強交互的時代,更應該多考慮這種用戶體驗較好的動效~ 實現方 ...
  • js - 數字轉中文 JavaScript 中將阿拉伯數字轉換為中文 轉換代碼 var _change = { ary0: ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'], ary1: ['', '十', '百', '千'], ary2: ['' ...
  • 如有需要請下載leader_line.js文件https://pan.baidu.com/s/1zNJhiGsH32hNpTwAEsyQaQ?pwd=1997 <template> <div style="height:100%;overflow: hidden;width:100%" v-load ...
  • 創建前端項目 npm install create-react-app -g create-react-app my-app-client create-react-app 是創建單頁面程式的腳手架 前端目錄結構 創建好項目之後,刪掉 src 目錄下的文件,按照以下結構創建目錄,根據具體項目情況進行 ...
  • 前言:用於展示生產線數據相關信息 在車輛生產線的小屏幕上【西門子的,比1980*1080的要小一圈,比pad要大一圈,專門給生產線做的】展示數據。數據用的flex佈局,很簡單的解決了自適應的問題。效果很好。 但,image展示的圖片的寬高不勻稱,比例不同。 這是開始的代碼,只摘取了關鍵部分: <bo ...
  • defineExpose要在變數和方法聲明定義之後再使用,否則瀏覽器的控制台會輸出很多警告,並且最終將該頁面卡死。 ...
  • 今天,有個群友在群里提問,使用 CSS 能否實現下述這個圖形: emmm,中間這個酷似三次貝塞爾曲線的造型,使用 CSS 不太好實現。我的建議是切圖實現,然而群友要求一定要用 CSS 實現。 雖然麻煩,但是這個圖形勉強也是能用 CSS 實現的。本文就將探討一下上述圖形的純 CSS 實現方式,並且從中 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...