go中用atomic.compareandswapint64等函数实现内存级乐观锁,通过比较并交换原子操作判断状态是否被修改,适用于计数器、状态机等场景,失败需手动重试。

Go 里怎么用 CAS 实现内存级乐观锁
直接用 atomic.CompareAndSwapInt64 或对应类型函数,不是靠 sync.Mutex 加锁,而是靠“比较-交换”原子操作判断状态是否被他人改过。
典型场景是计数器、状态机(比如任务只允许从 pending 变成 running 一次)、资源抢占。别把它当通用互斥工具——它不阻塞,失败得你自己重试。
- 必须用指针传入变量地址,比如
&counter,传值会编译报错 - 旧值必须是「你认为当前应该有的值」,不是随便填的;填错就永远失败
- 整数类型要严格匹配:用
int64就不能传int32,否则类型不兼容 - 浮点数、结构体不支持原生 CAS,得转成
uint64再用atomic.CompareAndSwapUint64,但要注意字节序和对齐风险
var state int64 = 0 // 0=pending, 1=running
if atomic.CompareAndSwapInt64(&state, 0, 1) {
// 成功抢到,开始执行
} else {
// 已被别人设为 1,放弃或重试
}
PostgreSQL 中用 WHERE version = ? 做数据库乐观锁
核心就是更新时带上版本号条件,让数据库自己判断“这行数据还是不是我读出来的那个版本”。不是靠 SELECT + UPDATE 两步,而是把校验压进 UPDATE 的 WHERE 子句里。
适用场景是业务逻辑简单、冲突不频繁、且能接受“更新失败后由应用层决定重试 or 报错”的流程。别在高并发抢同一行时硬上,容易反复失败。
立即学习“go语言免费学习笔记(深入)”;
- 字段类型推荐用
bigint(对应 Go 的int64),避免用timestamp做版本——精度和时钟漂移会导致误判 - UPDATE 必须返回影响行数:
sql.Result.RowsAffected(),等于 0 就代表 CAS 失败 - 别漏掉事务包裹:即使单条 UPDATE,也要在事务里做,否则可能和别的非事务操作产生中间态
- 注意 ORM 是否自动帮你加了 version 条件;GORM v2 默认不启用,得显式写
db.Where("version = ?", oldVersion).Update(...)
_, err := db.Exec("UPDATE orders SET status = ?, version = version + 1 WHERE id = ? AND version = ?",
"shipped", orderID, oldVersion)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows == 0 {
return errors.New("optimistic lock failed: version mismatch")
}
为什么不能直接用 SELECT ... FOR UPDATE 模拟 CAS
因为 SELECT ... FOR UPDATE 是悲观锁,会阻塞其他事务读写该行,违背乐观锁“先干再说、冲突再处理”的设计初衷。它解决的是另一类问题——确定要改,且不容许别人同时改。
在 API 网关、秒杀预扣库存这类场景下,用它反而会把吞吐压垮:一个慢查询卡住,后面全排队。
-
FOR UPDATE在事务提交前一直持锁,锁粒度还可能升级(比如从行锁变页锁),DBA 监控里容易看到锁等待飙升 - 它无法表达“只允许从 A 状态变到 B 状态”,只能保证“我改的时候没人动”,状态跃迁逻辑还得靠应用层二次校验,多一层风险
- 某些数据库(如 MySQL 的 READ COMMITTED)下,
FOR UPDATE不锁 gap,幻读仍可能发生,CAS 则天然规避这个问题
Golang struct 字段做 CAS 时最容易漏的坑
struct 本身不能直接原子操作,必须拆到字段级别。但很多人以为给 struct 加个 atomic.Value 就万事大吉,结果发现并发读写还是出错——因为 atomic.Value 只保证“设置整个值”是原子的,不保证 struct 内部字段修改安全。
- 如果只是读写整个 struct(比如配置快照),用
atomic.Value.Store/Load没问题;但一旦需要更新其中某个字段(比如cfg.Timeout++),就必须用独立的原子变量,或者加 mutex - 别把指针存进
atomic.Value后去改它指向的内容,那和没锁一样 - 嵌套 struct 更危险:外层用
atomic.Value,内层字段又被其他 goroutine 单独改,就会出现数据撕裂(tearing) - 生成代码时注意导出:未导出字段无法被
atomic.Value安全传递,运行时报cannot assign to unexported field
真正需要字段级 CAS 的,老老实实拆成 atomic.Int64、atomic.Pointer 这类专用类型,别图省事。










