Posted in

文件锁失效的5种隐蔽原因(fd复用、子进程继承、容器挂载、信号中断…)

第一章:Go语言写文件是否需要加锁?——一个被长期误读的核心命题

Go语言中对同一文件的并发写操作是否必须加锁,取决于具体场景而非语言本身强制要求。标准库 os.File 的 Write 方法在底层调用系统 write(2) 系统调用,而 POSIX 保证对普通文件的 write() 是原子的——但仅限于单次系统调用内、且写入数据不超过 PIPE_BUF(通常为 4096 字节)。超过该长度或多次 Write 调用,就无法保证内容不交错。

并发写入的典型风险场景

  • 多个 goroutine 同时调用 file.Write([]byte("log entry\n"))
  • 使用 fmt.Fprintln(file, msg) —— 内部先写内容再写换行符,两步非原子
  • 追加模式(os.O_APPEND)下,内核自动定位到文件末尾再写入,此时 write 是原子的;但若手动 Seek() 后 Write(),则完全失去原子性保障。

验证竞态的最小复现代码

package main

import (
    "os"
    "sync"
)

func main() {
    f, _ := os.Create("race_test.txt")
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 非原子:先写数字,再写换行符
            f.WriteString(string(rune('0' + id%10)))
            f.WriteString("\n") // 可能与其他 goroutine 的写入交错
        }(i)
    }
    wg.Wait()
    f.Close()
}

运行后 race_test.txt 中可能出现 0\n1\n2\n01\n3\n 类似碎片化行——证明无锁并发写存在数据交错。

安全策略选择表

场景 是否需显式加锁 推荐方案
单 goroutine 写 否 直接使用 os.File
多 goroutine 追加日志 否(内核保证) os.OpenFile(..., os.O_APPEND|os.O_WRONLY|os.O_CREATE)
多 goroutine 随机写/覆盖 是 sync.Mutex 或 sync.RWMutex
高吞吐结构化日志 是(推荐封装) 使用带缓冲与锁的日志 writer 封装体

关键结论:加锁不是 Go 的语法要求,而是由文件 I/O 的语义边界和 POSIX 行为共同决定的设计责任。

第二章:文件锁失效的底层机制剖析

2.1 文件描述符复用导致锁状态丢失:dup/dup2与close的隐式语义陷阱

Linux 中文件描述符(fd)是内核对打开文件的引用,而 fcntl() 加持的建议性文件锁(F_SETLK)绑定于 fd 表项本身,而非底层 struct file。dup() 和 dup2() 复制的是 fd 引用,但新 fd 拥有独立的锁状态视图——锁不继承。

锁状态隔离的本质

  • 内核中每个 struct file 维护一套锁链表;
  • 每个 fd 条目(task_struct->files->fdt->fd[])指向同一 struct file 时,fcntl(F_SETLK) 仅作用于当前 fd 条目;
  • close() 仅释放 fd 条目,不触碰 struct file,更不自动释放其关联锁。

典型误用场景

int fd = open("data.txt", O_RDWR);
fcntl(fd, F_SETLK, &(struct flock){.l_type = F_WRLCK}); // 加写锁
int fd2 = dup(fd); // fd2 无锁!
close(fd);         // 仅释放 fd 条目,锁仍挂在 struct file 上 → 但已无主控 fd!
// 此时 fd2 无法操作锁,原锁处于“悬空”状态,其他进程可成功加锁

逻辑分析:dup() 返回新 fd,但未复制锁状态;close(fd) 移除 fd 条目后,若无其他 fd 指向该 struct file,内核在最后 close() 时才清理锁。此前,锁持续存在却不可管理。

锁生命周期对照表

操作 是否影响锁状态 是否释放 struct file
dup(fd) 否 否
close(fd) 仅当 refcount=0 时清除锁 仅当 refcount=0 时释放
fcntl(fd, F_UNLCK) 是(显式清除) 否
graph TD
    A[open → fd1] --> B[fcntl F_SETLK]
    B --> C[fd1 持有锁视图]
    C --> D[dup → fd2]
    D --> E[fd2 无锁状态]
    E --> F[close fd1]
    F --> G[锁仍驻留 struct file,但不可控]

2.2 子进程继承fd引发的锁竞争:fork/exec后flock/fcntl锁的继承性与释放边界

锁继承行为差异

flock() 和 fcntl(F_SETLK) 在 fork() 后表现迥异:

  • flock():可继承,子进程共享同一内核文件锁(advisory lock),但 exec() 后仍保留;
  • fcntl():可继承 fd,但锁不继承——子进程需显式调用 F_SETLK 才能持锁。

典型竞态场景

int fd = open("/tmp/lockfile", O_RDWR);
flock(fd, LOCK_EX);  // 父进程加锁
if (fork() == 0) {
    execl("/bin/cat", "cat", "/tmp/lockfile", (char*)NULL); // exec 后仍持有 flock 锁!
}

flock 锁绑定于 open file description,fork() 复制 fd 指向同一描述符,故锁状态共享;exec() 不关闭 fd,锁持续有效。而 fcntl 锁绑定于 fd 本身,子进程虽继承 fd,但锁需重申请。

锁释放边界对比

锁类型 fork() 后是否继承锁 exec() 后是否保留锁 释放触发条件
flock() ✅ 是 ✅ 是 close() 或进程终止
fcntl() ❌ 否(需重设) ❌ 否(fd 保留但锁丢失) close() 或显式 F_UNLCK
graph TD
    A[fork()] --> B[子进程 fd 指向同一 open file description]
    B --> C{flock?} --> D[锁状态共享]
    B --> E{fcntl?} --> F[锁未复制,需重加]

2.3 容器环境挂载点隔离对锁可见性的影响:overlayfs、bind mount与/proc/self/fd路径歧义

在容器中,/proc/self/fd 指向的文件描述符路径受挂载命名空间影响,导致同一 inode 的锁状态在不同视图下不可见。

锁可见性断裂根源

  • overlayfs 下,upper/lower 层文件虽共享 inode 号,但 flock() 作用于文件描述符而非路径,跨层打开的 fd 视为独立锁域;
  • bind mount 若未使用 --rbind 或 MS_REC,子挂载点不继承父挂载的锁上下文;
  • /proc/self/fd/N 解析时穿越挂载点边界,可能返回 host 命名空间中的原始路径(如 /var/lib/docker/...),而容器内进程无法感知该路径的锁持有者。

典型复现代码

# 容器内执行(overlayfs + bind-mounted /data)
ls -l /proc/self/fd/3  # 输出:/data/lockfile (deleted) —— 实际指向 lowerdir 中已 unlinked 的 inode

此输出表明 fd 3 持有已删除文件的句柄,其 flock() 状态仅对该 fd 生效,其他通过 bind mount 路径 /data/lockfile 重新 open() 的 fd 不参与同一锁竞争。

挂载类型 锁域隔离性 /proc/self/fd/N 解析路径归属
overlayfs upper 强隔离 upperdir 绝对路径
bind mount(private) 进程级隔离 容器 rootfs 下路径
rbind + shared 跨命名空间可见 主机真实路径
graph TD
    A[进程调用 flock on /data/lock] --> B{fd 打开路径来源}
    B -->|overlayfs upper| C[锁仅对该 upper fd 有效]
    B -->|bind mount private| D[宿主机同路径 fd 不感知锁]
    B -->|/proc/self/fd/3| E[解析为 lowerdir inode<br>但锁元数据未跨层同步]

2.4 信号中断(EINTR)未重试导致锁获取失败:syscall.Syscall与runtime.LockOSThread协同失效场景

当 syscall.Syscall 被信号中断时,Linux 返回 EINTR 错误码,但 Go 标准库部分低层调用未自动重试。若此时恰与 runtime.LockOSThread() 协同使用(如绑定 goroutine 到 OS 线程执行系统级同步),线程可能因 EINTR 提前退出临界区,导致锁状态不一致。

典型失效路径

// 错误示例:忽略 EINTR 的 syscall 尝试获取 futex 锁
_, _, errno := syscall.Syscall(syscall.SYS_futex, uintptr(unsafe.Pointer(&addr)),
    _FUTEX_WAIT_PRIVATE, 0, 0, 0, 0)
if errno != 0 && errno != syscall.EINTR {
    panic("futex wait failed")
}
// ❌ 缺失 EINTR 重试逻辑 → 锁未获取即返回

此处 errno == syscall.EINTR 时应循环重试,否则 LockOSThread 绑定的线程可能在未持锁情况下继续执行,破坏独占语义。

关键参数说明

参数 含义 常见值
SYS_futex Linux futex 系统调用号 202 (x86_64)
_FUTEX_WAIT_PRIVATE 私有 futex 等待操作
addr 用户态锁变量地址 &state

失效链路(mermaid)

graph TD
A[Signal arrives] --> B[Syscall interrupted]
B --> C{errno == EINTR?}
C -- No --> D[Error handled]
C -- Yes --> E[❌ No retry → early return]
E --> F[LockOSThread thread proceeds without lock]
F --> G[Data race or corruption]

2.5 Go运行时goroutine调度与系统调用阻塞的锁生命周期错配:非阻塞锁+channel协作模式的反模式实践

锁与goroutine生命周期的隐式耦合

当使用 sync.Mutex 配合 select + time.After 实现“带超时的非阻塞获取”时,极易误判阻塞根源——实际是系统调用(如 read())导致 M 被挂起,而 P 已被抢占,goroutine 无法及时唤醒释放锁。

典型反模式代码

func badPattern(conn net.Conn) {
    mu.Lock() // 若此时 conn.Read() 阻塞在 syscall,mu 会长期持有
    defer mu.Unlock()
    select {
    case <-done:
        return
    default:
        conn.Read(buf) // 系统调用阻塞 → goroutine 挂起 → 锁未释放
    }
}

逻辑分析:mu.Lock() 在 syscall 前获取,但 Go 运行时无法在内核态中断该系统调用;锁生命周期被意外延长至整个阻塞周期,违背“锁仅保护临界区”的设计契约。conn.Read() 应置于 select 分支中,由 runtime 管理 goroutine 状态迁移。

正确协作模式对比

方式 锁持有时间 调度友好性 可取消性
Mutex + syscall 不可控(秒级) ❌(M 脱离 P) ❌
channel + context 纳秒级(仅临界区) ✅(runtime 接管) ✅
graph TD
    A[goroutine 尝试获取锁] --> B{syscall 阻塞?}
    B -->|是| C[OS 级挂起 M<br>Go runtime 失去控制]
    B -->|否| D[正常执行临界区]
    C --> E[锁长期占用→其他 goroutine 饥饿]

第三章:Go标准库与第三方锁方案的适用边界

3.1 os.File.Fd() + syscall.Flock的跨平台兼容性缺陷(Windows vs Linux vs macOS)

文件描述符语义差异

os.File.Fd() 返回底层文件描述符(fd),但其可移植性假定不成立:

  • Linux/macOS:fd 是内核级整数,syscall.Flock() 可直接作用于该 fd;
  • Windows:Fd() 返回的“句柄”实为 HANDLE(非 POSIX fd),syscall.Flock() 调用将静默失败或 panic(ENOSYS 或 EINVAL)。

syscall.Flock 行为对比

系统 Flock 支持 锁粒度 阻塞行为
Linux ✅ 原生 整个文件 syscall.LOCK_EX 可阻塞
macOS ✅ 兼容 整个文件 同 Linux
Windows ❌ 不支持 无对应系统调用 syscall.Flock 永远返回 ENOSYS
f, _ := os.OpenFile("data.txt", os.O_RDWR, 0644)
defer f.Close()
fd := f.Fd()
err := syscall.Flock(int(fd), syscall.LOCK_EX|syscall.LOCK_NB)
// ⚠️ 在 Windows 上:int(fd) 强转后为无效 fd,Flock 返回 ENOSYS
// Linux/macOS:成功获取建议性锁(advisory lock)

逻辑分析:syscall.Flock 是 POSIX 接口封装,依赖 fcntl(2) 系统调用。Windows 无 fcntl 实现,且 Go 的 syscall 包未桥接 Windows 的 LockFileEx,导致跨平台锁逻辑断裂。

替代路径示意

graph TD
    A[尝试 Flock] --> B{OS == “windows”?}
    B -->|Yes| C[使用 syscall.LockFileEx]
    B -->|No| D[保持 syscall.Flock]

3.2 fsnotify监听+原子重命名替代锁的工程权衡:基于eventual consistency的无锁设计验证

数据同步机制

利用 fsnotify 监听文件系统事件,配合 rename(2) 原子操作实现状态更新,规避分布式锁开销。关键在于接受短暂不一致,依赖最终一致性保障业务正确性。

核心实现片段

// 监听临时文件写入完成事件,触发原子重命名
watcher, _ := fsnotify.NewWatcher()
watcher.Add("/data/ingest/")
for {
    select {
    case event := <-watcher.Events:
        if event.Op&fsnotify.Write == fsnotify.Write && strings.HasSuffix(event.Name, ".tmp") {
            final := strings.TrimSuffix(event.Name, ".tmp")
            os.Rename(event.Name, final) // 原子替换,无需锁
        }
    }
}

os.Rename() 在同一文件系统内为原子操作;.tmp 后缀约定确保监听器仅响应已完成写入的文件;fsnotify.Write 过滤避免重复触发。

权衡对比

维度 传统文件锁方案 fsnotify + rename 方案
一致性模型 强一致性 最终一致性
故障恢复成本 需锁清理与状态回滚 无状态,重放即可
并发吞吐 锁争用瓶颈明显 线性可扩展

流程示意

graph TD
    A[Writer 写入 data.json.tmp] --> B[fsnotify 捕获 Write 事件]
    B --> C{是否以 .tmp 结尾?}
    C -->|是| D[原子 rename → data.json]
    C -->|否| E[忽略]
    D --> F[Consumer 读取最新 data.json]

3.3 github.com/fsnotify/fsnotify与github.com/gofrs/flock的集成实践与panic风险规避

文件监听与独占锁协同模型

fsnotify 监听文件系统事件,flock 确保同一时刻仅一个进程处理变更。二者需严格时序配合:先加锁,再读取/处理,最后解锁;否则竞态下可能触发 flock 的 nil pointer dereference panic(因 *flock.Flock 未初始化即调用 Unlock())。

关键防御措施

  • 使用 defer 时务必校验锁实例非 nil
  • fsnotify.Watcher 的 Events channel 需带缓冲并配超时 select
  • 错误路径必须统一 if err != nil { return },避免裸 panic()

典型安全封装示例

func safeHandleEvent(watcher *fsnotify.Watcher, lockPath string) error {
    fl, err := flock.NewFlock(lockPath)
    if err != nil {
        return fmt.Errorf("failed to init flock: %w", err)
    }
    if ok, _ := fl.TryLock(); !ok {
        return errors.New("lock acquired by another process")
    }
    defer func() {
        if fl != nil { // panic 风险规避核心:nil 检查
            fl.Unlock()
        }
    }()

    // ... 处理文件变更
    return nil
}

逻辑分析:fl 初始化失败时为 nil,直接 defer fl.Unlock() 将 panic;此处显式判空确保安全。TryLock() 返回 false 时不阻塞,避免死锁。

风险点 触发条件 缓解方式
nil pointer panic fl 为 nil 时调用 Unlock() defer 前判空
Watcher.Close() 泄漏 未关闭导致 fd 耗尽 defer watcher.Close()
graph TD
    A[收到 fsnotify Event] --> B{flock.TryLock?}
    B -->|true| C[安全处理文件]
    B -->|false| D[跳过,日志告警]
    C --> E[fl.Unlock()]
    D --> F[继续监听]

第四章:生产级文件并发写入的防御性架构设计

4.1 基于sync.Map+路径哈希的内存级写入队列:避免系统级锁争用的Go惯用法

传统并发写入常依赖 map + sync.RWMutex,高并发下读写锁成为瓶颈。sync.Map 提供无锁读、分片写语义,天然适配高频路径写入场景。

数据同步机制

路径字符串经 fnv64a 哈希后取模分片,映射到独立 sync.Map 实例,消除全局锁竞争:

func pathHash(path string) uint64 {
    h := fnv.New64a()
    h.Write([]byte(path))
    return h.Sum64() % uint64(shardCount)
}

fnv64a 高速且分布均匀;shardCount 通常设为 CPU 核心数,平衡负载与内存开销。

性能对比(10K 并发写入/秒)

方案 平均延迟 GC 压力 锁争用率
map+RWMutex 12.8ms 高 37%
sync.Map+路径哈希 1.3ms 低

写入流程(mermaid)

graph TD
    A[接收写入请求] --> B[计算路径哈希]
    B --> C[定位对应shard]
    C --> D[调用sync.Map.Store]
    D --> E[异步刷盘触发]

4.2 使用临时文件+os.Rename构建幂等写入:应对NFS、GlusterFS等分布式文件系统的锁不可靠性

数据同步机制的痛点

NFS v3/v4 及 GlusterFS 等分布式文件系统不保证 flock() 或 O_EXCL 的跨节点原子性,导致并发写入时出现竞态、数据截断或覆盖。

幂等写入的核心原理

利用 POSIX 文件系统对同一目录下 rename() 的原子性(即使跨存储节点),结合临时文件隔离写入过程:

func atomicWrite(path, content string) error {
    tmpPath := path + ".tmp"                 // 临时路径需与目标同挂载点
    err := os.WriteFile(tmpPath, []byte(content), 0644)
    if err != nil {
        return err
    }
    return os.Rename(tmpPath, path) // 原子替换,失败则原文件完好
}

✅ os.Rename 在同一文件系统内是原子操作,不受 NFS 锁失效影响;
⚠️ tmpPath 必须与 path 位于同一挂载点(否则 Rename 退化为 copy+delete);
🔒 写入权限需同时覆盖目标路径及父目录(rename 需父目录写权限)。

典型场景对比

场景 flock() O_EXCL tmp+Rename
单机本地文件系统 ✅ ✅ ✅
NFSv3 ❌(无状态) ❌(非原子) ✅(仅限同挂载点)
GlusterFS(AFR) ❌(不一致) ❌(元数据分裂) ✅(推荐方案)
graph TD
    A[应用发起写入] --> B[生成.tmp临时文件]
    B --> C[完整写入.tmp]
    C --> D[os.Rename atomically]
    D --> E[成功:.tmp消失,目标更新]
    D --> F[失败:.tmp残留,目标不变]

4.3 context.Context驱动的锁超时与取消:结合time.AfterFunc与defer unlock的资源泄漏防护

为什么传统 mutex.Unlock 可能失效

当 goroutine 在持有锁期间被阻塞或 panic,defer mu.Unlock() 未执行 → 锁永久占用 → 资源泄漏。

安全解锁的三重保障机制

  • context.WithTimeout 提供取消信号
  • time.AfterFunc 在超时后强制释放锁(兜底)
  • defer 确保正常路径下的优雅释放

核心实现代码

func guardedLock(ctx context.Context, mu *sync.Mutex) (unlock func(), err error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
        mu.Lock()
    }

    // 超时强制解锁(即使 defer 未触发)
    timer := time.AfterFunc(ctx.Deadline().Sub(time.Now()), func() {
        mu.Unlock() // 注意:此调用不保证原子性,仅作泄漏防护
    })

    unlock = func() {
        timer.Stop() // 防止重复/延迟触发
        mu.Unlock()
    }
    return unlock, nil
}

逻辑分析:

  • ctx.Deadline().Sub(time.Now()) 计算剩余超时时间,避免负值;
  • timer.Stop() 是关键防护:防止 unlock() 已执行后 AfterFunc 再次调用 Unlock() 导致 panic;
  • 函数返回 unlock 闭包,确保调用权交由上层控制,符合 RAII 惯用法。
风险场景 AfterFunc 补救 defer 保障 ctx.Done() 响应
goroutine panic ✅ ❌(跳过 defer) ❌
网络请求超时 ✅ ✅ ✅
正常流程完成 ❌(被 Stop) ✅ ❌
graph TD
    A[尝试获取锁] --> B{ctx.Done?}
    B -->|是| C[返回 ctx.Err]
    B -->|否| D[mu.Lock]
    D --> E[启动 AfterFunc 定时器]
    E --> F[返回 unlock 函数]
    F --> G[调用 unlock]
    G --> H[timer.Stop + mu.Unlock]

4.4 单机多实例场景下的Redis分布式锁兜底策略:Redlock在Go file write场景中的轻量适配

当单机部署多个Redis实例(如 redis-6379、redis-6380)模拟Redlock时,需规避主从延迟导致的锁失效风险。核心在于写操作原子性兜底——即使Redlock短暂失效,文件写入仍需幂等。

文件写入前的双重校验

  • 先尝试获取 Redlock(3个实例中2个成功即视为有效)
  • 再检查目标文件 last_modified 时间戳是否早于当前操作逻辑时间戳

轻量Redlock客户端封装(Go)

func TryAcquireRedlock(key string, ttl time.Duration) (string, bool) {
    // 使用 go-redsync 库简化实现,但此处仅连本地多端口实例
    nodes := []string{"127.0.0.1:6379", "127.0.0.1:6380", "127.0.0.1:6381"}
    var clients []*redis.Client
    for _, addr := range nodes {
        clients = append(clients, redis.NewClient(&redis.Options{Addr: addr}))
    }
    rs := redsync.New(clients...)
    mutex := rs.NewMutex(key, redsync.WithExpiry(ttl), redsync.WithTries(1))
    ok, err := mutex.Lock()
    return mutex.Value(), ok && err == nil
}

mutex.Value() 返回唯一令牌用于后续校验;WithTries(1) 避免重试放大竞争;ttl 建议设为写操作耗时的3倍(如 3s),防止死锁。

关键参数对照表

参数 推荐值 说明
ttl 3 * expectedWriteTime 防止写入阻塞导致锁长期持有
quorum 2(3实例) 满足多数派即可,兼顾性能与容错
graph TD
    A[Go Write Request] --> B{Try Redlock<br/>3 instances}
    B -->|Success ≥2| C[Check file mtime]
    B -->|Fail| D[Reject early]
    C -->|mtime < now| E[Atomic write + fsync]
    C -->|mtime ≥ now| F[Skip write]

第五章:结语——锁不是银弹,一致性才是目标

在高并发电商系统中,某次“618”大促期间,订单服务因过度依赖 synchronized 保护库存扣减逻辑,导致线程阻塞雪崩,TP99 延迟从 80ms 激增至 2.3s,最终触发熔断降级。事后复盘发现:加锁粒度覆盖了日志写入、缓存更新、消息投递等非核心路径,而真正需要强一致性的仅是“库存原子扣减+订单状态变更”这一对操作。

锁的代价远超直觉预期

以下为压测环境下不同同步策略的实测对比(QPS=3000,库存初始值1000):

策略 平均延迟(ms) 成功率(%) CPU占用率(%) 数据不一致次数
全局synchronized 412 92.3 94 17
Redis Lua脚本原子操作 87 99.99 68 0
分段库存+本地CAS 53 99.998 52 0

可见,粗粒度锁不仅拖慢响应,更因等待队列积压引发连锁超时。

一致性需分层设计而非统一加锁

某支付清分系统采用三级保障机制:

  • 强一致层:使用 PostgreSQL SELECT ... FOR UPDATE SKIP LOCKED 处理资金划转,配合唯一约束防止重复记账;
  • 最终一致层:通过 Debezium 捕获数据库变更,经 Kafka 投递至对账服务,补偿失败交易;
  • 监控兜底层:部署基于 Flink 的实时一致性校验作业,每5秒扫描账户余额与流水总和偏差,自动触发人工核查工单。
// 库存扣减的真实落地代码(Spring Boot + MyBatis)
@Update("UPDATE inventory SET stock = stock - #{quantity} " +
        "WHERE sku_id = #{skuId} AND stock >= #{quantity} AND version = #{version}")
int deductWithOptimisticLock(@Param("skuId") String skuId,
                             @Param("quantity") int quantity,
                             @Param("version") long version);

该SQL天然具备原子性与版本校验能力,避免了显式锁的复杂协调。

分布式场景下锁的脆弱性暴露

某跨地域物流调度系统曾使用 Redis Redlock 实现分布式锁,但在网络分区期间出现双主调度:北京节点判定上海节点失联后获取锁并派单,而上海节点因心跳包延迟未被剔除,同样获取锁并重复派单。最终通过引入 Paxos-based 分布式事务协调器(etcd Raft集群) 替代锁机制,将“任务分配决策”下沉至共识层,错误率归零。

flowchart LR
    A[客户端请求派单] --> B{是否已存在有效租约?}
    B -->|否| C[向etcd发起Propose]
    B -->|是| D[直接执行调度]
    C --> E[etcd Raft多数派确认]
    E --> F[写入/assign/{task-id} with lease]
    F --> G[返回租约ID]

当某次机房电力中断导致3台etcd节点宕机时,剩余2节点仍能通过Raft日志回放维持服务连续性,而Redlock在此场景下会因过半节点不可用而彻底失效。

锁的本质是人为引入的串行化瓶颈,而一致性是业务逻辑必须达成的状态契约。

一线开发者,热爱写实用、接地气的技术笔记。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注