适用读者 :后端开发工程师、系统架构师
前置知识 :Redis 基础、MySQL 事务、并发编程
更新日期:2026 年 2 月
一、什么是分布式锁?
在单机应用中,我们使用 synchronized、ReentrantLock 等机制保证线程安全。但在分布式系统中,多个服务实例部署在不同机器上,JVM 锁无法跨进程生效。
分布式锁 就是为了解决这一问题而生的------它是一种跨进程、跨机器的互斥机制,确保同一时刻只有一个客户端能执行某段关键代码。
🎯 核心要求
- 互斥性:同一时间只能有一个客户端持有锁
- 高可用:锁服务不能成为单点故障
- 防死锁:客户端崩溃后锁能自动释放
- 可重入性(可选):同一线程可重复获取锁
- 高性能:加锁/解锁延迟低
二、为什么不用数据库行锁或 Redis 单命令?
- MySQL 行锁:仅限于事务内,无法跨事务持有
- Redis SETNX :看似简单,但存在原子性缺失 和死锁风险
✅ 正确的分布式锁必须满足 "加锁 + 设置过期时间" 原子操作!
三、方案一:基于 Redis 的分布式锁(推荐)
Redis 因其高性能、原子操作支持,成为分布式锁的首选方案。
3.1 基础实现(SET 命令)
Redis 2.6.12+ 支持 SET key value [EX seconds] [PX milliseconds] [NX|XX],可原子地设置值并加过期时间。
python
import redis
import time
import uuid
class RedisDistributedLock:
def __init__(self, redis_client, lock_key, expire_time=30):
self.redis = redis_client
self.lock_key = lock_key
self.expire_time = expire_time # 锁自动过期时间(秒)
self.lock_value = str(uuid.uuid4()) # 唯一标识,用于防止误删
def acquire(self, timeout=10):
"""
获取锁
:param timeout: 尝试获取锁的最长时间(秒)
:return: True/False
"""
start_time = time.time()
while time.time() - start_time < timeout:
# 原子操作:仅当 key 不存在时设置,并设置过期时间
if self.redis.set(
self.lock_key,
self.lock_value,
ex=self.expire_time, # 自动过期,防止死锁
nx=True # 仅当 key 不存在时设置
):
return True
time.sleep(0.01) # 避免 busy-wait
return False
def release(self):
"""释放锁"""
# 使用 Lua 脚本保证"判断 + 删除"原子性
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis.eval(lua_script, 1, self.lock_key, self.lock_value)
3.2 关键设计解析
✅ 为什么用 UUID 作为 value?
- 防止 A 客户端误删 B 客户端的锁
- 释放锁前必须校验 value 是否匹配
✅ 为什么用 Lua 脚本释放锁?
GET + DEL不是原子操作!- 若 GET 后发生网络延迟,其他客户端可能已获取新锁,此时 DEL 会误删
✅ 过期时间如何设置?
- 太短:业务未执行完锁已释放 → 失去互斥性
- 太长:客户端崩溃后需等待很久 → 可用性下降
- 建议 :
expire_time = 业务最大执行时间 × 2
3.3 高级优化:Redlock 算法(防 Redis 单点故障)
当 Redis 是单节点时,若主节点宕机且未持久化,可能丢失锁信息。
Redlock(Redis 官方推荐)通过多数派机制解决:
- 向 N 个独立 Redis 节点(通常 N=5)请求加锁
- 只有超过半数(≥3)成功,且总耗时 < 锁过期时间,才算获取锁
- 释放锁时向所有节点发送 DEL
⚠️ 争议 :Martin Kleppmann 认为 Redlock 在时钟漂移下仍不安全。
✅ 实践建议:对一致性要求极高场景,改用 ZooKeeper;否则单 Redis + 持久化足够。
四、方案二:基于 MySQL 的分布式锁
MySQL 方案适用于已有数据库、不想引入 Redis的场景,但性能较低。
4.1 表结构设计
sql
CREATE TABLE distributed_lock (
lock_name VARCHAR(100) PRIMARY KEY,
token VARCHAR(100) NOT NULL, -- 锁持有者标识
expire_time BIGINT NOT NULL, -- 过期时间戳(毫秒)
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
4.2 加锁逻辑(利用唯一索引 + INSERT)
python
import pymysql
import time
import uuid
class MysqlDistributedLock:
def __init__(self, db_conn, lock_name, expire_time=30):
self.conn = db_conn
self.lock_name = lock_name
self.expire_time = expire_time
self.token = str(uuid.uuid4())
def acquire(self, timeout=10):
"""获取锁"""
start_time = time.time()
cursor = self.conn.cursor()
while time.time() - start_time < timeout:
try:
# 先清理过期锁
self._clean_expired_locks(cursor)
# 尝试插入新锁(唯一索引保证互斥)
expire_timestamp = int(time.time() * 1000) + self.expire_time * 1000
cursor.execute(
"INSERT INTO distributed_lock (lock_name, token, expire_time) VALUES (%s, %s, %s)",
(self.lock_name, self.token, expire_timestamp)
)
self.conn.commit()
return True
except pymysql.IntegrityError:
# 唯一索引冲突,说明锁已被占用
self.conn.rollback()
time.sleep(0.05)
except Exception as e:
self.conn.rollback()
raise e
return False
def release(self):
"""释放锁"""
cursor = self.conn.cursor()
try:
# 仅删除自己的锁
cursor.execute(
"DELETE FROM distributed_lock WHERE lock_name = %s AND token = %s",
(self.lock_name, self.token)
)
self.conn.commit()
finally:
cursor.close()
def _clean_expired_locks(self, cursor):
"""清理过期锁"""
current_time = int(time.time() * 1000)
cursor.execute(
"DELETE FROM distributed_lock WHERE expire_time < %s",
(current_time,)
)
4.3 关键设计解析
✅ 为什么用 INSERT 而不是 SELECT + UPDATE?
SELECT ... FOR UPDATE需要事务持有到业务结束,长时间阻塞其他事务INSERT利用唯一索引冲突天然实现互斥,无锁竞争
✅ 为什么需要定期清理过期锁?
- 客户端崩溃后不会主动释放锁
- 下次加锁前清理,避免"僵尸锁"占用资源
⚠️ 性能瓶颈
- 每次加锁需 1~2 次 SQL 查询
- 高并发下数据库压力大
- 建议 QPS < 1000 的场景使用
五、Redis vs MySQL 方案对比
| 特性 | Redis 方案 | MySQL 方案 |
|---|---|---|
| 性能 | 极高(微秒级) | 较低(毫秒级) |
| 可靠性 | 依赖 Redis 持久化 | 依赖数据库事务 |
| 实现复杂度 | 中等(需 Lua 脚本) | 简单 |
| 适用场景 | 高并发、低延迟 | 低频、已有 DB |
| 死锁防护 | 自动过期 + 唯一 value | 自动过期 + 清理任务 |
| 扩展性 | 支持 Redlock 集群 | 难以水平扩展 |
✅ 选择建议:
- 90% 场景选 Redis
- 无 Redis 且并发低 → 选 MySQL
- 金融级强一致 → 考虑 ZooKeeper / etcd
六、常见陷阱与解决方案
❌ 陷阱 1:锁过期但业务未完成
现象 :锁自动释放后,其他客户端进入临界区,导致数据错乱
解决方案:
-
启用 锁续期机制(Watchdog 线程定期延长过期时间)
-
示例(Redisson 的看门狗):
csharp// Redisson 自动每 10s 续期一次(只要线程 alive) RLock lock = redisson.getLock("myLock"); lock.lock(30, TimeUnit.SECONDS); // 30s 过期,但会自动续期
❌ 陷阱 2:主从切换导致锁丢失(Redis)
现象 :主节点加锁后未同步到从节点,主挂后从升主,锁消失
解决方案:
- 使用 Redlock(多节点)
- 或接受最终一致性,业务层做幂等处理
❌ 陷阱 3:MySQL 主从延迟
现象 :写主库后立即读从库,可能读不到锁记录
解决方案:
- 强制读主库
- 或使用 单库单表,避免主从架构
七、生产环境最佳实践
-
设置合理的超时时间
- 监控业务平均耗时,设置
expire_time = P99 × 2
- 监控业务平均耗时,设置
-
加锁必须带超时
- 避免无限等待导致线程池耗尽
-
finally 块中释放锁
csharplock = RedisDistributedLock(redis, "order_lock") try: if lock.acquire(): # 业务逻辑 finally: lock.release() -
监控锁竞争情况
- 记录加锁失败率、持有时间
- 告警异常长时间持锁
-
避免锁粒度太粗
- 例:不要用
"order_lock",改用"order_lock_{user_id}"
- 例:不要用
八、总结
| 方案 | 核心命令/操作 | 优势 | 劣势 |
|---|---|---|---|
| Redis | SET key value EX nx + Lua 脚本 |
高性能、原子性好 | 需维护 Redis 服务 |
| MySQL | INSERT 唯一索引 + 过期清理 |
无需新组件、ACID 保证 | 性能低、扩展难 |
💬 最后忠告 :
"分布式锁是最后的手段!优先考虑无锁设计(如 CAS、消息队列)。"当你真的需要它时,请务必理解上述细节,避免踩坑。
参考文献:
- Redis 官方文档:Distributed locks with Redis
- Martin Kleppmann: How to do distributed locking
- 《Redis 设计与实现》黄健宏