基础架构

计算

虚拟化技术

虚拟化技术:我熟悉如VMware, KVM和Hyper-V等虚拟化技术,了解其工作原理和如何进行资源隔离。

容器化和编排

容器化和编排:我对Docker和Kubernetes有深入的使用经验,包括微服务部署、网络策略和状态管理。

存储

块存储与对象存储:了解SAN、NAS的原理,以及对象存储如S3和Ceph的使用和性能优化。

数据库系统:熟悉关系型数据库如MySQL, PostgreSQL和非关系型数据库如MongoDB, Redis的架构和最佳实践。

关系型数据库 代表:MySQL, PostgreSQL

架构:

基于表的结构,表与表之间可以通过键关联。 使用SQL(结构化查询语言)进行查询。 支持ACID事务性质(原子性、一致性、隔离性、持久性)。 最佳实践:

规范化:将数据结构分解为较小的表以消除数据冗余。 备份:定期创建数据库备份。 索引:为常用查询的列创建索引以加速检索。 安全:限制对数据库的直接访问,使用存储过程和参数化查询。 非关系型数据库 代表:

MongoDB:一个文档型数据库。 Redis:一个键值存储。 架构:

MongoDB:基于文档的结构,每个文档可以有不同的结构。文档组织在集合中。 Redis:存储键值对,其中值可以是字符串、列表、集合、哈希或其他数据类型。 最佳实践:

数据模型: 对于MongoDB,设计文档以反映应用中的对象和查询模式。 对于Redis,选择正确的数据类型来存储信息。 持久性: MongoDB支持多种持久化策略,可以调整为特定的应用需求。 Redis可以配置为定期将数据写入磁盘或仅作为缓存使用。 扩展性:考虑如何分布数据以支持大规模操作。 MongoDB支持分片来分散数据。 Redis可以通过主从复制和分区进行扩展。

网络

网络拓扑:理解不同的网络架构如星形、总线、环形和网格。

负载均衡与CDN:熟悉L4、L7负载均衡技术,以及CDN的内容分发机制。

L4负载均衡(四层负载均衡): 基于网络层或传输层(TCP/UDP)进行负载均衡。 主要根据源IP地址、目标IP地址、源端口和目标端口来决定如何分发流量。 通常速度较快,因为它不需要查看数据包的内容。

L7负载均衡(七层负载均衡):

基于应用层(如HTTP/HTTPS)进行负载均衡。 可以根据HTTP头、URL结构、或其他应用级信息来决定如何路由流量。 允许更复杂的负载均衡策略,如基于内容的路由、HTTP头部的路由等。

CDN (内容分发网络)

CDN的目标是将内容缓存到距离终端用户最近的位置,从而加速内容的传输速度。

CDN的工作原理:

当用户第一次请求某个资源(例如一个图片或视频)时,请求会被路由到CDN的原始服务器。 CDN将该资源缓存到一个或多个边缘服务器上。 当其他用户请求相同的资源时,CDN会根据多种因素(如地理位置、带宽、服务器健康状况等)将请求路由到最佳的边缘服务器。 通过这种方式,CDN能够大大减少延迟,提供更快速的内容加载时间,同时减轻原始服务器的负担。

安全

身份验证与授权:理解OAuth, JWT等身份验证技术和RBAC的授权模型。

OAuth

OAuth是一个开放标准,用于授权。允许应用A访问用户在应用B上的信息,而不需要告诉应用A用户密码。

工作流程:

用户尝试登录第三方应用。 应用请求授权服务器的权限。 用户在授权服务器上登录并同意给予第三方应用访问权限。 授权服务器返回一个授权码到第三方应用。 应用使用授权码请求访问令牌。 授权服务器验证并返回访问令牌。 该令牌然后被用来访问受保护的资源。

JWT (JSON Web Token) JWT是一个用于在两方之间传递信息的开放标准。信息经过签名,可以验证和信任。

结构:

Header:包含令牌的类型和签名算法。 Payload:包含声明(如用户ID、角色等)。 Signature:确保令牌未被篡改。

应用场景通常是在服务之间进行身份验证和信息交换。

RBAC (Role-Based Access Control) RBAC就是:给角色分配权限,再把用户分配到这些角色。用户通过角色获得权限,而不是直接获得。

主要组件:

User:系统的最终用户。 Role:定义了一组访问权限的集合。例如,“管理员”或“编辑”。 Permission:访问特定资源的能力。例如,“读”或“写”。

网络安全:熟悉防火墙、IDS/IPS和WAF的配置和策略。

防火墙 (Firewall) 定义:防火墙是一种网络安全系统,用于监控并过滤进出网络的流量。

配置和策略:

访问控制列表 (ACL):定义哪些流量可以进入或离开网络。 端口控制:允许或拒绝特定端口的流量。 NAT (网络地址转换):转换公共IP地址和私有IP地址之间的流量。

IDS/IPS (入侵检测系统/入侵预防系统) 定义:

IDS:监测网络流量以寻找任何可疑活动,并在检测到时提供警报。 IPS:除了检测外,还会采取行动阻止或减轻攻击。 配置和策略:

签名规则:定义已知攻击模式的规则。 异常检测:学习正常流量模式并警告异常。 策略调整:定义应对检测到的威胁的反应。

WAF (Web应用防火墙) 定义:专门为了保护Web应用程序而设计的防火墙,监控并过滤HTTP流量。

配置和策略:

黑/白名单:指定允许或拒绝的IP地址或URL。 输入验证:防止如SQL注入、跨站脚本(XSS)的攻击。 会话保护:防止会话劫持或欺骗。 速率限制:减缓或阻止DDoS攻击。

监控和运维

  1. 日志管理:了解如何使用ELK Stack(Elasticsearch, Logstash, Kibana)进行日志聚合和分析。
  2. 性能监控:熟悉如Prometheus和Grafana等监控工具,用于实时监控系统性能和健康状况。

其他

在Go中,协程之间的通讯方式有几种?

Channels:这是Go中最常见的协程间通讯方法。Channel是一个通讯对象,可以让一个协程发送数据并让另一个协程接收数据。

使用make(chan Type)来创建一个新的channel。

使用<-操作符发送和接收数据。

ch := make(chan int)
go func() {
    ch <- 42  // send data
}()
value := <-ch  // receive data

共享内存:协程可以访问共享变量,但这需要使用同步原语,如互斥量(mutex),来确保并发访问时的数据安全。

var counter int
var mu sync.Mutex

go func() {
    mu.Lock()
    counter++
    mu.Unlock()
}()

sync/atomic 包:为了简化某些并发操作,Go提供了一个atomic包,可以执行原子操作,例如增加、减少、加载、存储等。

sync/atomic 包:为了简化某些并发操作,Go提供了一个atomic包,可以执行原子操作,例如增加、减少、加载、存储等。

var counter int32

go func() {
    atomic.AddInt32(&counter, 1)
}()

Select语句:用于在多个channel操作中执行一个操作。它可以用于同时从多个channel接收数据,或者从多个channel中选择一个来发送数据。

select {
case msg1 := <-ch1:
    fmt.Println("Received", msg1)
case msg2 := <-ch2:
    fmt.Println("Received", msg2)
case ch3 <- 3:
    fmt.Println("Sent 3 to ch3")
default:
    fmt.Println("No communication")
}

sync.WaitGroup:允许等待一组协程完成。

共享内存:

协程通过共享内存来交换数据。这意味着多个协程可以同时访问相同的内存位置,因此需要同步和互斥来避免数据竞争。 对应上面的:共享内存、sync/atomic 包和互斥量(mutex)。

消息传递: 协程不直接共享内存,而是通过发送和接收消息来交换数据。 对应上面的:Channels 和 Select语句。 信号量:

信号量是一个计数器,用于控制对共享资源的并发访问。 在上面提到的方法中,并没有直接涉及到信号量。但在Go的golang.org/x/sync/semaphore包中,提供了信号量的实现。这不是Go标准库的一部分,但是它经常被用来实现更复杂的同步场景。

事务的性质

原子性(Atomicity) 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

一致性(Consistency) 事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,不可能A扣了钱,B却没收到。

隔离性(Isolation) 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

持久性(Durability) 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

如何排查一条慢SQL

在很多数据库系统(如MySQL)中,可以开启慢查询日志功能,它会记录执行时间超过指定阈值的查询。 检查慢查询日志:

SHOW VARIABLES LIKE 'slow_query_log';

开启慢查询日志(如果未开启):

SET GLOBAL slow_query_log = 'ON';

设置慢查询的时间阈值(例如,记录查询时间超过2秒的查询):

SET GLOBAL long_query_time = 2;

设置慢查询日志文件的位置(可选):

SET GLOBAL slow_query_log_file = '/path/to/your/logfile.log'
tail -f /path/to/your/logfile.log

定期查看慢查询日志,找出运行缓慢的SQL。

EXPLAIN命令

对于找出的慢查询,使用EXPLAIN命令来查看查询的执行计划。这可以帮助理解SQL是如何被执行的,哪些索引被使用,以及哪些可能的优化点。

EXPLAIN SELECT ... [your slow query here] ...;

解读结果:

type: 显示了查询的类型,如const、ref、range、ALL等。通常,我们希望避免ALL类型,因为这意味着全表扫描。 possible_keys: 显示可能用于查询的索引。 key: 实际使用的索引。 rows: 估计要检查的行数。 Extra: 包含有关查询的其他信息,如是否使用了文件排序或临时表。

线程和进程的区别

线程是调度的基本单位(PC,状态码,通用寄存器,线程栈及栈指针);进程是资源分配的基本单位。 线程不拥有系统资源,但一个进程的多个线程可以共享隶属进程的资源;进程是拥有资源的独立单位。 线程创建销毁只需要处理PC值,状态码,通用寄存器值,线程栈及栈指针即可;进程创建和销毁需要重新分配及销毁task_struct结构。

在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?

DNS 查询: 浏览器将域名转换为IP地址。如果浏览器或操作系统的缓存中没有该域名的IP地址,它会请求DNS服务器进行解析。

建立TCP连接: 一旦获取到IP地址,浏览器会与服务器建立一个TCP连接。这通常使用三次握手完成。

发送HTTP请求: TCP连接建立后,浏览器会发送HTTP请求到服务器,请求输入的URL对应的资源。

服务器处理: 服务器收到请求后,开始处理这个请求(可能包括数据库查询、运行后端代码等),然后准备返回的响应。

服务器响应: 服务器将准备好的数据(如HTML、CSS、JS等)发送回浏览器。

浏览器渲染: 浏览器开始解析服务器返回的内容,渲染页面,加载图片、执行JavaScript等,直到页面完全显示给用户。

关闭连接: 如果不是使用持久连接,一旦所有数据交换完毕,TCP连接会被关闭。

Linux和windows下的进程通信方法和线程通信方法分别有哪些?

进程通信:

管道 (Pipe): 主要用于父子进程间通信。 命名管道 (Named Pipe): 不同进程间的通信方式。 信号 (Signal): 通知接收进程某个事件已经发生。 消息队列 (Message Queue): 允许进程将消息发送到队列,其他进程可以读取或写入。 共享内存 (Shared Memory): 允许多个进程访问同一块内存空间。 套接字 (Socket): 网络编程中用于进程间或不同机器间的通信。 信号量 (Semaphore): 主要用于同步,但也可以用于进程间通信。

线程通信:

临界区 (Critical Sections): 类似于Mutex。 事件 (Events): 通知线程某事件发生。 信号量 (Semaphore) 互斥体 (Mutex) 条件变量 (Condition Variables) 消息传递 (Message Passing): 通过消息队列传递消息给线程。

在并发编程时,在需要加锁时,不加锁会有什么问题?

数据竞争 (Race Conditions): 当多个并发执行的线程或进程尝试访问同一资源,并至少有一个是写入操作时,它们之间可能会发生数据竞争。这可能导致数据的不一致和不可预测的结果。

数据不一致 (Inconsistencies): 不加锁可能导致数据状态处于不一致的状态,因为多个线程/进程可能会同时修改数据。

丢失更新 (Lost Updates): 一个线程对数据的更新可能会被另一个线程的操作所覆盖,从而导致数据丢失。

死锁 (Deadlock) 的可能性增加: 虽然加锁也可能导致死锁,但不加锁的情况下,随意的资源访问可能更容易引入死锁情况。

HTTPS和HTTP的区别

HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全, HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

https协议需要到ca申请证书,

前者是80,后者是443

讲一下程序的内存分区/内存模型

代码区(Text Segment):存储程序的可执行代码。

数据区

初始化数据段(Initialized Data Segment):存储程序中已初始化的全局变量和静态变量。 未初始化数据段(Uninitialized Data Segment / BSS):存储程序中未初始化的全局变量和静态变量。

堆(Heap):用于动态内存分配,如C++的new和C的malloc函数分配的内存。它从低地址向高地址增长。

栈(Stack):用于存放函数的局部变量、函数参数、返回地址等。每当调用一个函数时,会为该函数分配一个新的栈帧。栈是自动管理的,从高地址向低地址增长。

常量区:存放如字符串常量的区域。

说说了解的死锁?包括死锁产生原因、必要条件、处理方法、死锁回复以及死锁预防等

互斥 不剥夺 请求保持 循环等待

数据库并发事务会带来哪些问题?/脏读、幻读、丢弃更改、不可重复读的区别?

脏读(Dirty Read):读取了其他事务未提交的更改。 不可重复读(Non-repeatable Read):同一事务中,多次读同一数据结果不同,因为被其他事务修改了。 幻读(Phantom Read):同一事务中,查询结果集数量变化,因为其他事务插入或删除了记录。 丢弃更改(Lost Update):两个事务同时修改同一数据,一个的修改被另一个覆盖。

HTTP 1.0、1.1、2.0和3.0的区别如下:

HTTP 1.0:

无连接:每次请求/响应后,连接断开。 无状态:服务器不会保留任何关于客户端请求的信息。 HTTP 1.1:

长连接:支持持久连接,即多个请求/响应可以使用同一连接。 管道机制:在同一连接上发送多个请求,但响应仍需按顺序。 增加了缓存处理、扩展了状态码、增加了一些新的方法如OPTIONS和PUT。 支持Host头,使得一个服务器能够托管多个域名。 HTTP 2.0:

二进制格式:使得传输更高效。 多路复用:单一连接上可以多个请求/响应同时进行,消除了水平线阻塞。 服务器推送:服务器可以主动向客户端推送数据。 首部压缩:减少了请求和响应的数据大小。 HTTP 3.0 (基于QUIC):

使用UDP代替TCP:提高了连接和数据传输的速度。 内建TLS:默认提供安全连接。 更好的并发:因为UDP的特性,减少了连接建立和丢包时的延迟。 改进的流控制、丢包恢复和拥塞控制

什么是IO多路复用

让一个线程/进程知道哪些通道(如电话)正在等待输入/输出,从而可以高效地处理多个通道,而不需要为每个通道都分配一个线程/进程。

IO多路复用是计算机网络编程中的一个技术,允许单个线程或进程监视多个文件描述符(通常是网络套接字),看看哪些是可读的、可写的或有异常。这使得一个单独的线程或进程可以同时处理多个并发的网络I/O操作,而无需为每一个I/O操作分配一个独立的线程或进程。

基本上,IO多路复用提供了一个机制,让程序能够不阻塞地等待多个I/O通道成为可读或可写的,从而提高了程序在处理I/O时的效率。

常见的IO多路复用技术包括:

select poll epoll (特定于Linux) kqueue (特定于BSD系统,如FreeBSD和macOS)

Linux中异常和中断的区别/键盘敲击发生的中断是怎么回事?

中断 (Interrupts):

来源:外部事件,例如I/O设备(如键盘、鼠标或硬盘)产生的信号。 目的:通知CPU某个设备需要处理,例如数据已经准备好被读取。 异步:它们是异步的,可以在任何时候发生,与当前执行的代码无关。

异常 (Exceptions): 来源:由于程序执行中的错误或某些特殊的指令,如除以零、无效内存访问等。 目的:提供一种机制来响应错误或特殊条件,通常会导致程序终止或产生核心转储。 同步:它们是由当前执行的代码直接引起的。

键盘敲击发生的中断: 当敲击键盘时,键盘的硬件会发送一个电信号给中断控制器。 中断控制器识别这个信号并将其传递给CPU,通知CPU键盘已经有数据准备好被读取。 CPU然后暂停其正在执行的任务,并通过预定义的中断服务程序(ISR)来处理这个中断。 ISR负责从键盘缓冲区读取数据,并将其存储在内存中,通常在某个队列中,供操作系统或应用程序稍后处理。 一旦数据被处理,CPU返回到被中断的任务,并继续执行。

中断:

来源:外部事件(如键盘敲击)。 目的:通知CPU有设备事件需要处理。 性质:异步。

异常:

来源:程序执行错误(如除以零)。 目的:响应程序中的错误。 性质:同步。 键盘敲击中断:当键盘被敲击,发送信号给CPU,暂停当前任务,读取键值,再继续任务。 键盘敲击中断就是当敲击键盘时,它发送一个信号给计算机,告诉它需要注意这个新输入,并相应地处理它。

Redis 的RedLock ?

edis的RedLock算法是一个分布式锁的实现。在许多并发系统中,我们需要确保某个时刻只有一个进程可以执行某个操作或访问某个资源,这就是所谓的“锁”。当这种情况跨越多个实例、节点或服务器时,传统的单点锁不再适用,因此需要一种分布式锁来处理这种情况。

RedLock是Redis作者Antirez(Salvatore Sanfilippo)提出的一个分布式锁算法。以下是其基本流程:

获取锁:客户端获取当前时间的毫秒数,作为参考时间。 尝试获取锁:客户端尝试使用相同的键和随机值在所有Redis实例上设置一个带有过期时间的锁

锁的有效性检查:只有当客户端在大多数Redis实例上成功地设置了锁,并且总的锁定时间超过了从第一步开始到现在的时间差,锁才被认为是有效的。

返回锁:如果锁有效,它会被返回给客户端。否则,客户端会删除在步骤2中创建的锁,并重新尝试。

解锁:由于锁带有随机值,所以只有创建该锁的客户端才能解锁它。客户端会在所有Redis实例上解锁,即使它只在少数实例上获得了锁。

基于 ZooKeeper 的分布式锁

方案 基于 ZK 的特性,很容易得出使用 ZK 实现分布式锁的落地方案:

使用 ZK 的临时节点和有序节点,每个线程获取锁就是在 ZK 创建一个临时有序的节点,比如在 /lock/ 目录下。 创建节点成功后,获取 /lock 目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点。 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的 前一个节点 添加一个事件监听。

缺陷 羊群效应:当一个节点变化时,会触发大量的 watches 事件,导致集群响应变慢。每个节点尽量少的 watches,这里就只注册 前一个节点 的监听 ZK 集群的读写吞吐量不高 网络抖动可能导致 Session 离线,锁被释放

go切片(slice)扩容的具体策略?

对于Go切片的扩容策略(即Go 1.16),当向一个切片追加元素并超出其容量时,Go会为切片分配一个新的更大的底层数组。具体的扩容规则如下:

底层维护了一个指向数组的指针,然后还维护了一个数组的长度和一个它的空间预存的一个 CAP 的容量值。

如果切片的当前容量小于1024个元素,那么扩容的策略是将容量翻倍。 如果切片的当前容量大于或等于1024个元素,扩容策略将会增加约25%的容量。 如果通过append函数追加的元素数量大于当前切片容量的25%,则新的容量将根据需要追加的元素数量来进行调整。 这个策略的设计是为了在内存使用和复制操作之间达到一个平衡。对于较小的切片,由于其总体内存占用较小,所以直接翻倍。而对于较大的切片,增加固定的25%可以避免分配太多不必要的内存。

需要注意的是,这些策略是Go当前版本的实现细节,并可能在未来的版本中发生变化。如果需要对切片的扩容行为有更精确的控制,可以考虑手动使用make和copy函数来管理切片的容量。

常见的HTTP状态码

1xx(信息响应类):

100 Continue:请求的初始部分已被服务器接受,客户端应继续请求。 101 Switching Protocols:服务器已理解切换协议的请求。 2xx(成功响应类):

200 OK:请求成功。 201 Created:请求已完成,并创建了新资源。 202 Accepted:请求已接受,但尚未处理。 204 No Content:请求已成功处理,但没有内容返回。 3xx(重定向类):

300 Multiple Choices:为请求的资源提供了多个选择。 301 Moved Permanently:资源永久重定向。 302 Found:资源临时重定向。 304 Not Modified:资源自上次请求后未发生变化,可直接使用缓存。 4xx(客户端错误类):

400 Bad Request:请求格式错误或请求无法处理。 401 Unauthorized:请求未经授权。 403 Forbidden:服务器拒绝执行请求。 404 Not Found:请求的资源在服务器上未找到。 429 Too Many Requests:由于请求频次达到上限,请求被限制。 5xx(服务器错误类):

500 Internal Server Error:服务器内部错误。 501 Not Implemented:服务器不支持当前请求的功能。 502 Bad Gateway:作为网关或代理的服务器从上游服务器收到无效响应。 503 Service Unavailable:服务器暂时无法处理请求(由于超载或维护)。 504 Gateway Timeout:作为网关或代理的服务器未及时从上游服务器或其他来源收到请求

链路追踪采样策略

一个客户端请求可能需要多个服务间的多次调用才能完成。对于高流量的系统,对每个请求进行追踪是非常昂贵的,这就涉及到“采样策略”。采样策略决定了哪些请求被追踪,哪些被忽略。

常见的链路追踪采样策略有:

固定率采样:这是最简单的策略。例如,系统可能决定追踪每100个请求中的一个。

概率采样:与固定率相似,但每个请求被追踪的概率是动态决定的。例如,可以基于当前系统的流量进行动态调整。

自适应采样:基于请求的某些属性(如URL、用户代理或其他头信息)来决定是否追踪。

速率限制采样:这种策略设定了一个追踪的速率上限,例如每秒不超过10个请求。

错误率采样:只追踪那些结果为错误或有异常的请求。

opentelemetry探针技术原理

OpenTelemetry 是一个开源项目,其目标是为观察性工具(包括但不限于分布式追踪和指标)提供统一的标准。OpenTelemetry 提供了一套 API、库、代理和工具集,用于捕获分布式系统中的事件和指标。

其探针(通常被称为 “instrumentation”)技术的基础原理如下:

自动化工具:OpenTelemetry 提供了自动化工具,可以无缝地为应用添加追踪代码。例如,如果使用 Java Spring Boot,OpenTelemetry 有专门的工具可以自动为的应用添加追踪代码。

API和SDK:OpenTelemetry 提供了一套 API 和 SDK,允许开发者为代码添加自定义的追踪和指标。

上下文传播:为了跟踪一个请求跨多个服务的完整路径,OpenTelemetry 使用上下文传播。当一个请求从一个服务转到另一个服务时,OpenTelemetry 自动将跟踪上下文嵌入到请求中(例如 HTTP 头部),并在下游服务中恢复该上下文。

Exporters:一旦数据被捕获,它需要被发送到一个后端系统(如 Jaeger、Zipkin 或云提供商的监视系统)进行分析和可视化。OpenTelemetry 提供了多种 “exporters”,用于将数据导出到这些后端系统。

与现有标准的兼容性:OpenTelemetry 旨在与现有的追踪和监控标准兼容,例如 OpenTracing 和 OpenCensus。

可插拔性:OpenTelemetry 设计为高度模块化和可插拔的。如果需要,开发者可以轻松更换或扩展标准的 SDK 功能,例如采样策略或导出行为。

分布式事务

两阶段提交 (2PC):

准备阶段:协调者询问所有参与者:“如果我现在告诉提交,是否能够提交?” 提交/中止阶段:基于参与者的反馈,协调者决定告诉参与者是提交还是中止事务。

优点:确保所有参与者都同步。 缺点:如果协调者崩溃,可能导致阻塞。

三阶段提交 (3PC): 询问阶段:协调者询问所有参与者:“现在是否有条件参与一个事务?” 准备阶段:如果所有的参与者都回应可以,协调者会说:“请准备提交但不要真的提交,等我的命令。” 提交/中止阶段:基于参与者准备好的反馈,协调者再决定是让参与者真正提交还是中止事务。

相比2PC,3PC在“准备”阶段之前增加了一个“询问”阶段,并且在各阶段有时间限制,从而减少了阻塞的风险。

在2PC的第二阶段,当协调者发送”COMMIT”指令给参与者后,如果因为网络故障或者协调者宕机等原因,这个指令没有被一部分或全部的参与者接收到,那么这些未收到指令的参与者就会处于不确定状态,它们不知道是应该提交还是中止事务。

由于2PC协议本身没有设定超时或失败重试机制,因此这些参与者就会一直等待指令,进入”阻塞”状态。它们不能单方面决定提交或中止,因为它们不知道其他参与者或协调者的状态和决策。这就是2PC中的阻塞问题。

简而言之,2PC中的阻塞问题是因为参与者在某些失败情况下,缺乏足够的信息来独立决策,而3PC通过更多的通信和超时机制,提供了额外的上下文,允许参与者在某些情况下做出自主决策,从而减少了阻塞。

引入超时机制: 与2PC不同,3PC为事务的每个阶段设定了超时时间。当某个阶段超时后,参与者可以根据当前的事务阶段和已知的系统状态来独立做出决策,而不是无期限地等待协调者的命令。

假设正在设计一个电商平台,其中订单、库存和支付三个服务都在不同的数据库中。当用户下单时,如何确保这三个服务中的数据操作都成功或都失败?

Saga模式:

将整个事务拆分为一系列较小的子事务或Saga。每个子事务只影响一个服务的数据库。 如果某个子事务失败,会触发一个补偿操作来回滚之前已经执行的子事务。例如,如果支付失败,库存和订单的相关操作也需要被回滚。

事件驱动架构:

使用事件驱动架构来协调Saga。当一个子事务完成时,该服务发布一个事件。其他相关的服务监听这些事件,并基于事件内容执行自己的子事务或补偿操作。

持久化事件日志:

为了确保系统的可靠性,特别是在失败的情况下,可以使用持久化的事件日志来记录所有的事件。这样,即使某个服务暂时不可用,当它重新上线时,也可以根据日志恢复其状态。

幂等性保证:

确保每个子事务是幂等的,这样即使某个操作被重复执行,也不会对系统状态产生不良影响。

监控与报警:

实施监控策略,以实时追踪所有分布式事务的状态。一旦检测到失败或不一致,立即触发报警机制并自动进行相应的补偿操作。

都有哪些锁?

  1. 共享锁(Shared Lock,S-Lock)
    • 允许多个事务或线程读取锁定的资源。
    • 阻止任何事务或线程写入锁定的资源。
  2. 排他锁(Exclusive Lock,X-Lock)
    • 一旦一个事务或线程获取了排他锁,其他事务既不能读也不能写锁定的资源。
  3. 意向锁(Intention Locks)
    • 这不是一个真正的锁,而是一个表明事务打算获取哪种类型锁(S-Lock 或 X-Lock)的标记。
    • 这有助于数据库管理系统实现多级锁策略,如表级和行级锁。
  4. 读锁(Read Lock)
    • 类似于共享锁,允许多个线程或事务读取数据,但阻止写入。
    • 通常用在读写锁的上下文中。
  5. 写锁(Write Lock)
    • 只允许持有写锁的线程或事务修改数据,其他线程或事务既不能读也不能写。
    • 通常用在读写锁的上下文中。
  6. 乐观锁(Optimistic Lock)
    • 通过数据版本(如时间戳或版本号)来实现。
    • 事务不在数据上设置真正的锁,而是在提交时检查数据版本。如果在事务开始后数据被其他事务修改,当前事务将失败。
  7. 悲观锁(Pessimistic Lock)
    • 假设数据会产生冲突,所以在读取或修改数据时立即设置锁。
    • 直到事务完成或释放锁,其他事务才能访问锁定的资源。

共享锁允许并发读取,但共享锁存在时不允许写入。排他锁则防止其他所有事务进行读取或写入,确保完全独占数据项。

当一个数据项上存在共享锁时,其他事务可以加共享锁来读取该数据项,但不能加排他锁来写入该数据项,直到所有的共享锁都被释放。当共享锁被释放,其他事务就可以申请排他锁来写入数据项。

共享锁 (S-Lock):

多个事务可以同时获得一个数据项的共享锁,因此它们可以并发地读取该数据项。 如果一个事务请求一个数据项的排他锁(为了写入),它必须等待直到所有共享锁被释放。

排他锁 (X-Lock):

一个数据项在任何时候只能有一个排他锁。 拥有排他锁的事务可以对数据项进行读取或写入。 其他事务不能对这个数据项加任何锁(共享或排他),直到原事务释放排他锁。

MySQL的行级锁有哪些?

MySQL 支持多种行级锁,包括:

共享锁 (Shared Locks, S-Locks):允许事务读取一行数据。当一个数据行被共享锁定时,其他事务可以读取但不能写入或锁定这一行。

排他锁 (Exclusive Locks, X-Locks):当事务需要修改数据时,它会锁定数据行。在持有排他锁的情况下,其他事务不能读取或写入该行,除非它们也获得排他锁。

意向锁 (Intention Locks):这不是直接应用于单个数据行的锁,而是表明事务希望在更细粒度上获得锁。意向锁有两种类型:

意向共享锁 (Intention Shared Lock, IS) 意向排他锁 (Intention Exclusive Lock, IX) 意向锁是为了在表级别上与其他锁协同工作,使得多个事务可以更高效地在表的不同部分进行工作。

记录锁 (Record Locks):这是一个行级锁,它锁定索引记录。

间隙锁 (Gap Locks):它不锁定索引记录,而是锁定索引之间的间隔。这是为了防止幻读 (Phantom Rows) 的出现。

临键锁 (Next-key Locks):这种锁结合了记录锁和间隙锁,锁定一个索引记录并锁定之前和之后的间隙。

读提交和可重复度的区别?

读提交可以防止脏读,但是无法解决幻读。可重复度可以解决幻读,这是因为加了间隙锁的原因。

读已提交 (Read Committed): 在这个隔离级别中,事务只能看到其他事务已经提交的更改。它不会看到其他正在进行中的事务所做的修改。 可重复读 (Repeatable Read): 一旦事务读取了数据,它会确保在该事务的整个生命周期内,这些数据都不会被其他事务更改。也就是说,如果事务A读取了一些数据,然后事务B更改并提交了这些数据,事务A再次读取时仍然会看到它最初读到的数据。

幻读的处理: 读已提交: 这个隔离级别可以防止脏读和不可重复读,但不能防止幻读(Phantom Reads)。幻读是当一个事务在读取某个范围的行时,另一个事务插入或删除了一些行,导致第一个事务再次读取时看到不同的行。 可重复读: 在MySQL中,该隔离级别使用间隙锁(Gap Locks)来防止幻读。这意味着在此隔离级别下,事务不仅锁定实际读取的行,还锁定读取范围内的间隙。

性能: 通常,读已提交的性能会优于可重复读,因为它需要的锁定更少,降低了事务之间的争用。 使用场景:

如果需要更严格的数据一致性,并且可以接受一些性能损失,可以使用可重复读。 如果希望获得更好的并发性能,并且可以接受可能出现的幻读,那么读已提交可能更合适。

读未提交 读提交 可重复读 串行化 都使用了什么锁?

读未提交 (Read Uncommitted):

主要使用 记录锁。 由于这个隔离级别允许读取尚未提交的数据更改,所以它很少使用锁来防止其他事务的访问,可能会看到其他事务中的未提交更改。

读已提交 (Read Committed):

主要使用 记录锁。 只在访问数据的时候临时地加锁,然后在读取数据之后立即释放。写操作(如UPDATE或DELETE)会持有锁直到事务完成。

可重复读 (Repeatable Read):

使用 记录锁 和 间隙锁。 在MySQL的InnoDB存储引擎中,可重复读是默认的隔离级别。它确保所读取的数据在当前事务内保持一致性。为了防止其他事务插入“幻影”行,它使用间隙锁来锁定范围。

串行化 (Serializable):

使用 记录锁、间隙锁 和 全表锁。 这是最高的隔离级别。它会对所有读取的行加锁,并且如果它不能获取锁,它会等待。这基本上确保每次只有一个事务可以执行,使所有事务变得串行化,从而避免了并发问题。

原子操作

原子操作通常是通过底层硬件指令或操作系统的特性来实现的。原子操作确保一个给定的序列的操作要么完全执行,要么完全不执行,而且在执行的过程中,不会被其他操作中断或干扰。

以下是原子操作实现的一些常见方法:

硬件支持:许多现代处理器提供了特定的指令集,这些指令集可以在单个指令周期内执行复杂的操作,如比较和交换 (compare-and-swap, CAS)、获取和释放锁等。

锁机制:

使用互斥锁或自旋锁来保护代码段,确保同一时间只有一个线程或进程可以访问它。 当锁被持有时,其他试图获取锁的线程会被阻塞或旋转,直到锁被释放。 数据库事务:数据库系统提供了事务机制来确保一系列的操作是原子的。这是通过提交或回滚来实现的,确保在事务中的所有操作要么都成功执行,要么都不执行。

软件方法:某些原子操作可以通过软件算法实现,尤其是在没有硬件原子性支持的环境中。Lamport的bakery算法是一个著名的例子。

中断禁用:在某些嵌入式系统或实时操作系统中,为了确保操作的原子性,系统可能会短暂地禁用中断,这样当前的操作就不会被其他任何操作中断。

乐观并发控制:在这种方法中,系统假设冲突是罕见的,并允许多个操作并发执行,但在提交更改之前,会检查是否有冲突。如果有冲突,操作会回滚并重新尝试。

守护进程、僵尸进程和孤儿进程

僵尸进程(Zombie Process)是一个已经完成执行但还在进程表中占用表项的进程。当一个进程的子进程比父进程先结束,而父进程没有调用wait或waitpid函数来获取子进程的结束状态,那么子进程的进程描述符仍然保存在系统中,这样的进程称为僵尸进程。

在Unix和类Unix操作系统(如Linux)中,每个进程都有一个父进程。当一个进程结束时,它的退出状态需要被父进程收集。这是通过wait系列的系统调用完成的。如果父进程没有执行这些调用,子进程就会变成僵尸进程。

僵尸进程自身不占用系统资源(如CPU和内存),但会占用进程表的一个条目。大量的僵尸进程可能会耗尽系统的进程表空间,导致新进程无法创建。

如何处理僵尸进程 父进程调用wait或waitpid: 父进程通过调用这些函数来收集子进程的退出状态,从而使子进程成功地退出。

信号处理: 父进程可以捕获SIGCHLD信号,并在信号处理函数中调用wait或waitpid。

父进程结束: 如果父进程结束,所有的僵尸子进程会被init进程(进程ID为1)接管,init会自动为其子进程调用wait,从而释放这些僵尸进程。

手动清除: 管理员可以手动找出僵尸进程,并向其父进程发送SIGCHLD信号,促使其调用wait,或者直接结束父进程以清除僵尸进程。

守护进程 (Daemon Process):

守护进程是在后台运行的进程,它与终端会话分离,通常用于执行诸如服务器、日志记录、任务调度等长时间或永久性任务。 它们不与用户直接交互,通常由系统在启动时自动启动并运行。 为了成为守护进程,进程通常会“孤立”自己:它会断开所有终端关联、关闭所有继承的文件描述符、更改其工作目录和重置其umask等。

僵尸进程 (Zombie Process):

当一个子进程终止,但其父进程尚未检索其终止状态时,该子进程成为僵尸进程。 它仍然在进程表中保留一个条目(通常为了让父进程稍后能够检索其子进程的终止状态),但不再执行任何操作。 一旦父进程检索了子进程的终止状态(通常通过调用wait()函数),僵尸进程的条目就会从进程表中删除。 如果父进程在其子进程之前终止,init进程(PID为1)会“领养”子进程,并负责清理任何变为僵尸的子进程。

孤儿进程 (Orphan Process):

当父进程在其子进程之前终止时,该子进程成为孤儿进程。 孤儿进程不是由其原始父进程,而是由init进程(PID为1)“领养”。init进程定期检查并“领养”孤儿进程,确保它们在终止后被正确清理。 总结:守护进程是后台任务进程,僵尸进程是已经终止但其状态未被父进程检索的进程,而孤儿进程是其父进程已终止的进程。

Go 的内存分配器

Go语言的内存管理并非基于tcmalloc。实际上,Go有其自己的内存分配器。但Go语言的内存分配器的确从tcmalloc中借鉴了一些思想。以下是Go内存分配器的主要设计思想和原理:

分层设计:Go的内存分配器分为多个层次:

MSpan:管理固定大小的内存块,称为span。 MCentral:管理一系列大小类似的span。 MHeap:是所有MCentral的集合,并包括管理大块内存的功能。

大小分类:Go将内存分为很多大小类。每个大小类都由一个MCentral管理。这意味着对于常见的内存分配大小,Go可以快速地找到合适大小的内存块。

线程本地存储(Thread Local Storage, TLS):为了减少全局锁的争用,Go的内存分配器使用线程本地存储。每个P(处理器)都有一个本地缓存,称为mcache,用于满足小的内存分配请求。

大块内存分配:对于大于32KB的内存分配,Go直接从MHeap分配内存。

垃圾回收:Go使用一个并发的、三色标记清除的垃圾回收器。这意味着Go可以在程序运行时进行垃圾回收,而不需要停止整个程序。

内存释放策略:当MSpan中的对象都被释放时,MSpan可能会返回给MCentral,以便将来重用。当MCentral有多余的空闲MSpan时,它们可能会被返回给MHeap。最后,MHeap可能会将未使用的内存页返回给操作系统。

减少锁的使用:通过使用mcache和其他技术,Go努力减少内存分配时的锁争用。这对于高并发的程序来说是非常重要的。

总之,Go的内存分配器结合了多级设计、大小分类、线程本地存储等技术,以实现高效、可扩展和低碎片化的内存管理。

Go 的内存分配主要基于一个分层的方法,以加速小型和常见的分配,同时也可以处理大型和高并发的分配。我会简要地从顶层概念到具体细节来描述这个过程。

基于大小的分配: Go 将对象分成多个大小类,每个大小类对应一定的字节大小。这意味着同一个大小类的所有对象都有相同的大小。

mspan:
在 Go 中,内存被组织为多个连续的区块,称为 spans(或 mspan)。每个 mspan 包含一组固定大小的对象。这些对象的大小就是 mspan 的大小类。

mcache:
为了加速内存分配,Go 引入了线程本地存储。每个 P(处理器)都有自己的 mcache,即本地缓存,其中缓存了一组 mspan。当一个 goroutine 想要分配内存时,它首先会查看其关联 P 的 mcache。

mcentral:
每个大小类都有一个全局的 mcentral,它维护着 mspan 的列表。当 mcache 中的 mspan 用完或不足时,它会从相应的 mcentral 获取新的 mspan。反过来,当 mcache 的 mspan 空闲时,它们可以被返回给 mcentral。

mheap:
mheap 是全局的大型存储,跨所有大小类。当 mcentral 需要更多的 mspan 时,它从 mheap 获取。mheap 包含所有已分配但未使用的页面。如果 mheap 也没有足够的空间,它会直接从操作系统请求更多的内存。

总的来说,当 Go 程序需要分配内存时,它首先从 mcache 开始,然后是 mcentral,最后是 mheap。这种层次化的分配策略为 Go 提供了高效且并发友好的内存管理机制。

Go 语言内存分配器的实现原理

内存管理一般包含三个不同的组件,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector),当用户程序申请内存时,它会通过内存分配器申请新内存,而分配器会负责从堆中初始化相应的内存区域.

分级分配 线程缓存分配(Thread-Caching Malloc,TCMalloc)是用于分配内存的机制,它比 glibc 中的 malloc 还要快很多2。Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。

Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种: 因为程序中的绝大多数对象的大小都在 32KB 以下,而申请的内存大小影响 Go 语言运行时分配内存的过程和开销,所以分别处理大对象和小对象有利于提高内存分配器的性能。

内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存:

线程缓存属于每一个独立的线程,它能够满足线程上绝大多数的内存分配需求,因为不涉及多线程,所以也不需要使用互斥锁来保护内存,这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时,运行时会使用中心缓存作为补充解决小对象的内存分配,在遇到 32KB 以上的对象时,内存分配器会选择页堆直接分配大内存。

这种多层级的内存分配设计与计算机操作系统中的多级缓存有些类似,因为多数的对象都是小对象,我们可以通

Go 语言内存分配器

内存空间的组成:Go 语言中的数据和变量都会被分配到虚拟内存中,主要包括两个区域:栈区(Stack)和堆区(Heap)。函数调用的参数、返回值和局部变量主要分配在栈上,由编译器管理。而堆上的对象由内存分配器分配,并由垃圾收集器回收。

设计原理:

内存管理组件:包括用户程序(Mutator)、分配器(Allocator)和收集器(Collector)。用户程序通过分配器申请新内存,而分配器则负责从堆中初始化相应的内存区域。 分配方法:Go 的内存分配器主要使用两种方法:线性分配器(Bump Allocator)和空闲链表分配器(Free-List Allocator)。 线性分配器:高效但有局限性。它在内存中维护一个指针,当申请内存时,只需检查剩余空闲内存、返回分配的内存区域并修改指针位置。但它无法在内存被释放时重用。 空闲链表分配器:可以重用已释放的内存。它维护一个链表结构,申请内存时遍历空闲内存块,找到合适的内存后修改链表。 多级缓存:Go 的内存分配器借鉴了 TCMalloc 的设计,使用多级缓存将对象按大小分类,并按类别实施不同的分配策略。这些级别包括线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)。

线性分配器(Bump Allocator):

这种分配器非常简单和高效。它在内存中维护一个指针,每次申请内存时,只需检查是否有足够的空闲内存,然后返回当前指针指向的内存区域,并将指针向前移动相应的大小。 由于它的简单性,线性分配器在某些场景下非常高效,特别是当知道内存分配是短暂的或者生命周期是可预测的时候。 但这种方法的缺点是它无法有效地重用已释放的内存,因为它不跟踪哪些内存已经被释放。 空闲链表分配器(Free-List Allocator):

Go 的内存分配器使用空闲链表来跟踪和管理已释放的内存块。 当申请内存时,分配器会遍历空闲链表,寻找一个合适大小的内存块。一旦找到,它就从链表中移除该块并返回给用户。 当内存被释放时,它会被添加回空闲链表,以便将来重用。 在实际应用中,Go 的内存分配器会根据具体的需求和情境选择使用哪种方法。例如,对于小的、短暂的分配,线性分配器可能更有优势,而对于大的、长时间存在的对象,空闲链表分配器可能更合适。

虚拟内存布局:Go 1.10 以前,堆区的内存空间是连续的。但从 1.11 版本开始,Go 使用稀疏的堆内存空间替代了连续的内存,解决了连续内存的限制和可能的问题。

MSpan、Mcentral 和 MHeap 是 Go 语言内存分配器中的核心数据结构,它们在内存分配和释放过程中起到关键作用。以下是它们的具体定义和作用:

MSpan (Memory Span):

MSpan 表示一系列连续的内存页。每个 MSpan 包含了相同大小的对象。 它记录了这些对象的大小、位置、已分配的数量、未分配的数量等信息。 MSpan 可以处于不同的状态,例如:空闲、部分分配或完全分配。 Mcentral (Memory Central):

Mcentral 是一个中心存储结构,用于管理一组特定大小的 MSpan。 对于每个对象大小的类别,都有一个对应的 Mcentral。 当线程缓存(Thread Cache)中没有可用的空间时,它会从 Mcentral 获取或释放 MSpan。 Mcentral 主要用于协调多个 Goroutine 对相同大小的对象的并发分配请求。 MHeap (Memory Heap):

MHeap 是一个全局的数据结构,管理所有的 MSpan。 它维护了一个空闲链表,用于跟踪未分配的内存页。 当 Mcentral 需要更多的内存时,它会从 MHeap 请求。同样,当 MSpan 变为空闲时,它会返回给 MHeap。 MHeap 还负责与操作系统交互,申请或释放内存页。 这三个结构体在 Go 的内存分配器中相互协作,确保内存的高效分配和回收。MSpan 提供了对具体内存页的细粒度管理,Mcentral 为特定大小的对象提供了中心化的管理,而 MHeap 则为整个系统提供了全局的内存管理。

内存分配器包含哪些分配方法?

编程语言的内存分配器一般包含两种分配方法,一种是线性分配器(Sequential Allocator,Bump Allocator),另一种是空闲链表分配器(Free-List Allocator),这两种分配方法有着不同的实现机制和特性。

线性分配器 线性分配(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们使用线性分配器时,只需要在内存中维护一个指向内存特定位置的指针,如果用户程序向分配器申请内存,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针:

空闲链表分配器 空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表:

首次适应(First-Fit)— 从链表头开始遍历,选择第一个大小大于申请内存的内存块; 循环首次适应(Next-Fit)— 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块; 最优适应(Best-Fit)— 从链表头遍历整个链表,选择最合适的内存块; 隔离适应(Segregated-Fit)— 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块;

代理服务器和反向代理服务器的区别是什么

代理服务器(Proxy Server)和反向代理服务器(Reverse Proxy Server)都是在客户端和服务器之间起到中介的作用,但它们的目的、使用场景和工作方式有所不同。以下是两者之间的主要区别:

目的和使用场景:

代理服务器:主要用于为客户端提供资源访问服务,常见的使用场景包括内容过滤、提供匿名访问、缓存内容以加速访问速度、限制对特定内容的访问等。例如,公司内部网络可能使用代理服务器来过滤员工访问的内容或提供缓存服务。

反向代理服务器:主要用于为服务器提供保护和负载均衡,常见的使用场景包括保护后端服务器、分发客户端请求到多个后端服务器、缓存内容以减轻后端服务器的负担、SSL终结等。

工作方式:

代理服务器:客户端配置代理服务器的地址,当客户端想要访问某个资源时,请求首先发送到代理服务器,代理服务器再将请求转发到目标服务器,并将目标服务器的响应返回给客户端。

反向代理服务器:客户端并不知道存在反向代理,它直接向反向代理发送请求。反向代理决定将请求转发到哪个后端服务器,并将后端服务器的响应返回给客户端。

位置:

代理服务器:位于客户端和互联网之间。

反向代理服务器:位于后端服务器和互联网之间。

知晓对象:

代理服务器:客户端知道代理服务器的存在,并直接与其交互。 反向代理服务器:客户端通常不知道反向代理的存在,它认为自己直接与后端服务器交互。 控制方:

代理服务器:通常由客户端或客户端所在的组织控制。 反向代理服务器:通常由后端服务器或后端服务器所在的组织控制。 总的来说,代理服务器主要为客户端服务,提供内容访问和过滤功能,而反向代理服务器主要为后端服务器服务,提供保护、负载均衡和缓存功能。

Tags: 基础架构
Share: X (Twitter) Facebook LinkedIn