go 面试题

 1 gochannelclose后读的问题

在Go语言中,当我们调用close(ch)关闭一个channel后,仍然可以从该channel中读取数据,直到channel中的数据全部被读取完。但需要注意以下几点:

  1. 从关闭的channel中读取数据时,接收操作不会阻塞,会一直返回channel中剩余的数据,直到channel为空。
  2. 当channel关闭后,再次尝试从该channel中接收数据时,将得到对应类型的零值和一个布尔值false,这个布尔值用来表示是否成功从channel中获取到了有效数据。例如,对于整型channel ch := make(chan int), 从关闭的ch中读取数据可能是这样的:value, ok := <-ch,如果channel已经关闭并且没有数据可读,那么ok为false,value为0(int类型的零值)。

示例代码:

 

Go

1ch := make(chan int)
2go func() {
3    defer close(ch)
4    ch <- 1
5    ch <- 2
6}()
7
8for i := 0; i < 3; i++ {
9    value, ok := <-ch
10    if ok {
11        fmt.Println("Received:", value)
12    } else {
13        fmt.Println("Channel is closed and no more data to receive.")
14        break
15    }
16}

上述代码中,goroutine会向channel中发送两个整数然后关闭它,主goroutine则会从channel中接收三次数据,前两次能成功接收到值并打印出来,第三次由于channel已关闭且无数据可读,因此会输出"Channel is closed and no more data to receive."并退出循环。

2 在Linux系统中,如果你想要使用grep命令来查找日志文件中的相关内容,你可以按照以下基本语法操作:

 

Bash

1grep '搜索内容' /path/to/logfile.log

举例说明:

  1. 查找名为system.log的日志文件中包含字符串"error"的所有行:
 

Bash

1grep 'error' /var/log/system.log
  1. 忽略大小写查找(匹配时不论大小写):
 

Bash

1grep -i 'ERROR' /var/log/system.log
  1. 显示匹配行及其所在的文件名和行号:
 

Bash

1grep -Hn 'error' /var/log/*.log

上述命令会查找/var/log/目录下所有.log扩展名的文件,并显示包含"error"的行及其完整路径和行号。

  1. 使用正则表达式进行更复杂的匹配:
 

Bash

1grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}.*error.*$' /var/log/system.log

这个例子会查找以四位年份、两位月份、两位日期开头且包含"error"的行。

  1. 递归查找当前目录及其子目录下所有文件中包含指定内容的行:
 

Bash

1grep -rnw '/path/to/somewhere/' -e 'pattern'

其中:

  • -r 或 -R 表示递归查找。
  • -n 表示输出行号。
  • -w 表示只匹配整个单词。
  • -e 指定要匹配的模式或正则表达式。

请根据实际需求调整上述命令参数及搜索内容

3. 数组与slice 的区别

在Go语言中,数组和切片(slice)是两种不同的序列类型,它们有以下主要区别:

  1. 数组

    • 数组是一个具有固定长度的元素集合,一旦创建后,其长度不可变。
    • 声明数组时需要指定元素类型和长度,例如 var arr [5]int 表示一个包含5个整数的数组。
    • 数组是值类型,当作为函数参数传递时,会复制整个数组内容;修改副本不会影响原始数组。
    • 数组可以通过索引直接访问每个元素。
  2. 切片(Slice)

    • 切片是动态大小的、灵活的序列,它基于底层数组实现,并提供对数组部分或全部元素的引用。
    • 切片没有固定的长度,可以扩展或缩小。
    • 创建切片不需要预先知道元素数量,声明时不指定长度,如:var s []int。或者通过数组创建:s := arr[0:3],这将创建一个原数组从索引0开始到索引2(不包括3)的新切片。
    • 切片是引用类型,当作为函数参数传递时,传递的是指向底层数组的指针以及长度和容量信息,因此函数内对切片元素的修改会影响到原始切片。
    • 切片支持和数组类似的下标操作,并且可以使用内置函数len()获取切片长度,cap()获取切片容量(即它可以扩展到的最大长度而不重新分配内存)。

总结来说,数组更适合于已知大小并且不需动态增长或收缩的情况,而切片则更适用于大多数日常编程需求,因为它提供了更大的灵活性。

slice 扩容规则:

Go 语言中的切片(slice)在执行append操作时,如果其容量不足,会自动进行扩容。扩容的规则遵循以下几点:

  1. 初始创建切片时,如果没有指定容量,默认容量为长度值。
  2. 当切片需要扩容时,新的容量并不是简单地增加固定大小,而是按一定的倍数进行增长,以提高连续添加元素的效率。
  3. Go 标准库中切片扩容的具体算法是:新容量 = 原容量的2倍 + 10(若原容量小于1024),或新容量 = 原容量的1.25倍(若原容量大于等于1024)。这个策略既避免了频繁的小幅度扩容,又避免了大幅度扩容导致的内存浪费。

例如:

  • 如果一个切片当前长度为0,当你第一次添加元素时,其容量会变为1。
  • 如果你已经有一个长度为3、容量为4的切片,并尝试添加第4个元素,那么切片将会扩容到容量8。
  • 若切片容量已达到1024及以上,每次扩容将增加现有容量的25%。

以下是扩容代码实现的简化版本:

 

Go

1func grow(slice []type, cap int) []type {
2    if cap < len(slice)+10 {
3        // 小于1024时,按照原容量的2倍加10来扩容
4        newCap := 2*cap + 10
5    } else {
6        // 大于等于1024时,按照原容量的1.25倍来扩容
7        newCap := cap + (cap >> 2)
8    }
9    return append(slice[:len(slice):len(slice)], make([]type, newCap-len(slice))...)
10}

实际上,在Go运行时源码中,扩容的具体逻辑更为复杂和高效,这里仅提供了一个简化的示例说明扩容的基本原理。实际使用时,无需手动调用此函数,当通过append操作扩展切片时,Go运行时会自动处理扩容。

4 make 与new的区别

在Go语言中,makenew是两个用于内存分配的内建函数,但它们有着不同的用途和行为:

  1. new()

    • new()函数会为指定类型分配内存,并初始化为零值。它返回的是指向新分配内存的指针,即类型为T的指针,其中T是传入的类型。
     Go 
    1var x *int = new(int) // 分配一个int类型的内存空间并初始化为0,x是一个指向该内存空间的指针
  2. make()

    • make()函数主要用于创建 slice、map 和 channel 这三种内建引用类型,这些类型虽然也是通过指针访问,但它们内部包含了一些额外的状态信息,因此不能直接使用new()来创建。
     Go 
    1var s []int = make([]int, 5) // 创建一个长度为5的int类型的切片
    2var m map[string]int = make(map[string]int) // 创建一个空的字符串到整数映射
    3var c chan int = make(chan int) // 创建一个无缓冲的int类型通道

总结来说,在Go语言中:

  • 如果你想分配一个基础类型或结构体类型的内存,并将其初始化为零值,你应该使用new()函数。
  • 如果你需要创建一个slice、map或channel,应该使用make()函数。

5 B+树结构and为什么

B+树是一种自平衡的树数据结构,广泛应用于数据库和文件系统中作为索引结构。选择B+树作为数据库索引结构的原因主要包括以下几个关键点:

  1. 磁盘IO效率

    • B+树每个节点可以存储更多的键值对(通常远大于二叉搜索树),这意味着在同样大小的数据块上,B+树能拥有更少的层级深度。由于数据库中的索引通常存储在磁盘上,而磁盘I/O是非常耗时的操作,减少树的高度就意味着查询时需要访问的磁盘次数显著降低,从而提高了查询效率。
  2. 顺序访问优化

    • B+树的所有叶子节点形成了一个有序链表,这使得范围查询极其高效。当进行区间查询或全表扫描时,无需回溯到父节点即可连续读取所有相关的键值对,非常利于大量连续数据的读取操作,如排序、分页查询等。
  3. 内部节点仅存键值

    • B+树的非叶子节点并不存储实际的数据记录,它们只存储键值以及指向子节点的指针。这样设计的好处是每个节点可以容纳更多键,进一步减少了树的层级,同时也意味着索引结构占用空间相对较小。
  4. 聚集索引与非聚集索引

    • 在数据库中,使用B+树可以实现聚集索引(即叶子节点直接包含行数据或者指向行数据的物理地址)和非聚集索引(叶子节点存储键值及其对应的主键值)。B+树的特性允许它既能用于快速定位单行记录,也能支持高效的范围查找和排序。

综上所述,B+树之所以成为数据库索引的理想选择,是因为其设计能够有效利用磁盘预读机制,最大程度地减少随机I/O,提升大数据量下的检索性能,并且方便进行大规模数据的处理和查询。

5 io多路复用,epoll和select的区别

IO多路复用技术允许单个线程同时监听多个文件描述符(通常是网络套接字)的事件,从而在大量并发连接中实现高效的资源利用率。selectpoll 和 epoll 是Linux系统下实现这一功能的三种不同的机制:

  1. select

    • select函数需要一个包含所有待监视文件描述符集合的三个位图,分别对应读就绪、写就绪和异常状态。
    • 每次调用select时都需要将所有要监控的文件描述符复制到内核空间,当文件描述符数量很大时,这会导致较大的内存拷贝开销。
    • 当有事件发生时,select返回并告诉用户哪些描述符准备好了,但是它并不直接提供具体的事件类型,需要再次遍历所有描述符来确定具体是哪个描述符发生了什么类型的事件。
  2. poll

    • poll改进了select的缺点,它使用一个pollfd结构体数组来存储待监控的文件描述符及其事件,不再需要维护位图。
    • 同样支持同时监控多个文件描述符,并且没有最大文件描述符数量的限制(不像早期select版本有限制)。
    • 虽然poll解决了select的大数量描述符问题,但在处理大量描述符场景下依然效率不高,因为每次调用poll时,无论多少描述符准备好,都得遍历整个结构体数组。
  3. epoll

    • epoll在Linux 2.6内核及以后版本中引入,是一种更高级别的IO多路复用机制。
    • epoll通过创建一个epoll实例(称为epoll句柄),然后向这个句柄添加或删除感兴趣的文件描述符。
    • epoll有两种工作模式:LT(水平触发,Level Triggered)和ET(边缘触发,Edge Triggered)。
    • 当有文件描述符就绪时,epoll_wait仅返回就绪的描述符,而不是全部,因此减少了无谓的遍历操作。
    • epoll使用事件驱动模型,当描述符状态变化时,内核会主动通知用户空间,避免了每次轮询时都要检查所有描述符的状态。
    • epoll可以以O(1)的时间复杂度管理大量的文件描述符,对于高并发场景非常高效。

总结来说,epoll相比select和poll的主要优势在于:

  • 更好的性能:只关注发生的事件,而不是所有待监控的描述符;
  • 更少的系统调用:支持批量添加/删除事件,以及一次性获取多个就绪描述符;
  • 更低的内存消耗:无需每次都复制整个描述符集合,尤其是对大连接数服务尤为有利。

6 计网七层协议、线程进程区别

  1. 七层网络协议模型: 七层网络协议模型,也称OSI(Open Systems Interconnection)模型,是理论上较为完整和全面的网络通信参考模型,将网络通信分为七个层次。每个层次完成特定的功能,并向其上层提供服务,同时依赖于下层提供的服务。这七个层次从低到高分别为:

    • 第一层:物理层(Physical Layer) 负责传输比特流,定义了设备之间数据传输的电气、机械和功能特性,如电缆规格、信号速率、编码方式等。

    • 第二层:数据链路层(Data Link Layer) 提供相邻节点间可靠的数据传输,负责错误检测与修正,帧同步以及寻址,例如以太网MAC地址和LLC子层。

    • 第三层:网络层(Network Layer) 负责路径选择和IP地址路由,实现不同网络之间的通信,比如IP协议。

    • 第四层:传输层(Transport Layer) 确保端到端的数据传输,提供可靠性保障,TCP协议和UDP协议就工作在这个层面上。

    • 第五层:会话层(Session Layer) 主要负责建立、管理和终止会话,但在实际应用中这一层在TCP/IP模型中通常被合并到了其他层。

    • 第六层:表示层(Presentation Layer) 处理数据格式化、加密解密、压缩解压等功能,确保应用程序能够正确理解数据。

    • 第七层:应用层(Application Layer) 直接为用户提供服务,支持各种应用程序,如HTTP、FTP、SMTP、DNS等协议。

  2. 线程与进程的区别

    • 进程(Process)是操作系统资源分配的基本单位,它拥有独立的虚拟内存空间、系统资源(如文件描述符)和一个完整的运行环境。每个进程执行一个独立的程序,包含至少一个线程,多个进程之间互不影响,各自运行自己的上下文。

    • 线程(Thread)是操作系统调度的基本单位,它是进程中的一条执行路径或控制流程。同一进程内的所有线程共享相同的进程上下文,包括堆、全局变量等内存区域,但每个线程有自己的栈空间。线程可以并发执行,使得在同一进程内部可以实现多任务并行处理。

    区别要点

    • 资源分配:进程间资源是独立且受保护的;而同一进程内的线程共享大部分资源。
    • 创建开销:进程创建时需要分配独立的资源,开销较大;线程创建成本较低,因为它复用了进程的许多资源。
    • 通信复杂度:进程间通信相对复杂,往往需要借助IPC机制(如管道、消息队列、共享内存等);而同一进程内的线程间通信简单高效,可以直接访问共享内存。
    • 上下文切换:进程间的上下文切换涉及更多的状态保存和恢复;线程间的上下文切换开销较小,但仍需维护线程特有的寄存器上下文。

7 zookeeper 实现分布式锁

Apache ZooKeeper是一个分布式的、开源的协调服务,它可以用来实现分布式锁。在ZooKeeper中,通过创建临时有序节点的方式来实现分布式锁,通常有两种主要方式:互斥锁(Mutex Lock) 和 读写锁(Read-Write Lock)

  1. 基于临时有序节点的互斥锁

    • 创建一个名为/lock的父节点。
    • 当客户端需要获取锁时,在/lock下创建一个临时有序节点,如/lock/0000000001,序号由ZooKeeper自动维护。
    • 客户端获取所有子节点,并找出序号最小且不是自己的节点作为前驱节点。
    • 如果当前客户端创建的节点序号是最小的,那么它就获得了锁;否则,它需要监听前驱节点的删除事件,当前驱节点被删除后,轮到自己获得锁。
  2. 释放锁

    • 当客户端完成操作后,只需删除自己创建的临时有序节点即可。由于是临时节点,所以如果客户端进程挂掉,ZooKeeper会自动清理该节点,从而保证锁能够被其他客户端获取。
  3. 读写锁

    • 实现读写锁时,可以在/lock下创建两个子节点,比如/lock/readLocks/lock/writeLocks
    • 读锁:多个客户端可以在/lock/readLocks下创建临时有序节点来同时持有读锁,原理与互斥锁类似,但不需要等待其他读锁持有者释放锁。
    • 写锁:只有一个客户端能在/lock/writeLocks下持有写锁,同时,任何写锁请求到来时,都会阻塞后续所有的读锁请求,以确保数据一致性。

这种基于ZooKeeper实现的分布式锁具有高可用性、强一致性等特点,适用于分布式系统中的资源同步场景。

8 什么情况下redis 的分布式锁被别人抢走

Redis 分布式锁在某些特定情况下可能被其他客户端抢走,这些情况包括但不限于以下几点:

  1. 锁过期

    • 当客户端获取锁后,并未在锁的有效期内完成任务或续期(如通过EXPIRE命令为锁设置的TTL过期),那么当锁过期时,其他客户端可以使用SETNX(设置不存在的键)或其他分布式锁算法再次获取到锁。
  2. 网络延迟

    • 在释放锁的过程中,如果持有锁的客户端与Redis服务器之间的网络出现延迟或故障,导致解锁操作没有成功执行,而锁已过期,则其他客户端有可能在此期间获取锁。
  3. 客户端异常

    • 如果持有锁的客户端因为某种原因崩溃或进程终止,而未正常释放锁,一旦锁过期,这个锁也会变为可被获取的状态。
  4. 锁释放逻辑不严谨

    • 如果客户端在释放锁时采用的是非原子性的操作,例如先删除锁再检查是否成功,中间环节可能存在并发问题,导致锁提前被其他客户端抢占。
  5. 竞争激烈

    • 在高并发场景下,尤其是在锁即将过期的瞬间,多个客户端可能同时尝试获取锁,尽管Redis提供了原子性操作,但在锁到期后的极短时间窗口内,可能会发生竞态条件。
  6. 误删

    • 错误的代码逻辑或者运维操作可能导致一个并未持有的锁被错误地删除,从而使得其他客户端能够获取到锁。

为了减少以上情况的发生,通常会采用一些优化措施来增强分布式锁的安全性和可靠性,例如:

  • 使用setnxexpire组合成原子操作(Lua脚本或者Redis 2.6.12及以后版本支持的set命令参数NXPX)。
  • 设置合理的锁超时时间,并结合锁自动续期机制以防止死锁。
  • 实现公平锁,确保锁被正确释放后才能被下一个等待者获取。

9 Redis 实现分布式公平锁通常依赖于其强大的数据结构和命令支持。Redisson 是一个基于 Redis 实现的在 Java 语言中的客户端库,它提供了丰富的数据结构和分布式服务,包括实现公平锁(Fair Lock)的功能。

Redisson 的公平锁原理大致如下:

  1. 基于有序集合(Sorted Set):Redisson 公平锁使用了多个 Redis 数据结构来协同工作,其中包括一个有序集合(ZSET),用于存储等待获取锁的线程及其等待时间戳,从而保证按照请求顺序分配锁。

  2. 队列(List):每个锁都有一个关联的队列,当锁被占用时,新的请求会被添加到这个队列中排队等待,即 threadsQueueName 键对应的列表结构。

  3. 超时机制:通过设置过期时间(TTL)确保锁能自动释放,并且在尝试获取锁时,可以指定最大等待时间,超过这个时间则放弃获取。

  4. 心跳机制:持有锁的客户端会定期发送心跳信号以更新锁的过期时间,防止因网络延迟或其他原因导致的锁意外释放。

  5. 公平性保证:当锁变为可用状态时,Redisson 不是简单的再次争夺锁,而是从有序集合中找到等待时间最长的线程并将其唤醒,使得等待时间最长的线程有更高的优先级获得锁,从而实现了公平锁的特性。

具体实现细节涉及复杂的逻辑,包括但不限于:

  • 使用 Lua 脚本保证原子操作。
  • 利用发布/订阅(Pub/Sub)模式通知等待线程锁的状态变化。
  • 内部维护线程状态以及重入计数等信息。

通过这些机制,Redisson 提供了一个高并发环境下的高效、可重入并且遵循公平原则的分布式锁。

10 线程怎么调度

线程调度是操作系统或者执行环境(如Java虚拟机)管理多个线程并发执行时,决定哪个线程应该获得CPU资源进行执行的过程。线程调度可以采用不同的策略和算法,下面简要介绍两种常见的线程调度模型:

  1. 分时调度(Time-Sharing Scheduling): 在分时调度模型中,系统将CPU时间划分为一系列的时间片(time slice或time quantum),所有线程按照某种顺序轮流使用一个时间片的CPU执行时间。当一个时间片用完后,操作系统会暂停当前正在运行的线程,并将其状态更改为就绪态,然后选择另一个处于就绪态的线程继续执行。这种调度方式下,所有线程理论上可以得到平等的执行机会。

  2. 抢占式调度(Preemptive Scheduling): 抢占式调度根据优先级或其他条件动态地为线程分配CPU使用权。即使某个线程正占用着CPU,但在更高优先级的线程进入就绪状态时,操作系统会立即中断当前运行的低优先级线程,并将CPU控制权交给高优先级线程。在优先级相同的线程之间,也可以通过时间片轮转的方式实现公平调度。

在Java环境中,线程调度由JVM和底层操作系统共同协作完成。Java默认使用的是非协同式的、基于优先级的抢占式调度。每个Java线程都有一个优先级,但实际调度仍取决于操作系统的线程调度机制。此外,Java提供了Thread.sleep()Thread.yield()等方法影响线程的执行,但并不能直接控制线程调度,而是提供了一种建议给调度器,例如sleep()可以让线程放弃其剩余时间片并进入等待状态。

线程调度的关键时机包括但不限于:

  • 当前线程的时间片结束。
  • 高优先级线程变为可运行状态。
  • 线程主动调用阻塞操作(如I/O请求)而进入阻塞态。
  • 发生了外部中断事件。
  • 线程完成了自己的任务,主动退出。

11 进程通信方法

进程通信(IPC,Inter-Process Communication)是指在操作系统中,不同进程之间交换信息或数据的方式。以下是几种常见的进程间通信方法:

  1. 管道(Pipe)

    • 无名管道(Anonymous Pipe):在同一台机器上的父子进程之间单向或双向传递数据,是内核提供的一种半双工通信方式。
    • 命名管道(FIFO,First-In-First-Out):也称命名管道或有名管道,可以在没有亲缘关系的进程之间进行通信,它在文件系统中作为一个特殊文件存在。
  2. 信号(Signal)

    • 信号是一种软件中断机制,用于通知接收进程发生了某种事件,但不能携带大量数据,主要用于简单控制和同步目的。
  3. 消息队列(Message Queue)

    • 消息队列允许进程间以异步的方式发送、接收数据块,每个数据块称为一个消息,可以带有优先级,并且能避免丢失信息。
  4. 共享内存(Shared Memory)

    • 共享内存是在同一台机器上多个进程可以直接访问的一段物理内存区域,通过映射到进程地址空间来实现高效的数据交换。
  5. 信号量(Semaphore)

    • 信号量是用来解决多进程对共享资源访问时的同步与互斥问题的机制,它可以是一个计数器,通过P(wait)和V(signal)操作来控制进程进入或离开临界区。
  6. 套接字(Socket)

    • 套接字主要应用于网络环境下的进程间通信,也可以用于同一主机的不同进程之间的通信。
  7. 内存映射文件(Memory-Mapped Files)

    • 内存映射文件允许进程将磁盘文件映射到自己的地址空间,这样就可以直接通过读写内存的方式来访问文件内容,从而实现进程间通信。
  8. 套接字pairs/UNIX域套接字(Socket Pairs / UNIX Domain Sockets)

    • 在Unix-like系统中,UNIX域套接字提供了一种在同一个主机上的进程间通信机制,类似于网络套接字,但是工作在文件系统领域而不是网络协议栈。

每种IPC机制都有其适用场景和优缺点,选择哪种方式取决于实际应用的需求,如数据量大小、实时性要求、同步需求以及是否跨网络等条件

12 go 线程 进程 协程

在Go语言中,进程、线程和协程是三个不同层次的并发执行概念:

  1. 进程(Process)

    • 进程是操作系统分配资源的基本单位,每个进程都有独立的内存空间,包括代码段、数据段、堆和栈等。一个进程中可以包含多个线程。
    • 在Go语言中,当你运行一个Go程序时,它就是一个进程。不同进程之间不能共享内存(除非使用特定的进程间通信机制如匿名映射或者管道),并且它们由操作系统调度。
  2. 线程(Thread)

    • 线程是操作系统能够进行调度和执行的基本单位,它是进程中实际执行指令的一条执行流,拥有独立的寄存器状态、栈以及在同一个进程内的共享资源访问权限。
    • Go运行时环境默认情况下会为你的程序启动一组工作线程(worker threads),这些线程由Goroutine调度器管理,并负责执行goroutine。
  3. 协程(Coroutine)/ Goroutine

    • 在Go语言中,协程被实现为Goroutine,这是一种轻量级的用户态线程,由Go运行时库而非操作系统内核直接调度。
    • 创建Goroutine的成本非常低,只需要很小的内存开销(比如几个KB)。通过go关键字即可启动一个新的Goroutine,它们可以在同一地址空间内并行地执行任务,但并不直接对应于操作系统级别的线程。
    • Goroutine之间的通信和同步主要依赖于通道(channels)和其他同步原语(如互斥锁mutexes),这使得Go语言能够实现高效的并发编程模型。
    • 默认情况下,Go运行时系统会根据需要动态调整工作线程的数量来更好地利用CPU资源,以达到高效利用多核处理器的目的。

总结来说,在Go语言中,进程是最外层的概念,一个Go程序通常是一个单独的进程;线程是由操作系统管理和调度的内核级并发单元,而Go程序中的并发执行主要通过用户态的Goroutine来实现,Go运行时基于这些Goroutine在操作系统提供的线程上执行任务。

13 tcp保证可靠性

TCP(Transmission Control Protocol)通过一系列机制来确保数据的可靠性传输,这些机制包括但不限于:

  1. 顺序控制

    • TCP为每个发送的数据段(Segment)分配一个唯一的序列号,接收方根据这个序列号来确认数据包的正确接收顺序,并且能够重新组装成原始的数据流。
  2. 确认应答(ACKnowledgement)机制

    • 发送端每发送一个数据段后,会等待接收端返回一个确认报文。接收端在接收到数据后,会发送一个包含确认序号(acknowledgment number)的TCP报文,该序号指示了期望接收的下一个字节的编号。
  3. 超时重传

    • 如果发送端没有在预设时间内收到对某个已发送数据段的确认,它将重新发送该数据段。这种机制避免了网络丢包造成的通信问题。
  4. 流量控制

    • 使用滑动窗口协议进行流量控制,允许接收端限制发送端的发送速率,防止接收方处理速度跟不上发送方导致的数据丢失或拥塞。
  5. 拥塞控制

    • 当检测到网络拥塞时,TCP可以通过慢启动、拥塞避免、快速重传和快速恢复等算法动态调整其发送速率以缓解网络拥塞。
  6. 校验和

    • 每个TCP数据段都包含一个校验和字段,用于检测数据在传输过程中是否出现错误,如果校验失败,则接收方会丢弃该数据段并要求发送方重传。
  7. 连接管理

    • TCP采用三次握手建立连接,四次挥手断开连接,这样可以确保双方都能同步地开启或关闭连接状态。
  8. 数据分块与重排序

    • TCP可以根据网络状况动态调整数据段大小,并在必要时重组乱序到达的数据段,保证数据按正确顺序交付给上层应用。

综上所述,TCP通过这些复杂的控制机制保障了在网络环境下的可靠数据传输,即使在网络存在各种不稳定因素的情况下,也能尽可能保证数据的完整性、有序性和可靠性。

14 go slice和array区别

在Go语言中,数组(array)和切片(slice)都是用于存储一系列元素的数据结构,但它们之间存在显著的区别:

  1. 长度与大小

    • 数组(array):具有固定长度,创建时需要指定其大小,并且在程序运行过程中无法改变。例如 [3]int 表示一个包含3个整数的数组。
    • 切片(slice):动态长度,虽然底层依赖于固定长度的数组,但其本身可以随着数据的增加或减少而调整长度。创建时可以不指定具体长度,或者通过 make() 函数初始化一个具有初始长度和容量的切片。
  2. 声明与初始化

    • 数组声明时必须提供所有元素的值或进行初始化。
     Go 
    1var arr [5]int = [5]int{1, 2, 3, 4, 5}
    • 切片可以在任何时候创建和修改,可以指定一个范围来从数组或另一个切片创建切片。
     Go 
    1data := []int{1, 2, 3, 4, 5} // 直接创建并初始化
    2slice := data[1:3]             // 创建data的一个子序列切片
  3. 内存分配与拷贝

    • 数组是值类型,当数组作为函数参数传递时,会复制整个数组内容到新栈空间。
    • 切片则是引用类型,传递的是指向底层数组的指针以及长度和容量信息,不会复制所有元素,因此更高效。
  4. 扩容

    • 数组一旦创建就无法扩展或收缩。
    • 切片可以通过 append() 函数自动增长,如果底层数组容量不足,Go会自动分配新的数组并复制原有数据。
  5. 索引访问

    • 数组和切片都可以通过索引访问和修改元素,但超出切片当前长度的操作会导致运行时错误。

总结来说,数组适用于已知固定数量元素且不需要频繁更改大小的情况,而切片则提供了更加灵活和方便的集合操作,适用于大部分需要动态增删元素的场景。

15 GMP模型

GMP模型是Go语言(Golang)中实现并发和并行编程的核心机制之一,全称为 Goroutine-Processor-Monitor(或解释为 Goroutine-Manager-Processors)。这个模型描述了Go语言中如何高效地管理和调度协程(Goroutines)在多个内核线程(Threads)上运行的架构。

以下是GMP模型的关键组成部分及其功能:

  1. G (Goroutine)

    • Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时创建和管理。它是一种用户态的线程,创建和销毁的成本非常低。
    • 开发者可以方便地通过 go 关键字启动一个新的 Goroutine,以实现并发执行。
  2. P (Processor/Worker Thread)

    • P 在 GMP 模型中代表一个工作线程或处理器,每个 P 都关联着一个上下文以及一个本地的任务队列(即存放待处理的 Goroutine 的队列)。
    • P 数量可配置,并且与操作系统的线程数量相关联,但通常少于实际的系统线程数。
    • P 负责从全局队列或者其本地队列中取出 Goroutine 并执行,同时也负责在必要时与 M 进行协作。
  3. M (Machine Thread)

    • M 表示操作系统级别的线程,每一个 M 都会绑定到一个 P 上。
    • M 通过与 P 协作来执行 Goroutine,当 M 执行完当前的 Goroutine 后,它会从关联的 P 的本地队列获取下一个 Goroutine 执行,如果本地队列为空,则尝试从全局队列获取,或者阻塞等待新的任务到来。
    • Go 调度器会根据实际情况动态调整 M 的数量,确保有足够的资源去运行所有的 Goroutine。
  4. 调度器

    • Go 的调度器基于上述组件实现了高效的多线程并发,通过控制 G、P 和 M 的关系,保证了 Goroutine 的高效切换和资源利用。
    • 当 Goroutine 遇到 I/O 操作或者其他阻塞情况时,调度器能够将该 Goroutine 移出执行线程,从而释放资源给其他 Goroutine 使用,这有助于避免过多线程被阻塞而浪费系统资源。

综上所述,GMP 模型使得 Go 程序能够在有限数量的操作系统线程基础上支持成千上万个并发执行的 Goroutine,并保持良好的性能和资源利用率。

GMP模型-优快云博客

16 分布式缓存框架,singleflight并发,深挖,一直挖到sync.WaitGroup

分布式缓存框架在处理高并发请求时,常常需要解决多个请求同时访问相同资源导致的重复计算和网络IO问题。Go语言标准库中的sync/singleflight包提供了一种机制来解决这个问题,它能够保证任意时刻对于同一条key的请求只执行一次(即所谓的"单飞"),并且其他等待这个key的请求可以共享其结果。

singleflight的工作原理:

  1. 当有多个goroutine并发发起对同一个键值的请求时,它们会先尝试加入一个与该键关联的group,这个group通过singleflight.Group类型表示。
  2. 第一个加入group的goroutine(即第一次请求)会执行实际的操作,如从分布式缓存中获取数据或执行耗时计算。
  3. 其他后续请求在加入group后并不会立即执行操作,而是进入等待状态。
  4. 当第一个请求完成并返回结果后,它会通知所有等待在这个group中的goroutine,这些goroutine会得到相同的结果而无需再次执行操作。
  5. 这个过程中就巧妙地使用了sync.WaitGroup来同步等待和唤醒goroutine。

具体实现上,singleflight内部维护了一个映射表和一个WaitGroup,映射表用于存储每个键对应的请求组,WaitGroup则用来确保所有等待同一请求结果的goroutine都能正确收到通知。

例如:

 

Go

1import (
2	"fmt"
3	"sync"
4	"golang.org/x/sync/singleflight"
5)
6
7func main() {
8	sf := &singleflight.Group{}
9	key := "someKey"
10
11	// 并发调用Do方法
12	for i := 0; i < 10; i++ {
13		go func() {
14			resp, _, _ := sf.Do(key, func() (interface{}, error) {
15				// 模拟耗时操作,比如从分布式缓存或数据库获取数据
16				time.Sleep(1 * time.Second)
17				return fmt.Sprintf("Value for key: %s", key), nil
18			})
19			fmt.Println(resp.(string))
20		}()
21	}
22
23	// 等待所有goroutine结束
24	time.Sleep(2 * time.Second)
25}

在这个例子中,尽管我们并发启动了10个goroutine去获取同一个key的值,但实际只会执行一次耗时操作,并且所有goroutine都会接收到相同的响应结果。这就是singleflight在分布式缓存框架中提高性能和减少冗余请求的关键作用。

17 缓存击穿和缓存雪崩是分布式缓存系统中常见的两种失效场景,它们都可能导致数据库压力增大,影响系统性能。

  1. 缓存击穿

    • 缓存击穿是指某个热点key在高并发场景下失效时,大量的请求直接穿透缓存到达数据库,对数据库造成巨大压力。
    • 例如:某个商品的库存信息被频繁查询,正常情况下这个数据会被缓存起来,但在库存更新导致缓存失效的那一瞬间,如果有大量并发请求过来,则所有请求都会去数据库查询,从而引发缓存击穿问题。
    • 解决方案通常包括设置热点数据永不过期、加互斥锁(如Redis的SETNXLock)来保证只有一个请求穿透到数据库并重新填充缓存,或者采用“提前刷新”的策略,在缓存即将过期前预先更新缓存。
  2. 缓存雪崩

    • 缓存雪崩指的是大量缓存在同一时刻几乎全部失效,导致所有的请求都落到了数据库上,数据库短时间内承受极大的压力,可能会因此崩溃,进而导致整个服务不可用。
    • 出现这种情况的原因可能有:缓存服务器整体宕机、缓存集中到期、程序bug导致大量缓存被清除等。
    • 针对缓存雪崩,可以采取以下策略:
      • 分布式缓存集群以提高可用性,即使部分节点失效也不会导致全局雪崩。
      • 设置合理的缓存过期时间,避免缓存集体失效的情况。
      • 为防止大量请求打到数据库,可设置熔断机制(如Hystrix),在后端压力过大时暂时拒绝服务,同时配合降级策略返回默认值或者错误提示给用户。
      • 使用二级缓存,比如本地缓存+远程缓存,当远程缓存失效时,还可以从本地缓存获取,以减轻数据库压力,并尽快恢复远程缓存。

总之,为了应对缓存击穿和缓存雪崩等问题,需要设计一套完善的缓存失效处理机制以及容错机制,确保系统的稳定性和高可用性。

18 sync.WaitGroup

sync.WaitGroup 是 Go 语言标准库 sync 包中提供的一个并发同步原语,用于在多线程(goroutine)环境下等待一组事件的完成。WaitGroup 可以协调多个 goroutine 的执行顺序,确保在所有 goroutine 完成其任务后,主线程或其他 goroutine 才能继续执行后续操作。

使用 sync.WaitGroup 主要涉及三个方法:

  1. Add(delta int)

    • 用于初始化或增加 WaitGroup 中的任务计数器。当 delta > 0 时,增加待完成的任务数量;反之则减少。
    • 在创建新的 goroutine 并开始执行一个任务前调用 Add(1),表示有一个任务即将开始。
  2. Done()

    • 当一个任务完成后,调用此方法表示该任务已经结束,它会将 WaitGroup 中的任务计数器减一。
    • 每个通过 Add(1) 添加的任务都应该在其完成时对应调用一次 Done()。
  3. Wait()

    • 在需要等待所有任务都完成的地方调用此方法,它会使当前的 goroutine 阻塞,直到 WaitGroup 中的任务计数器变为零。
    • 通常在主 goroutine 或需要等待所有子任务结束后才进行下一步操作的地方调用 Wait()。

示例代码:

 

Go

1package main
2
3import (
4	"fmt"
5	"sync"
6)
7
8func worker(wg *sync.WaitGroup, id int) {
9	defer wg.Done()
10	fmt.Printf("Worker %d started\n", id)
11	// 假设此处是耗时的操作
12	time.Sleep(time.Second)
13	fmt.Printf("Worker %d finished\n", id)
14}
15
16func main() {
17	var wg sync.WaitGroup
18
19	for i := 1; i <= 3; i++ {
20		wg.Add(1) // 每启动一个 worker,就添加一个待完成的任务
21		go worker(&wg, i)
22	}
23
24	wg.Wait() // 等待所有 worker 完成
25	fmt.Println("All workers have finished")
26}

在这个例子中,main 函数创建了 3 个并发运行的 worker,并且每个 worker 在开始和结束时分别调用了 wg.Add(1) 和 wg.Done()main 函数中的 wg.Wait() 将阻塞,直到所有 worker 都完成了它们的工作

19 cookie session

Cookie和Session是Web应用程序中用于保持用户状态的两种常见机制:

  1. Cookie

    • Cookie是由服务器发送到客户端(通常是浏览器)的一小段文本信息,客户端会在后续请求中自动将其回送给服务器。
    • Cookie可以存储在客户端本地,包含诸如用户ID、自定义设置或其他小量数据等信息。
    • 安全性:由于Cookie数据存储在客户端,对于敏感信息存在泄露风险,因此一般不建议直接在Cookie中保存如密码等敏感信息。可以通过加密或使用HttpOnly、Secure等属性来提高安全性。
  2. Session

    • Session是一种服务器端的机制,用于跟踪用户的会话状态。当用户访问网站时,服务器为每个用户创建一个唯一的Session ID,并将其发送给客户端(通常通过Cookie方式实现),客户端在后续请求中携带这个Session ID返回给服务器。
    • 服务器端维护着与Session ID关联的用户会话数据,这些数据可以包括登录状态、购物车内容等任何需要持久化在会话期间的信息。
    • 安全性:相比Cookie,Session将重要数据保存在服务器端,只有无害的Session ID在客户端和服务器之间传输,降低了敏感数据泄露的风险。

结合使用Cookie(作为Session ID载体)和Session可以在保证用户体验的同时增强安全性。例如,在Express.js框架中,cookie-session中间件就是用来处理这种场景的,它将session数据以加密的形式存储在客户端的cookie中,从而简化了session管理并减小了服务器端存储压力。

20 设计学生成绩数据库,并写出查询语文成绩top3的人。

设计一个学生成绩数据库时,通常会包含学生信息表、课程信息表以及成绩表。以下是一个简化的数据库设计:

  1. 学生信息表(students)

    • student_id (主键, 自增)
    • name
    • class_id
    • other_info
  2. 课程信息表(courses)

    • course_id (主键)
    • course_name (如:语文)
    • teacher_id
    • other_info
  3. 成绩表(scores)

    • score_id (主键, 自增)
    • student_id (外键,关联students表的student_id)
    • course_id (外键,关联courses表的course_id)
    • score
    • exam_date

假设我们已经有了这样的数据结构,并且使用了MySQL数据库,查询语文成绩前三名的学生可以使用如下SQL语句:

 

Sql

1SELECT s.student_id, s.name, sc.score
2FROM students AS s
3JOIN scores AS sc ON s.student_id = sc.student_id
4JOIN courses AS c ON sc.course_id = c.course_id
5WHERE c.course_name = '语文'
6ORDER BY sc.score DESC
7LIMIT 3;

这个查询首先通过JOIN操作将三个表连接起来,确保只选择语文这门课的成绩记录。然后按照分数从高到低排序,并使用LIMIT 3来限制结果集只显示前三名学生的ID、姓名及其对应的语文成绩。

21 url输入全过程,从此引出后端除了响应请求还有什么。

当用户在浏览器中输入URL并按下回车键后,发生了一系列复杂的网络和服务器交互过程,这个过程大致包括以下几个步骤:

  1. 域名解析

    • 浏览器首先检查本地DNS缓存是否有目标网址对应的IP地址。
    • 若无,则查询操作系统中的DNS缓存。
    • 再次未命中则读取系统hosts文件查找。
    • 最后,如果还是没有找到,会向互联网上的DNS服务器发送请求解析域名。
  2. 建立TCP连接

    • 解析出IP地址后,浏览器发起与服务器的TCP连接,通常使用HTTP协议默认端口80(或HTTPS时的443)进行三次握手建立连接。
  3. 发送HTTP请求

    • 连接建立后,浏览器构造一个HTTP请求报文,其中包含了方法(如GET、POST等)、URL路径、请求头(如User-Agent、Accept、Cookie等)以及可能的请求体数据,并将此报文通过TCP连接发送给服务器。
  4. 服务器处理请求

    • 服务器接收到请求后,由Web服务器软件(如Apache、Nginx等)解析请求,根据请求的内容调度相应的资源或应用服务程序(如PHP、Python、Java等)来处理请求。
  5. 后端处理除了响应请求外的任务

    • 认证与授权:验证用户的登录状态或执行权限控制。
    • 业务逻辑处理:根据请求执行特定的业务操作,例如查询数据库、调用API接口、执行计算任务、生成动态内容等。
    • 数据持久化:与数据库进行交互,如读取、写入或更新记录。
    • 会话管理:维护用户的会话状态,如设置和更新session信息。
    • 缓存处理:从缓存系统获取数据或者将结果缓存以提高性能。
    • 日志记录:记录访问日志、错误日志和其他必要的审计信息。
    • 异步任务处理:对于耗时较长的操作,可能会安排异步任务队列,以便后台进程异步处理并通知客户端结果。
  6. 构建HTTP响应

    • 后端完成所有必要处理后,构建HTTP响应报文,其中包括状态码、响应头和响应体数据(如HTML、JSON等)。
  7. 服务器发送响应

    • 将构建好的HTTP响应通过TCP连接发送回浏览器。
  8. 页面渲染

    • 浏览器接收响应数据并开始解析,如果是HTML则构建DOM树,下载CSS和JavaScript资源,执行脚本代码,布局和绘制页面,最终呈现给用户。
  9. 关闭连接

    • 在完成响应处理后,TCP连接通常会按照四次挥手的过程关闭,不过现代HTTP/2及HTTP/3协议可能采用持久连接或多路复用机制,使得多个请求可以在同一连接上并发处理。

总之,在后端除了响应请求这一核心功能之外,还涉及到了大量的系统管理和业务逻辑处理工作

22 什么是分布式系统

分布式系统是一种由多个独立的、通过网络进行通信和协调的组件组成的计算机系统。在这样的系统中,各个节点(或称为子系统)相互协作以完成一个共同的目标,而这些节点可以是物理上分布于不同位置的计算机或服务器。每个节点都具有处理能力,并且能够存储数据或执行服务。

在分布式系统中,几个关键特性包括:

  1. 分散性:系统的组件分布在不同的地理位置,通过网络互联。
  2. 并发性:系统中的多个组件可以并行地执行任务。
  3. 透明性:用户和服务调用者通常无需了解数据和服务的实际物理位置,系统设计力求让分布式环境对用户来说看起来像是一个整体。
  4. 冗余与容错性:通过副本(Replica)机制实现数据和服务的冗余备份,以提高系统的可用性和容错性。
  5. 一致性与协同:分布式系统必须管理各节点之间的状态一致性,确保在并发操作下的数据正确性,这可能涉及到复杂的同步和协调算法。
  6. 可扩展性:分布式系统可以通过增加新的节点来水平扩展性能,从而处理更大的负载和存储需求。

总之,分布式系统旨在利用多台计算机的优势,通过高效的通信协议和算法来提供高效能、高可靠的服务,同时隐藏了内部复杂的交互过程,为用户提供统一的操作视图。

引出负载均衡

在分布式系统中,负载均衡是一个至关重要的组成部分。当系统中的服务或资源分布在多个节点上时,为了有效地利用这些资源,并确保所有请求都能得到及时且高质量的响应,就需要引入负载均衡机制。

负载均衡(Load Balancing)的主要目标是将客户端发来的请求均匀地分配到各个服务器节点上,避免单个节点过载而导致响应延迟甚至服务中断。这样做的好处包括:

  1. 提高可用性:通过将请求分散到不同的服务器,即使某个服务器出现故障,其他服务器仍然可以继续处理请求,从而提高了系统的整体可用性。
  2. 提升性能:负载均衡允许系统根据每个服务器的实际处理能力动态调整分发策略,优化资源使用率,从而提高整个系统的处理速度和吞吐量。
  3. 易于扩展:当需要处理更多请求或者提供更强大的计算能力时,只需向分布式系统中添加新的服务器节点,并将其纳入负载均衡策略,即可实现水平扩展。

负载均衡器通常有以下几种实现方式:

  • 硬件负载均衡器:如F5、A10 Networks等提供的专用设备,具备高性能和高可靠性的特点。
  • 软件负载均衡器:例如Nginx、HAProxy、Envoy等开源软件,它们可以在通用服务器上部署并作为负载均衡器使用。
  • 云服务负载均衡器:如AWS的ELB、Azure Load Balancer、Google Cloud Load Balancing等云服务商提供的负载均衡服务。

负载均衡算法也有很多种,常见的包括:

  • 轮询(Round Robin)
  • 最少连接数(Least Connections)
  • 源IP哈希(Source IP Hash)
  • 会话保持(Session Persistence)
  • 加权轮询/加权最少连接数(Weighted Round Robin/Weighted Least Connections)

通过合理的负载均衡策略,分布式系统能够在复杂多变的网络环境中保持高效稳定的运行状态。

会话保持(Session Persistence 或 Session Affinity)是指在负载均衡器将客户端的请求分配到后端服务器时,确保来自同一个客户端的后续请求会被转发到同一台服务器上处理,以便维持该客户端与服务器之间的状态信息。在分布式系统中,会话保持对于需要维护用户会话状态的应用程序(如Web应用、流媒体服务等)尤其重要。

实现会话保持的方式有多种,以下是几种常见的方法:

  1. 基于Cookie的会话保持

    • 负载均衡器可以设置或修改客户端的cookie,其中包含指向特定后端服务器的标识符。
    • 当客户端发起新的请求时,浏览器会自动携带这个cookie,负载均衡器根据cookie中的值确定应该将请求转发到哪台服务器。
  2. 源IP地址哈希(Source IP Hashing)

    • 根据客户端的IP地址计算一个哈希值,然后用这个哈希值来决定请求应该被路由到哪个后端服务器。
    • 这种方式假设同一个客户端的请求总是来自同一个IP地址,因此能保证请求分发的一致性。
  3. HTTP头插入

    • 负载均衡器可以在首次转发请求给后端服务器时,在HTTP头中添加一个自定义字段,用于标识后端服务器的信息。
    • 后续请求到达时,负载均衡器通过检查这个自定义头字段将请求再次转发到之前处理过该会话的服务器。
  4. SSL Session ID

    • 对于HTTPS连接,可以利用SSL/TLS协议的Session ID特性进行会话保持。
    • SSL Session ID由服务器生成并在握手过程中返回给客户端,之后客户端会在新的连接中带上此ID,服务器可以根据ID找到对应的会话状态。
  5. 应用程序级会话管理

    • 在某些情况下,负载均衡器可能不直接支持会话保持功能,这时需要在应用程序层面实现。
    • 例如,使用共享存储(如数据库、Redis等)保存会话信息,无论请求被分配到哪个服务器,都可以从共享存储中获取和更新会话状态。

以上就是实现会话保持的一些常见方法,实际应用中选择哪种方式取决于具体的技术环境和业务需求。

23 一致性哈希算法深挖。一致性哈希与普通哈希的区别。

一致性哈希算法与普通哈希算法在设计目标和应用场景上有显著的区别。虽然它们都基于散列函数,但一致性哈希主要用于解决分布式系统中的动态负载均衡和数据分布问题,而普通哈希函数通常应用于查找表、缓存键值对映射等静态或单点环境。

普通哈希(常规哈希)

  • 设计目的:将任意大小的数据通过散列函数映射到一个固定大小的地址空间中,用于快速查找、索引、去重等。
  • 特点:
    • 哈希冲突是常见现象,一般通过开放寻址法、链地址法等方法来处理冲突。
    • 当存储空间或节点数量固定时,良好的哈希函数能够实现均匀分布。
    • 对于添加或删除节点后的影响,所有数据可能需要重新哈希并重新分配。

一致性哈希(Consistent Hashing)

  • 设计目的:在分布式环境中,当服务节点增加或减少时,尽可能少地改变已分配给各个节点的资源(如缓存项、数据块、请求流量)的分布情况,从而最小化网络流量和系统状态的变化。
  • 特点:
    • 将数据项和服务器节点均采用相同的哈希函数映射到一个环状虚拟空间上,通常是0到2^32-1这样的整数环。
    • 每个节点只负责环上的一个连续区域,而不是整个哈希空间。
    • 当新增或移除节点时,只影响该节点“左右”相邻节点上的数据分配,其他大部分数据在新的映射关系下仍然保持不变,这大大减少了数据迁移的工作量。
    • 可以通过虚拟节点技术进一步优化数据分布,每个物理节点关联多个虚拟节点,使得分布更加均匀。

总结来说,一致性哈希的核心优势在于它能较好地适应分布式系统的动态变化,有效解决了传统哈希算法在节点规模变动时导致大量数据迁移的问题,提高了系统的可扩展性和稳定性。

24 手撕代码LRU go

在Go语言中,我们可以使用内置的map和双向链表来实现LRU缓存。下面是一个基于Go语言的数据结构及GetPut方法的实现:

 

Go

1package main
2
3import (
4	"container/list"
5)
6
7// LRUCache 结构体定义
8type LRUCache struct {
9	capacity int
10	cache    map[int]*list.Element
11	list     *list.List
12}
13
14// Node 结构体用于存储键值对以及在链表中的位置
15type Node struct {
16	key   int
17	value int
18}
19
20// NewLRUCache 创建一个新的LRUCache实例
21func NewLRUCache(capacity int) *LRUCache {
22	return &LRUCache{
23		capacity: capacity,
24		cache:    make(map[int]*list.Element),
25		list:     list.New(),
26	}
27}
28
29// Get 方法获取key对应的value,返回-1表示key不存在
30func (lru *LRUCache) Get(key int) int {
31	if ele, ok := lru.cache[key]; ok {
32		lru.list.MoveToFront(ele)
33		return ele.Value.(*Node).value
34	}
35	return -1
36}
37
38// Put 方法添加或更新key-value对
39func (lru *LRUCache) Put(key int, value int) {
40	if ele, ok := lru.cache[key]; ok {
41		ele.Value.(*Node).value = value
42		lru.list.MoveToFront(ele)
43	} else {
44		newNode := &Node{key: key, value: value}
45		if lru.list.Len() >= lru.capacity {
46			backEle := lru.list.Back()
47			delete(lru.cache, backEle.Value.(*Node).key)
48			lru.list.Remove(backEle)
49		}
50		newEle := lru.list.PushFront(newNode)
51		lru.cache[key] = newEle
52	}
53}
54
55func main() {
56	lru := NewLRUCache(2)
57	lru.Put(1, 1)
58	lru.Put(2, 2)
59	fmt.Println(lru.Get(1)) // 输出:1
60	lru.Put(3, 3)
61	fmt.Println(lru.Get(2)) // 输出:-1(因为容量为2,添加新的key时淘汰了最近最少使用的key)
62	lru.Put(4, 4)
63	fmt.Println(lru.Get(1)) // 输出:-1(同理,再次添加新key时淘汰了key为1的项)
64	fmt.Println(lru.Get(3)) // 输出:3
65	fmt.Println(lru.Get(4)) // 输出:4
66}

这个实现中,我们用Go标准库container/list实现了双向链表,并结合map来维护元素的顺序和快速查找。当需要从缓存中获取一个元素时,如果该元素存在,则将其移到链表头部;如果要添加新的元素且缓存已满,就将链表尾部的元素移除并删除其在map中的映射,然后将新元素添加到链表头部并更新map。

手撕代码生产者消费者模型 go

在Go语言中,我们可以利用channel来实现生产者消费者模型。下面是一个简单的生产者消费者模型的代码实现:

 

Go

1package main
2
3import (
4	"fmt"
5	"sync"
6)
7
8// 生产者
9func producer(ch chan int, wg *sync.WaitGroup) {
10	defer wg.Done()
11	for i := 0; i < 10; i++ {
12		fmt.Printf("Producer produced: %d\n", i)
13		ch <- i // 将产品放入通道
14	}
15	close(ch) // 生产完毕,关闭通道
16}
17
18// 消费者
19func consumer(ch <-chan int, wg *sync.WaitGroup) {
20	defer wg.Done()
21	for v := range ch { // 遍历通道直到关闭
22		fmt.Printf("Consumer consumed: %d\n", v)
23		// 这里可以对消费的产品进行进一步处理
24	}
25}
26
27func main() {
28	ch := make(chan int, 3) // 创建一个容量为3的通道(非阻塞)
29
30	var wg sync.WaitGroup
31	wg.Add(2) // 添加两个等待组成员:一个生产者和一个消费者
32
33	go producer(ch, &wg) // 启动生产者
34	go consumer(ch, &wg) // 启动消费者
35
36	wg.Wait() // 等待所有goroutine完成任务
37}
38

在这个示例中:

  • producer函数作为生产者,将0到9的整数通过通道ch发送出去。
  • consumer函数作为消费者,从通道ch接收并打印这些整数。
  • 使用了sync.WaitGroup来确保主goroutine等待所有的生产者和消费者完成任务后再结束。

注意,这里创建的通道具有容量3,这意味着它可以暂存最多3个未被消费者消费的数据项,超过这个数量时,生产者会阻塞直到有消费者消费数据腾出空间。如果希望生产者永远不阻塞,可以将通道容量设置为0或省略容量参数(即创建无缓冲通道)。

25 手撕代码反转连表 go

在Go语言中,反转一个链表可以通过迭代或递归的方式来实现。这里提供一个基于迭代的链表反转代码:

 

Go

1package main
2
3import "fmt"
4
5// 定义链表节点结构体
6type ListNode struct {
7	Val  int
8	Next *ListNode
9}
10
11// 反转链表函数
12func reverseList(head *ListNode) *ListNode {
13	var prev *ListNode = nil // 初始化前驱节点为nil
14	curr := head              // 当前节点初始化为头节点
15
16	for curr != nil { // 遍历整个链表
17		nextTemp := curr.Next // 保存当前节点的下一个节点(临时变量)
18		curr.Next = prev      // 将当前节点的next指向前驱节点
19		prev = curr          // 更新前驱节点为当前节点
20		curr = nextTemp       // 更新当前节点为之前保存的下一个节点
21	}
22
23	return prev // 返回新的头节点(原链表的尾节点)
24}
25
26// 创建链表示例
27func createLinkedList(nums []int) *ListNode {
28	if len(nums) == 0 {
29		return nil
30	}
31	head := &ListNode{Val: nums[0]}
32	curr := head
33	for i := 1; i < len(nums); i++ {
34		curr.Next = &ListNode{Val: nums[i]}
35		curr = curr.Next
36	}
37	return head
38}
39
40func printLinkedList(head *ListNode) {
41	for head != nil {
42		fmt.Printf("%d -> ", head.Val)
43		head = head.Next
44	}
45	fmt.Println("nil")
46}
47
48func main() {
49	nums := []int{1, 2, 3, 4, 5}
50	head := createLinkedList(nums)
51	fmt.Println("Original linked list:")
52	printLinkedList(head)
53
54	reversedHead := reverseList(head)
55	fmt.Println("Reversed linked list:")
56	printLinkedList(reversedHead)
57}

这段代码首先定义了一个ListNode结构体表示链表节点,然后提供了reverseList函数用于反转链表,通过一个辅助指针prev来记录当前节点的前一个节点,不断更新这三个指针的位置,直到遍历完整个链表。最后,我们创建了一个简单的测试用例来展示反转前后的链表情况。

26 session如何存储

在Web开发中,session(会话)数据的存储方式取决于具体的实现和技术栈。以下是几种常见的session存储机制:

  1. 内存存储

    • 最简单的实现是在服务器端内存中使用哈希表等数据结构存储session信息。如PHP中的$_SESSION、Node.js中的express-session中间件等,默认情况下可能会采用这种方式。
    • 优点:速度快,实时更新。
    • 缺点:当服务器重启或负载均衡场景下,session数据无法共享,可能导致用户需要重新登录;且随着在线用户数量增加,内存消耗会增大。
  2. 文件系统存储

    • 将session数据序列化后保存到文件系统中,每个session对应一个文件。
    • 优点:相对持久化,服务器重启后session数据仍可恢复。
    • 缺点:大量session时文件系统IO压力较大,不适合高并发场景。
  3. 数据库存储

    • 可以将session数据存储在关系型数据库(如MySQL、PostgreSQL)或NoSQL数据库(如MongoDB)中。
    • 优点:易于管理和扩展,可以在多台服务器间共享session数据,支持高并发和分布式部署。
    • 缺点:每次读写session都需要进行数据库操作,性能相比内存稍低。
  4. 缓存存储

    • 利用Redis、Memcached等内存数据库作为session存储介质,因其高效的数据存取速度而广泛应用于session存储。
    • 优点:既能保持内存存储的速度优势,又能通过集群支持session数据在多服务器间的共享。
    • 缺点:需要额外维护缓存服务的稳定性与可用性。
  5. 基于Token的无状态方案

    • 不直接依赖于session存储,而是将必要的用户身份验证信息编码到token(比如JWT)中,客户端负责携带该token并在后续请求中发送给服务器验证。
    • 优点:无需服务器端存储session,利于水平扩展,适合微服务架构。
    • 缺点:所有有效信息都包含在token中,如果token过期策略不当或者被窃取,可能带来安全风险。

选择哪种方式存储session数据,通常要根据应用的需求、规模、安全性以及运维成本等因素综合考虑。

27 多台服务器session存储怎么设计。除了存redis还能怎么存

在多台服务器的环境下,为了实现session共享,除了将session数据存储在Redis这样的分布式缓存系统中之外,还有其他几种常见的解决方案:

  1. 数据库存储

    • 将session信息存放在关系型数据库(如MySQL、PostgreSQL)或NoSQL数据库(如MongoDB、Cassandra)中。每条session记录对应一个数据库记录,通过用户会话ID查询和更新。
    • 优点:易于管理和扩展,支持跨多个服务器共享session数据。
    • 缺点:相比内存和缓存数据库,读写速度较慢,高并发场景下可能成为性能瓶颈。
  2. Memcached

    • Memcached与Redis类似,也是内存键值存储系统,但不支持持久化和复杂的数据结构。可作为分布式session存储的备选方案。
    • 优点:速度快,适合于大规模并发访问时的session存储。
    • 缺点:不保证数据一致性,不适合需要强一致性的应用;没有持久化功能,服务器重启后session数据丢失。
  3. 共享文件系统(NFS)

    • 使用网络文件系统(Network File System, NFS)共享会话存储目录,让所有服务器都能访问同一份session文件。
    • 优点:简单易用,对已有环境改动较小。
    • 缺点:文件系统的读写速度相对于内存/缓存系统较慢,且在高并发场景下可能会遇到性能瓶颈;另外,NFS自身的稳定性和可用性也会影响session共享的效果。
  4. 基于Token的身份验证机制

    • 不依赖session存储,采用JWT(JSON Web Tokens)等token认证方式,将必要的用户身份和权限信息编码到token中,并由客户端携带。每次请求都附带token,服务器端验证token的有效性和内容。
    • 优点:无状态设计,易于水平扩展,避免了session同步问题。
    • 缺点:所有的授权信息必须包含在token中,可能导致token过长;此外,一旦token被窃取,除非设置了有效的过期策略和刷新机制,否则安全性风险较高。
  5. 自建Session服务

    • 构建一个专门用来存储和管理session的服务,服务器之间通过API接口通信获取和更新session信息。
    • 优点:可以灵活定制和控制session的生命周期及存储策略。

28 http和https区别

HTTP(HyperText Transfer Protocol,超文本传输协议)和HTTPS(Hypertext Transfer Protocol Secure,安全超文本传输协议)是用于在Web上传输数据的主要协议。它们之间主要区别在于安全性:

  1. 加密

    • HTTP:HTTP通信是明文的,即在网络上传输的数据可以被任何监听者捕获并阅读。
    • HTTPS:HTTPS使用了SSL/TLS(Secure Sockets Layer/Transport Layer Security)协议对通信内容进行加密,确保数据在网络中传输时的安全性,防止数据被窃取或篡改。
  2. 认证

    • HTTP:客户端无法验证服务器的身份,存在冒充风险。
    • HTTPS:通过SSL证书,客户端可以确认服务器的身份,防止中间人攻击和伪装。
  3. 信任

    • HTTP:用户无法确定他们正在访问的网站是否真实可靠。
    • HTTPS:浏览器会检查由权威机构签发的SSL证书,并显示锁图标来表明连接是安全的,从而增加用户的信任度。
  4. SEO影响

    • HTTPS:搜索引擎如Google更倾向于将HTTPS站点排在HTTP站点之前,因为它认为HTTPS能提供更好的用户体验和更高的安全性。
  5. 性能

    • HTTP:相比HTTPS,由于没有加密环节,理论上HTTP的处理速度更快一些。
    • HTTPS:虽然有加密解密过程,增加了计算负担,但在现代硬件加速的支持下,这一差异已经大大减少。而且,HTTP/2及后续版本对于HTTPS进行了优化,例如多路复用技术使得HTTPS性能损失进一步减小。
  6. 默认端口

    • HTTP:默认使用80端口。
    • HTTPS:默认使用443端口。

综上所述,HTTPS是对HTTP协议的安全扩展,它提供了身份验证、数据加密以及数据完整性校验等功能,更适合于涉及敏感信息传输的场景,如在线购物、登录注册等。随着网络安全意识的提高和浏览器强制策略的实施,越来越多的网站开始采用HTTPS作为标准通信方式。

29 getpost区别。还有什么其他方法。分别说说是做什么的。

GET和POST是HTTP协议中最常用的两种请求方法,它们的主要区别在于用途、可见性以及如何处理数据:

  1. GET 请求

    • 目的:用于获取资源,即从服务器检索信息。它是幂等(idempotent)的,意味着多次执行相同请求不会对服务器状态产生不同的影响。
    • 数据传输:参数附在URL之后,作为查询字符串,可以被浏览器历史记录、缓存并且容易被用户看到。
    • 数据限制:理论上没有明确的标准限制其长度,但实际上,大多数浏览器和服务器会对URL长度有所限制(通常几KB到几十KB不等)。
    • 安全性:由于数据直接暴露在URL中,不适合传输敏感信息。
  2. POST 请求

    • 目的:用于向服务器提交数据,比如表单提交或执行某个操作,可能改变服务器状态(非幂等)。
    • 数据传输:请求主体(Request Body)中携带数据,数据内容对用户不可见,不会显示在URL中。
    • 数据类型与大小:POST方法可以发送大量数据,并且支持多种数据格式(如JSON、XML、表单数据等),没有明确的标准限制其大小。
    • 安全性:相对于GET来说,POST更适用于传输敏感信息,因为数据不直接暴露在URL中。

除了GET和POST之外,HTTP/1.1还定义了其他几种请求方法,包括但不限于:

  • HEAD:类似于GET,但只返回响应头信息,不包含响应体内容,用于检查资源的元数据而不必下载整个内容。
  • PUT:用于替换服务器上的现有资源,或者如果不存在则创建资源,它需要客户端提供完整的资源实体。
  • PATCH:用于更新服务器上的现有资源的部分内容,而不是替换整个资源。
  • DELETE:用于删除指定的资源。
  • OPTIONS:允许客户端查看服务器支持哪些HTTP方法,用于预检请求,确定实际请求的安全性和正确性。
  • CONNECT:建立一个到由目标资源标识的服务器的TCP连接通道。
  • TRACE:用于回显服务器收到的请求,主要用于诊断和调试目的。

每种方法都有特定的使用场景和语义,设计时需根据HTTP标准和应用需求来选择合适的方法。

30 web安全问题

Web安全问题是一个涵盖多个方面的广泛主题,以下是一些常见的Web安全威胁及简要说明:

  1. 跨站脚本攻击(XSS, Cross-Site Scripting)

    • 原理:攻击者通过注入恶意脚本到网页中,当用户浏览该页面时,这些脚本在用户的浏览器上执行。
    • 防御方式:对用户输入进行严格的过滤和转义,使用HTTP头中的Content Security Policy (CSP)来限制脚本来源,避免直接输出未经处理的用户数据。
  2. 跨站请求伪造(CSRF, Cross-Site Request Forgery)

    • 原理:利用网站用户的登录状态,在用户不知情的情况下,构造并执行恶意请求,如转账、修改账户设置等操作。
    • 防御方式:使用CSRF Tokens,即为每个重要操作生成一次性的令牌,并要求客户端在发起请求时携带这个令牌;或者检查Referer头信息,但此方法并非完全可靠。
  3. SQL注入

    • 原理:攻击者将恶意SQL代码插入到Web应用程序使用的查询字符串中,从而获取、修改或删除数据库中的敏感信息。
    • 防御方式:参数化查询(Prepared Statements)或ORM工具,不拼接SQL语句,确保所有用户输入都经过适当的验证和清理。
  4. 会话劫持与固定

    • 原理:攻击者窃取或预测合法用户的会话标识符(如session ID),伪装成合法用户进行操作。
    • 防御方式:使用安全的cookie属性(HttpOnly防止JavaScript访问,Secure强制HTTPS传输),定期更换会话ID(Session Regeneration),以及实施严格的身份验证和授权机制。
  5. 不安全的直接对象引用

    • 原理:攻击者能够通过猜测或篡改资源的直接URL,非法访问原本不应公开的文件或其他对象。
    • 防御方式:对资源进行权限控制,对资源链接进行加密或编码处理,实现基于角色的访问控制(RBAC)。
  6. 不安全的通信

    • 原理:未加密的数据在网络中传输,容易被监听和篡改。
    • 防御方式:强制使用HTTPS协议以加密通信内容,启用HSTS策略防止降级攻击。
  7. 弱身份认证

    • 原理:过于简单的密码策略、明文存储密码或缺乏二次身份验证措施导致账号易被破解。
    • 防御方式:采用复杂密码策略,使用哈希加盐存储密码,提供多因素认证(MFA)。
  8. 未更新的安全补丁

    • 原理:软件漏洞如果不及时修补,会被黑客利用发起攻击。
    • 防御方式:定期更新系统、框架和应用的所有组件,保持最新的安全补丁和配置。
  9. 点击劫持(Clickjacking)

    • 原理:攻击者覆盖一个透明的界面在受信任的网页上,诱骗用户点击实际隐藏在其下的按钮或链接。
    • 防御方式:使用X-Frame-Options HTTP头部来阻止网站内容在iframe中加载。
  10. 拒绝服务攻击(DoS/DDoS)

    • 原理:通过大量无效请求淹没服务器,使其无法响应正常请求。
    • 防御方式:使用防火墙、负载均衡器和其他网络设备限制流量,部署DDoS防护服务,设计高可用架构。

以上列举的是Web安全领域的一些常见问题和相应的防御措施。为了保护Web应用程序的安全,开发人员应遵循安全编程的最佳实践,实施纵深防御,并持续监控和审计系统安全状况。

31 设计一个短链接服务。如何抗住大qps,抗大流量

设计一个高并发、抗大流量的短链接服务,需要从架构设计、数据库优化、缓存策略、负载均衡和水平扩展等多个层面考虑。以下是一些建议:

  1. 分布式存储与数据库优化

    • 使用分布式数据库或NoSQL数据库(如Redis、MongoDB等),支持快速读写操作,并能横向扩展。
    • 对长链接到短链接的映射关系进行分片存储,分散访问压力,避免单点瓶颈。
    • 对热点数据使用缓存技术,将高频查询的短链接直接从内存中获取,减少对数据库的访问。
  2. 缓存系统

    • 高效利用Redis等内存型数据库作为缓存层,存储短链接与长链接之间的映射关系,使得大部分请求可以直接命中缓存,无需查询后端数据库。
    • 设置合理的缓存过期时间,确保在大量新生成短链接时不会导致缓存溢出,同时保证数据一致性。
  3. 负载均衡

    • 在服务器前端部署负载均衡器(如Nginx、HAProxy等),根据算法(如轮询、最少连接数、哈希等)将请求均匀分布到后端多个节点上。
    • 如果有多个数据中心,可以采用全局负载均衡(GSLB),基于地理位置和网络状况分配流量。
  4. API接口设计

    • 设计高效的短链接生成算法,尽量减少短链长度以节省存储空间并降低解析复杂度。
    • 确保生成和解析短链接的操作尽可能快且资源消耗小,避免阻塞线程或进程。
  5. 异步处理

    • 对于耗时较长的操作,例如日志记录、统计分析等,可以采用异步处理方式,不影响主线业务流程。
  6. 水平扩展能力

    • 服务组件应具备良好的水平扩展性,当流量增加时,可通过增加服务器节点来提升系统整体处理能力。
    • 数据库、缓存等存储组件也需能够平滑地扩容,确保随着数据量增长性能不下降。
  7. 限流与降级

    • 实现服务限流机制,在流量过大时保护系统,防止因瞬间峰值导致系统崩溃。
    • 设定合理的服务降级策略,比如在资源紧张时优先保障核心功能的可用性。
  8. 监控与报警

    • 完善的监控体系是必不可少的,包括但不限于CPU、内存、磁盘I/O、网络带宽以及各种应用级别的指标。
    • 当关键指标超过阈值时,立即触发警报通知运维人员,以便及时调整资源配置或优化服务。

通过以上方案综合实施,可以构建一个高并发、抗大流量的短链接服务,满足在大QPS场景下的稳定运行要求

32 全局负载均衡(GSLB)

全局负载均衡(Global Server Load Balancing,GSLB)是一种网络服务,它在多个地理位置的分布式数据中心之间实现流量分配和故障切换。GSLB的主要目标是在保证服务高可用性的同时优化全球范围内的用户访问体验,尤其对于互联网服务提供商、大型企业以及需要处理全球大流量的网站而言至关重要。

GSLB通过以下几种方式工作:

  1. 地理定位:根据用户的地理位置信息将请求转发到最近或最优的数据中心,从而降低延迟并提升用户体验。

  2. 健康检查与故障切换:持续监控各个数据中心服务器的运行状况,并在检测到某个数据中心发生故障时,自动将流量重新定向至其他正常工作的数据中心。

  3. 会话持久化:确保来自同一用户的连续请求被发送到同一数据中心,以维持会话状态的一致性。

  4. 动态负载均衡:实时监测各数据中心的负载情况,根据预设策略(如最少连接数、轮询、加权轮询等)智能地分配流量,避免单个数据中心过载。

  5. 内容分发网络(CDN)集成:某些GSLB解决方案可以结合CDN服务使用,进一步提高内容分发效率,减轻源站压力。

  6. DNS级别调度:许多GSLB服务是在DNS层级实现的,当用户发起域名解析请求时,DNS服务器返回的是当前最佳的数据中心IP地址,这样实现了透明的流量调度。

总之,全局负载均衡系统是构建大规模分布式网络架构的关键组件之一,能够帮助企业在不同地域间高效地分配流量,确保业务连续性和性能优化。

33 url哈希函数怎么设计(怎么存,怎么统计qps)

设计一个用于生成短链接(URL哈希)的函数,其主要目标是将长链接映射到较短的、唯一的标识符。为了实现这个功能并统计QPS(每秒查询率),可以按照以下步骤进行:

设计哈希函数

  1. 唯一性:确保不同的原始URL经过哈希后得到不同的短链接ID。这可以通过使用强哈希函数如MD5或SHA-2系列来实现,但因为这些函数输出较长,通常需要进一步处理(截取部分字节或者进行Base64编码)以达到所需的长度。

  2. 冲突处理:尽管哈希函数理论上可能存在碰撞,但在实际应用中应该尽可能减少碰撞概率。可通过加盐(salt)、时间戳或者其他附加信息来增加输入域的多样性,并在碰撞时采用链地址法、开放寻址法等冲突解决策略。

  3. 可读性/长度控制:为了让生成的短链接更易于传播和记忆,可以选择保留一定数量的字符,或者对哈希值进行额外转换,比如数字与字母组合的六二进制编码(Base62)。

存储与统计QPS

  1. 存储方案

    • 数据库:使用关系型数据库(MySQL、PostgreSQL等)或NoSQL数据库(Redis、MongoDB等)存储长链接与短链接的映射关系。
    • 哈希表:对于内存数据库如Redis,可以直接使用哈希表结构存储键值对,key为短链接ID,value为原始长链接。
  2. 统计QPS

    • 实时统计:每当有请求到达时,记录访问时间和请求类型(这里指短链接请求)。可以利用Redis等内存数据库的原子操作,例如INCR命令对每个短链接ID对应的计数器进行累加。
    • 定时任务:每隔一段时间(通常是1秒)执行一次统计任务,计算这段时间内所有短链接的请求次数总和,从而得出QPS。
    • 流式处理:如果系统规模很大,可以借助消息队列和流处理框架(如Kafka+Spark Streaming或Flink)实时处理日志事件并实时统计QPS。

示例伪代码:

 

Python

1# 假设已经有一个哈希函数 generate_short_url(long_url)
2# 并且有一个 Redis 实例 redis_client
3
4def handle_request(long_url):
5    short_url = generate_short_url(long_url)
6    # 存储映射关系
7    redis_client.hset("url_mapping", short_url, long_url)
8    
9    # 记录访问
10    redis_client.incr(f"qps:{short_url}:counter")
11    # 或者在单个键下用有序集合(ZSET)按时间戳记录,便于统计某一时间窗口内的请求数
12    redis_client.zadd(f"qps:all_urls", time.time(), short_url)
13
14# 每秒统计一次QPS
15def calculate_qps():
16    current_second = int(time.time())
17    last_second = current_second - 1
18    for short_url in redis_client.hkeys("url_mapping"):
19        prev_count = redis_client.get(f"qps:{short_url}:counter") or 0
20        new_count = redis_client.zcount(f"qps:all_urls", (last_second, current_second])
21        qps = new_count - prev_count
22        # 更新或记录QPS数据
23        save_to_metrics(short_url, qps)
24        # 清零当前秒的计数器
25        redis_client.set(f"qps:{short_url}:counter", 0)

注意:上述代码仅作示例用途,实际开发中可能需要根据具体场景和技术栈进行调整优化。同时,在生产环境中应考虑并发问题,确保计数操作的线程安全性。

GO INCR命令对每个短链接ID对应的计数器进行累加

在Go语言中,如果你使用Redis作为存储系统,并且想通过INCR命令对每个短链接ID对应的计数器进行累加以统计QPS,可以修改统计部分的代码如下:

首先,在初始化Redis连接后,为每个短链接创建一个独立的计数器键(例如:qps_counter:<shortID>)。

 

Go

1func init() {
2    // ... 初始化rdb
3
4    // 示例:为每个新生成的短链接ID预先创建计数器键
5    rdb.HSet(ctx, "url_mapping", shortID, longURL)
6    rdb.SetNX(ctx, "qps_counter:"+shortID, 0, 0).Val()
7}

然后,每当处理请求时,使用INCR命令增加对应短链接ID的计数器:

 

Go

1func HandleRequest(longURL string) (string, error) {
2    shortID, err := GenerateShortID(longURL)
3    if err != nil {
4        return "", err
5    }
6
7    StoreURLMapping(longURL, shortID)
8
9    // 使用INCR命令累加计数器
10    ctx := context.Background()
11    _, _ = rdb.Incr(ctx, "qps_counter:"+shortID).Result()
12
13    return shortID, nil
14}

最后,为了统计每秒的QPS,可以设置一个定时任务来获取并重置计数器。这里假设你有一个全局的定时任务执行器(如time.Ticker),并在每秒结束时执行以下操作:

 

Go

1// 假设ticker是全局的定时器,每秒触发一次
2func OnTickerTick() {
3    ctx := context.Background()
4
5    // 获取所有短链接ID对应的当前计数
6    keys, _ := rdb.Keys(ctx, "qps_counter:*").Result()
7    for _, key := range keys {
8        shortID := strings.TrimPrefix(key, "qps_counter:")
9        count, _ := rdb.Get(ctx, key).Int64()
10        
11        // 记录或处理QPS数据
12        handleQPS(shortID, count)
13
14        // 重置计数器
15        rdb.Set(ctx, key, 0, 0).Err()
16    }
17}
18
19// handleQPS 是用于处理QPS数据的函数,根据实际需求实现
20func handleQPS(shortID string, qps int64) {
21    // 这里可以将QPS记录到数据库、日志或其他监控系统中
22    log.Printf("ShortID: %s, QPS: %d", shortID, qps)
23}

注意

  1. 上述代码中的错误处理应按实际情况完善。
  2. 对于大规模应用,直接使用Keys命令可能效率较低,应当考虑其他方式比如维护一个已知短链接ID列表,或者在创建短链接时就订阅其计数器变化事件等更高效的方法。

开放寻址法

开放寻址法是解决哈希表中冲突(collision)的一种策略,当两个或多个键通过哈希函数映射到同一个位置时发生冲突。在开放寻址法中,不使用链表或其他数据结构来存储冲突的元素,而是直接在散列表(数组)中寻找下一个空闲的位置来存放元素。

具体实现上,通常采用以下几种探测方法:

  1. 线性探测(Linear Probing):当碰撞发生时,尝试在原哈希地址基础上依次增加固定步长(通常是1),直到找到一个空槽位为止。例如,如果初始位置h(key)被占用,则检查h(key) + 1, h(key) + 2, ...等位置。

  2. 二次探测(Quadratic Probing):与线性探测类似,但是步长不再是固定的1,而是在每次探测后增加平方项,如h(key) + 1^2, h(key) + 2^2, ...

  3. 双重散列(Double Hashing):使用第二个哈希函数进行探测,即在冲突发生时,计算h2(key)作为增量,并结合原始哈希值h1(key)来确定新的探测位置,例如h1(key) + i * h2(key),其中i从0开始递增。

开放寻址法要求哈希表中的所有槽位都是连续的,并且需要一种机制来保证即使经过多次探测也能找到空槽位,这可能导致装载因子不能过高,否则将引发聚集现象(clustering),降低查找效率。

另外,在删除操作中,开放寻址法需要特殊的处理方式来标记已删除的元素,防止误填入和丢失数据(例如使用特殊值“ tombstone”标记)。

装载因子α定义为哈希表中实际元素数量n除以表的总槽位数m,即 α = n/m,为了保持较好的性能,一般会限制α不超过某个阈值(比如0.75)。

33 tcp和udp的特性,tcp三次握手、四次挥手

TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)是两种主要的传输层协议,它们各自具有不同的特性和用途:

TCP 特性:

  • 面向连接:在数据传输前必须先建立连接。
  • 可靠性:提供可靠的数据传输服务,通过序列号、确认应答、重传机制保证数据包无丢失、重复或乱序到达。
  • 流量控制:通过滑动窗口协议动态调整发送速率以适应接收端处理能力,避免拥塞。
  • 拥塞控制:当网络状况不佳时,采取慢启动、快速重传、快速恢复等策略来防止过多的数据包涌入网络造成拥堵。
  • 全双工通信:允许同时进行双向数据传输。
  • 数据流服务:基于字节流方式传输数据,不区分消息边界。

UDP 特性:

  • 无连接:不需要预先建立连接,直接发送数据报文。
  • 不可靠:不保证数据包能到达,也不保证顺序,没有重传机制。
  • 速度快:由于无需握手过程和确认机制,UDP比TCP更简单、更快捷。
  • 实时性:适用于对延迟敏感但对丢包有一定容忍度的应用,如在线游戏、视频会议、VoIP电话等。
  • 数据报服务:基于数据报的方式发送数据,每个数据报都是独立的实体,保持应用层的数据边界。

TCP 的三次握手与四次挥手:

三次握手(建立连接):
  1. 客户端向服务器发送一个带有SYN标志的数据段,并设置一个初始序列号ISN(c)。
  2. 服务器接收到客户端的SYN后,回复一个带有SYN和ACK标志的数据段,确认客户端的序列号,并设置自己的初始序列号ISN(s)。
  3. 客户端再次发送一个带有ACK标志的数据段,确认服务器的序列号,至此双方都已知对方的接收和发送能力,并建立了连接。
四次挥手(断开连接):
  1. 主动关闭方(例如客户端)发送一个带有FIN标志的数据段,表示自己已经没有数据要发送了。
  2. 服务器收到FIN后,发送一个带有ACK标志的数据段,确认已收到客户端的断开请求,此时服务器到客户端的方向仍可继续传输数据。
  3. 服务器完成数据发送任务后,也发送一个带有FIN标志的数据段给客户端,表示它也准备关闭连接。
  4. 客户端回应一个带有ACK标志的数据段,确认服务器的FIN。自此,连接完全关闭
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值