← 返回题目列表

如何用数据库实现分布式锁?有哪几种方式?

高频 中等 第 6 / 26 题 更新于 2026/07/28
数据库分布式锁悲观锁乐观锁

简化版

用数据库实现分布式锁主要有三种方式:① 唯一索引(插入即加锁)——往锁表插一条记录,唯一约束保证只有一个能插成功;② 悲观锁(SELECT ... FOR UPDATE——用行锁锁住某行,事务提交才释放;③ 乐观锁(版本号)——update ... where version=?,靠 CAS 更新成功与否判断。数据库锁实现简单、无需额外中间件,但性能差、有单点风险、防死锁麻烦,一般只在并发不高或作为兜底方案时用。

详细版

① 唯一索引方式:建一张锁表,method_name 加唯一索引。加锁 = INSERT 一条记录,插入成功即获得锁,插入失败(唯一冲突)即锁被占。解锁 = DELETE 这条记录。

  • 优点:实现简单。
  • 缺点:非阻塞(失败要自己轮询重试)、不可重入(要额外记录持有者+计数)、没有过期机制(持锁进程宕机不删记录会死锁,要靠定时任务清理超时记录)。

② 悲观锁 SELECT ... FOR UPDATE:在事务里 SELECT * FROM lock_table WHERE id=? FOR UPDATE,数据库给这行加排他行锁,其他事务的 FOR UPDATE 会阻塞等待,直到本事务提交/回滚释放锁。

  • 优点:阻塞等待天然、由数据库管理锁、事务结束自动释放(防死锁较好)。
  • 缺点:占用数据库连接和行锁,高并发下连接池易耗尽;要确保走索引,否则可能升级为表锁。

③ 乐观锁(版本号 / CAS)UPDATE t SET data=?, version=version+1 WHERE id=? AND version=?,影响行数 = 1 表示抢到、= 0 表示被别人改过(没抢到)。

  • 优点:不加锁、并发高。
  • 缺点:冲突多时大量重试;严格说更像「无锁并发控制」而非互斥锁。

完整版教学

一、唯一索引方式:把「加锁」变成「插入」

最朴素的思路:建一张 distributed_lock 表,对「锁的名字」字段加唯一索引。谁能成功 INSERT 一行,谁就持有锁;因为唯一约束,同名的第二次插入会失败,就是「锁被占」。解锁就是 DELETE 掉那行。这利用了数据库唯一索引的原子性来实现互斥,简单直接。但缺陷明显:

  • 没有过期时间:如果持锁进程 INSERT 后就宕机,那行记录永远在,锁永远不释放 → 死锁。补救:记录加锁时间,用定时任务清理「超时未释放」的锁记录(但清理阈值不好定,和 Redis 过期时间是同样的两难)。
  • 非阻塞:插入失败只能返回失败,客户端要自己轮询重试,不像 FOR UPDATE 能阻塞排队。
  • 不可重入:默认不支持,要额外存持有者标识和计数。

二、悲观锁:借用数据库的行锁

SELECT ... FOR UPDATE 直接复用数据库自带的排他行锁。在一个事务里对某行加 FOR UPDATE,别的事务想对同一行 FOR UPDATE 就得排队等,直到本事务 COMMIT/ROLLBACK。好处是「阻塞排队」和「事务结束自动释放锁」都由数据库天然提供,比唯一索引方式更完善(宕机时事务回滚会释放锁,防死锁更好)。注意:

  • 查询条件必须走索引,否则 MySQL(InnoDB) 可能把行锁升级成表锁,锁住整张表,并发雪崩。
  • 整个持锁期间占用一个数据库连接和事务,持锁时间长会耗尽连接池。

三、乐观锁:其实是「无锁并发控制」

版本号方式严格说不是「互斥锁」,而是「冲突检测」:读数据时带出 version,更新时 WHERE version=旧值,如果这期间没人改过(version 还是旧值)就更新成功、version+1;如果被别人改过(version 变了),影响行数为 0,说明冲突,本次失败、需重试或放弃。它没有「阻塞等待」,靠「更新成功与否」来仲裁并发。适合冲突不激烈的更新场景(如库存扣减),冲突激烈时大量重试反而更慢。

记忆点:唯一索引 = 插入即加锁(要自己搞过期);FOR UPDATE = 借数据库行锁阻塞排队(事务结束自动释放);版本号 = 无锁 CAS 冲突检测。前两种是真互斥,第三种是乐观并发。

四、数据库锁的整体短板

  • 性能差:数据库本身是稀缺资源,用它做锁会占用连接、行锁、事务,QPS 远不如 Redis(内存)。
  • 单点问题:单库是单点,挂了锁服务就没了(要靠数据库高可用兜底)。
  • 防死锁麻烦:唯一索引方式没有自动过期,要额外定时清理。
  • 可重入、阻塞、公平等特性都要自己费劲实现。

所以数据库锁一般只在:并发量低、不想引入 Redis/ZK 中间件、或作为其他锁方案的兜底时使用。高并发场景优先 Redis/ZooKeeper。

五、数据库锁落地自检

数据库锁适合低频、短事务、已有数据库强约束的场景,不适合高频热点抢锁。落地时至少要检查 3 件事:锁表的唯一键是否命中、事务是否足够短、持锁方宕机后是否有过期字段或清理任务。否则一个残留锁记录就可能让后台任务停摆几十分钟。

六、常见误区与追问

这道题不能只背概念,要把「数据库分布式锁」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。

回答层次要讲清的内容容易漏掉的边界
核心结论数据库锁可通过唯一约束、悲观锁或乐观锁实现互斥,简单可靠但性能和死锁风险要控制不要停在名词解释
流程机制尝试插入唯一锁记录或 select for update -> 成功进入临界区 -> 提交或删除记录释放 -> 失败重试或返回 -> 通过超时字段清理过期锁说明触发方、参与方、状态变化和兜底
工程取舍插入 lock_name=jobA 的唯一记录成功表示拿锁,重复插入失败表示锁被占用分布式锁只能控制临界区进入,不能替代业务幂等和状态校验
数据库分布式锁 面试拆解:
1. 尝试插入唯一锁记录或 select for update
2. 成功进入临界区
3. 提交或删除记录释放
4. 失败重试或返回
5. 通过超时字段清理过期锁

记忆钩子:先说为什么单机锁失效,再说明加锁、过期、续期、解锁、防误删和故障恢复;回答时要紧扣「数据库分布式锁」这道题,不要把相邻概念混成一段泛泛的分布式套话。

  • 误区:数据库锁一定比 Redis 锁安全。 数据库事务强但也有死锁、连接耗尽和性能瓶颈。
  • 误区:唯一约束锁不需要过期。 持锁进程宕机后记录可能残留,需要 owner、过期时间和清理。
  • 误区:乐观锁就是分布式锁。 乐观锁适合并发更新冲突检测,不一定提供进入临界区前的互斥。
  • 追问:数据库锁有哪些实现? 唯一索引插入、select for update、version 乐观锁。
  • 追问:适合什么场景? 低频后台任务、管理操作、已有数据库依赖且并发不高的场景。
  • 追问:如何防死锁? 固定加锁顺序、短事务、超时、索引命中和异常释放。

七、加强记忆

数据库分布式锁三种:唯一索引(INSERT 成功即持锁、DELETE 解锁,缺过期机制要定时清理、非阻塞不可重入)、悲观锁 SELECT...FOR UPDATE(借数据库排他行锁阻塞排队、事务结束自动释放、须走索引防表锁)、乐观锁版本号update...where version=? 的 CAS 冲突检测,不阻塞、适合低冲突)。整体优点是简单无需中间件,缺点是性能差、单点、防死锁麻烦,只适合低并发或兜底,高并发用 Redis/ZK。