并发控制与缓存一致性技术选型之权衡术

核心权衡点:锁的量级与性能损耗的权衡 选择哪种并发控制或一致性手段,本质是在 一致性强度 与 系统吞吐 / 延迟 之间做交易。不存在普适最优解,只有基于回源成本、并发量级和跨实例协调代价的按需组合。 量级轻 → 延迟低,但保护弱:进程内 singleflight,非阻塞软标记,纯 TTL 过期。 量级重 → 一致性强,但代价高:跨实例分布式锁,强同步写穿透。 下面从“数据流”与“控制面”两个维度梳理关键技术,并给出组合原则。 1. 数据一致性模式(如何同步缓存与数据库) 这些模式关注当源数据变更时,缓存如何更新或失效,不直接涉及并发控制,但影响选型。 Cache Aside(旁路缓存) 模式:读未命中则查 DB 并回写缓存;写直接更新 DB,然后删除缓存。 锁量级:写操作无锁,仅单一 DEL;读可配合 singleflight 防击穿。 权衡:最终一致窗口 = 删缓存到下次重建之间;删除失败需重试或 TTL 兜底。 适用:读多写少,可接受短暂不一致的场景(大多数互联网业务)。 Read/Write Through(读写穿透) 模式:缓存层代理所有 DB 读写,业务只与缓存交互。 锁量级:同步写,需保证缓存与 DB 的原子更新,往往引入分布式锁或事务消息。 权衡:一致性强,但每次写都要同时操作缓存+DB,写入延迟高,实现复杂。 Write Behind(异步回写) 模式:写只更新缓存,异步批量刷回 DB。 锁量级:缓存写入轻量,但需要队列/日志保证不丢数据。 权衡:写入性能极高,一致性很弱,允许丢数据窗口。 延迟双删与订阅刷新 延迟双删:写 DB 前先删缓存,DB 更新完成后延迟再删一次(用于规避主从延迟导致的不一致)。 Binlog 订阅(如 Canal):异步监听 DB 变更流水,精确删除或更新缓存。 锁量级:均为异步,无额外锁竞争,但引入消息延迟和架构复杂度。 本系统选择:Cache Aside + MQ 异步删除,利用已有死信队列保障最终一致性,兼顾简单和可靠。 2. 并发控制机制(如何防击穿、防并发重建) singleflight(进程内请求合并) 量级:极轻,无网络开销,仅内存 map + 阻塞等待。 保护范围:单进程内相同请求的合并。 性能损耗:少数请求等待首次执行完毕,延迟可忽略。 适用:高并发下同一批缓存键临时穿透 Redis 的场景。 分布式锁(如 Redis SET NX EX) 量级:重,涉及网络往返、轮询等待、锁 TTL 管理和 Lua 释放。 保护范围:跨实例互斥,确保单点重建。 性能损耗:锁竞争导致额外延迟(轮询 100ms+),可能成为瓶颈。 适用:回源成本极其高昂且要求强一致的场景(如复杂报表、详情缓存重建)。 软标记 / SETNX 提示(非阻塞跨实例通知) 量级:极轻,一次 Redis SETNX 无等待。 保护范围:跨实例“知情权”,让其他实例主动降级而非常规等待。 性能损耗:基本无延迟,仅需判断标记存在与否。 适用:重建允许降级或短暂不一致的场景(批量读、冷缓存预热)。 仅靠 TTL 量级:无。 保护:无任何并发控制。 损耗:零,但可能发生缓存击穿或雪崩。 适用:数据可大量容忍陈旧,或变更极低频。 3. 组合决策矩阵 场景 回源成本 推荐组合 理由 视频实体批量读取(高频、可降级) 中(批量主键查询) singleflight + 软标记 进程内去重 + 跨实例非阻塞防多余穿透 视频详情页读取(中频、不期望旧数据) 中高(复杂 SQL 或关联查询) 分布式锁 + double-check 强控单实例重建,避免多实例重复计算 关注流冷缓存重建(低频、允许最终一致) 高(多表聚合排序) 软标记 + 单一执行 不强制等待,接受少量重复重建成本优于锁等待 写操作后缓存失效(Cache Aside) 极低(DEL 命令) 无需锁,直接 DEL 并发写由 DB 串行化,缓存仅打扫战场 4. 一句话选型指南 先上 singleflight,解决绝大多数进程内并发穿透。 多实例部署且重建轻量时,补 软标记 提示降级,避免引入锁。 只有当“多个实例并发重建会造成无法接受的成本或数据矛盾”时,再引入 分布式锁。 缓存更新一律走 Cache Aside 异步删除,放弃复杂同步,靠重试和 TTL 兜底。 记住:锁是最后的手段,不是默认选项。 能靠最终一致和降级解决的,不要用强同步去惩罚高并发。

May 5, 2026 · 1 min · 165 words · Jamaisvu

悲观锁、乐观锁与三大分布式锁总结

1. 悲观锁与乐观锁 1.1 悲观锁 核心思想:默认一定会存在资源竞争,提前锁定资源独占使用,其他请求阻塞等待,使用完毕再释放锁 特点:先上锁,执行业务,阻塞其他并发请求 1.2 乐观锁 核心思想:默认并发冲突极少发生,不主动加排他锁,所有请求并行执行,出现冲突后再做重试、回滚处理 特点:执行业务在前,冲突处理在后,全程不阻塞其他请求 特别注意:纯版本号乐观锁,作用是解决多人并发修改冲突,无法用于拦截单人重复提交;误用会触发不必要的并发修改异常,业务语义错乱。 2. Redis 分布式锁 异步弱一致、看门狗续命、自旋抢锁、存在丢锁风险 存储唯一uuid标识锁持有者,防止误删除他人锁 2.1 数据结构 2.1.1 简易普通分布式锁(String 类型) 结构:String Key → Value Key:lock:stock:1001 Value:唯一随机 UUID(锁持有者标识) 附带:expire 自动过期时间 无内置版本号,value仅做身份标识,无序无递增时序 2.1.2 Redisson 可重入分布式锁(Hash 类型) Hash Key:lock:stock:1001 Hash Field:客户端ID + 线程ID Hash Value:锁重入次数 附带:全局过期时间 示例: lock:stock:1001 ├─ client001-thread1 : 2 // 重入2次 └─ client002-thread2 : 0 2.2 实现流程 2.2.1 抢锁 生成唯一随机token; 执行原子命令 SETNX key token EX 过期时间 key:锁名称 value:唯一随机标识 NX:不存在才创建 EX:过期时间秒 SET lock:order:1001 uuid_8888 NX EX 30 命令返回成功 = 抢到锁,失败 = 未抢到。 ...

May 4, 2026 · 2 min · 240 words · Jamaisvu

golang并发编程

几个概念 并行和并发 并行 多线程程序在多个核的cpu上运行,就是并行 并发 多线程程序在一个核的cpu上运行,就是并发 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。比如你运行一个 Go 程序 go run main.go,操作系统就会创建一个进程。每个进程有自己独立的内存空间、文件描述符、环境变量等,不同进程之间的资源默认是隔离的。 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。多个线程共享所属进程的所有资源(内存、文件句柄等) C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。 协程和线程 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。 Goroutine 启动单个goroutine goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。 启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。 1func hello() { 2 fmt.Println("Hello Goroutine!") 3} 4func main() { 5 go hello() 6 fmt.Println("main goroutine done!") 7 time.sleep(time.Second) 8} 执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。 启动多个goroutine 1var wg sync.WaitGroup 2 3func hello(i int) { 4 defer wg.Done() // goroutine结束就登记-1 5 fmt.Println("Hello Goroutine!", i) 6} 7func main() { 8 9 for i := 0; i < 10; i++ { 10 wg.Add(1) // 启动一个goroutine就登记+1 11 go hello(i) 12 } 13 wg.Wait() // 等待所有登记的goroutine都结束 14} 若主协程退出,其他任务被迫终止。没有主协程,新协程也不进行。 ...

April 24, 2026 · 5 min · 1006 words · Jamaisvu

Golang八股文

本文持续更新 堆、栈 它们是程序运行时,两块用途完全不同的内存区域。 栈(Stack)—— 函数的临时小抽屉 每个函数运行时,都会在栈上占一小块空间; 函数里的局部变量、参数默认都往栈上放; 函数一结束,栈空间自动清空,变量直接销毁; 速度极快,几乎零开销; 不需要 GC 垃圾回收。 特点:自动申请、自动释放,快、小、临时。 堆(Heap)—— 全局公共大仓库 一块共享的大内存; 放那些函数结束后还需要活着的变量; 不会自动销毁,靠 Go 的 GC 来回收; 分配慢、寻址慢、有 GC 开销。 特点:手动 / 编译器决定分配,GC 回收,慢、大、持久。 在栈还是堆 直接用 Go 自带的逃逸分析命令看: 1go build -gcflags="-m" main.go 输出里会出现两种关键行: does not escape → 变量在 栈 上 escapes to heap → 变量逃逸到 堆 上 slice 数据结构 slice是引用类型,共享内存地址 切片扩容 只有append才会触发扩容!手动make只是新建切片。 扩容会彻底替换底层数组: 计算新的容量(按增长规则) 在堆上新建一个更大的底层数组 把旧数组的所有数据完整拷贝到新数组 切片指针指向新数组,更新 len/cap 旧数组如果没有其他引用,会被 GC 自动回收 容量增长规则(Go 1.18+ 最新版) 小切片快速扩容,大切片平缓扩容,避免内存浪费 ...

April 22, 2026 · 5 min · 1000 words · Jamaisvu

Hugo API:快速发布你的hugo文章!

这是博主开源到github的另一个项目,此处贴的是使用文档。如果是跟我一样使用hugo建站并且苦于如何发布的小伙伴们,我强力推荐这个api,你会用上的!! ✿✿✿来支持一波吧✿✿✿ 👇👇👇 grayfalcon666/hugo_api: Quickly publish posts for your Hugo blog! preview: 一个用 Go 编写的轻量级 API 服务,支持通过 表单提交(直接复制 Markdown)快速创建 Hugo 静态博客文章,自动生成 Front Matter 并触发 Hugo 构建,无需手动操作文件或执行命令。 🌟 核心功能 发送文章与动态: /api/hugo/create-post /api/hugo/create-moment 外置config文件: 可自定义文章发布路径、密钥、api监听端口号 自动处理: 生成 Hugo 标准 Front Matter(标题、时间、标签、分类等) 自动触发 Hugo 构建,发布后立即生效 🚀 快速开始 1. 克隆仓库到本地 2. 配置 config.json 在项目根目录创建 config.json 文件,按实际环境填写配置: 1{ 2 "api_key": "your-strong-secret-key", 3 "hugo_content_path": "/home/user/blog/content/posts", 4 "hugo_moment_path": "/home/user/blog/content/moments", 5 "hugo_project_path": "/home/user/blog", 6 "hugo_exec_path": "/usr/local/bin/hugo", 7 "listen_addr": ":8080" 8} 3. 编译与启动 1# 编译(生成可执行文件) 2go build -o hugo-api hugo-api.go 3 4# 启动服务 5./hugo-api 后台运行 linux 写一个系统服务即可,以下为示例: ...

October 6, 2025 · 2 min · 278 words · Jamaisvu