暂时未有相关云产品技术能力~
一 前言前几天面试某大厂的云原生岗位,原本是一个轻松+愉快的过程,当问到第二个问题,我就发现事情的不对劲,先复盘一下面试官有关Channel的问题,然后再逐一解决,最后进行扩展,这次一定要一次性通关channel。答应我,看完这篇文章,不要再被Channel吊打了。面试题介绍一下ChannelChannel在go中起什么作用Channel为什么需要两个队列实现Go为什么要开发Channel,而别的语言为什么没有Channel底层是使用锁控制并发的,为什么不直接使用锁然后我们进行一下扩展,玩转ChannelChannel的底层原理和数据结构Channel的读写流程Channel为什么能做到线程安全操作Channel可能出现的情况Channel有哪些常见的使用场景Channel的读写操作是否是原子性的,如何实现如何避免在Channel中出现死锁的情况Channel可以在多个goroutine之间传递什么类型的数据如何在Channel中使用缓存区在使用Channel时,如何保证数据的同步性和一致性如何保证Channel的安全性Channel的大小是否对性能有影响Channel的内存模型是什么如何在Channel中传递复杂的数据类型Channel和goroutine之间的关系是什么在Go语言中,Channel和锁的使用场景有哪些区别二 解决面试题1. 介绍一下ChannelChannel是Go语言中的一种并发原语,用于在goroutine之间传递数据和同步执行。Channel实际上是一种特殊类型的数据结构,可以将其想象成一个管道,通过它可以发送和接收数据,实现goroutine之间的通信和同步。Channel的特点包括:Channel是类型安全的,可以确保发送和接收的数据类型一致。Channel是阻塞的,当发送或接收操作没有被满足时,会阻塞当前goroutine,直到满足条件。Channel是有缓存的,可以指定缓存区大小,当缓存区已满时发送操作会被阻塞,当缓存区为空时接收操作会被阻塞。Channel是可以关闭的,可以使用close()函数关闭Channel,关闭后的Channel不能再进行发送操作,但可以进行接收操作。Channel的使用方式包括:创建Channel:使用make()函数创建Channel,指定Channel的类型和缓存区大小。发送数据:使用<-运算符将数据发送到Channel中。接收数据:使用<-运算符从Channel中接收数据。关闭Channel:使用close()函数关闭Channel。2. Channel在go中起什么作用在 Go 中,channel 是一种用于在 goroutine 之间传递数据的并发原语。channel 可以让 goroutine 在发送和接收操作之间同步,从而避免了竞态条件,从而更加安全地共享内存。channel 类似于一个队列,数据可以从一个 goroutine 中发送到 channel,然后从另一个 goroutine 中接收。channel 可以是有缓冲的,这意味着可以在 channel 中存储一定数量的值,而不仅仅是一个。如果 channel 是无缓冲的,则发送和接收操作将会同步阻塞,直到有 goroutine 准备好接收或发送数据。注:我这里提到了Channel底层用到了两个队列实现。所以就有了下面的问题3. Channel为什么需要两个队列实现一个Channel可以被看作是一个通信通道,用于在不同的进程之间传递数据。在具体的实现中,一个Channel通常需要使用两个队列来实现。这两个队列是发送队列和接收队列。发送队列是用来存储将要发送的数据的队列。当一个进程想要通过Channel发送数据时,它会将数据添加到发送队列中。发送队列中的数据会按照先进先出的顺序被逐个发送到接收进程。如果发送队列已经满了,那么发送进程就需要等待,直到有足够的空间可以存储数据。接收队列是用来存储接收进程已经准备好接收的数据的队列。当一个进程从Channel中接收数据时,它会从接收队列中取出数据。如果接收队列是空的,那么接收进程就需要等待,直到有新的数据可以接收。使用两个队列实现Channel的主要原因是为了实现异步通信。发送进程可以在发送数据之后立即继续执行其他任务,而不需要等待接收进程确认收到数据。同样,接收进程也可以在等待数据到达的同时执行其他任务。这种异步通信的实现方式可以提高系统的吞吐量和响应速度。4. Go为什么要开发Channel,而别的语言为什么没有在Go语言中,Channel是一种非常重要的并发原语。Go语言将Channel作为语言内置的原语,可能是出于以下几个方面的考虑:并发安全:在多线程并发环境下,使用Channel可以保证数据的安全性,避免多个线程同时访问共享数据导致的数据竞争和锁的开销。简单易用:Go语言中的Channel是一种高度抽象的概念,可以非常方便地实现不同线程之间的数据传输和同步。通过Channel,程序员不需要手动地管理锁、条件变量等底层的同步原语,使得程序的编写更加简单和高效。天然支持并发:Go语言中的Channel与goroutine密切相关,这使得Channel天然地支持并发。程序员可以通过使用Channel和goroutine来实现非常高效的并发编程。虽然其他编程语言中没有像Go语言中的Channel这样的内置并发原语,但是许多编程语言提供了类似于Channel的实现,比如Java的ConcurrentLinkedQueue、Python的Queue、C++的std::queue等。这些实现虽然没有Go语言中的Channel那么简单易用和高效,但也能够满足多线程编程中的数据传输和同步需求。注:我这里提到了Channel底层是使用锁实现。所以就有了下面的问题5. Channel底层是使用锁控制并发的,为什么不直接使用锁虽然在Go语言中,Channel底层实现是使用锁控制并发的,但是Channel和锁的使用场景是不同的,具有不同的优势和适用性。首先,Channel比锁更加高级和抽象。Channel可以实现多个goroutine之间的同步和数据传递,不需要程序员显式地使用锁来进行线程间的协调。Channel可以避免常见的同步问题,比如死锁、饥饿等问题。其次,Channel在语言层面提供了一种更高效的并发模型。在使用锁进行并发控制时,需要程序员自己手动管理锁的获取和释放,这增加了代码复杂度和错误的风险。而使用Channel时,可以通过goroutine的调度和Channel的阻塞机制来实现更加高效和简单的并发控制。此外,Channel还可以避免一些由锁导致的性能问题,如锁竞争、锁粒度过大或过小等问题。Channel提供了一种更加精细的控制机制,能够更好地平衡不同goroutine之间的并发性能。总的来说,虽然Channel底层是使用锁控制并发的,但是Channel在语言层面提供了更加高级、抽象和高效的并发模型,可以使程序员更加方便和安全地进行并发编程。三 扩展面试题1. Channel的底层原理和数据结构在Go语言中,Channel是通过一个有缓存的队列来实现的,底层数据结构是一个双向链表。是一个叫做hchan的结构体,每个Channel都有一个send队列和一个receive队列,用于存放发送和接收操作的goroutine。当发送操作和接收操作发生时,它们会被添加到对应的队列中,等待对方的操作来满足条件。type hchan struct { //channel分为无缓冲和有缓冲两种。 //对于有缓冲的channel存储数据,借助的是如下循环数组的结构 qcount uint // 循环数组中的元素数量 dataqsiz uint // 循环数组的长度 buf unsafe.Pointer // 指向底层循环数组的指针 elemsize uint16 //能够收发元素的大小 closed uint32 //channel是否关闭的标志 elemtype *_type //channel中的元素类型 //有缓冲channel内的缓冲数组会被作为一个“环型”来使用。 //当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置 sendx uint // 下一次发送数据的下标位置 recvx uint // 下一次读取数据的下标位置 //当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列 //当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列 recvq waitq // 读等待队列 sendq waitq // 写等待队列 lock mutex //互斥锁,保证读写channel时不存在并发竞争问题 }对于有缓存的Channel,缓存区的大小即为队列的长度,当缓存区已满时,发送操作会被阻塞,直到有接收操作来取走数据;当缓存区为空时,接收操作会被阻塞,直到有发送操作来填充数据。Channel底层的同步机制是基于等待队列和信号量实现的。每个Channel都维护着一个等待队列,其中包含了所有等待操作的goroutine;同时还维护着一个计数器,用于记录当前缓存区中的元素数量。当发送操作需要等待时,会将当前goroutine添加到等待队列中,并使计数器减一;当接收操作需要等待时,会将当前goroutine添加到等待队列中,并使计数器加一。当有其他操作满足条件时,会从等待队列中取出相应的goroutine,并将其重新加入到可执行队列中,等待调度器的调度。2. Channel的读写流程向 channel 写数据:若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。从 channel 读数据若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。关闭 channel1.关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。panic 出现的场景还有:关闭值为 nil 的 channel关闭已经关闭的 channel向已经关闭的 channel 中写数据3. Channel为什么能做到线程安全Channel的线程安全主要是通过其内部的同步机制实现的。Channel 可以理解是一个先进先出的队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。当多个goroutine通过Channel进行通信时,Channel会保证每个操作的原子性和顺序性,避免了多个goroutine同时访问共享变量导致的数据竞争问题。Channel的阻塞特性也保证了在发送和接收操作发生时,它们会被添加到等待队列中,直到满足条件后才会被唤醒,从而避免了死锁问题。4. 操作Channel可能出现的情况channel存在3种状态:nil,未初始化的状态,只进行了声明,或者手动赋值为nilactive,正常的channel,可读或者可写closed,已关闭,千万不要误认为关闭channel后,channel的值是nil操作一个零值nil通道一个非零值但已关闭的通道一个非零值且尚未关闭的通道关闭产生恐慌产生恐慌成功关闭发送数据永久阻塞产生恐慌阻塞或者成功发送接收数据永久阻塞永不阻塞阻塞或者成功接收5. Channel有哪些常见的使用场景任务分发和处理:可以通过Channel将任务分发给多个goroutine进行处理,并将处理结果发送回主goroutine进行汇总和处理。并发控制:可以通过Channel来进行信号量控制,限制并发的数量,避免资源竞争和死锁等问题。数据流处理:可以通过Channel实现数据流的处理,将数据按照一定的规则传递给不同的goroutine进行处理,提高并发处理效率。事件通知和处理:可以通过Channel来实现事件的通知和处理,将事件发送到Channel中,让订阅了该Channel的goroutine进行相应的处理。异步处理:可以通过Channel实现异步的处理,将任务交给其他goroutine处理,自己继续执行其他任务,等待处理结果时再从Channel中获取。6. Channel的读写操作是否是原子性的,如何实现Channel的读写操作是原子性的,并且是由Go语言内部的同步机制来保证的。当一个goroutine进行Channel的读写操作时,Go语言内部会自动进行同步,保证该操作的原子性和顺序性。这种同步机制主要涉及到两个部分:基于锁的同步:在Channel的底层实现中,使用了一种基于锁的同步机制,它可以保证每个读写操作都是原子性的,避免了多个goroutine同时读写导致的数据竞争问题。基于等待的同步:当一个goroutine进行Channel的读写操作时,如果Channel当前为空或已满,它就会被添加到等待队列中,直到满足条件后才会被唤醒,这种等待的同步机制可以避免因Channel状态不满足条件而导致的死锁问题。通过这种基于锁和等待的同步机制,Go语言保证了Channel的读写操作是原子性的,可以在多个goroutine之间安全地进行通信和同步。7. 如何避免在Channel中出现死锁的情况避免在单个goroutine中对Channel进行读写操作:如果一个goroutine同时进行Channel的读写操作,很容易出现死锁的情况,因为该goroutine无法切换到其他任务,导致无法释放Channel的读写锁。因此,在进行Channel的读写操作时,应该尽量将它们分配到不同的goroutine中,以便能够及时切换任务。使用缓冲Channel:缓冲Channel可以在一定程度上缓解读写操作的同步问题,避免因为Channel状态不满足条件而导致的死锁问题。如果Channel是非缓冲的,那么写操作必须等到读操作执行之后才能完成,反之亦然,这种同步会导致程序无法继续执行。而如果使用缓冲Channel,就可以避免这种同步问题,即使读写操作之间存在时间差,也不会导致死锁。使用select语句:select语句可以在多个Channel之间进行选择操作,避免因为某个Channel状态不满足条件而导致的死锁问题。在使用select语句时,应该注意判断每个Channel的状态,避免出现同时等待多个Channel的情况,这可能导致死锁。使用超时机制:在进行Channel的读写操作时,可以设置一个超时时间,避免因为Channel状态不满足条件而一直等待的情况。如果超过一定时间仍然无法读写Channel,就可以选择放弃或者进行其他操作,以避免死锁。8. Channel可以在多个goroutine之间传递什么类型的数据在Go语言中,Channel可以在多个goroutine之间传递任何类型的数据,包括基本数据类型、复合数据类型、结构体、自定义类型等。这些数据类型在传递过程中都会被封装成对应的指针类型,并由Channel进行传递。9. 如何在Channel中使用缓存区在Go语言中,我们可以使用带缓冲的Channel来实现Channel的缓存区功能。带缓冲的Channel可以存储一定数量的元素,而不必立即将它们交给接收方。这样可以减少发送和接收操作之间的同步,从而提高程序的性能。使用带缓冲的Channel,可以通过在Channel声明时指定缓冲区的大小来实现。例如,声明一个容量为10的缓冲Channel可以使用以下语句:ch := make(chan int, 10)1在这个例子中,我们创建了一个整型缓冲Channel,其容量为10。这意味着在Channel中可以存储10个整型元素,而不必立即将它们发送到接收方。当Channel中的元素数量达到缓冲区容量时,再进行写入操作时,写入操作就会被阻塞,直到有接收方读取了Channel中的元素。10. 在使用Channel时,如何保证数据的同步性和一致性在使用Channel时,为了保证数据的同步性和一致性,可以采用以下几种方式:合理设计Channel的容量:当Channel容量过小时,容易出现发送者和接收者之间的阻塞,而当容量过大时,可能会出现数据不一致的问题。因此,在设计Channel时,需要根据实际情况合理设定容量大小,以避免数据同步性和一致性的问题。使用互斥锁保证数据访问的互斥性:如果多个goroutine同时对某个共享的数据进行访问,可能会导致数据不一致的问题。此时,可以使用互斥锁来保证数据访问的互斥性,以避免多个goroutine同时对同一份数据进行访问。使用同步机制实现数据同步:在某些情况下,我们可能需要在多个goroutine之间进行数据同步,以确保数据的一致性。此时,可以使用一些同步机制,例如WaitGroup、Barrier、Cond等,来实现数据同步。11. 如何保证Channel的安全性确保Channel的正确使用:在使用Channel时,需要确保发送和接收操作的正确性。特别是在并发环境下,必须正确处理并发操作,避免出现竞争条件或死锁等问题。因此,在使用Channel时,需要根据实际情况选择合适的同步机制,例如互斥锁、条件变量、原子操作等,以确保Channel的正确使用。避免Channel的泄漏:如果Channel没有被及时关闭,可能会导致资源泄漏和性能问题。因此,在使用Channel时,需要确保及时关闭Channel,避免出现资源泄漏的情况。避免Channel的阻塞:如果Channel的容量较小,可能会导致发送和接收操作的阻塞。此时,可以使用缓冲Channel或者带超时的发送和接收操作,避免Channel的阻塞。避免Channel的死锁:如果多个goroutine之间出现死锁,可能会导致程序的停滞和性能问题。因此,在使用Channel时,需要避免死锁的情况,例如避免循环依赖、避免同时使用多个Channel等。12. Channel的大小是否对性能有影响Channel的大小对性能会产生一定的影响。Channel的大小是指Channel可以容纳的元素数量,可以通过在创建Channel时指定容量大小来控制。当Channel的容量较小时,可能会导致发送和接收操作的阻塞,从而影响程序的性能。而当Channel的容量较大时,可能会增加系统的内存开销,也可能会导致Channel中的元素被占用的时间较长,从而影响程序的响应性。13. Channel的内存模型是什么在Go语言中,Channel的内存模型是基于通信顺序进程(Communicating Sequential Processes,CSP)模型的。CSP模型是一种并发计算模型,它将并发程序看作是一组顺序进程,这些进程通过Channel进行通信和同步。在CSP模型中,每个进程都是独立的,它们之间通过Channel进行通信。Channel是一个具有FIFO特性的数据结构,用于在多个进程之间传递数据。当一个进程向Channel发送数据时,它会阻塞等待,直到另一个进程从Channel中接收到数据。同样地,当一个进程从Channel中接收数据时,它也会阻塞等待,直到另一个进程向Channel发送数据。在Go语言中,Channel的内存模型采用了CSP模型的概念,即每个Channel都是一个独立的顺序进程。当一个进程向Channel发送数据时,数据会被复制到Channel的缓冲区或者直接发送到接收方。当一个进程从Channel中接收数据时,数据会被从Channel的缓冲区中取出或者等待发送方发送数据。14. 如何在Channel中传递复杂的数据类型在Go语言中,Channel可以传递任何类型的数据,包括复杂的数据类型。如果要在Channel中传递复杂的数据类型,可以将其定义为一个结构体,然后通过Channel进行传递。例如,假设我们有一个结构体类型Person,它包含姓名和年龄两个字段:type Person struct { Name string Age int }我们可以定义一个Channel,用于传递Person类型的数据:ch := make(chan Person)现在我们可以在不同的Goroutine中向Channel发送和接收Person类型的数据:// 发送Person类型数据到Channel go func() { p := Person{Name: "Alice", Age: 18} ch <- p }() // 从Channel接收Person类型数据 p := <-ch fmt.Println(p.Name, p.Age)注意,如果要在Channel中传递复杂的数据类型,需要确保该类型是可导出的。15. Channel和goroutine之间的关系是什么在Go语言中,Channel和Goroutine是密切相关的,它们可以说是Go语言并发编程的两个重要组件。Goroutine是Go语言中轻量级的线程实现,可以在一个进程中创建成千上万个Goroutine,并且它们的创建和销毁的代价非常小,因此非常适合在高并发的场景下使用。Goroutine的调度是由Go运行时系统(runtime)负责的,它采用协作式调度,可以自动地在多个线程之间切换,以达到高效利用CPU的目的。Channel是Goroutine之间通信的一种方式,它可以用于在不同的Goroutine之间传递数据。Channel提供了两个基本操作:发送和接收。通过向Channel发送数据,一个Goroutine可以将数据传递给另一个Goroutine;通过从Channel接收数据,一个Goroutine可以获取其他Goroutine传递过来的数据。因此,可以说Channel和Goroutine之间是一种协作关系:Goroutine可以通过Channel与其他Goroutine进行通信,以实现协作和共享数据,从而完成复杂的并发任务。同时,Channel的实现也依赖于Goroutine和Go运行时系统,它们共同构成了Go语言并发编程的基础。16. 在Go语言中,Channel和锁的使用场景有哪些区别在Go语言中,Channel和锁(sync.Mutex等)都可以用于并发编程中的同步和共享数据,但它们的使用场景有一些区别。Channel通常用于Goroutine之间传递数据,并发的Goroutine之间可以通过Channel进行同步。使用Channel可以避免锁的问题,例如死锁、饥饿等问题。Channel可以将数据在多个Goroutine之间进行传递和共享,而且在数据传递的过程中,不需要使用锁来保证数据的安全性,这也是Channel比锁更加安全和高效的原因之一。因此,当需要在不同的Goroutine之间传递数据时,使用Channel是比较合适的选择。锁通常用于对共享资源进行保护,防止多个Goroutine同时访问和修改同一个共享资源,从而导致数据的竞争和不一致。使用锁可以保证同一时刻只有一个Goroutine能够访问和修改共享资源,从而保证数据的安全性和一致性。当需要对共享资源进行保护时,使用锁是比较合适的选择。Channel和锁都是Go语言中常用的并发编程工具,它们各自有不同的使用场景。在实际开发中,应根据具体的需求选择合适的并发编程工具来实现同步和共享数据。四 最后通过这场面试,感觉大厂比较考验发散性思维,为什么这样做,这样做有什么用,会得到什么好处,跟其他相比有什么优势,这确实是我之前所不具备的,思考问题一定要深入原理,多思考背后的问题,这样才能快速成长起来。希望能够坚持到这里朋友们,以后再遇到Channel的问题,不会再被难住,加油。如果友友们觉得写的还可以,记得一键三连哦。未来不是预测,而是创造。只要我们努力、积极地行动,未来就充满着无限的可能
随机森林(Random Forest)是一种集成学习(Ensemble Learning)算法,由于其优秀的表现在数据挖掘、机器学习等领域得到广泛应用。随机森林通过同时使用多个决策树对数据集进行训练,并通过投票机制或平均化方式来得出最终的预测结果。本文将对随机森林算法的基本原理、优点和缺点以及实现过程进行详细介绍。一 随机森林算法的基本原理随机森林算法是基于决策树算法的一种集成学习算法。决策树是一种树形结构,其中每个内部节点表示一个特征或属性,每个分支代表这个特征或属性的一个值,每个叶子节点表示一个分类或回归结果。通过决策树,我们可以把数据集分成多个子集,每个子集包含了具有相同特征或属性的数据。然后我们可以对每个子集进行分析,并将其分类或回归。随机森林有两个重要的随机性来源:一是数据的随机性,二是特征的随机性。对于数据的随机性,随机森林使用自助采样法(bootstrap sampling)从原始数据集中随机选择 n 个样本(n 通常小于原始数据集的样本数),组成一个新的训练数据子集。这个新的数据子集被用来训练一个新的决策树。对于特征的随机性,随机森林在每个决策树的节点上,随机选择 m 个特征(m 远小于总特征数),并从这 m 个特征中选择最佳的特征用于分裂。随机森林算法的训练过程可以概括为以下几个步骤:从原始数据集中随机选择 n 个样本,组成一个新的训练数据子集。随机选择 m 个特征,从这 m 个特征中选择最佳的特征用于分裂。根据选定的特征进行分裂,得到一个子节点。重复 1-3 步,直到决策树生长完毕。重复 1-4 步,生成多个决策树。预测时,将测试数据集在每个决策树上运行,得到每个决策树的预测结果,然后取所有决策树的预测结果的平均值(对于回归问题)或多数表决(对于分类问题)作为最终的预测结果。二 随机森林算法的优点随机森林算法具有以下优点:1. 随机森林算法具有很高的准确性和鲁棒性随机森林算法对于噪声和异常值等不利因素具有很高的鲁棒性。这是因为随机森林算法同时使用多个决策树对数据进行训练,可以通过平均化或投票机制得出一个更加稳定和可靠的预测结果。此外,随机森林算法能够自动处理数据集中的缺失值,这进一步增强了算法的鲁棒性。2. 随机森林算法可以有效地避免过拟合问题过拟合是机器学习中的一个常见问题,它会导致模型在训练集上表现很好,但在测试集上表现很差。随机森林算法通过使用随机子集和随机特征的方式,可以有效地避免过拟合问题。因为每个决策树都是在不同的随机子集上训练的,这使得每个决策树之间的差异性更大,从而减少了模型的方差。3. 随机森林算法可以处理高维度数据随机森林算法可以处理高维度数据,因为它只选择一部分随机特征进行训练。这使得随机森林算法不需要对所有特征进行计算,从而可以提高算法的效率。4. 随机森林算法可以评估特征的重要性随机森林算法可以通过计算每个特征在所有决策树中的重要性来评估特征的重要性。这个重要性指标可以帮助我们选择最相关的特征,从而提高算法的效率和准确性。三 随机森林算法的缺点随机森林算法的缺点主要包括以下几点:1. 随机森林算法对于少量数据集表现不佳随机森林算法需要大量的数据才能表现出它的优势。对于较小的数据集,随机森林算法往往表现不如其他算法。因为对于较小的数据集,随机森林算法很容易出现过拟合现象,这会导致算法的性能下降。2. 随机森林算法的结果不够直观随机森林算法通常输出的是一组结果,例如一组类别或一组数值。这样的结果不够直观,可能需要进一步的处理才能得到更加直观的结果。3. 随机森林算法的训练时间较长随机森林算法需要同时训练多个决策树,并且每个决策树的训练需要对数据集进行随机采样和特征选择等操作。这些操作会使得随机森林算法的训练时间较长。此外,当决策树的数量增加时,随机森林算法的训练时间也会增加。4. 随机森林算法对于分类不平衡的数据集表现不佳对于分类不平衡的数据集,随机森林算法往往会出现偏差。因为在多数投票机制中,具有较多样本的类别更容易成为预测结果的主导因素。为了解决这个问题,我们可以采用加权随机森林算法或通过重采样等方式来平衡类别权重。随机森林算法应用我们将使用Python的scikit-learn库实现一个基于随机森林的分类模型,并以鸢尾花数据集为例进行演示。数据集鸢尾花数据集是一个常用的分类问题数据集,它包含了三个不同种类的鸢尾花的花萼和花瓣的长度和宽度。数据集中的三个类别分别是山鸢尾(Iris Setosa)、变色鸢尾(Iris Versicolour)和维吉尼亚鸢尾(Iris Virginica)。数据集共有150个样本,其中每个类别各有50个样本。我们可以使用Python的scikit-learn库加载鸢尾花数据集,代码如下:from sklearn.datasets import load_iris iris = load_iris()然后我们可以将数据集分为训练集和测试集,训练集用于训练模型,测试集用于测试模型的预测准确率。我们可以使用scikit-learn库中的train_test_split函数将数据集划分为训练集和测试集。代码如下:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.3)随机森林分类模型在进行数据预处理后,我们可以使用scikit-learn库中的RandomForestClassifier来构建随机森林分类模型。随机森林算法有一些需要设置的参数,例如树的数量、树的最大深度等。在这里,我们使用默认的参数。代码如下:from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier() model.fit(X_train, y_train.values.ravel())模型评估我们使用测试集来评估模型的预测准确率。代码如下:from sklearn.metrics import accuracy_score y_pred = model.predict(X_test) accuracy = accuracy_score(y_test, y_pred) print('Accuracy:', accuracy)在本例中,我们使用了默认参数的随机森林分类模型,在测试集上的预测准确率为0.978。这个结果表明,该模型可以很好地对鸢尾花进行分类。
这是美团2024届暑期实习后端岗位的第一轮笔试,总共有五道编程题,四道 情景算法题,一道 二叉树题目,时长两个小时,我用的是go语言,只AC了前两道,第三道死活通不过,第四道模拟情况太复杂,放弃了,第五道马上写完,可惜没时间了,还是得合理分配时间才行,哭死!!!Coding 一题目描述:小美有一个由数字字符组成的字符串。现在她想对这个字符串进行一些修改。 具体地,她可以将文个字符串中任意位置字符修改为任意的数字字符。她想知道,至少进行多少次修改,可以使得“修改后的字符串不包含两个连续相同的字符?例如,对于字符串”111222333", 她可以进行3次修改将其变为” 121212313"。输入描述一行,一个字符串s,保证s只包含数字字符。1<=|s|<= 100000输出描述一行,一个整数,表示修改的最少次数。思路:本题可以使用回溯,也可以使用动态规划解决,下面是动规的两种解决方法func main() { var s string fmt.Scan(&s) length := len(s) dp := make([][10]int, length+1) for i := 1; i <= length; i++ { for j := 0; j < 10; j++ { dp[i][j] = length } for j := 0; j < 10; j++ { if int(s[i-1]) == '0'+j { for k := 0; k < 10; k++ { if j != k { dp[i][j] = min(dp[i][j], dp[i-1][k]) } } } else { for k := 0; k < 10; k++ { if j != k { dp[i][j] = min(dp[i][j], dp[i-1][k]+1) } } } } } res := length for i := 0; i < 10; i++ { res = min(res, dp[length][i]) } fmt.Println(res) } func main1() { scanner := bufio.NewScanner(os.Stdin) scanner.Scan() s := scanner.Text() n := len(s) dp := make([][2]int, n) // dp[i][0]表示s[i]不变的最小修改次数,dp[i][1]表示s[i]改为另一个数字的最小修改次数 for i := 0; i < n; i++ { if i == 0 { dp[i][0] = 0 dp[i][1] = 1 } else { if s[i] == s[i-1] { dp[i][0] = dp[i-1][1] // s[i]不变,必须将s[i-1]改为另一个数字 dp[i][1] = dp[i-1][0] + 1 // s[i]改为另一个数字,s[i-1]可以不变或改为另一个数字 } else { dp[i][0] = dp[i-1][0] // s[i]不变,s[i-1]不变或改为另一个数字都可以 dp[i][1] = dp[i-1][1] + 1 // s[i]改为另一个数字,s[i-1]不变或改为另一个数字都可以 } } } fmt.Println(min(dp[n-1][0], dp[n-1][1])) } func min(a, b int) int { if a > b { return b } return a }Coding 二题目描述:小团在一个n*m的网格地图上探索。 网格地图上第i行第j列的格子用坐标(i,j)简记。初始时,小团的位置在地图的左上角,即坐标(1,1)。 地图上的每个格子 上都有一定的金币, 特别地,小团位于的初始位置(1,1)上的金币为0。小团在进行探索移动时,可以选择向右移动-格(即从(x,y)到达(x,y+1))或向下移动一格(即从(x,y)到达(x+1,y)) 。地图上的每个格子都有一个颜色,红,色或蓝色。如果小团次移动前后的两个格子颜色不同,那么他需要支付k个金币才能够完成这-次移动;如果移动前后的两个格子颜色相同,则不需要支付金币。小团可以在任意格子选择结束探索。现在给你网格地图上每个格子的颜色与金币数量,假设小团初始时的金币数量为0,请你帮助小团计算出最优规划,使他能获得最多的金币,输出能获得的最多 金币数量即可。注意:要求保证小团任意时刻金币数量不小于零。输入描述第一行是三个用空格隔开的整数n、m和k,表示网格地图的行数为n,列数为m,在不同颜色的两个格子间移动需要支付k个金币。接下来n行,每行是一个长度为m的字符串, 字符串仅包含字符R’或’ B’。第i行字符串的第j个字符表示地图上第i行第j列的格子颜色,如果字符为’ R’ 则表示格子颜色为红色,为’B’ 表示格子颜色为蓝色。接下来是个n行m列的非负整数矩阵,第i行第j列的数字表示地图上第行第j列的格子上的金币数量。保证所有数据中数字大小都是介于[0, 10]的整数。1<=n,m<=200, 1<=k<=5。输出描述一行 一个整数, 表示小团能获得的最多 金币数量。func main() { scanner := bufio.NewScanner(os.Stdin) scanner.Scan() line := strings.Split(scanner.Text(), " ") n, _ := strconv.Atoi(line[0]) m, _ := strconv.Atoi(line[1]) k, _ := strconv.Atoi(line[2]) grid := make([][]rune, n) for i := 0; i < n; i++ { scanner.Scan() grid[i] = []rune(scanner.Text()) } coins := make([][]int, n) for i := 0; i < n; i++ { coins[i] = make([]int, m) scanner.Scan() for j, v := range strings.Split(scanner.Text(), " ") { coins[i][j], _ = strconv.Atoi(v) } } // 初始化dp dp := make([][]int, n) for i := 0; i < n; i++ { dp[i] = make([]int, m) for j := 0; j < m; j++ { dp[i][j] = -1 } } dp[0][0] = 0 for i := 0; i < n; i++ { for j := 0; j < m; j++ { if dp[i][j] == -1 { continue } //右 if j+1 < m { c := 0 if grid[i][j] != grid[i][j+1] { c = k } dp[i][j+1] = max(dp[i][j+1], dp[i][j]+coins[i][j+1]-c) } //下 if i+1 < n { c := 0 if grid[i][j] != grid[i+1][j] { c = k } dp[i+1][j] = max(dp[i+1][j], dp[i][j]+coins[i+1][j]-c) } } } fmt.Println(dp[n-1][m-1]) } func max(a, b int) int { if a > b { return a } return b }Coding 三题目描述:小美是位天文爱好者, 她收集了接下来段时间中所有 会划过她所在的观测地上空的流星信息。具体地,她收集了n个流星在她所在观测地上空的出现时刻和消失时刻。对于一个流星,若’其的出现时刻为s,消失时刻为t,那么小美在时间段[s, t]都能够观测到它。对于一个时刻,观测地上空出现的流星数量越多,则小美认为该时刻越好。小美希望能够选择一个最佳的时刻进行观测和摄影,使她能观测到最多数量的流星。现在小美想知道 ,在这个最佳时刻,她最多能观测到多少个流星以及一共有多少个最佳时刻可供她选择。输入描述第一行是一个正整数n,表示流星的数量。第二行是n个用空格隔开的正整数,第i个数si表示第i个流星的出现时间。第三行是n个用空格隔开的正整数,第i个数ti表示第i个流星的消失时间。1<=n<=100000, 1<=si<=ti<=10^9输出描述输出一行用空格隔开的两个数x和y,其中x表示小美能观测到的最多流星数,y表示可供她选择的最佳时刻数量。算法思路:首先,我们将每个流星的出现和消失时间转换为一系列时间事件,每个事件包括时间点和流星数量变化。然后,按时间点对这些事件进行排序。接下来,我们从左到右遍历这些事件,并统计当前观测地上空的流星数量。在遍历过程中,我们记录最大的流星数量以及达到最大数量的时间点个数。最终,输出最大数量和时间点个数即可。时间复杂度:O ( n log n ) O(n\log n)O(nlogn),其中 n nn 是流星的数量。我们需要对所有流星的出现和消失时间进行排序,时间复杂度为 O ( n log n ) O(n\log n)O(nlogn)。接下来,我们遍历这些事件,时间复杂度为 O ( n ) O(n)O(n)。因此,总时间复杂度为 O ( n log n ) O(n\log n)O(nlogn)。空间复杂度:O ( n ) O(n)O(n),我们需要保存每个流星的出现和消失时间,以及每个时间点的流星数量变化。func main() { scanner := bufio.NewScanner(os.Stdin) // 读取流星数量 scanner.Scan() n := toInt(scanner.Bytes()) // 读取每个流星的出现和消失时间 starts := make([]int, n) ends := make([]int, n) for i := 0; i < n; i++ { scanner.Scan() starts[i] = toInt(scanner.Bytes()) } for i := 0; i < n; i++ { scanner.Scan() ends[i] = toInt(scanner.Bytes()) } // 将每个时间点的流星数量统计出来 events := make([][2]int, 2*n) for i := 0; i < n; i++ { events[2*i][0] = starts[i] events[2*i][1] = 1 events[2*i+1][0] = ends[i] + 1 events[2*i+1][1] = -1 } sort.Slice(events, func(i, j int) bool { return events[i][0] < events[j][0] }) // 找出最佳时刻 maxCount := 0 bestTimes := 0 count := 0 for i := 0; i < len(events); i++ { count += events[i][1] if count > maxCount { maxCount = count bestTimes = 1 } else if count == maxCount { bestTimes++ } } fmt.Printf("%d %d\n", maxCount, bestTimes) } func toInt(b []byte) int { n := 0 for _, c := range b { n = n*10 + int(c-'0') } return n }Coding 四题目描述:小D和小W最近在玩坦克大战,双方操控自己的坦克在16*1 6的方格图上战斗,小D的坦克初始位置在地图的左上角,朝向为右,其坐标(0,0), 小W的坦克初始位置在地图右下角,朝向为左,坐标为(15,15)。坦克不能移动到地图外,坦克会占领自己所在的格子,己方的坦克不可以进入对方占领过的格子。每一个回合双方必须对自己的坦克下达以下5种指令中的一种:.移动指令U:回合结束后,使己方坦克朝向为上,若上方的格子未被对方占领,则向当前朝向移动一个单位(横坐标-1),否则保持不动;.移动指令D:回合结束后,使己方坦克朝向为下,若下方的格子未被对方占领,则向当前朝向移动一个单位(横坐标+1),否则保持不动,.移动指令L:回合结束后,使己方坦克朝向为左,若左侧的格子未被对方占领,则向当前朝向移动一个单位(纵坐标-1) ,否则保持不动;.移动指令R:回合结束后,使己方坦克朝向为右,若右侧的格子未被对方占领,则向当前朝向移动一个单位(纵坐标+1),否则保持不动;. 开火指令F:己方坦克在当前回合立即向当前朝向开火;己方坦克开火后,当前回合己方坦克的正前方若有对方的坦克,对方的坦克将被摧毁,游戏结束,己方获得胜利;若双方的坦克在同一-回合被摧毁,游戏结束,判定为平局;若双方的坦克在同一回合内进入到同一个未被占领的格子,则双方的坦克发生碰撞,游戏结束,判定为平局;当游戏进行到第256个回合后,游戏结束,若双方坦克均未被摧毁,则占领格子数多的一方获得胜利,若双方占领的格子数一样多,判定为平局。*注意, 若-方开火, 另-方移动,则认为是先开火,后移动。现在小D和小W各自给出一串长度为256的指令字符串, 请你帮助他们计算出游戏将在多少个回合后结束,以及游戏的结果。输入描述输入共两行,每行为一串长度为256的指令宁符串,字符串中只包含“U”,“D",“L" “R”,“F"这五个字符。第一行表示小D的指令,第工行表示小W的指令。输出描述输出一共两行,第一行一个整数k,表示游戏将在k个回合后结束。第二行为游戏的结 果,若小D获胜则输出“D",若小W获胜则输出“W”若平局则输出“P”思路:本题模拟坦克即可,考虑的情况挺多的,当时没AC出来,后来也懒得做了Coding 五题目描述:给一棵有n个点的有根树,点的编号为1到n,根为1。每个点的颜色是红色或者蓝色。对于树上的一个点,如果其子树中(不包括该点本身)红色点和蓝色点的数量相同,那么我们称该点是平衡的。请你计算给定的树中有多少个点是平衡点。输入描述第一行是一个正整数n,表示有n个点。接下来行一个长度为n的字符串,仅包含字符R’和’B’, 第i个字符表示编号为的节点的颜色,字符为’R’ 表示红色,’ B’ 表示蓝色。接下来一行n-1个用空格隔开的整数,第1个整数表示编号为i+ 1的点的父亲节点编号。1<=n<=10000输出描述一行一个整数,表示树上平衡点的个数。思路:根据题意,我们可以使用深度优先搜索(DFS)来遍历树的每个节点,并计算出每个节点子树中红色和蓝色节点的数量。对于每个节点,我们可以将其子树中红色和蓝色节点的数量保存在节点的 cnt 属性中。同时,我们也需要记录该节点的父节点,以便在遍历子树时跳过父节点。在DFS的过程中,我们可以计算出子树中红色和蓝色节点的数量,并根据节点自身的颜色计算出该节点子树中红色和蓝色节点的数量。然后我们将该节点的子节点作为新的起点进行DFS,直到遍历完整棵树。对于每个节点,如果其子树中红色和蓝色节点的数量相同,那么它就是一个平衡点。最后,我们可以将平衡点的数量相加,得到最终的结果。type Node struct { color string cnt [2]int child []*Node } func dfs(node *Node, parent *Node, cntRed int, cntBlue int) int { node.cnt[0] = cntRed node.cnt[1] = cntBlue res := 0 for _, child := range node.child { if child == parent { continue } childCntRed := 0 childCntBlue := 0 if node.color == "R" { childCntRed = cntRed + 1 childCntBlue = cntBlue } else { childCntRed = cntRed childCntBlue = cntBlue + 1 } res += dfs(child, node, childCntRed, childCntBlue) } if node.cnt[0] == node.cnt[1] { res += 1 } return res } func main() { scanner := bufio.NewScanner(os.Stdin) // 读取输入 scanner.Scan() n := parseNum(scanner.Text()) scanner.Scan() colors := strings.Split(scanner.Text(), "") nodes := make([]*Node, n+1) for i := 1; i <= n; i++ { nodes[i] = &Node{ color: colors[i-1], cnt: [2]int{}, child: []*Node{}, } } for i := 2; i <= n; i++ { scanner.Scan() parentIndex := parseNum(scanner.Text()) nodes[parentIndex].child = append(nodes[parentIndex].child, nodes[i]) nodes[i].child = append(nodes[i].child, nodes[parentIndex]) } // DFS计算平衡点数量 res := dfs(nodes[1], nil, 0, 0) // 输出结果 fmt.Println(res) } func parseNum(s string) int { var res int for _, c := range s { res = res*10 + int(c-'0') } return res }
一 谈谈对微服务的理解1. 什么微服务?微服务是一种软件开发技术- 面向服务的体系结构(SOA)架构样式的一种变体,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够独立地部署到生产环境、类生产环境等。应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建。2. 微服务体系服务描述类似服务的说明文档,简单但不可或缺。比如,服务调用首先要解决的问题就是服务如何对外描述。你对外提供了一个服务,那么这个服务的服务名叫什么?调用这个服务需要提供哪些信息?调用这个服务返回的结果是什么格式的?这些就是服务描述要解决的问题。注册中心下一步要解决的问题就是服务的发布和订阅,就是说你提供了一个服务(Provider),如何让外部(Consumer)想调用你的服务的人知道。这个时候就需要一个类似注册中心(Registry)的角色,服务提供者将自己提供的服务以及地址登记到注册中心,服务消费者则从注册中心查询所需要调用的服务的地址,然后发起请求。服务框架通过注册中心,服务消费者就可以获取到服务提供者的地址,有了地址后就可以发起调用。但在发起调用之前你还需要解决以下几个问题。服务通信采用什么协议?是RESTful API还是gRPC?数据传输采用什么方式数据压缩采用什么格式?这些通常集成到了我们的服务框架里面。服务监控一旦服务消费者与服务提供者之间能够正常发起服务调用,你就需要对调用情况进行监控,以了解服务是否正常。通常来讲,服务监控主要包括三个流程,指标收集,数据处理,数据展示。监控是为了发现问题和异常,如果要进一步跟踪和定位问题,则需要进一步了解服务追踪。服务追踪除了需要对服务调用情况进行监控之外,你还需要记录服务调用经过的每一层链路,以便进行问题追踪和故障定位,最后达到接近问题的目的。服务监控和追踪可以合并起来,但是要明确各自的职责是不一样的。服务治理服务监控能够发现问题,服务追踪能够定位问题所在,而解决问题就得靠服务治理了。服务治理就是通过一系列的手段来保证在各种意外情况下,服务调用仍然能够正常进行。3. 微服务优点易于开发和维护启动较快局部修改容易部署技术栈不受限按需伸缩4. 微服务缺点运维要求较高分布式的复杂性接口调整成本高重复劳动5. 什么是gRPC?是谷歌开源的RPC框架。该技术是为了解决什么问题?解决了多语言数据传输的简单、通用、高效、安全的问题。它的调用流程?以client 调用 server 的 add函数为例。客户端:gRPC Server服务端:gRPC Stub交互过程就是客户端发送请求到服务端,服务端处理请求并将结果发送响应返给客户端。处理请求过程:客户端把请求的参数进行序列化,序列化是通过ProtoBuf协议【protobuf】来编码的,编完码向服务端发送请求,请求跨网络,基于HTTP2.0协议,此时服务端接收到了请求,服务端同样通过ProtoBuf协议进行解码,反序列化,发现本地有add函数,将结果序列化,用ProtoBuf协议编码,通过网络来到客户端,客户端通过ProtoBuf协议协议反序列化,把响应的结果用到客户端的程序代码里。分析完过程,gRPC不会一个人唱独角戏,它必须结合ProtoBuf协议协议才能发挥功效。6. ProtoBuf协议好处?跨平台——方便传输数据跨语言——方便解析数据,解析成自己语言的语法结构比json,xml小而快7. gPRC和ProtoBuf联系?gRPC要实现跨平台+跨语言+序列化、反序列化,就要用到ProtoBuf协议来实现。二 本次微服务项目学习流程梳理从微服务入门开始,讲解go-micro的transport通讯层grpc原理。以及grpc数据的传输序列化和反序列化protobuf的原理以业务最简单的用户模块为例,串联gorm等知识,理解如何将模块开发封装。介绍go-micro中的Consul组件实现注册中心和配置中心的基本原理。利用Docker快速安装Consul,介绍实际工作中的使用经验,并将其接入项目。介绍go-micro中的Jaeger组件原理和作用。为项目集成链路追踪功能,完成链路观测台熔断,限流,负载均衡是微服务保稳三剑客,介绍hystrix-go组件原理和作用,介绍限流问题的代码包 uber/ratelimit的基本使用微服务Go化,一般都是因为在意性能,所以性能监控非常重要。使用Docker快速完成promethues+grafana安装,并接入项目中,完成性能监控台通过zap 工具进行日志记录,统一收集到日志中心ELK中。再统一配置,将前面的链路追踪、负载均衡、监控日志等全部集成在一起,完成服务级观测台三 微服务项目一般开发流程梳理拉取micro 镜像 docker pull micro/micro生成模块项目文件 docker run -rm -v 根目录:根目录 -w.proto文件 编写接口生成micro.go文件完成model文件 编写数据表struct映射体编写reponsitory 文件,用gorm对数据库操作在service 完成对接口的实现针对protoc暴露接口,实现接口(request参数赋值)接入各种微服务插件,如配置、注册中心,链路追踪,限流,ELK等等完成main.go开发,完成微服务项目部署四 从本次微服务项目中学到了什么Go微服务架构技术栈以及各层面应用到的服务在本次微服务项目开发中,可以学习掌握以下技术:Go语言的使用Docker、Docker-compose的使用Proto的使用、编写、命令微服务开发流程:4.1 创建项目(Docker或go-micro)4.2 编写proto文件,并生成.go文件4.3 编写domain数据库方面,包含(model层,repository层,service层)等4.4 编写Handle层,实现proto定义接口4.5 编写common层,配置,mysql,公共函数,jaeger(链路追踪)等4.6 编写main函数,完成项目闭环go-micro目录生成,使用,介绍,安装微服务组件的认识(注册中心和配置中心(Consul)、链路追踪(jaeger)、限流(服务端)、负载均衡7(客户端)、ELk等等)gorm数据库开发Prometheus监控服务ELK介绍kibana日志可视化filebeat日志上传logstash收集日志elasticsearch 日志搜索zap日志封装五 对于微服务的一些经验之谈Go 语言目前的应用趋势 Go语言为什么适合做微服务?1、Go高并发等特性,更适合大型系统。2、Go编译速度快,无依赖环境,更适合容器化3、Go将会成为云计算时代的基础设施编程语言,特别适合微服务对于真正微服务项目来说,服务开发只是第一步,容器化、弹性伸缩和可观测才是真正关键。微服务技术体系微服务容器化六 如何学习微服务1. 微服务如何从0到1入门Docker掌握其在微服务开发中的运用掌握微服务必备注册中心与配置中心掌握微服务观测链路追踪系统接入掌握微服务保稳熔断、限流、负载均衡掌握微服务维护性能监控与日志系统接入掌握微服务部署Docker与K8s部署微服务项目2. 从功能特性入手服务间通信,包括服务治理、负载均衡、服务间调用;服务容错和异常排查,包括流量整形、降级熔断、调用链追踪;分布式能力建设,包括微服务网关、分布式事务、消息驱动、分布式配置中心。从微服务组件的功能维度来讲,服务间通信是最基础的功能特性,这个功能模块是最适合作为初学者学习微服务技术的切入点。当我们构建起基础的通信能力之后,接下来就要考虑如何构建服务容错能力,提高服务调用的稳定性了。在这之后,我们就可以从全局的角度构建一些分布式支持特性。这样,就有了一条难度平缓上升的学习曲线,也不会从入门到放弃了。七 遇到以下问题怎么办?1.有太多东西没学没有足够的时间心里很焦虑怎么办题目描述往往学技术的小伙伴会冲破一个基础阶段,踏上更高的发展阶段,当眼界被打开的时候,你会发现有学多新知识在等着花时间学习,往往这断时间会出现焦虑情绪,这时候该怎么办?提示:结合自己的经历,分享下自己的心路历程,可以将您的答案写在评论区,与博主和小伙伴一起交流。建议:首先能有这样的想法说明你是有上进心的人,这点在你后面的职业发展中非常重要,焦虑对我们来说谁都会有。谁都一样,别人估计比你更加焦灼难安。下面给出自己的一些注意点:处理情绪上的问题,这么多年学到的一点是,发现了问题先不着急得出结论,先冷静分析为什么会这样,先把心态和基调放平。每个人焦虑的出发点可能都不一样,但是一样的是过度的焦虑会适得其反,消耗我们正常的注意力,让原本正常的状态受到影响。所以不要过度焦虑,也没必要,也没作用(自己思考下是不是)。把焦虑转化为你成长的动力,用好它,适当的焦虑能够帮你不断的成长,当你没有目标的时候,会比过度焦虑更可怕,人往往是在发展中被淘汰的。2.开发起一个计划总是遥遥无期,它总被一些非技术性的原因阻挠题目描述你往往主张的一项活动,一个话题,一个方向只有你在非常high的进行,别人都不关系,该如何是好?提示:结合自己的经历,分享下自己的心路历程,可以将您的答案写在评论区,与博主和小伙伴一起交流。建议:面对这个问题大家要考虑的一个问题是,你的活动,话题等是否是大家共识的。没有共识就没有共同语言,如果你想要达到大家都理解的效果,方便以后开展工作,但是又没法推进,这时候你可以发挥好你上司的职责,让他帮助你推广和实施,只要大家达成共识,认为这件事情是对的,确实需要做,事情就成功一般了,接下来就交给团队,组织的力量远超过个人,最终的结果可能会超出你的预期。3.该如何应对截止日期和从属关系的压题目描述这个问题一直以来绝大部分人都会面临,公司的发展业绩,领导的业务指标都会占用你的正常生活,甚至下班和放假在家你也会处于时刻紧张的状态。提示:结合自己的经历,分享下自己的心路历程,可以将您的答案写在评论区,与博主和小伙伴一起交流。建议:截止时间带来的压力是比领导给的压力还严重,甚至有的时候由于要提前完成事情,连续通宵好几天。长期处于高压状态,导致经常失眠,身体也不是很健康。后来意识到这个问题,积极参与这个工期的制作中,让自己即使没有心态先放正,对要做的事情有预期,关键的时候还能说下自己的想法,要点自己需要的资源,逐步缓解当前现状。回家之后,尽量只做自己喜欢的事情,比如看电影,电视,下棋等。实践中发现换脑袋带来的轻松感,有助于创造性的工作。山一天移不完,饭一天吃不完,凡是不要过早下结论,草草开始,自己先通盘思考下,缺什么,要什么,达到什么。生活和工作尽量划分开。八 最后至此,go-micro微服务项目全部正式完成。对于微服务和Go感兴趣的小伙伴们,都可以与博主聊一聊,分享学习经验,共同成长希望大家关注博主和关注专栏,每篇博客都干货满满。
一 Prometheus监控介绍1.微服务监控系统promethues介绍是一套开源的监控&报警&时间序列数据库的组合基本原理是通过HTTP协议周期性抓取被监控组件的状态适合Docker、 Kubernetes环境的监控系统2.微服务监控系统promethues工作流程Prometheus server定期从配置好的jobs/exporters/Pushgateway中拉数据Prometheus server记录数据并且根据报警规则推送alert数据Alertmanager 根据配置文件,对接收到的警报进行处理,发出告警。在图形界面中,可视化采集数据二 Prometheus监控重要组件和重要概念1.微服务监控系统promethues重要组件Prometheus Server:用于收集和存储时间序列数据。Client Library:客户端库成相应的metrics并暴露给Prometheus serverPush Gateway:主要用于短期的jobsExporters: 用于暴露已有的第三方服务的metrics给PrometheusAlertmanager: 从Prometheus server端接收到alerts后,会进行2.微服务监控系统promethues重要概念Prometheus 中存储的数据为时间序列格式上由metric的名字和一系列的标签(键值对)唯一标识组成不同的标签则代表不同的时间序列Counter 类型: - -种累加的指标,如:请求的个数,出现的错误数等Gauge 类型:可以任意加减,如:温度,运行的goroutines的个数Histogram 类型:可以对观察结果采样,分组及统计,如:柱状图Summary 类型:提供观测值的count和sum功能,如:请求持续时间instance : -个单独监控的目标,一般对应于一 个进程。jobs:一组同种类型的instances (主要用于保证可扩展性和可靠性)三 微服务监控系统grafana看板拥有 丰富dashboard和图表编辑的指标分析平台拥有自己的权限管理和用户管理系统Grafana 更适合用于数据可视化展示四 Prometheus监控+Grafana看板安装1.安装Prometheus监控拉取镜像:docker pull bitnami/prometheus运行:docker run -d -p 9090:9090 bitnami/prometheus查看是否运行:docker ps图形化界面:浏览器访问 http://127.0.0.1:9090/2.安装Grafana看板拉取镜像:docker pull grafana/grafana运行:docker run -d -p 3000:3000 grafana/grafana查看是否运行:docker ps图形化界面:浏览器访问 http://127.0.0.1:3000/五 Prometheus监控使用在micro目录下新建prometheus.go文件go get github.com/prometheus/client_golang/prometheus/promhttp编写以下代码:package micro import ( "github.com/prometheus/client_golang/prometheus/promhttp" "log" "net/http" "strconv" ) func PrometheusBoot(host string,port int){ http.Handle("/metrics",promhttp.Handler()) //启动web服务 go func() { err := http.ListenAndServe(host+":"+strconv.Itoa(port),nil) if err!= nil{ log.Fatal(("监控启动失败")) } log.Fatal("监控启动,端口为: "+strconv.Itoa(port)) }() }六 Prometheus监控注册服务导包:go get github.com/micro/go-plugins/wrapper/monitoring/prometheus/v2在main.go文件中写入以下代码// 7.暴露监控地址 micro2.PrometheusBoot(micro2.ConsulInfo.Prometheus.Host, int(micro2.ConsulInfo.Prometheus.Port))注:传的参数是使用consul导入的,可以换成自定义的注册服务//添加监控 micro.WrapHandler(prometheus.NewHandlerWrapper()),七 Prometheus监控图形化界面运行项目Prometheus原生看板Grafana看板首次登录,账号:admin;密码:admin八 最后至此,go-micro微服务Prometheus监控工作就正式完成。接下来就开始微服务ELK介绍的代码编写了,希望大家关注博主和关注专栏,第一时间获取最新内容,每篇博客都干货满满。
一 Consul介绍Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 Consul是分布式的、高可用的、可横向扩展的。1. 注册中心Consul基本介绍Consul是一种服务网格解决方案提供具有服务发现,配置和分段功能的全功能控制平面Consul 附带-个简单的内置代理,可以开箱即用2.注册中心Consul关键功能服务发现:客户端可以注册服务,程序可以轻松找到它们所依赖的服务运行状况检查:Consul客户端可以提供任意数量的运行状况检查KV 存储:应用程序可以将Consul的层级键/值存储用于任何目的,包括动态配置,功能标记,协调,领导者选举等安全服务通信:Consul 可以为服务生成和分发TLS证书,建立相互的TLS连接多数据中心:Consul 支持多个数据中心3.注册中心Consul两个重要协议Gossip Protocol (八卦协议)Raft Protocol ( 选举协议)对于想要学习Consul原理的,可以自行百度详细了解这两个协议。二 Consul安装1.使用docker拉取镜像打开终端,输入以下命令:docekr pull consul等待一段时间后拉取成功启动命令:docker run -d -p 8500:8500 consulconsul会被运行在本机的8500端口上检查是否运行docker ps可视化界面打开浏览器,输入http://127.0.0.1:8500三 Config配置在config目录下新建一个config.yaml可以把配置相关信息先放在config.yaml里,之后放在consul中。编写代码:name: "Account" title: "账号功能" mode: "dev" port: 9580 version: "v0.0.1" log: level: "debug" filename: "Account.log" max_size: 200 max_age: 30 max_backips: 7 mysql: host: "127.0.0.1" port: 3306 user: "root" password: "xxx" dbname: "micro" max_open_conns: 200 max_idle_conns: "50" redis: host: "127.0.0.1" port: 6379 password: "xxx" db: 4 pool_size: 100 email: user: "xxx@qq.com" pass: "xxx" host: "smtp.qq.com" port: 465 rename: "Account" # 配置、注册中心 consul: host: "localhost" port: 8500 prefix: "/micro/config" consulRegistry: "127.0.0.1:8500" # 链路追踪 jaeger: serviceName: "go.micro.service.account" addr: "localhost:6831" # 监控服务 prometheus: host: "0.0.0.0" port: 9089 # 限流 ratelimit: QPS: 1000 # 微服务 micro: name: "go.micro.service.account" version: "latest" address: ":9580"注意,以下字段自行修改:mysql.passwordredis.passwordemail.useremail.pass四 Consul代码编写在micro目录下新建一个consul.go文件1.设置consul配置中心// GetConsulConfig 设置配置中心 func GetConsulConfig(host string, port int64, prefix string) (config.Config, error) { consulSource := consul.NewSource( //设置配置中心的地址 consul.WithAddress(host+":"+strconv.FormatInt(port, 10)), //设置前缀,不设置默认前缀 /micro/config consul.WithPrefix(prefix), //是否移除前缀,这里是设置为true,表示可以不带前缀直接获取对应配置 consul.StripPrefix(true), ) //配置初始化 newConfig, err := config.NewConfig() if err != nil { return newConfig, err } //加载配置 err = newConfig.Load(consulSource) return newConfig, err }2.获取consul配置中心的数据编写结构体(嵌套结构)type Account struct { Name string `json:"name"` Title string `json:"title"` Mode string `json:"mode"` Port int64 `json:"port"` Version string `json:"version"` } type Mysql struct { Host string `json:"host"` User string `json:"user"` Pwd string `json:"pwd"` Database string `json:"database"` Port int64 `json:"port"` } type Log struct { Level string `json:"level"` Filename string `json:"filename"` MaxSize int64 `json:"max_size"` MaxAge int64 `json:"max_age"` MaxBackips int64 `json:"max_backips"` } type Redis struct { Host string `json:"host"` Port int64 `json:"port"` Password string `json:"password"` Db int64 `json:"db"` PoolSize int64 `json:"pool_size"` } type Email struct { User string `json:"user"` Pass string `json:"pass"` Host string `json:"host"` Port int64 `json:"port"` Rename string `json:"rename"` } type Consul struct { Host string `json:"host"` Port int64 `json:"port"` Prefix string `json:"prefix"` ConsulRegistry string `json:"consulRegistry"` } type Jaeger struct { ServiceName string `json:"serviceName"` Addr string `json:"addr"` } type Prometheus struct { Host string `json:"host"` Port int64 `json:"port"` } type Ratelimit struct { QPS int64 `json:"QPS"` } type Micro struct { Name string `json:"name"` Version string `json:"version"` Address string `json:"address"` } type ConsulConfig struct { Account Account `json:"account"` Mysql Mysql `json:"mysql"` Log Log `json:"log"` Redis Redis `json:"redis"` Email Email `json:"email"` Consul Consul `json:"consul"` Jaeger Jaeger `json:"jaeger"` Prometheus Prometheus `json:"prometheus"` Ratelimit Ratelimit `json:"ratelimit"` Micro Micro `json:"micro"` }获取consul数据var( ConsulInfo *ConsulConfig ) // GetAccountFromConsul 获取 consul 的配置 func GetAccountFromConsul(config config.Config, path ...string) error { consulData := &ConsulConfig{} config.Get(path...).Scan(consulData) ConsulInfo = consulData return nil }3.consul可视化界面数据编写点击Key/Value,再点击Create输入项目名称: micro/config/account选择JSON输入以下代码:{ "account":{ "name": "Account", "title": "账号功能", "mode": "dev", "port": 9580, "version": "v0.0.1" }, "log":{ "level": "debug", "filename": "Account.log", "max_size": 200, "max_age": 30, "max_backips": 7 }, "mysql":{ "host":"127.0.0.1", "user":"root", "pwd":"xxx", "database":"micro", "port":3306 }, "redis":{ "host": "127.0.0.1", "port": 6379, "password": "123456", "db": 4, "pool_size": 100 }, "consul":{ "host": "localhost", "port": 8500, "prefix": "/micro/config", "consulRegistry": "127.0.0.1:8500" }, "email":{ "user": "xxx@qq.com", "pass": "xxx", "host": "smtp.qq.com", "port": 465, "rename": "Account" }, "jaeger":{ "serviceName": "go.micro.service.account", "addr": "localhost:6831" }, "prometheus":{ "host": "0.0.0.0", "port": 9089 }, "ratelimit":{ "QPS": 1000 }, "micro":{ "name": "go.micro.service.account", "version": "latest", "address": ":9580" } }注意JSON格式,点击Save4. main.go代码编写// 1.配置中心 consulConfig, err := micro.GetConsulConfig("localhost", 8500, "/micro/config") if err != nil { fmt.Printf("Init consulConfig failed, err: %v\n", err) } // 2.注册中心 consulRegistry := consul.NewRegistry(func(options *registry.Options) { options.Addrs = []string{ "127.0.0.1:8500", } }) if err := micro.GetAccountFromConsul(consulConfig, "account"); err != nil { fmt.Printf("Init consul failed, err: %v\n", err) } fmt.Println(micro.ConsulInfo)这时候,micro中的ConsulInfo可以用来使用consul中的数据了,使用 . 取数据五 最后至此,go-micro微服务consul配置、注册中心开发工作就正式完成。接下来就开始Mysql的代码编写了,希望大家关注博主和关注专栏,第一时间获取最新内容,每篇博客都干货满满。
一 微服务项目介绍账户功能是每一个系统都绕不开的一部分,所以本次搭建的微服务项目就是账户微服务项目,其中向外暴露的功能有:登录注册查询用户信息修改信息发送注册邮件发送重置密码邮件重置密码获取权限修改权限退出账号删除账号禁用账号启用账号提供的功能总共有13个,基本上包含了账户相关的所有功能!在本次微服务项目中使用到的技术包括:go-micro v2DockerMysqlRedisZap日志Consul配置、注册中心发送邮件(QQ邮箱)Jaeger链路追踪Prometheus监控雪花算法生成用户IDJWT跨域认证Makefile自动化编译工具Elasticsearch搜索引擎Logstash日志搜集Kibana日志分析可视化界面Filebeat数据收集负载均衡本次微服务项目中使用到的技术,基本上包含了微服务相关的技术,所以想要掌握微服务的小伙伴们,要跟紧我们的项目开发,早日掌握微服务。二 go-micro安装大家自行安装Dokcer终端输入docker --version即可查看docker是否安装成功1.拉取micro镜像打开Goland终端,输入:docker pull micro/micro:v2.9.32.生成项目目录镜像拉取成功后,输入命令生成项目目录docker run --rm -v /d/学习/GO_project/micro/micro-common:/d/学习/GO_project/micro/micro-common -w /d/学习/GO_project/micro/micro-common micro/micro:v2.9.3 new account命令讲解:–rm 是不再生成新的micro镜像/d/学习/GO_project/micro/micro-common 为你当前目录的路径地址,注意格式,使用的是 / 。micro/micro:v2.9.3 指定使用的镜像名称account 生成的项目名称,我使用的是 account。命令执行后等待即可生成go-micro项目目录,如果报错,可能跟网络有关,可以多次输入命令进行尝试。三 项目搭建使用DDD模式开发项目:打开项目,在根目录下新建 client、common、config、domain目录在common目录下新建 mail、micro、snow_flake、token、utils目录在config目录下新建 config、logger、mysql、redis目录在domain目录下新建 model、repository、service目录data目录不用新建,可以使用filebeat自动生成,存放的信息为日志信息。项目目录如下所示:四 最后至此,go-micro微服务项目搭建工作就正式完成。接下来就开始正式的代码编写了,希望大家关注博主和关注专栏,第一时间获取最新内容,每篇博客都干货满满。
前言本人在学习go-micro中,用到Makefile,本人之前用过Makefile,但是不知道为什么这会就不能用了,我找了好多教程都没能解决问题,大多数是linux的安装教程,另一部分是下载mingw,但是没能解决我的问题,最后花了我一下午的时间才解决,特此记录一下,给其他的小伙伴提个醒,希望能够解决大家的make安装问题。Makefile简介当用户编译文件过多的时候,使用makefile可以帮助模块化编译文件,makefile是一个脚本文件,根据规则,来执行相应的脚本文件,实现自动化编译。make作用想要使用Makefile文件,首先需要确保本地可以使用make命令,如果没有安装make,goland编译器会出现报错:> Error running 'docs': Cannot run program "\usr\bin\make" (in directory > "F:\xx\goland-api\xxxx-go"... 安装make:1.windows上安装:chocolatey一、Chocolatey介绍Chocolatey是一款专为Windows系统开发的、基于NuGet的包管理器工具,类似于Node.js的npm,MacOS的brew,Ubuntu的apt-get,它简称为choco。Chocolatey的设计目标是成为一个去中心化的框架,便于开发者按需快速安装应用程序和工具。Chocolatey的官网: https://chocolatey.org/二、Chocolatey安装要安装Chocolatey很容易,必须以管理员权限打开cmd命令行提示,执行如下内容:@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin还有一种安装方法,使用PowerShell,同样必须以管理员权限打开PowerShell,执行如下命令iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))安装make安装完成chocolatey后,在cmd或者PowerShell中执行:choco install make配置make连接然后golang 的setting菜单搜索make 配置make的路径:make的安装路径是:C:\ProgramData\chocolatey\bin\make.exe之后就可以使用make了!
1. Kafka介绍1.1.1. Kafka是什么 kafka使用scala开发,支持多语言客户端(c++、java、python、go等) Kafka最先由LinkedIn公司开发,之后成为Apache的顶级项目。 Kafka是一个分布式的、分区化、可复制提交的日志服务 LinkedIn使用Kafka实现了公司不同应用程序之间的松耦和,那么作为一个可扩展、高可靠的消息系统 支持高Throughput的应用 scale out:无需停机即可扩展机器 持久化:通过将数据持久化到硬盘以及replication防止数据丢失 支持online和offline的场景1.1.2. Kafka的特点Kafka是分布式的,其所有的构件borker(服务端集群)、producer(消息生产)、consumer(消息消费者)都可以是分布式的。在消息的生产时可以使用一个标识topic来区分,且可以进行分区;每一个分区都是一个顺序的、不可变的消息队列, 并且可以持续的添加。同时为发布和订阅提供高吞吐量。据了解,Kafka每秒可以生产约25万消息(50 MB),每秒处理55万消息(110 MB)。消息被处理的状态是在consumer端维护,而不是由server端维护。当失败时能自动平衡1.1.3. 常用的场景监控:主机通过Kafka发送与系统和应用程序健康相关的指标,然后这些信息会被收集和处理从而创建监控仪表盘并发送警告。消息队列: 应用程度使用Kafka作为传统的消息系统实现标准的队列和消息的发布—订阅,例如搜索和内容提要(Content Feed)。比起大多数的消息系统来说,Kafka有更好的吞吐量,内置的分区,冗余及容错性,这让Kafka成为了一个很好的大规模消息处理应用的解决方案。消息系统 一般吞吐量相对较低,但是需要更小的端到端延时,并尝尝依赖于Kafka提供的强大的持久性保障。在这个领域,Kafka足以媲美传统消息系统,如ActiveMR或RabbitMQ站点的用户活动追踪: 为了更好地理解用户行为,改善用户体验,将用户查看了哪个页面、点击了哪些内容等信息发送到每个数据中心的Kafka集群上,并通过Hadoop进行分析、生成日常报告。流处理:保存收集流数据,以提供之后对接的Storm或其他流式计算框架进行处理。很多用户会将那些从原始topic来的数据进行 阶段性处理,汇总,扩充或者以其他的方式转换到新的topic下再继续后面的处理。例如一个文章推荐的处理流程,可能是先从RSS数据源中抓取文章的内 容,然后将其丢入一个叫做“文章”的topic中;后续操作可能是需要对这个内容进行清理,比如回复正常数据或者删除重复数据,最后再将内容匹配的结果返 还给用户。这就在一个独立的topic之外,产生了一系列的实时数据处理的流程。日志聚合:使用Kafka代替日志聚合(log aggregation)。日志聚合一般来说是从服务器上收集日志文件,然后放到一个集中的位置(文件服务器或HDFS)进行处理。然而Kafka忽略掉 文件的细节,将其更清晰地抽象成一个个日志或事件的消息流。这就让Kafka处理过程延迟更低,更容易支持多数据源和分布式数据处理。比起以日志为中心的 系统比如Scribe或者Flume来说,Kafka提供同样高效的性能和因为复制导致的更高的耐用性保证,以及更低的端到端延迟持久性日志:Kafka可以为一种外部的持久性日志的分布式系统提供服务。这种日志可以在节点间备份数据,并为故障节点数据回复提供一种重新同步的机制。Kafka中日志压缩功能为这种用法提供了条件。在这种用法中,Kafka类似于Apache BookKeeper项目。1.1.4. Kafka中包含以下基础概念 1.Topic(话题):Kafka中用于区分不同类别信息的类别名称。由producer指定 2.Producer(生产者):将消息发布到Kafka特定的Topic的对象(过程) 3.Consumers(消费者):订阅并处理特定的Topic中的消息的对象(过程) 4.Broker(Kafka服务集群):已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker). 消费者可以订阅一个或多个话题,并从Broker拉数据,从而消费这些已发布的消息。 5.Partition(分区):Topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset) Message:消息,是通信的基本单位,每个producer可以向一个topic(主题)发布一些消息。1.1.5. 消息消息由一个固定大小的报头和可变长度但不透明的字节阵列负载。报头包含格式版本和CRC32效验和以检测损坏或截断1.1.6. 消息格式 1. 4 byte CRC32 of the message 2. 1 byte "magic" identifier to allow format changes, value is 0 or 1 3. 1 byte "attributes" identifier to allow annotations on the message independent of the version bit 0 ~ 2 : Compression codec 0 : no compression 1 : gzip 2 : snappy 3 : lz4 bit 3 : Timestamp type 0 : create time 1 : log append time bit 4 ~ 7 : reserved 4. (可选) 8 byte timestamp only if "magic" identifier is greater than 0 5. 4 byte key length, containing length K 6. K byte key 7. 4 byte payload length, containing length V 8. V byte payload2. Kafka深层介绍2.1.1. 架构介绍[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ieGSQMR1-1670167723057)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/82134aab3d36429ebeb079a7b84390a5~tplv-k3u1fbpfcp-watermark.image?)]Producer:Producer即生产者,消息的产生者,是消息的⼊口。kafka cluster:kafka集群,一台或多台服务器组成Broker:Broker是指部署了Kafka实例的服务器节点。每个服务器上有一个或多个kafka的实 例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的 编号,如图中的broker-0、broker-1等……Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上 都可以创建多个topic。实际应用中通常是一个业务线建一个topic。Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞 吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的⽂件夹!Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的 时候会选择一个备胎(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10 个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机 器对同一个分区也只可能存放一个副本(包括自己)。Consumer:消费者,即消息的消费方,是消息的出口。Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分 区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个 topic的不同分区的数据,这也是为了提高kafka的吞吐量!2.1.2. ⼯作流程我们看上⾯的架构图中,producer就是生产者,是数据的入口。Producer在写入数据的时候会把数据 写入到leader中,不会直接将数据写入follower!那leader怎么找呢?写入的流程又是什么样的呢?我 们看下图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NEZFHbN8-1670167723058)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc72b5ee79be4f84a2b767ead1b3c9bd~tplv-k3u1fbpfcp-watermark.image?)] 1.⽣产者从Kafka集群获取分区leader信息 2.⽣产者将消息发送给leader 3.leader将消息写入本地磁盘 4.follower从leader拉取消息数据 5.follower将消息写入本地磁盘后向leader发送ACK 6.leader收到所有的follower的ACK之后向生产者发送ACK2.1.3. 选择partition的原则那在kafka中,如果某个topic有多个partition,producer⼜怎么知道该将数据发往哪个partition呢? kafka中有几个原则: 1.partition在写入的时候可以指定需要写入的partition,如果有指定,则写入对应的partition。 2.如果没有指定partition,但是设置了数据的key,则会根据key的值hash出一个partition。 3.如果既没指定partition,又没有设置key,则会采用轮询⽅式,即每次取一小段时间的数据写入某 个partition,下一小段的时间写入下一个partition2.1.4. ACK应答机制producer在向kafka写入消息的时候,可以设置参数来确定是否确认kafka接收到数据,这个参数可设置 的值为 0,1,all0代表producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效 率最高。1代表producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功。all代表producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保 leader发送成功和所有的副本都完成备份。安全性最⾼高,但是效率最低。最后要注意的是,如果往不存在的topic写数据,kafka会⾃动创建topic,partition和replication的数量 默认配置都是1。2.1.5. Topic和数据⽇志topic 是同⼀类别的消息记录(record)的集合。在Kafka中,⼀个主题通常有多个订阅者。对于每个 主题,Kafka集群维护了⼀个分区数据⽇志⽂件结构如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rUtESF3x-1670167723059)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ab0bf83c38604244ac4b723fac2ea710~tplv-k3u1fbpfcp-watermark.image?)]每个partition都是⼀个有序并且不可变的消息记录集合。当新的数据写⼊时,就被追加到partition的末 尾。在每个partition中,每条消息都会被分配⼀个顺序的唯⼀标识,这个标识被称为offset,即偏移 量。注意,Kafka只保证在同⼀个partition内部消息是有序的,在不同partition之间,并不能保证消息 有序。Kafka可以配置⼀个保留期限,⽤来标识⽇志会在Kafka集群内保留多⻓时间。Kafka集群会保留在保留 期限内所有被发布的消息,不管这些消息是否被消费过。⽐如保留期限设置为两天,那么数据被发布到 Kafka集群的两天以内,所有的这些数据都可以被消费。当超过两天,这些数据将会被清空,以便为后 续的数据腾出空间。由于Kafka会将数据进⾏持久化存储(即写⼊到硬盘上),所以保留的数据⼤⼩可 以设置为⼀个⽐较⼤的值。2.1.6. Partition结构Partition在服务器上的表现形式就是⼀个⼀个的⽂件夹,每个partition的⽂件夹下⾯会有多组segment ⽂件,每组segment⽂件⼜包含 .index ⽂件、 .log ⽂件、 .timeindex ⽂件三个⽂件,其中 .log ⽂ 件就是实际存储message的地⽅,⽽ .index 和 .timeindex ⽂件为索引⽂件,⽤于检索消息。2.1.7. 消费数据多个消费者实例可以组成⼀个消费者组,并⽤⼀个标签来标识这个消费者组。⼀个消费者组中的不同消 费者实例可以运⾏在不同的进程甚⾄不同的服务器上。如果所有的消费者实例都在同⼀个消费者组中,那么消息记录会被很好的均衡的发送到每个消费者实 例。如果所有的消费者实例都在不同的消费者组,那么每⼀条消息记录会被⼴播到每⼀个消费者实例。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U3eYhFiv-1670167723060)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ecf0667fcf641ed80af3aa49c4c0fee~tplv-k3u1fbpfcp-watermark.image?)]举个例⼦,如上图所示⼀个两个节点的Kafka集群上拥有⼀个四个partition(P0-P3)的topic。有两个 消费者组都在消费这个topic中的数据,消费者组A有两个消费者实例,消费者组B有四个消费者实例。 从图中我们可以看到,在同⼀个消费者组中,每个消费者实例可以消费多个分区,但是每个分区最多只 能被消费者组中的⼀个实例消费。也就是说,如果有⼀个4个分区的主题,那么消费者组中最多只能有4 个消费者实例去消费,多出来的都不会被分配到分区。其实这也很好理解,如果允许两个消费者实例同 时消费同⼀个分区,那么就⽆法记录这个分区被这个消费者组消费的offset了。如果在消费者组中动态 的上线或下线消费者,那么Kafka集群会⾃动调整分区与消费者实例间的对应关系。3. 操作Kafka3.1.1. saramaGo语言中连接kafka使用第三方库: github.com/Shopify/sarama。3.1.2. 下载及安装 go get github.com/Shopify/sarama注意事项: sarama v1.20之后的版本加入了zstd压缩算法,需要用到cgo,在Windows平台编译时会提示类似如下错误: github.com/DataDog/zstd exec: “gcc”:executable file not found in %PATH% 所以在Windows平台请使用v1.19版本的sarama。(如果不会版本控制请查看博客里面的go module章节)3.1.3. 连接kafka发送消息package main import ( "fmt" "github.com/Shopify/sarama" ) // 基于sarama第三方库开发的kafka client func main() { config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll // 发送完数据需要leader和follow都确认 config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出一个partition config.Producer.Return.Successes = true // 成功交付的消息将在success channel返回 // 构造一个消息 msg := &sarama.ProducerMessage{} msg.Topic = "web_log" msg.Value = sarama.StringEncoder("this is a test log") // 连接kafka client, err := sarama.NewSyncProducer([]string{"127.0.0.1:9092"}, config) if err != nil { fmt.Println("producer closed, err:", err) return } defer client.Close() // 发送消息 pid, offset, err := client.SendMessage(msg) if err != nil { fmt.Println("send msg failed, err:", err) return } fmt.Printf("pid:%v offset:%v\n", pid, offset) }3.1.4. 连接kafka消费消息package main import ( "fmt" "github.com/Shopify/sarama" ) // kafka consumer func main() { consumer, err := sarama.NewConsumer([]string{"127.0.0.1:9092"}, nil) if err != nil { fmt.Printf("fail to start consumer, err:%v\n", err) return } partitionList, err := consumer.Partitions("web_log") // 根据topic取到所有的分区 if err != nil { fmt.Printf("fail to get list of partition:err%v\n", err) return } fmt.Println(partitionList) for partition := range partitionList { // 遍历所有的分区 // 针对每个分区创建一个对应的分区消费者 pc, err := consumer.ConsumePartition("web_log", int32(partition), sarama.OffsetNewest) if err != nil { fmt.Printf("failed to start consumer for partition %d,err:%v\n", partition, err) return } defer pc.AsyncClose() // 异步从每个分区消费信息 go func(sarama.PartitionConsumer) { for msg := range pc.Messages() { fmt.Printf("Partition:%d Offset:%d Key:%v Value:%v", msg.Partition, msg.Offset, msg.Key, msg.Value) } }(pc) } }
String 简单动态字符串 Simple Dynamic String, SDSRedis没有直接使用C语言的传统字符串表示,而是自己构建了一种名为简单动态字符串(Simple Dynamic String, SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。 每个sds.h/sdshdr结构表示一个SDS值: struct sdshdr { int len; // 记录buf数组中已经使用的字节数量 int free; // 记录buf数组中未使用字节的数量 char buf[]; // 字节数组,用于保存字符串。SDS遵循C字符串以空字符结尾的惯例 }Redis持久化持久化的作用:redis所有数据保存在内存中,对数据的更新将异步地保存到磁盘上。主流数据库持久化实现方式:快照(MySQL Dump/Redis RDB),写日志(MySQL Binlog/Redis AOF)RDB:创建RDB文件(二进制文件)到硬盘中,启动后载入RDB文件到内存三种触发机制save(同步) - 会产生阻塞文件策略:如存在老的RDB文件,新的替换老的,新的会先生成到一个临时文件bgsave(异步) - 不会阻塞客户端执行bgsave之后,redis会使用linux的一个fork()命令生成主进程的一个子进程(fork的操作会执行一个内存页的拷贝,使用copy-on-write策略),子进程会创建RDB文件,创建完毕后将成功的消息返回给redis。fork()出来的子进程执行快的话不会阻塞主进程,否则也会阻塞redis,阻塞的实际点就是生成出来这个子进程。由于是异步,在创建的过程中还有其他命令在执行,如何保证RDB文件是最新的呢?在数据量大的时候bgsave才能突出优点。命令savebgsaveIO类型同步异步阻塞是是(阻塞发生在fork子进程复杂度O(n)O(n)优点不会消耗额外内存不阻塞客户端命令缺点阻塞客户端命令需要fork,消耗内存自动触发:多少秒内有多少changes会异步(bgsave)生成一个RDB文件,如60秒内有1W条changes,默认的规则,可以改;不是很好吧,无法控制频率;另外两条是900秒内有1条changes, 300秒内有10条changes;配置dbfilename dump.rdbdir ./stop-writes-on-bgsave-error yes 当bgsave发生错误是停止写RDB文件rdbcompression yes 采用压缩格式rdbchecksum yes 采用校验和其他不能忽视的点:全量复制;debug reload;shutdown save会执行rdb文件的生成AOF:RDB现存问题:耗时,耗性能(fork,IO),不可控(突然宕机)AOF:redis中的cmd会先刷新到缓冲区,然后更具配置AOF的策略,异步存追加到AOF文件中,发生宕机后,可以通过- AOF恢复,基本上数据是完整的AOF的三种策略(配置的三种属性)always:来一条命令写一条;不丢失数据,IO开销较大everysec:每秒把缓冲区fsync到AOF文件;丢1秒数据no:操作系统决定什么时候把缓冲区同步到AOF就什么时候追加;不用配置,但是不可控,取决于操作系统AOF重写如果AOF文件很大的话,恢复会很慢,AOF的重写是优化一些命名,使其变成1条,对于过期数据没必要Log,本质是把过期的没有用的,重复的过滤掉,以此减少磁盘占用量,加速恢复。极端的例子,1亿次incr,实际只需要set counter n就够了重写的两种方式bgrewriteaof:异步执行,redis fork出一个子进程,然后进行AOF重写AOF重写配置auto-aof-rewrite-min-size: AOF文件到达多大的时候才开始重写auto-aof-rewrite-percentage: AOF文件的增长率到达了多大才开始重写统计aof_current_size AOF当前尺寸 字节aof_base_size AOF上次重启和重写的尺寸 字节,方便自动重写判断重写触发机制(同时满足如下两条)aof_current_size > auto-aof-rewrite-min-size(aof_current_size - aof_base_size) / aof_base_size > auto-aof-rewrite-percentage其他配置appendonly yesappendfilename “”appendfsync everysecdir /xxno-appendfsync-on-rewrite yes AOF在重启之后恢复,要权衡是否开启AOF日志追加的功能,这个时候IO很大,如果设置为yes,也就意味着在恢复之前的日志数据会丢失RDB & AOF最佳策略:RDB优先于AOF先启用RDB:建议关掉,集中管理,在从节点开RDBAOF:建议开启,每秒刷盘最佳策略:小分片(log文件分片)常见问题fork操作:是一个同步操作,做一个内存页的拷贝;与内存量息息相关,内存越大,耗时越长;执行info命令,有个latest_fork_usec的值,看下上次fork执行耗时进程外开销:CPU:RDB AOF文件生成,属于CPU密集型操作(不要和CPU密集型应用部署在一起,减少RDB AOF频率);内存:fork内存开销;硬盘:IO开销大,选用SSD磁盘AOF追加阻塞:主线程将命令刷到AOF缓冲区,同步线程同步命令到硬盘,同时主线程会对比上次fsync的时间,如果大于2秒就阻塞主线程,否则不阻塞,主线程这么做是为了达到每秒刷盘的目的,让子线程完成AOF,以此来达到数据同步。AOF发生阻塞怎么定位:redis日志/info persistence(aof_delayed_fsync累计阻塞次数,是累计,不好分清什么时候发生阻塞)单机多实例部署高可用Redis主从复制主从复制:单机故障/容量瓶颈/QPS瓶颈;一个master可以有多个slave,一个slave只能有一个master,数据必须是单流向,从master流向slave复制的配置:使用slaeof命令,在从redis中执行slave masterip:port使其成为master的从服务器,就能从master拉取数据了;执行slaveof no one清除掉不成为从节点,但是数据不清楚;修改配置, slaveof ip port / slave-read-only yes(从节点只做都操作);配置要更改的话,要重启,所以选择的时候谨慎全量复制run_id(使用info server可以看到run_id),重启之后run_id就没有了,当从服务器去复制主服务器,主服务器run_id会在从服务器上做一个标识,当从服务器发现主服务器的run_id发生了变化,说明主服务器发生了变化(重启或者什么的),那么从服务器就要把主服务器的数据都同步过来偏移量:部分复制中的一个依据,后面说解析下上面的全量复制的过程,slave向master发送psync的命令要去master全量复制数据(PSYNC ,其中?表示我不知道master的runId啊,第一次连嘛,-1表示我都要,这时候slava咱啥也不知道),master大人收到了小弟的请求之后,大方的把自己的runId/offset发了过去,小弟收到后先存下来;在master大人把自个的信息发给小弟之后,立马投入了创建快照RDB的工作,一个bgsave命令立马开工,RDB生产了就发给slave;咦,细心的我们发现你这不对啊,你master创建快照到创建完成这之间新增的数据咋办,master吭吭了两声,我在开始快照的那一刻,后期的所有写命令都额外往buffer中存了一份,来保证我给你的是完整的,当我发送完RDB之后,立马给你发buffer;slave小弟内心对master大人产生了膜拜之情,收到了RDB/buffer之后,先把自己的老数据flush掉,然后load RDB,把最新的buffer刷一遍,分分钟让自己向master看齐。开销:bgsave时间, RDB文件网络传输时间,从节点清空数据时间,从节点加载RDB的时间,可能的AOF重写时间解释下上面的部分复制的过程,当遇到网络抖动,那这段时间内数据在slave上就会发生丢失,那么这些数据slave是不知道的,在2.8之前redis会重新做一次全量复制,但是很显然这样做开销很大,2.8之后提出部分复制的功能;当matster发现slave连接不上的时候,master在进行写操作的时候,也会往缓冲区写,等到下一次slave连上之后,slave会发送一条pysnc {offset}{runId}的命令,其中offset是slave自己的,相当于告诉master我的偏移量是多少,master判断slave的offset在缓冲区内(缓冲区有start/end offset)就向slave发送continue命令,然后把这部分数据发送给slave;当master发现slave这个offset偏移量很大的时候,也就意味着slave丢失了很多数据,那么就进行一次全量复制故障处理:master/slave宕机的情况,主从模式没有实现故障的完全自动转移常见问题:读写分离:读流量分摊到从节点,可能遇到复制数据延迟,也可能读到过期的数据,从节点故障怎么办主从配置不一致:主从maxmemory不一致,可能会丢失数据;主从内存不一致规避全量复制:第一次不可避免;小主节点,低峰处理(夜间);主节点重启后runId发生了变化规避复制风暴单机主节点复制风暴,如果是1主N从,当master重启之后,所有的slave都会发生全量复制,可想而知这样非常容易造成redis服务的不可用Redis事务Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:批量操作在发送 EXEC 命令前被放入队列缓存。收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。Redis事务从开始到执行会经历以下三个阶段:开始事务 -> 命令入队 -> 执行事务。单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。这是官网上的说明 From redis docs on transactions: It’s important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.Redis 通过监听一个 TCP 端口或者 Unix socket 的方式来接收来自客户端的连接,当一个连接建立后,Redis 内部会进行以下一些操作:首先,客户端 socket 会被设置为非阻塞模式,因为 Redis 在网络事件处理上采用的是非阻塞多路复用模型。然后为这个 socket 设置 TCP_NODELAY 属性,禁用 Nagle 算法然后创建一个可读的文件事件用于监听这个客户端 socket 的数据发送Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。管道技术最显著的优势是提高了 redis 服务的性能。Redis 分区分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。分区的优势:通过利用多台计算机内存的和值,允许我们构造更大的数据库。通过多核和多台计算机,允许我们扩展计算能力;通过多台计算机和网络适配器,允许我们扩展网络带宽。分区的不足:涉及多个key的操作通常是不被支持的。举例来说,当两个set映射到不同的redis实例上时,你就不能对这两个set执行交集操作。涉及多个key的redis事务不能使用。当使用分区时,数据处理较为复杂,比如你需要处理多个rdb/aof文件,并且从多个实例和主机备份持久化文件。增加或删除容量也比较复杂。redis集群大多数支持在运行时增加、删除节点的透明数据平衡的能力,但是类似于客户端分区、代理等其他系统则不支持这项特性。然而,一种叫做presharding的技术对此是有帮助的。分区类型:Redis 有两种类型分区。 假设有4个Redis实例 R0,R1,R2,R3,和类似user:1,user:2这样的表示用户的多个key,对既定的key有多种不同方式来选择这个key存放在哪个实例中。也就是说,有不同的系统来映射某个key到某个Redis服务,关注+转发后,私信【Redis】获取300多页的Redis实战学习笔记。范围分区最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。比如,ID从0到10000的用户会保存到实例R0,ID从10001到 20000的用户会保存到R1,以此类推。这种方式是可行的,并且在实际中使用,不足就是要有一个区间范围到实例的映射表。这个表要被管理,同时还需要各 种对象的映射表,通常对Redis来说并非是好的方法。哈希分区另外一种分区方法是hash分区。这对任何key都适用,也无需是object_name:这种形式,像下面描述的一样简单:用一个hash函数将key转换为一个数字,比如使用crc32 hash函数。对key foobar执行crc32(foobar)会输出类似93024922的整数。对这个整数取模,将其转化为0-3之间的数字,就可以将这个整数映射到4个Redis实例中的一个了。93024922 % 4 = 2,就是说key foobar应该被存到R2实例中。注意:取模操作是取除的余数,通常在多种编程语言中用%操作符实现。
前言优雅关机就是服务端关机命令发出后不是立即关机,而是等待当前还在处理的请求全部处理完毕后再退出程序,是一种对客户端友好的关机方式。而执行Ctrl+C关闭服务端时,会强制结束进程导致正在访问的请求出现问题。实现原理Go 1.8版本之后, http.Server 内置的 Shutdown() 方法就支持优雅地关机,说明一下Shutdown工作的机制:当程序检测到中断信号时,我们调用http.server种的shutdown方法,该方法将阻止新的请求进来,同时保持当前的连接,知道当前连接完成则终止程序!实现优雅重启package main import ( "context" "fmt" "github.com/spf13/viper" "go.uber.org/zap" "log" "net/http" "os" "os/signal" "syscall" "time" ) func main() { //启动服务(优雅关机) srv := &http.Server{ Addr: fmt.Sprintf(":%d", viper.GetInt("app.port")), Handler: r, } go func() { // 开启一个goroutine启动服务 if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) } }() // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时 quit := make(chan os.Signal, 1) // 创建一个接收信号的通道 // kill 默认会发送 syscall.SIGTERM 信号 // kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号 // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它 // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞 <-quit // 阻塞在此,当接收到上述两种信号时才会往下执行 zap.L().Info("Shutdown Server ...") // 创建一个5秒超时的context ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出 if err := srv.Shutdown(ctx); err != nil { zap.L().Fatal("Server Shutdown: ", zap.Error(err)) } zap.L().Info("Server exiting") } 实现平滑重启import ( "log" "net/http" "time" "github.com/fvbock/endless" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "hello xiaosheng !") }) // 默认endless服务器会监听下列信号: // syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP // 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号) // 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机 // 接收到 SIGUSR2 信号将触发HammerTime // SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数 if err := endless.ListenAndServe(":8080", router); err!=nil{ log.Fatalf("listen: %s\n", err) } log.Println("Server exiting...")测试我们通过执行kill -1 pid命令发送syscall.SIGINT来通知程序优雅重启,具体做法如下:打开终端,go build -o graceful_restart编译并执行./graceful_restart,终端输出当前pid(假设为43682)将代码中处理请求函数返回的hello gin!修改为hello q1mi!,再次编译go build -o graceful_restart打开一个浏览器,访问127.0.0.1:8080/,此时浏览器白屏等待服务端返回响应。在终端迅速执行kill -1 43682命令给程序发送syscall.SIGHUP信号等第3步浏览器收到响应信息hello gin!后再次访问127.0.0.1:8080/会收到hello q1mi!的响应。在不影响当前未处理完请求的同时完成了程序代码的替换,实现了优雅重启。但是需要注意的是,此时程序的PID变化了,因为endless 是通过fork子进程处理新请求,待原进程处理完当前请求后再退出的方式实现优雅重启的。所以当你的项目是使用类似supervisor的软件管理进程时就不适用这种方式了。总结无论是优雅关机还是优雅重启归根结底都是通过监听特定系统信号,然后执行一定的逻辑处理保障当前系统正在处理的请求被正常处理后再关闭当前进程。使用优雅关机还是使用优雅重启以及怎么实现,这就需要根据项目实际情况来决定了。以上就是使用go如何优雅关机和平滑重启 的详细内容,更多关于go关机重启 的内容请关注博主的其它相关文章!
前言1.工作中,经常需要合并多个Excel文件。如果文件数量比较多,则工作量大,易出错,此时,可以使用Python来快速的完成合并。2.使用方法:将需要合并的多个Excel文件放到同一个文件夹下,程序运行后,输入文件夹目录的地址即可,完成合并后会将合并后的文件放在当前目录下。3.可以查看代码的详细编写,或者直接到最后复制完整代码使用。代码编写1.导包import os import xlrd2 import xlsxwriter import glob2.定义位置和表头biao_tou = "NULL" wei_zhi = "NULL"3.获取要合并的所有exce表格def get_exce(): global wei_zhi wei_zhi = input("请输入Excel文件所在的目录:") all_exce = glob.glob(wei_zhi + "\*.xlsx") print("该目录下有" + str(len(all_exce)) + "个excel文件:") if (len(all_exce) == 0): return 0 else: for i in range(len(all_exce)): print(all_exce[i]) return all_exce4.打开Exce文件def open_exce(name): fh = xlrd2.open_workbook(name) return fh5.获取exce文件下的所有sheet# 获取exce文件下的所有sheet def get_sheet(fh): sheets = fh.sheets() return sheets6.获取sheet下有多少行数据def get_sheetrow_num(sheet): return sheet.nrows7.获取sheet下的数据def get_sheet_data(sheet, row): for i in range(row): if (i == 0): global biao_tou biao_tou = sheet.row_values(i) continue values = sheet.row_values(i) all_data1.append(values) return all_data18.主函数if __name__ == '__main__': all_exce = get_exce() # 得到要合并的所有exce表格数据 if (all_exce == 0): print("该目录下无.xls文件!请检查您输入的目录是否有误!") os.system('pause') exit() all_data1 = [] # 用于保存合并的所有行的数据 # 下面开始文件数据的获取 for exce in all_exce: fh = open_exce(exce) # 打开文件 sheets = get_sheet(fh) # 获取文件下的sheet数量 for sheet in range(len(sheets)): row = get_sheetrow_num(sheets[sheet]) # 获取一个sheet下的所有的数据的行数 all_data2 = get_sheet_data(sheets[sheet], row) # 获取一个sheet下的所有行的数据 all_data1.insert(0, biao_tou) # 表头写入 # 下面开始文件数据的写入 new_exce = wei_zhi + "\总数据.xlsx" # 新建的exce文件名字 fh1 = xlsxwriter.Workbook(new_exce) # 新建一个exce表 new_sheet = fh1.add_worksheet() # 新建一个sheet表 for i in range(len(all_data1)): for j in range(len(all_data1[i])): c = all_data1[i][j] new_sheet.write(i, j, c) fh1.close() # 关闭该exce表 print("文件合并成功,请查看“" + wei_zhi + "”目录下的总数据.xlsx文件!") os.system('pause') os.system('pause')完整代码import os import xlrd2 import xlsxwriter import glob biao_tou = "NULL" wei_zhi = "NULL" # 获取要合并的所有exce表格 def get_exce(): global wei_zhi wei_zhi = input("请输入Excel文件所在的目录:") all_exce = glob.glob(wei_zhi + "\*.xlsx") print("该目录下有" + str(len(all_exce)) + "个excel文件:") if (len(all_exce) == 0): return 0 else: for i in range(len(all_exce)): print(all_exce[i]) return all_exce # 打开Exce文件 def open_exce(name): fh = xlrd2.open_workbook(name) return fh # 获取exce文件下的所有sheet def get_sheet(fh): sheets = fh.sheets() return sheets # 获取sheet下有多少行数据 def get_sheetrow_num(sheet): return sheet.nrows # 获取sheet下的数据 def get_sheet_data(sheet, row): for i in range(row): if (i == 0): global biao_tou biao_tou = sheet.row_values(i) continue values = sheet.row_values(i) all_data1.append(values) return all_data1 if __name__ == '__main__': all_exce = get_exce() # 得到要合并的所有exce表格数据 if (all_exce == 0): print("该目录下无.xlsx文件!请检查您输入的目录是否有误!") os.system('pause') exit() all_data1 = [] # 用于保存合并的所有行的数据 # 下面开始文件数据的获取 for exce in all_exce: fh = open_exce(exce) # 打开文件 sheets = get_sheet(fh) # 获取文件下的sheet数量 for sheet in range(len(sheets)): row = get_sheetrow_num(sheets[sheet]) # 获取一个sheet下的所有的数据的行数 all_data2 = get_sheet_data(sheets[sheet], row) # 获取一个sheet下的所有行的数据 all_data1.insert(0, biao_tou) # 表头写入 # 下面开始文件数据的写入 new_exce = wei_zhi + "\总数据.xlsx" # 新建的exce文件名字 fh1 = xlsxwriter.Workbook(new_exce) # 新建一个exce表 new_sheet = fh1.add_worksheet() # 新建一个sheet表 for i in range(len(all_data1)): for j in range(len(all_data1[i])): c = all_data1[i][j] new_sheet.write(i, j, c) fh1.close() # 关闭该exce表 print("文件合并成功,请查看“" + wei_zhi + "”目录下的总数据.xlsx文件!") os.system('pause') os.system('pause') \报错修改(1)Python3.9版本在利用第三方库xlrd读取.xlsx格式的Excel文件时报错:AttributeError: ‘ElementTree‘ object has no attribute ‘getiterator‘报错原因:在新版python3.9中,windows中使用的更新删除了getiterator方法,所以我们老版本的xlrd库调用getiterator方法时会报错。解决方法:另外(1)需要将.xls文件合并成.xlsx文件的(2)需要将.xlsx文件合并成.xls文件的并且此处合并后的文件名可以自行更改。最后到此这篇关于教你用Python代码实现合并excel文件的文章就介绍到这了,更多相关Python合并excel文件内容请查看博主其他的文章或继续浏览下面的相关文章希望大家以后多多支持!
前言抖音里面的视频保存之后,会发现全都带有水印,所以如何解决视频去除水印就很有必要,所以教程来了,本次教程不仅会教大家如何去除视频里的水印,并且教大家将程序制作成exe可执行文件,可以发给你的好友使用并进行炫耀一番。一.问题分析首先,在抖音APP中分享视频,点击复制链接,即可得到如下所示的分享视频链接。在浏览器中打开后,发现链接变成了我们抓下包发现了 item_ids 的接口,后面跟的值就是重定向url的最后这部分(6999605370222054663),我判断这应该是视频的ID了。接口地址如下:接下来我们看看这个接口请求返回的数据,当我点开Preview的时候,有视频的文案、作者、音乐、缩略图、地址等等。我拿出视频的地址后,复制到浏览器打开。视频url如下:打开后发现,视频左上角的水印还是在啊。但是链接中去掉 wm,然后复制到浏览器打开,视频的水印没了。视频无水印的地址如下:经过简单的分析,抖音去水印的原理就被搞清楚了,既然原理搞明白了,那写代码就非常简单了。二.代码编写1.导包和定义请求头import requests headers = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3904.108 Safari/537.36", }2.获取分享链接def get_share_url(url): try: r = requests.get(url, headers=headers, allow_redirects=False) return r.headers['location'] except Exception as e: print("解析失败") print(e)3.获取视频链接def get_video_url(url): if not url: return try: vid = url.split("/?")[0].split("video/")[1] xhr_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid}' r = requests.get(xhr_url, headers=headers).json() video_url = r['item_list'][0]['video']['play_addr']['url_list'][0] return video_url except Exception as e: print("解析失败") print(e)4.下载视频def download_video(url, name): if not url: return try: r = requests.get(url, headers=headers) with open(name + '.mp4', 'wb') as f: f.write(r.content) print("下载完成") except Exception as e: print("下载失败") print(e)5.调用if __name__ == "__main__": while 1 < 2: # 抖音APP分享的短链接 url = input("请输入抖音视频链接: ") # url = "https://v.douyin.com/R4tKg6C/" name = input("请给视频起个名字吧: ") share_url = get_share_url(url) video_url = get_video_url(share_url) download_video(video_url, name) print("去除水印完成!")全部代码import requests headers = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3904.108 Safari/537.36", } def get_share_url(url): try: r = requests.get(url, headers=headers, allow_redirects=False) return r.headers['location'] except Exception as e: print("解析失败") print(e) def get_video_url(url): if not url: return try: vid = url.split("/?")[0].split("video/")[1] xhr_url = f'https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid}' r = requests.get(xhr_url, headers=headers).json() video_url = r['item_list'][0]['video']['play_addr']['url_list'][0] return video_url except Exception as e: print("解析失败") print(e) def download_video(url, name): if not url: return try: r = requests.get(url, headers=headers) with open(name + '.mp4', 'wb') as f: f.write(r.content) print("下载完成") except Exception as e: print("下载失败") print(e) if __name__ == "__main__": while 1 < 2: # 抖音APP分享的短链接 url = input("请输入抖音视频链接: ") # url = "https://v.douyin.com/R4tKg6C/" name = input("请给视频起个名字吧: ") share_url = get_share_url(url) video_url = get_video_url(share_url) download_video(video_url, name) print("去除水印完成!") 三.使用教程1.运行程序后输入分享的抖音视频链接;2.再输入去除水印后的视频的名称;3.运行程序成功会将视频保存下来;四.打包成exe可执行文件4.1 安装pyinstaller,使用安装命令:pip install pyinstaller4.2进行打包pyinstaller -F -w -i 图标名.后缀 源文件.py # 例子:pyinstaller -F -w -i qt-logo.ico one.py4.3最终效果打包完成后会在当前目录下生成dist文件夹,打开后双击exe接可以使用了是不是很简单啊,最后附上效果图五.总结到此这篇关于Python抖音视频去水印,并打包成exe可执行文件的详细操作指南的文章就介绍到这了,十分感谢大家的观看,喜欢的可以点个关注哦。
前言我们写好的gin项目想要部署在服务器上,我们应该怎么做呢,接下来我会详细的讲解一下部署教程。1.首先我们要有一台虚拟机,虚拟机上安装好go框架。2.将写好的项目上传到虚拟机上。3.下载好项目运行时所需要的各种依赖。4.配置启动项目5.后台配置启动项目一.安装go环境1.1 解压标准安装包Go提供了编译好的包直接解压就可以用wget https://golang.google.cn/dl/go1.18.3.linux-amd64.tar.gz -P /usr/local/src rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.3.linux-amd64.tar.gz1.2配置环境变量编辑文件 “/etc/profile”vim etc/profile # 输入以下信息并保存 export GOROOT=/usr/local/go export PATH=$PATH:$GOROOT/bin export GOPATH=$HOME/go #(可选设置)使环境变量及时生效source /etc/profile变量解释:GOROOT: 类似于JAVA_HOME,Go的执行文件所在目录GOPATH: 从go 1.8开始,GOPATH 环境变量现在有一个默认值,如果它没有被设置。 它在Unix上默认为$HOME/go,$GOPATH 目录约定有三个子目录:src 存放源代码(比如:.go .c .h .s等)pkg 编译后生成的文件(比如:.a)bin 编译后生成的可执行文件从 Go1.11 开始, Go 官方加入 Go Module 支持, Go1.12 成为默认支持; 从此告别源码必须放在 Gopath。1.3设置仓库代理由于google被阻拦,所以要设置代理go env -w GOPROXY=https://goproxy.cn,direct也可以将GOPROXY写到环境变量配置文件"/etc/profile"中1.4检查是否安装gogo version二.上传项目可以通过WinScpJ将项目上传到服务器上,或者使用xftp上传项目这里不再讲述上传文件方法三.下载运行依赖进入项目主文件夹,下载各种运行依赖。如果项目使用到数据库或者redis时,需要在服务器上安装配置后才可以使用。go mod tidy四.配置启动项目直接启动项目go run main.go五.后台配置启动项目后台启动项目nohup go run main.go &五.总结到此这篇关于gin项目部署到服务器并后台启动的详细操作指南的文章就介绍到这了,十分感谢大家的观看,喜欢的可以点个关注哦。
2023年05月