分布式锁详解:基于 Redis 与 MySQL 的实现方案(2026 实战指南)

适用读者 :后端开发工程师、系统架构师
前置知识 :Redis 基础、MySQL 事务、并发编程
更新日期:2026 年 2 月


一、什么是分布式锁?

在单机应用中,我们使用 synchronizedReentrantLock 等机制保证线程安全。但在分布式系统中,多个服务实例部署在不同机器上,JVM 锁无法跨进程生效。

分布式锁 就是为了解决这一问题而生的------它是一种跨进程、跨机器的互斥机制,确保同一时刻只有一个客户端能执行某段关键代码。

🎯 核心要求

  1. 互斥性:同一时间只能有一个客户端持有锁
  2. 高可用:锁服务不能成为单点故障
  3. 防死锁:客户端崩溃后锁能自动释放
  4. 可重入性(可选):同一线程可重复获取锁
  5. 高性能:加锁/解锁延迟低

二、为什么不用数据库行锁或 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 官方推荐)通过多数派机制解决:

  1. 向 N 个独立 Redis 节点(通常 N=5)请求加锁
  2. 只有超过半数(≥3)成功,且总耗时 < 锁过期时间,才算获取锁
  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 主从延迟

现象 :写主库后立即读从库,可能读不到锁记录
解决方案

  • 强制读主库
  • 或使用 单库单表,避免主从架构

七、生产环境最佳实践

  1. 设置合理的超时时间

    • 监控业务平均耗时,设置 expire_time = P99 × 2
  2. 加锁必须带超时

    • 避免无限等待导致线程池耗尽
  3. finally 块中释放锁

    csharp 复制代码
    lock = RedisDistributedLock(redis, "order_lock")
    try:
        if lock.acquire():
            # 业务逻辑
    finally:
        lock.release()
  4. 监控锁竞争情况

    • 记录加锁失败率、持有时间
    • 告警异常长时间持锁
  5. 避免锁粒度太粗

    • 例:不要用 "order_lock",改用 "order_lock_{user_id}"

八、总结

方案 核心命令/操作 优势 劣势
Redis SET key value EX nx + Lua 脚本 高性能、原子性好 需维护 Redis 服务
MySQL INSERT 唯一索引 + 过期清理 无需新组件、ACID 保证 性能低、扩展难

💬 最后忠告
"分布式锁是最后的手段!优先考虑无锁设计(如 CAS、消息队列)。"

当你真的需要它时,请务必理解上述细节,避免踩坑。


参考文献

相关推荐
你这个代码我看不懂8 小时前
@RefreshScope刷新Kafka实例
分布式·kafka·linq
麟听科技15 小时前
HarmonyOS 6.0+ APP智能种植监测系统开发实战:农业传感器联动与AI种植指导落地
人工智能·分布式·学习·华为·harmonyos
Wzx19801218 小时前
高并发秒杀下,如何避免 Redis 分布式锁的坑?
数据库·redis·分布式
Francek Chen18 小时前
【大数据存储与管理】分布式文件系统HDFS:01 分布式文件系统
大数据·hadoop·分布式·hdfs·架构
石去皿19 小时前
分布式原生:鸿蒙架构哲学与操作系统演进的范式转移
分布式·架构·harmonyos
KANGBboy21 小时前
spark参数优化
大数据·分布式·spark
我就是全世界21 小时前
RabbitMQ架构核心拆解:从消息代理到四大组件,一文看懂异步通信基石
分布式·架构·rabbitmq
DeepFlow 零侵扰全栈可观测1 天前
使用 eBPF 零代码修改绘制全景应用拓扑
java·前端·网络·分布式·微服务·云原生·云计算
玄〤1 天前
RabbitMQ 入门篇总结(黑马微服务课day10)(包含黑马商城业务改造)
java·笔记·分布式·spring cloud·微服务·rabbitmq·wpf