悟已往之不谏,知来者之可追

  • ✨ 欢迎来到我的个人博客

  • 🤔 我在这里分享技术、读书、生活还有思考。

Restful API设计最佳实践?

REST API? REST API 是当今最常见的 Web 接口形式之一 ,在确保客户端和服务器之间的顺利通信方面发挥了重要作用。 你可以把客户端看作前端,把服务器看作后端。 客户端(前端)和服务器(后端)之间的通信通常不是直接的。因此,我们使用 API(应用编程接口),作为客户端和服务器之间的中介。 因为 API 在这种 client–server 通信中起着至关重要的作用,所以我们在设计 API 时应该遵循最佳实践。这有助于开发人员更好地维护和使用它们。 本篇通过 REST API 时可以遵循的一些最佳实践。这将帮助创建高效,安全,易于使用和维护的 API。 1.什么是 REST API? REST 是 Representational State Transfer 的缩写。它是由 Roy Fielding 博士在 2000 年他的博士论文中提出一种软件架构风格,用于指导网络应用的设计和开发,使得 Web API(网络应用编程接口)更加简单、灵活、可扩展和易于理解。 任何遵循 REST 设计原则的 API 都被称为 RESTful API。 简单地说,REST API 是两台计算机通过 HTTP(超文本传输协议)进行通信的媒介,与客户端和服务器的通信方式相同。 REST API 使用 URL 唯一标识和定位 RESTful 服务中的资源,一个完整的 URL 结构如下: 2.REST API 设计建议 1. 用名词表示资源 当你设计一个 REST API 时,端点应该使用名词而非动词。 这是因为 HTTP 方法已经以动词形式执行基本的 CRUD(创建、读取、更新、删除)操作。 GET、POST、PUT、PATCH 和 DELETE 是最常见的 HTTP 动词。还有其他非 HTTP 标准动词,如 COPY、PURGE、LINK、UNLINK 等等。 因此,举例来说,一个端点不应该是这样的: ...

2025年12月10日 · Mumu

Go 中的 Lock使用?

锁(Lock)是并发编程中用于保护共享资源、防止数据竞争(data race)的关键同步原语。 1.sync.Mutex (互斥锁) 特点 同一时间只允许一个 goroutine 持有锁。 不可重入(reentrant):同一个 goroutine 再次调用 Lock() 会导致死锁。 var mu sync.Mutex var count int func increment() { mu.Lock() defer mu.Unlock() count++ } 注意 必须配对使用:每个 Lock() 必须对应一个 Unlock()。 避免在持有锁时进行阻塞操作(如 I/O、channel 发送/接收),否则会降低并发性能。 不要复制已使用的 Mutex(因为其内部状态不可复制)。 2.sync.RWMutex 读写锁 特点 支持 多个 reader 或 一个 writer。 读锁(RLock):允许多个 goroutine 同时读。 写锁(Lock):独占,阻塞所有读和其他写。 适用于“读多写少”的场景。 var rwmu sync.RWMutex var cache map[string]string // 读操作 func get(key string) string { rwmu.RLock() defer rwmu.RUnlock() return cache[key] } // 写操作 func set(key, value string) { rwmu.Lock() defer rwmu.Unlock() cache[key] = value } 注意 写锁优先级问题:某些实现中,持续的读请求可能“饿死”写请求(Go 的 RWMutex 在写等待时会阻止新读者进入,缓解此问题)。 不要在读锁内修改共享数据!仅用于读取。 和 Mutex 一样,不可重入、不可复制。 3.sync.Map 并发安全的map 特点 内置并发安全的 map,内部使用分段锁或原子操作优化。 适用于读远多于写、key 集合不固定(频繁增删)的场景。 不是通用替代品,仅在特定场景比 map + RWMutex 更高效。 var m sync.Map m.Store("key", "value") if v, ok := m.Load("key"); ok { fmt.Println(v) } 注意 接口类型:key 和 value 都是 interface{},有类型转换开销。 不支持遍历快照:Range 回调期间 map 可能被修改。 不适合复杂操作(如“读-改-写”原子操作),此时仍需配合锁。 4.原子操作(sync/atomic)—— 无锁 特点 对简单类型(int32/int64/pointer 等)提供原子读写、CAS(Compare-And-Swap)。 性能极高,无 goroutine 阻塞。 适用于计数器、标志位等简单状态。 var counter int64 // 原子自增 atomic.AddInt64(&counter, 1) // 原子加载 val := atomic.LoadInt64(&counter) 注意 仅适用于对齐的、简单内存操作。 不能用于保护复杂数据结构(如 slice、struct 字段组合)。 5.使用注意 锁粒度控制 尽量缩小临界区,只保护真正需要同步的代码。 ...

2025年12月9日 · Mumu

Kafka的rebalance?什么情况下会出现?

Kafka 的 Rebalance (重平衡) 是 Consumer Group (消费者组) 中的一个核心机制,它用于在 Consumer Group 内部重新分配 Topic 的分区(Partition)所有权。 Rebalance 确保了在集群运行过程中,Consumer Group 里的所有消费者能均匀、独占地消费所有相关的分区。 1.什么是 Rebalance (重平衡)? 在 Kafka 中,一个 Consumer Group 消费一个或多个 Topic。每个分区在同一时刻只能被 Consumer Group 内的一个 Consumer 实例消费。 Rebalance 就是 Consumer Group 内部达成一致,确定“谁”来消费“哪个”分区的过程。 1.1 核心目标 负载均衡:将分区均匀地分配给组内所有健康的 Consumer 实例。 高可用性:当有 Consumer 实例失败或退出时,Rebalance 机制会将它之前负责的分区重新分配给组内其他Consumer,确保消费不会中断。 1.2 Rebalance 的过程 整个过程由 Consumer Group 的 Coordinator(协调器,通常是某个 Broker) 负责协调: -Join Group (加入组):新的 Consumer 加入或旧的 Consumer 重新连接时,会向 Coordinator 发送请求。 -Sync Group (同步组):Coordinator 在收到所有 Consumer 的 Join 请求后,会选出一个 Leader Consumer。 -分配方案:Leader Consumer 负责制定分区到 Consumer 的映射关系(分配策略)。 -执行分配:Coordinator 将分配方案通知给所有 Consumer,各个 Consumer 按照方案开始消费新分配的分区。 2.什么情况下会出现 Rebalance? 任何导致 Consumer Group 内部成员发生变化或分区信息发生变化的操作,都会触发 Rebalance。 ...

2025年10月14日 · Mumu

Kafka如何实现主从同步?

Kafka 实现主从同步(即 Leader 副本和 Follower 副本之间的数据同步)是其保证数据高可用性和持久性的核心机制。这个过程是**异步拉取(Pull)的,并由 ISR(同步副本集合)机制严格管理。 1.异步拉取 与一些数据库的 Push 模式不同,Kafka 的副本同步采用 Pull 模型: 主动方 (Follower):Follower 副本是主动方。它会不断地向 Leader 副本发送请求,请求拉取新的消息数据。 拉取单位:Follower 拉取的最小单位是 **日志段(Log Segment)**中的一批消息。 这种拉取模式允许 Follower 控制自己的复制速率。如果 Follower 暂时负载过高,它可以减慢拉取速度,避免被 Leader 的高速写入压垮。 2.关键同步指标 Follower 在同步过程中,会维护和使用两个关键的偏移量(Offset): LEO (Log End Offset):表示该 Follower 已成功写入本地日志的最新消息的下一个 Offset。 HW (High Watermark):表示所有 ISR 集合中的副本都已经复制并确认写入的最新消息的下一个 Offset。 重要性:HW 之前的消息对 Consumer 是可见且安全的,而 HW 之后的 Leader 消息对 Consumer 是不可见的,以防 Leader 宕机导致数据丢失。 3.ISR (In-Sync Replicas) 机制的保障 同步副本集合(ISR)是衡量同步状态的核心机制: Leader 维护 ISR:Leader 副本负责维护 ISR 列表。ISR 列表包括 Leader 自身和所有与 Leader 保持“同步”的 Follower 副本。 同步判断标准: -Follower 必须在配置的时间阈值(replica.lag.time.max.ms)内持续向 Leader 发送拉取请求。 -Follower 的 LEO 必须与 Leader 的 LEO 保持在一个可接受的范围内。 副本移出:如果 Follower 无法满足上述条件(如网络延迟过高、宕机),它会被 Leader 移出 ISR。 数据持久性保证:当生产者(Producer)设置为 acks=all 时,Leader 必须等待 ISR 中的所有副本都确认写入了消息,才会返回 ACK 成功。这确保了只要 ISR 中有一个副本存活,数据就不会丢失。 4.主从同步流程简述 Follower 发送 Fetch 请求:Follower 向 Leader 发送 Fetch Request,请求从自己的 LEO 开始的新消息。 Leader 发送消息:Leader 从自己的日志中读取从 Follower LEO 开始的消息,并返回给 Follower。 Follower 写入并更新 LEO:Follower 接收到消息后,将其追加写入到自己的本地日志中,并更新自己的 LEO。 Leader 更新 HW:Leader 收到 Follower 的成功响应后,会检查 所有 ISR 副本的 LEO,并更新 HW 为所有副本 LEO 的最小值。

2025年10月14日 · Mumu

Type Assertion 类型断言

在 Go 语言中,类型断言(Type Assertion) 是一种用于从接口值中提取其底层具体类型的操作。它是 Go 实现多态和类型安全的重要机制之一。 一、基本语法 value, ok := interfaceVar.(ConcreteType) interfaceVar:一个接口类型的变量 ConcreteType:你期望它实际存储的具体类型(如 int, string, MyStruct 等) value:如果断言成功,就是该类型的值 ok:布尔值,表示断言是否成功 二、为什么需要类型断言? Go 的接口(interface)可以存储任何类型的值,但当你想使用这个值的具体方法或字段时,就必须知道它的真实类型。 var i interface{} = “hello” // 我知道它是 string,但接口本身不能直接调用 len() s := i.(string) // 类型断言:断言 i 是 string fmt.Println(len(s)) // 现在可以了 三、两种写法 安全断言(推荐) —— 带 ok 判断 s, ok := i.(string) if ok { fmt.Println("字符串长度:", len(s)) } else { fmt.Println("i 不是一个字符串") } 优点:不会 panic,适合不确定类型时使用。 直接断言 —— 不检查 ok s := i.(string) // 如果 i 不是 string,会 panic! 面临的风险:如果类型不匹配,程序会崩溃(panic)。 仅在你100%确定类型时使用。 ...

2025年10月14日 · Mumu

了解分布式锁吗?

分布式锁是分布式系统从“松散运行”走向“协作、可靠运行”的关键桥梁。 1.什么是分布式锁? 分布式锁是用于在分布式系统中协调多个进程或线程访问共享资源的一种机制,确保在任何时刻只有一个客户端能够操作特定的资源,从而保证数据的一致性。 2.分布式锁的基本要素 无论是基于 Redis、ZooKeeper 还是数据库实现,一个可靠的分布式锁必须满足三个要素: *加锁(Lock):在资源上设置一个锁的标记。 *设置过期时间(TTL):防止客户端宕机导致锁无法释放,造成死锁。这是分布式锁与本地锁(如 Java 的 ReentrantLock)最核心的区别。 *释放锁(Unlock):客户端完成操作后,安全地移除锁。 3.分布式锁面临问题和解决 3.1 死锁 如果客户端在获取锁后,由于某种原因(例如,程序崩溃)未能正常释放锁,导致锁一直被占用,其他客户端无法获取锁,造成死锁。解决方案:在加锁的同时设置过期时间,即使客户端未能正常释放锁,锁也会在过期后自动释放。或者使用 Redlock 算法,提高锁的可靠性,防止死锁。 3.2 锁的误删 -如果客户端 A 获取锁后,由于执行时间过长,导致锁过期自动释放。此时,客户端 B 获取了锁。然后,客户端 A 执行完业务逻辑后,尝试释放锁,但实际上释放的是客户端 B 的锁,造成锁的误删除。解决方案:在加锁时,将锁的值设置为一个唯一标识(例如,UUID),在释放锁时,先判断锁的值是否与自己的唯一标识相等,如果相等,则释放锁;否则,不释放锁。此过程要保证原子性,可以使用 Lua 脚本实现。 -在网络分区的情况下,可能会导致多个进程同时认为自己持有锁。解决方案:在获取锁时生成一个唯一的 UUID,并将其存储在锁的 Key 中。在释放锁时,先检查当前存储的 UUID 是否与自己的 UUID 匹配,只有匹配时才释放锁。 3.3 锁的续期 如果客户端在加锁后,执行时间超过了锁的过期时间,导致锁被自动释放。此时,其他客户端可能会获取锁,造成并发问题。解决方案:客户端在获取锁后,启动一个后台线程,定期检查锁的剩余时间,如果剩余时间小于一定阈值,则使用 EXPIRE 命令续期锁的过期时间。此流程可以自己实现,也可以使用开源框架,例如Redisson 框架不仅提供了自动续期的功能,还可以简化分布式锁的实现。 3.4 锁的竞争 在高并发场景下,多个进程可能会同时竞争锁,导致锁的获取失败率较高。解决方案:可以使用随机退避重试策略,在获取锁失败后,随机等待一段时间后再次重试。 3.5 锁的重入性 如果同一个进程多次尝试获取锁,可能会导致锁的获取失败。解决方案:在锁的 Key 中存储一个计数器,表示当前进程获取锁的次数。每次获取锁时增加计数器,释放锁时减少计数器,只有计数器为 0 时才删除锁的 Key。 3.6 锁的公平性 多个进程同时请求锁时,可能会出现“饥饿”现象,某些进程长时间无法获取锁。解决方案:可以使用 Redis 的 List 数据结构实现排队机制,确保请求锁的进程按照顺序获取锁。或者使用成熟的分布式锁实现库,如 Redisson,它提供了公平锁和可重入锁等功能。

2025年10月14日 · Mumu

你了解内存管理吗?

栈分配和堆分配确实都发生在“对象分配器”的分配阶段, 但它们走的是不同的路径: 栈分配走的是编译期静态分配; 堆分配走的是运行时对象分配器(runtime.mallocgc)。 1.程序启动阶段 Go 运行时启动时(runtime 初始化),会: 向操作系统申请一大块虚拟内存(称为 arena); 由 页分配器(page allocator) 管理这块内存; 构建堆内存管理结构(mheap、mcentral、mcache)。 这部分只是“预留”内存,真正的对象分配还没发生。 2.用户程序触发分配(对象分配阶段) 当用户代码中创建变量时,比如: x := MyStruct{} 编译器会在编译阶段决定这个对象是: 分配在栈上(stack allocation) 还是分配在堆上(heap allocation) 这个决策是通过 逃逸分析(Escape Analysis) 完成的。 3.栈分配的过程 如果编译器认为对象只在当前函数作用域内使用,不会被外部引用: 这个对象会直接分配在栈上; 不会调用运行时的内存分配器; 栈内存是随函数调用帧自动增长/释放的; GC 不需要扫描或回收它。 > 关键:栈分配是编译期确定的,性能最好。 4.堆分配的过程 如果对象被闭包引用、返回地址或传递给其他 goroutine,则会发生逃逸: 编译器在生成代码时,会调用运行时的分配器 runtime.mallocgc; mallocgc 会从当前 P 的 mcache 尝试获取一个合适的 span; 若 mcache 缓存不足,就从 mcentral → mheap 逐层申请; 分配完成后,GC 会在堆上追踪这个对象。 >关键:堆分配是运行时动态完成的,涉及 GC 管理。 5.回收阶段 当对象不再被引用时,GC 会标记并清除; 被清除的内存重新回收到 mcache / mcentral / mheap; 长期未使用的页可能由scavenger(拾荒器)归还给 OS。 6.对比栈和堆分配 类型 分配阶段 分配位置 分配速度 是否由 GC 管理 是否逃逸 栈分配 编译期(静态) 每个 goroutine 的调用栈 极快 否 否 堆分配 运行时(动态) 运行时堆(mheap) 慢 是 是

2025年10月14日 · Mumu

如何保证 Redis 与数据库的数据一致性?

1.为什么会不一致 ? Redis 是缓存层,数据库是持久层。 二者数据可能不一致的原因包括: 更新数据库成功,但更新缓存失败; 缓存提前过期; 并发写操作覆盖(顺序问题); 异步更新延迟。 2.更新策略 2.1 Cache Aside(旁路缓存) 读操作 1. 读缓存 2. 如果缓存不存在 ,再读数据库 3. 将数据写入缓存(设置过期时间) 写操作: 先更新数据库,再删除缓存 缺点:删除缓存可能失败;删除顺序不当会不一致 2.2 Read/Write Through(读写穿透) 应用不直接访问DB,所有读写都经由缓存代理完成 缺点:实现复杂,性能略低 2.3 Write Behind(异步写回) 只写缓存,由缓存异步刷回数据库 缺点:容易丢数据 3.处理并发问题 3.1延迟双删策略(Double Delete) 1. 更新数据库; 2. 删除缓存; 3. 延迟 500ms 再删一次缓存。 //可以应对并发中缓存被“脏写”回的情况。 3.2异步消息队列(MQ) 数据更新时发送 MQ 消息,异步同步缓存状态。 3.3分布式锁 保证更新操作串行执行,避免交叉覆盖。

2025年10月14日 · Mumu

什么是“重锁” Heavy Lock?

1.什么是“重锁”(Heavy Lock) 在 Go 性能调优或并发编程中,我们常说的 “重锁”(heavy lock)不是官方术语,而是一个工程上的概念,指的是:锁竞争严重、临界区较大、持锁时间较长的互斥锁(sync.Mutex)。 1.多个 goroutine 同时频繁地去争夺同一把锁; 2.加锁的代码块中做了比较“重”的操作(比如 I/O、JSON 编码、数据库操作); 导致 goroutine 阻塞、上下文切换频繁,最终造成性能瓶颈。 2.为什么会出现“重锁”问题 1.临界区太大(锁保护的范围过广); 2.频繁写操作导致锁争用; 3.使用全局变量或共享状态; 4.没有分片(sharding)或局部化锁机制; 5.锁中包含耗时操作(例如网络请求、磁盘 I/O)。 var mu sync.Mutex var cache = make(map[string]string) func Set(k, v string) { mu.Lock() defer mu.Unlock() cache[k] = v } #当高并发调用 Set() 时,所有 goroutine 都在争抢同一把 mu,这就形成“重锁”。 3.优化思路与替代方案 3.1 使用 sync.Map 适用于读多写少的场景: var m sync.Map m.Store("a", 1) v, _ := m.Load("a") #sync.Map 内部采用分片和原子操作,避免了全局锁竞争。 3.2 使用原子操作(sync/atomic) 适用于简单的计数、标志位等操作: var count int64 atomic.AddInt64(&count, 1) #无锁化操作,性能更高,且不阻塞其他 goroutine。 3.3 优化锁粒度(细化锁) 将一把全局锁拆分成多把局部锁: ...

2025年10月14日 · Mumu

什么是零拷贝?

零拷贝(Zero-Copy)是一种计算机操作技术,主要应用于高性能网络和文件 I/O 领域。它的核心目标是减少 CPU 在传输数据时进行不必要的内存数据拷贝,以及减少用户空间和内核空间之间的上下文切换次数。 1.核心原理 ? 在传统的 I/O 操作中,数据通常需要经历四次拷贝才能完成传输(例如将文件通过网络发送给客户端): 第一次拷贝: 数据从磁盘读取到操作系统内核的缓冲区(通常是 Page Cache)。 第二次拷贝: 数据从内核缓冲区拷贝到应用程序的用户缓冲区。 第三次拷贝: 数据从用户缓冲区拷贝回内核的 Socket 缓冲区。 第四次拷贝: 数据从 Socket 缓冲区拷贝到网络接口卡(NIC)的缓冲区,最终发送。 零拷贝技术通过特定的系统调用和硬件支持,消除了步骤 2 和 3 的 CPU 拷贝。 2.常见的零拷贝实现方式 2.1 sendfile 这是最常见的零拷贝实现,例如 Apache Kafka 和 Nginx 等 Web 服务器广泛使用它来高效传输文件数据。 实现机制: 它将数据从一个文件描述符直接传输到另一个文件描述符(例如从磁盘文件 FD 到网络 Socket FD)。 消除拷贝: sendfile 允许数据在内核缓冲区和 Socket 缓冲区之间直接传输,跳过了用户缓冲区,从而消除了两次 CPU 拷贝。 2.2 内存映射文件 (mmap) 内存映射文件技术通过 mmap 系统调用将文件内容直接映射到进程的虚拟地址空间。 实现: 应用程序通过指针直接读写映射的内存地址,而这个地址对应的物理内存正是内核缓冲区。 消除拷贝: 它消除了数据从内核缓冲区拷贝到用户缓冲区的步骤,因为内核缓冲区和用户空间共享了同一块物理内存。 3.零拷贝的优势 降低 CPU 开销: 减少了 CPU 进行数据拷贝的工作量。 减少延迟: 数据传输路径更短。 提高吞吐量: 特别适用于高并发、I/O 密集型的场景,如文件服务器、Web 服务器和消息中间件(如 Kafka)。

2025年10月14日 · Mumu