Raft算法之Leader选举

news2024/11/27 11:56:26

Raft算法之Leader选举

一、Leader选举概述

Raft 使用心跳(heartbeat)触发Leader选举。当服务器启动时,初始化为Follower。Leader向所有Followers周期性发送heartbeat。如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举。

Follower将其当前term加一然后转换为Candidate。它首先给自己投票并且给集群中的其他服务器发送 RequestVote RPC 。结果有以下三种情况:

1、赢得了多数的选票,成功选举为Leader;

2、收到了Leader的消息,表示有其它服务器已经抢先当选了Leader;

3、没有服务器赢得多数的选票,Leader选举失败,等待选举时间超时后发起下一次选举。

在这里插入图片描述

选举出Leader后,Leader通过定期向所有Followers发送心跳信息维持其统治。若Follower一段时间未收到Leader的心跳则认为Leader可能已经挂了,会再次发起Leader选举过程。

二、ETCD中raft模块的Leader选举

大多数Raft算法的实现都有一个庞大的设计,包括存储处理,消息序列化,以及网络传输。etcd中的raft遵循最小设计哲学,仅仅实现Raft算法的核心。

首先看raft模块里面启动节点的入口代码:

// raft/node.go 文件
// StartNode returns a new Node given configuration and a list of raft peers.
// It appends a ConfChangeAddNode entry for each given peer to the initial log.
//
// Peers must not be zero length; call RestartNode in that case.
func StartNode(c *Config, peers []Peer) Node {
	if len(peers) == 0 {
		panic("no peers given; use RestartNode instead")
	}
	rn, err := NewRawNode(c) // ref-1 创建初始的节点(Node)
	if err != nil {
		panic(err)
	}
	rn.Bootstrap(peers) // ref-2 对节点进行引导

	n := newNode(rn)

	go n.run() // ref-3  让节点跑起来
	return &n
}

ref-1处的代码会创建一个初始的节点,细节如下:

// raft/rawnode.go文件
// NewRawNode instantiates a RawNode from the given configuration.
//
// See Bootstrap() for bootstrapping an initial state; this replaces the former
// 'peers' argument to this method (with identical behavior). However, It is
// recommended that instead of calling Bootstrap, applications bootstrap their
// state manually by setting up a Storage that has a first index > 1 and which
// stores the desired ConfState as its InitialState.
func NewRawNode(config *Config) (*RawNode, error) {
	r := newRaft(config) // ref-4 创建一个raft出来,我理解这儿的raft就代表一个集群
	rn := &RawNode{
        
        
		raft: r,
	}
	rn.prevSoftSt = r.softState()
	rn.prevHardSt = r.hardState()
	return rn, nil
}

ref-4会创建一个raft出来,详细代码如下:

// raft/raft.go文件
func newRaft(c *Config) *raft {
	...... // 省略
	r.becomeFollower(r.Term, None) // ref-5 变更当前节点角色为Follower

	var nodesStrs []string
	for _, n := range r.prs.VoterNodes() {
		nodesStrs = append(nodesStrs, fmt.Sprintf("%x", n))
	}

	r.logger.Infof("newRaft %x [peers: [%s], term: %d, commit: %d, applied: %d, lastindex: %d, lastterm: %d]",
		r.id, strings.Join(nodesStrs, ","), r.Term, r.raftLog.committed, r.raftLog.applied, r.raftLog.lastIndex(), r.raftLog.lastTerm())
	return r
}

节点角色一共分为Follower、Leader和candidate,ref-5处代码会切换角色为Follower,并且传递的from参数是None。我理解这是一个初始的状态。

接下来我们看ref-2处的代码细节,看看是初始节点是如何启动的:

// raft/bootstrap.go文件
// Bootstrap initializes the RawNode for first use by appending configuration
// changes for the supplied peers. This method returns an error if the Storage
// is nonempty.
//
// It is recommended that instead of calling this method, applications bootstrap
// their state manually by setting up a Storage that has a first index > 1 and
// which stores the desired ConfState as its InitialState.
func (rn *RawNode) Bootstrap(peers []Peer) error {
	if len(peers) == 0 {
		return errors.New("must provide at least one peer to Bootstrap")
	}
	lastIndex, err := rn.raft.raftLog.storage.LastIndex()
	if err != nil {
		return err
	}

	if lastIndex != 0 {
		return errors.New("can't bootstrap a nonempty Storage")
	}

	// We've faked out initial entries above, but nothing has been
	// persisted. Start with an empty HardState (thus the first Ready will
	// emit a HardState update for the app to persist).
	rn.prevHardSt = emptyState

	// TODO(tbg): remove StartNode and give the application the right tools to
	// bootstrap the initial membership in a cleaner way.
	rn.raft.becomeFollower(1, None) // ref-6 变更角色为follower
	ents := make([]pb.Entry, len(peers))
	for i, peer := range peers {
		cc := pb.ConfChange{Type: pb.ConfChangeAddNode, NodeID: peer.ID, Context: peer.Context}
		data, err := cc.Marshal()
		if err != nil {
			return err
		}

		ents[i] = pb.Entry{Type: pb.EntryConfChange, Term: 1, Index: uint64(i + 1), Data: data}
	}
	rn.raft.raftLog.append(ents...)

	// Now apply them, mainly so that the application can call Campaign
	// immediately after StartNode in tests. Note that these nodes will
	// be added to raft twice: here and when the application's Ready
	// loop calls ApplyConfChange. The calls to addNode must come after
	// all calls to raftLog.append so progress.next is set after these
	// bootstrapping entries (it is an error if we try to append these
	// entries since they have already been committed).
	// We do not set raftLog.applied so the application will be able
	// to observe all conf changes via Ready.CommittedEntries.
	//
	// TODO(bdarnell): These entries are still unstable; do we need to preserve
	// the invariant that committed < unstable?
	rn.raft.raftLog.committed = uint64(len(ents))
	for _, peer := range peers {
		rn.raft.applyConfChange(pb.ConfChange{NodeID: peer.ID, Type: pb.ConfChangeAddNode}.AsV2())
	}
	return nil
}

ref-6处的代码会将角色变更为follower,注意它的term传递的是1.

我们接着看ref-3处的代码,看看节点是如何run起来的,代码细节如下:

// raft/node.go文件
func (n *node) run() {
	var propc chan msgWithResult
	var readyc chan Ready
	var advancec chan struct{}
	var rd Ready

	r := n.rn.raft

	lead := None

	for {
		if advancec != nil {
			readyc = nil
		} else if n.rn.HasReady() {
			// Populate a Ready. Note that this Ready is not guaranteed to
			// actually be handled. We will arm readyc, but there's no guarantee
			// that we will actually send on it. It's possible that we will
			// service another channel instead, loop around, and then populate
			// the Ready again. We could instead force the previous Ready to be
			// handled first, but it's generally good to emit larger Readys plus
			// it simplifies testing (by emitting less frequently and more
			// predictably).
			rd = n.rn.readyWithoutAccept()
			readyc = n.readyc
		}

		if lead != r.lead { // ref-7 这儿再判断leader是否发生了变更
			if r.hasLeader() { // 判断是否有leader
				if lead == None { // 初始状态,从没有leader的情况选举出来了leader
					r.logger.Infof("raft.node: %x elected leader %x at term %d", r.id, r.lead, r.Term)
				} else { // leader变更的情况
					r.logger.Infof("raft.node: %x changed leader from %x to %x at term %d", r.id, lead, r.lead, r.Term)
				}
				propc = n.propc
			} else { // 集群失去leader
				r.logger.Infof("raft.node: %x lost leader %x at term %d", r.id, lead, r.Term)
				propc = nil
			}
			lead = r.lead
		}

		select {
		// TODO: maybe buffer the config propose if there exists one (the way
		// described in raft dissertation)
		// Currently it is dropped in Step silently.
		case pm := <-propc:
			m := pm.m // 收到的消息
			m.From = r.id
			err := r.Step(m) // ref-8
			if pm.result != nil {
				pm.result <- err
				close(pm.result)
			}
		case m := <-n.recvc:
			// filter out response message from unknown From.
			if pr := r.prs.Progress[m.From]; pr != nil || !IsResponseMsg(m.Type) {
				r.Step(m) // ref-9
			}
		case cc := <-n.confc:
			_, okBefore := r.prs.Progress[r.id]
			cs := r.applyConfChange(cc)
			// If the node was removed, block incoming proposals. Note that we
			// only do this if the node was in the config before. Nodes may be
			// a member of the group without knowing this (when they're catching
			// up on the log and don't have the latest config) and we don't want
			// to block the proposal channel in that case.
			//
			// NB: propc is reset when the leader changes, which, if we learn
			// about it, sort of implies that we got readded, maybe? This isn't
			// very sound and likely has bugs.
			if _, okAfter := r.prs.Progress[r.id]; okBefore && !okAfter {
				var found bool
			outer:
				for _, sl := range [][]uint64{cs.Voters, cs.VotersOutgoing} {
					for _, id := range sl {
						if id == r.id {
							found = true
							break outer
						}
					}
				}
				if !found {
					propc = nil
				}
			}
			select {
			case n.confstatec <- cs:
			case <-n.done:
			}
		case <-n.tickc:
			n.rn.Tick()
		case readyc <- rd:
			n.rn.acceptReady(rd)
			advancec = n.advancec
		case <-advancec:
			n.rn.Advance(rd)
			rd = Ready{}
			advancec = nil
		case c := <-n.status:
			c <- getStatus(r)
		case <-n.stop:
			close(n.done)
			return
		}
	}
}

注意ref-8和ref-9处的step(m)函数,他们就是在处理具体的投票信息,函数细节如下:

// raft/raft.go文件
func (r *raft) Step(m pb.Message) error {
	// Handle the message term, which may result in our stepping down to a follower.
	switch { // 这个switch是在处理任期数据
	case m.Term == 0:
		// local message
	case m.Term > r.Term
        // 大概意思是要是在超时时间内收到了来自当前leader的投票请求,那么就不会更新自己的term,也不会再授予投票
		if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {
			force := bytes.Equal(m.Context, []byte(campaignTransfer))
			inLease := r.checkQuorum && r.lead != None && r.electionElapsed < r.electionTimeout
			if !force && inLease {
				// If a server receives a RequestVote request within the minimum election timeout
				// of hearing from a current leader, it does not update its term or grant its vote
				r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] ignored %s from %x [logterm: %d, index: %d] at term %d: lease is not expired (remaining ticks: %d)",
					r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term, r.electionTimeout-r.electionElapsed)
				return nil
			}
		}
		switch { 
		case m.Type == pb.MsgPreVote:
			// Never change our term in response to a PreVote
		case m.Type == pb.MsgPreVoteResp && !m.Reject: // 要是term是来自拒绝我们的投票的节点,那么我们就会成为一个follower
			// We send pre-vote requests with a term in our future. If the
			// pre-vote is granted, we will increment our term when we get a
			// quorum. If it is not, the term comes from the node that
			// rejected our vote so we should become a follower at the new
			// term.
		default:
			r.logger.Infof("%x [term: %d] received a %s message with higher term from %x [term: %d]",
				r.id, r.Term, m.Type, m.From, m.Term)
            // 变更自己成为一个follower
			if m.Type == pb.MsgApp || m.Type == pb.MsgHeartbeat || m.Type == pb.MsgSnap {
				r.becomeFollower(m.Term, m.From)
			} else {
				r.becomeFollower(m.Term, None)
			}
		}

	case m.Term < r.Term:
		if (r.checkQuorum || r.preVote) && (m.Type == pb.MsgHeartbeat || m.Type == pb.MsgApp) {
			// We have received messages from a leader at a lower term. It is possible
			// that these messages were simply delayed in the network, but this could
			// also mean that this node has advanced its term number during a network
			// partition, and it is now unable to either win an election or to rejoin
			// the majority on the old term. If checkQuorum is false, this will be
			// handled by incrementing term numbers in response to MsgVote with a
			// higher term, but if checkQuorum is true we may not advance the term on
			// MsgVote and must generate other messages to advance the term. The net
			// result of these two features is to minimize the disruption caused by
			// nodes that have been removed from the cluster's configuration: a
			// removed node will send MsgVotes (or MsgPreVotes) which will be ignored,
			// but it will not receive MsgApp or MsgHeartbeat, so it will not create
			// disruptive term increases, by notifying leader of this node's activeness.
			// The above comments also true for Pre-Vote
			//
			// When follower gets isolated, it soon starts an election ending
			// up with a higher term than leader, although it won't receive enough
			// votes to win the election. When it regains connectivity, this response
			// with "pb.MsgAppResp" of higher term would force leader to step down.
			// However, this disruption is inevitable to free this stuck node with
			// fresh election. This can be prevented with Pre-Vote phase.
			r.send(pb.Message{To: m.From, Type: pb.MsgAppResp})
		} else if m.Type == pb.MsgPreVote {
			// Before Pre-Vote enable, there may have candidate with higher term,
			// but less log. After update to Pre-Vote, the cluster may deadlock if
			// we drop messages with a lower term.
			r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] rejected %s from %x [logterm: %d, index: %d] at term %d",
				r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
			r.send(pb.Message{To: m.From, Term: r.Term, Type: pb.MsgPreVoteResp, Reject: true})
		} else {
			// ignore other cases
			r.logger.Infof("%x [term: %d] ignored a %s message with lower term from %x [term: %d]",
				r.id, r.Term, m.Type, m.From, m.Term)
		}
		return nil
	}

	switch m.Type { // 这个switch是在处理消息类型
	case pb.MsgHup:
		if r.preVote {
			r.hup(campaignPreElection) // 处理选举消息 // ref-10
		} else {
			r.hup(campaignElection) // 处理选举消息 // ref-11
		}

	case pb.MsgVote, pb.MsgPreVote:
		// We can vote if this is a repeat of a vote we've already cast...
		canVote := r.Vote == m.From ||
			// ...we haven't voted and we don't think there's a leader yet in this term...
			(r.Vote == None && r.lead == None) ||
			// ...or this is a PreVote for a future term...
			(m.Type == pb.MsgPreVote && m.Term > r.Term)
		// ...and we believe the candidate is up to date.
		if canVote && r.raftLog.isUpToDate(m.Index, m.LogTerm) {
			// Note: it turns out that that learners must be allowed to cast votes.
			// This seems counter- intuitive but is necessary in the situation in which
			// a learner has been promoted (i.e. is now a voter) but has not learned
			// about this yet.
			// For example, consider a group in which id=1 is a learner and id=2 and
			// id=3 are voters. A configuration change promoting 1 can be committed on
			// the quorum `{2,3}` without the config change being appended to the
			// learner's log. If the leader (say 2) fails, there are de facto two
			// voters remaining. Only 3 can win an election (due to its log containing
			// all committed entries), but to do so it will need 1 to vote. But 1
			// considers itself a learner and will continue to do so until 3 has
			// stepped up as leader, replicates the conf change to 1, and 1 applies it.
			// Ultimately, by receiving a request to vote, the learner realizes that
			// the candidate believes it to be a voter, and that it should act
			// accordingly. The candidate's config may be stale, too; but in that case
			// it won't win the election, at least in the absence of the bug discussed
			// in:
			// https://github.com/etcd-io/etcd/issues/7625#issuecomment-488798263.
			r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] cast %s for %x [logterm: %d, index: %d] at term %d",
				r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
			// When responding to Msg{Pre,}Vote messages we include the term
			// from the message, not the local term. To see why, consider the
			// case where a single node was previously partitioned away and
			// it's local term is now out of date. If we include the local term
			// (recall that for pre-votes we don't update the local term), the
			// (pre-)campaigning node on the other end will proceed to ignore
			// the message (it ignores all out of date messages).
			// The term in the original message and current local term are the
			// same in the case of regular votes, but different for pre-votes.
			r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
			if m.Type == pb.MsgVote {
				// Only record real votes.
				r.electionElapsed = 0
				r.Vote = m.From
			}
		} else {
			r.logger.Infof("%x [logterm: %d, index: %d, vote: %x] rejected %s from %x [logterm: %d, index: %d] at term %d",
				r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), r.Vote, m.Type, m.From, m.LogTerm, m.Index, r.Term)
			r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
		}

	default:
		err := r.step(r, m)
		if err != nil {
			return err
		}
	}
	return nil
}

ref-10和ref-11调用的hup函数如下所示:

// raft/raft.go文件
func (r *raft) hup(t CampaignType) {
	if r.state == StateLeader {
		r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
		return
	}

	if !r.promotable() {
		r.logger.Warningf("%x is unpromotable and can not campaign", r.id)
		return
	}
	ents, err := r.raftLog.slice(r.raftLog.applied+1, r.raftLog.committed+1, noLimit)
	if err != nil {
		r.logger.Panicf("unexpected error getting unapplied entries (%v)", err)
	}
	if n := numOfPendingConf(ents); n != 0 && r.raftLog.committed > r.raftLog.applied {
		r.logger.Warningf("%x cannot campaign at term %d since there are still %d pending configuration changes to apply", r.id, r.Term, n)
		return
	}

	r.logger.Infof("%x is starting a new election at term %d", r.id, r.Term)
	r.campaign(t) // ref-12
}

ref-12处代码调用的campaign函数如下所示:

// raft/raft.go文件
// campaign transitions the raft instance to candidate state. This must only be
// called after verifying that this is a legitimate transition.
func (r *raft) campaign(t CampaignType) {
	if !r.promotable() {
		// This path should not be hit (callers are supposed to check), but
		// better safe than sorry.
		r.logger.Warningf("%x is unpromotable; campaign() should have been called", r.id)
	}
	var term uint64
	var voteMsg pb.MessageType
	if t == campaignPreElection { // 选举的第一阶段 
 		r.becomePreCandidate()
		voteMsg = pb.MsgPreVote
		// PreVote RPCs are sent for the next term before we've incremented r.Term.
		term = r.Term + 1
	} else { // 选举的第二阶段
		r.becomeCandidate()
		voteMsg = pb.MsgVote
		term = r.Term
	}
    // 拉取投票结果
	if _, _, res := r.poll(r.id, voteRespMsgType(voteMsg), true); res == quorum.VoteWon {
		// We won the election after voting for ourselves (which must mean that
		// this is a single-node cluster). Advance to the next state.
		if t == campaignPreElection {
			r.campaign(campaignElection)
		} else {
			r.becomeLeader()
		}
		return
	}
	var ids []uint64
	{
		idMap := r.prs.Voters.IDs()
		ids = make([]uint64, 0, len(idMap))
		for id := range idMap {
			ids = append(ids, id)
		}
		sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
	}
	for _, id := range ids {
		if id == r.id {
			continue
		}
		r.logger.Infof("%x [logterm: %d, index: %d] sent %s request to %x at term %d",
			r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), voteMsg, id, r.Term)

		var ctx []byte
		if t == campaignTransfer {
			ctx = []byte(t)
		}
        // 发送投票请求
		r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
	}
}

参考资料

1、Raft算法详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/715632.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图像视频基础

参考学习资料&#xff1a;https://blog.csdn.net/qq_28258885/article/details/116192244 文章目录 图像颜色深度分辨率 视频帧率比特率帧类型消除冗余的方法时间冗余&#xff08;帧间预测&#xff09;空间冗余&#xff08;帧内预测&#xff09; 视频编码器1.分区2.预测3.转换…

软件测试基础教程学习4

文章目录 软件测试技术和方法4.1 静态测试和动态测试4.2 黑盒测试和白盒测试概述4.3 黑盒测试技术4.3.1 等价类划分4.3.2 边值分析4.3.3 因果图法4.3.4 正交实验设计法4.4.5 决策表驱动测试4.5.6 错误推荐法 4.4 白盒测试技术4.4.1 程序结构分析测试4.4.2 逻辑覆盖测试4.4.3 路…

JSP页面跳转刷新

问题: 当前的jsp页面触发ajax请求后,能够获得新的相应页面,但是浏览器上展示的依然是老的页面,数据不刷新 尝试使用页面重定向依然无效, 最后使用js的window.location.href, 让浏览器的页面url 重加载才ok function submitDate() {var date1 document.getElementById("d…

【uniapp】uniapp反向代理解决跨域问题(devServer)

背景介绍 前段时间&#xff0c;在拿uniapp开发的时候&#xff0c;出现了跨域问题&#xff0c;按理说跨域应该由后端解决&#xff0c;但既然咱前端可以上&#xff0c;我想就上了&#xff08;顺手装个13&#xff09; 首先介绍什么是跨域 出于浏览器的同源策略&#xff0c;在发…

docker-使用harbor搭建私有仓库

前提 安装docker-ce 安装docker-compose 安装 安装docker-ce # step 1: 安装必要的一些系统工具 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 # Step 2: 添加软件源信息 sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce…

金蝶云星空无需代码连接钉钉考勤的方法

金蝶云星空用户使用场景&#xff1a; 企业的销售渠道人员出差之前需要在金蝶云星空上提交出差申请单&#xff0c;并等待审批通过&#xff1b;每当销售任务完成&#xff0c;金蝶云星空上的审批通过后&#xff0c;需要人力在考勤系统中手动修改考勤信息。看似比较简单的流程&…

基于Java+SpringBoot+vue的汽车改装方案网站设计与实现

博主介绍&#xff1a;✌擅长Java、微信小程序、Python、Android等&#xff0c;专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不然下次找不到哟 Java项目精品实战案…

Bitbucket 新版本的安全限制

新版本的安全限制 是继续按照他给的第二个链接进入Bitbucket仓库后台添加App密码&#xff0c;也就是每个仓库需要单独的秘密码&#xff0c;这样的话就更加安全。 生产新密码&#xff1a; 这一坨务必要妥善保存&#xff0c;因为一旦点了关闭之后你就再也没有机会看到这个密码了…

Hive一分钟

分区和分桶 1.分区表是将大的表文件划分成多个小文件以利于查询&#xff0c;但是如果数据分布不均衡&#xff0c;也会影响查询效率。 2.桶表可以对数据进行哈希取模&#xff0c;目的是让数据能够均匀的分布在表的各个文件中。 3.物理上&#xff0c;每个桶就是表和分区目录里的…

14个在你的WordPress网站上使用OpenAI的最好方法(2003)

您是否想知道如何在您的WordPress网站上使用OpenAI和ChatGPT&#xff1f; OpenAI可以提供一切帮助&#xff0c;从为您的帖子生成元描述到撰写电子邮件销售文案。您可以在您的WordPress网站上使用OpenAI来节省时间、降低成本、改善您的搜索引擎优化和工作流程&#xff0c;并发展…

推荐大型电商项目【谷粒商城】

谷粒商城项目是尚硅谷研究院最新推出的完整大型分布式架构电商平台&#xff0c;技术全面、业务深入&#xff0c;全网无出其右。 技术涵盖&#xff1a;微服务架构分布式全栈集群部署自动化运维可视化CICD&#xff0c;对标阿里P6/P7&#xff0c;冲击40-60w年薪。 项目由业务集群…

吐血整理!可免费使用的国产良心软件分享,几乎满足你办公需求

在这个信息化时代&#xff0c;软件已经成为我们办公和生活的必备工具。然而&#xff0c;市面上的大部分国产软件都需要付费才能使用&#xff0c;给我们的经济负担增加了不少。幸运的是&#xff0c;国内有一些良心软件&#xff0c;它们质量上乘&#xff0c;功能强大&#xff0c;…

myCobot机器人ChatGPT应用:设计原则和模型能力

我们将 ChatGPT 的功能扩展到机器人&#xff0c;并通过语言直观地控制机器人手臂、无人机和家庭助理机器人等多个平台。 你有没有想过用你自己的话告诉机器人该怎么做&#xff0c;就像你对人类一样&#xff1f;只是告诉你的家庭助理机器人&#xff1a;“请加热我的午餐”&…

SpringBoot+ Dubbo + Mybatis + Nacos +Seata整合来实现Dubbo分布式事务

1.简介 “ 本文主要介绍SpringBoot2.1.5 Dubbo 2.7.3 Mybatis 3.4.2 Nacos 1.1.3 Seata 0.8.0整合来实现Dubbo分布式事务管理&#xff0c;使用Nacos 作为 Dubbo和Seata的注册中心和配置中心,使用 MySQL 数据库和 MyBatis来操作数据。 ” 如果你还对SpringBoot、Dubbo、Nacos…

数据湖真的能取代数据仓库吗?【SNP SAP数据转型 】

数据湖和数据仓库的存在并不冲突&#xff0c;也并不是取代的关系&#xff0c;而是相互的融合关系。 数据湖是近两年中比较新的技术在大数据领域中&#xff0c;对于一个真正的数据湖应该是什么样子&#xff0c;现在对数据湖认知还是处在探索的阶段&#xff0c;像现在代表的开源产…

(五)复函数积分的定义与性质

本文内容主要如下&#xff1a; 1. 复积分的概念1.1. 复积分的定义1.2. 复积分的存在性与计算1.3. 一个圆周上的重要积分公式1.4. 复积分的基本性质 1. 复积分的概念 1.1. 复积分的定义 定义&#xff1a; 如图&#xff0c;C为平面上一条光滑的简单曲线: z z ( t ) x ( t )…

GAD7980/CL1680/AD7980详解与开发说明

目录 1 概述2 GAD7980简介3 用法时序4 参数计算与参数解释4.1 采样率4.2 转换时间4.3 采集时间5 采样数值折算6 设计注意事项7 代码demo 1 概述 本文用于讲述GAD7980的功能与用法&#xff0c;以及其中一些参数的计算方法&#xff0c;用法时序&#xff0c;输出数值等等&#xf…

chatglm+langchain

目录 chatglmlangchain 1.1. 主要功能&#xff1a; 1.2. Langchain中提供的模块 1.3. Langchain应用场景 2.1. chatglm应用&#xff1a; 1.1. 基于单一文档问答的实现原理 chatglmlangchain GitHub - imClumsyPanda/langchain-ChatGLM: langchain-ChatGLM, local knowledge bas…

基于Java+SpringBoot+Mybaties-plus+Vue+ElementUI 在线考试管理系统的设计与实现

一.项目介绍 学生在线考试系统分为三类角色 超管、老师、学生 超级管理员&#xff1a;维护考试管理、提供管理、成绩查询、学生管理以及教师管理 老师&#xff1a;维护考试管理、提供管理、成绩查询以及学生管理 学生&#xff1a;我的试卷…

Linux入门介绍-CentOS和VMware虚拟机下载安装

Linux 学自尚硅谷武晟然老师&#xff0c;结合老师课堂内容和自己笔记所写博文。 文章目录 Linux入门篇Linux概述Linux vs WindowsLinux安装CentOS的版本选择和下载VMware下载VMware安装创建虚拟机安装CentOS 入门篇 Linux概述 Linux是一个操作系统&#xff0c;一切皆文件&…