
别再无脑上 Redis 分布式锁了99% 场景用DB 状态机 乐观锁就够了标签#并发控制#数据库设计#乐观锁#架构设计适合所有写过防并发代码的后端开发者一个尴尬的真实场景你正在写一个兑换码业务用户输入兑换码 → 校验 → 发放虚拟商品需求里有一行小字“同一兑换码同一时间只能被一个用户成功兑换”。99% 的开发者第一反应上 Redis 分布式锁lock:redisLock.Acquire(cdkey:key)deferlock.Release()// ... 业务逻辑写完很爽问题来了❌ 多了一个 Redis 依赖❌ Redis 挂了业务直接不可用❌ 锁超时怎么续期❌ 程序崩了锁怎么释放❌ 主从切换数据不一致怎么办❌ Redlock 算法争议至今没结论有没有更简单的方案有而且代码更少、依赖更少、性能更好。核心方案DB 状态机 乐观锁状态机设计把兑换码抽象成一个有 4 个状态的对象[0 失效] ──激活──→ [1 可用] ──抢锁──→ [2 锁定] ──完成──→ [3 已用] │ └──回滚──→ [1 可用]DB 表设计CREATETABLEexchange_codes(idBIGINTPRIMARYKEY,cdkeyVARCHAR(64),user_idVARCHAR(64)DEFAULT,-- 默认空字符串statusTINYINTDEFAULT1,-- 0失效 1可用 2锁定 3已用order_noVARCHAR(64),end_timeBIGINT,INDEXidx_cdkey(cdkey));三条核心 SQL-- ① 抢锁只有 user_id 才改成功UPDATEexchange_codesSETstatus2,user_id?WHEREid?ANDuser_id;-- ② 标记已用只有自己锁定的才能改UPDATEexchange_codesSETstatus3,order_no?WHEREid?ANDuser_id?ANDstatus2;-- ③ 解锁回滚自己锁定的才能解UPDATEexchange_codesSETstatus1,user_idWHEREid?ANDuser_id?ANDstatus2;为什么这就是锁关键点MySQL 的UPDATE是单语句原子操作。假设两个用户 A、B 同时来抢同一个兑换码时刻 t1: A 执行 UPDATE ... WHERE user_id → 影响行数 1抢到了 时刻 t2: B 执行 UPDATE ... WHERE user_id → 影响行数 0user_id 已经不为空了B 的 UPDATE 不会报错只是影响行数为 0。Go 代码里检查影响行数即可result:db.Exec(UPDATE ... WHERE id? AND user_id,id)ifresult.RowsAffected0{returnerrors.New(已被抢)// 没抢到}// 抢到了继续业务完整 Go 代码const(StatusDisabled0StatusAvailable1StatusLocked2StatusUsed3)// 抢锁funcLock(userIdstring,codeIduint)bool{result:db.Table(exchange_codes).Where(id ? AND user_id ,codeId).Updates(map[string]interface{}{status:StatusLocked,user_id:userId,})returnresult.RowsAffected0}// 标记已用funcMarkUsed(codeIduint,userId,orderNostring)bool{result:db.Table(exchange_codes).Where(id ? AND user_id ? AND status ?,codeId,userId,StatusLocked).Updates(map[string]interface{}{status:StatusUsed,order_no:orderNo,})returnresult.RowsAffected0}// 解锁回滚funcUnlock(userIdstring,codeIduint)bool{result:db.Table(exchange_codes).Where(id ? AND user_id ? AND status ?,codeId,userId,StatusLocked).Updates(map[string]interface{}{status:StatusAvailable,user_id:,})returnresult.RowsAffected0}完整业务流程funcExchange(userId,cdkeystring)error{record,_:GetByKey(cdkey)// 1. 抢锁if!Lock(userId,record.Id){returnerrors.New(被人抢了)}// 2. 调下游支付/发货orderNo,err:pay.CreateOrder(userId,record.ItemId)iferr!nil{Unlock(userId,record.Id)// 失败回滚returnerr}// 3. 标记已用if!MarkUsed(record.Id,userId,orderNo){returnerrors.New(标记失败)}returnnil}对比 Redis 分布式锁DB 状态机Redis 锁额外依赖无DB 本来就有必须 Redis故障点DB 挂了大家都挂合理Redis 挂了 锁失效锁超时不需要状态字段永久必须设 TTL可能要续期死锁风险无程序崩了锁锁住直到 TTL性能单条 UPDATE毫秒级一次 SETNX 一次 DEL业务幂等天然带已完成状态需额外幂等表主从一致性DB 保证Redlock 有争议适用场景✅ 适合用 DB 状态机的场景业务对象有明确状态流转订单待支付/已支付/已取消、兑换码、限量商品、抽奖资格并发量在万级 QPS 以下DB 完全扛得住需要审计 / 历史追溯状态字段天然就是审计日志❌ 不适合的场景没有具体业务对象的纯互斥比如全局只能有一个定时任务在跑→ 用分布式锁超高并发 极短锁定时间比如秒杀 100w QPS→ Redis Lua跨多张表的复杂临界区→ 数据库行锁 / 分布式事务进阶和幂等性的关系DB 状态机还有一个白送的好处天然幂等。// 用户重复点击兑换按钮Exchange(userId,cdkey)// 第一次成功Exchange(userId,cdkey)// 第二次状态已是 StatusUsedLock 返回 false不需要额外建幂等表、写幂等键。小结错误认知正确认知并发控制 分布式锁单条 UPDATE 也是锁Redis 锁更专业DB 锁更简单更可靠加锁 性能差简单状态判断 索引就够写并发代码的优先级单语句原子操作 DB 行锁/状态机 分布式锁 分布式事务能用简单方案就别上复杂方案。少一个组件少一个故障点。下一篇“先调下游再写本地”——为什么这个顺序如此重要如果觉得有用点赞收藏关注 ⭐