目录

为什么推荐使用 redis 分布式锁

在分布式系统成为主流的今天,多个服务实例如何安全地共享资源成为了每个开发者必须面对的挑战。

作为一名长期奋战在一线的 Go 开发者,我将在本文中详细分析 Redis 分布式锁的独特价值,并分享在实际项目中的实战经验。

一、为什么需要分布式锁?

在单机时代,我们依赖 synchronized 或 Mutex 等本地锁就能解决并发问题。

但当系统扩展到多实例部署时,这些本地锁立即失效——因为它们只能控制单个JVM或进程内的线程同步,无法跨实例协调。

本地锁的局限性示例

// 单机环境下有效的本地锁
var mutex sync.Mutex

func localLockExample() {
    mutex.Lock()
    defer mutex.Unlock()
    // 临界区操作
}

但在分布式环境中,这个锁对其它服务实例完全无效。

这时,我们需要一个所有实例都能访问的中央协调点,而Redis正是理想选择

二、传统方案痛点

在分布式系统中,多个服务实例或进程可能同时操作共享资源(如数据库中的库存数据、订单编号生成),若缺乏有效控制,会导致数据不一致(如超卖、重复订单)。

分布式锁的核心作用就是在分布式环境下保证共享资源的互斥访问,确保同一时间只有一个进程能执行临界操作。​

传统实现分布式锁的方案存在明显缺陷:​

  1. 数据库悲观锁:通过 SELECT … FOR UPDATE 实现,会导致数据库性能下降,且容易引发死锁,不适用于高并发场景;​

  2. 数据库乐观锁:基于版本号控制,虽然性能优于悲观锁,但冲突频繁时会出现大量重试,影响用户体验;​

  3. ZooKeeper 分布式锁:可靠性高,但部署和维护成本高,且性能开销较大,不适用于高频次的锁竞争场景。​

这些方案的痛点(性能低、维护复杂、高并发适配差),让 Redis 分布式锁成为更优选择。​

三、Redis 分布式锁的核心优势

(一)高性能,适配高并发​

Redis 基于内存操作,单实例 QPS 可轻松支撑 10 万 +,远高于数据库和 ZooKeeper。

分布式锁的加锁、解锁操作本质是 Redis 的 SET DEL 命令,执行效率极高,能应对高并发场景下的频繁锁竞争。​

与基于数据库的锁(需要磁盘I/O)或Zookeeper锁(需要节点间协调)相比,Redis在性能上具有明显优势性能对比数据

  • Redis锁:平均耗时1-2毫秒
  • 数据库锁:平均耗时10-20毫秒
  • Zookeeper锁:平均耗时5-10毫秒

(二)原子性与可靠性保障​

Redis 提供了原生的原子命令(如 SET key value NX EX),能确保加锁操作的原子性,避免出现 “锁竞争导致的误判”。

同时,通过合理设置锁超时时间、主从复制 + 哨兵模式,可进一步提升锁的可靠性,避免单点故障导致的锁失效。

AOF 和 RDB 持久化机制也保证了数据的可靠性。

Redis提供多种原子命令,如SETNX、SET EX PX NX等,使得实现分布式锁变得简单而可靠。

特别是Redis 2.6.12+版本的SET命令扩展参数,单条命令即可完成加锁和过期时间设置

import (
    "github.com/go-redis/redis/v8"
    "context"
    "time"
)

func acquireLock(rdb *redis.Client, lockKey string, value string, expiration time.Duration) bool {
    // 单条原子命令实现加锁
    result, err := rdb.SetNX(context.Background(), lockKey, value, expiration).Result()
    if err != nil {
        return false
    }
    return result
}

(三)部署简单,生态丰富

Redis 部署架构简单(支持单机、主从、哨兵、集群模式),且生态成熟,运维工具丰富。

相比 ZooKeeper 复杂的集群部署和选举机制,Redis 分布式锁的接入和维护成本更低,开发门槛也更低。

几乎所有主流语言都有成熟的 Redis 客户端,且大多提供了分布式锁的高级封装。

在 Go 生态中,go-redis 库就提供了良好的支持

(四)灵活性高,支持多样化场景

Redis 分布式锁支持自定义锁超时时间、可重入锁、公平锁等扩展需求,还能通过 Lua 脚本实现复杂的锁逻辑。

通过 EXPIRE 时间设置,Redis 分布式锁可以自动释放,有效防止因客户端崩溃导致的死锁问题。

无论是简单的临界资源保护,还是复杂的分布式事务场景,都能灵活适配。​

这是相比基于数据库的分布式锁的重要优势。

(五)低延迟,跨地域适配​

Redis 支持多区域部署,且内存操作的低延迟特性,使得分布式锁的响应时间极短(毫秒级)。

即使是跨地域的分布式系统,也能通过 Redis 集群实现低延迟的锁控制,避免因网络延迟导致的锁等待过长。

四、与其他分布式锁方案的对比

(一)vs 数据库分布式锁

基于数据库唯一索引的实现

-- 创建锁表
CREATE TABLE distributed_lock (
    id INT PRIMARY KEY AUTO_INCREMENT,
    lock_name VARCHAR(100) UNIQUE,
    holder_id VARCHAR(100),
    expire_time DATETIME
);

缺点

  • 性能瓶颈:频繁的数据库IO操作
  • 死锁风险:需要额外机制处理客户端崩溃
  • 扩展性差:数据库容易成为单点瓶颈

(二)vs Zookeeper分布式锁

Zookeeper通过临时顺序节点实现分布式锁,提供强一致性但性能较低:

优势:强一致性、原生监控机制

劣势:性能较低、部署复杂

(三)选择建议

场景 推荐方案 理由
高并发秒杀 Redis分布式锁 性能要求高,可容忍极小概率的锁失效
金融交易 Zookeeper锁 强一致性要求高于性能要求
简单后台任务 数据库锁 并发量低,简化技术栈

五、Go 实战

以下基于 go-redis 客户端实现 Redis 分布式锁,包含加锁、解锁、重试逻辑,兼顾原子性和可靠性。​

完整代码实现(可参考):

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

// RedisLock Redis 分布式锁结构体
type RedisLock struct {
    client     *redis.Client
    lockKey    string        // 锁的 key
    lockValue  string        // 锁的 value(用于解锁时的身份验证)
    expireTime time.Duration // 锁的过期时间
    retryTimes int           // 加锁失败后的重试次数
    retryDelay time.Duration // 重试间隔
}

// NewRedisLock 创建 Redis 分布式锁实例
func NewRedisLock(client *redis.Client, lockKey string) *RedisLock {
    return &RedisLock{
        client:     client,
        lockKey:    lockKey,
        lockValue:  fmt.Sprintf("lock_val_%d", time.Now().UnixNano()), // 用时间戳生成唯一 value
        expireTime: 3 * time.Second,                                  // 锁默认过期时间 3 秒
        retryTimes: 3,                                                // 默认重试 3 次
        retryDelay: 500 * time.Millisecond,                           // 重试间隔 500 毫秒
    }
}

// Lock 加锁(支持重试)
func (rl *RedisLock) Lock(ctx context.Context) (bool, error) {
    // 循环重试加锁
    for i := 0; i tryTimes; i++ {
        // SET key value NX EX:原子操作,确保加锁原子性
        // NX:只有 key 不存在时才设置成功(加锁成功)
        // EX:设置 key 的过期时间,避免死锁
        success, err := rl.client.SetNX(
            ctx,
            rl.lockKey,
            rl.lockValue,
            rl.expireTime,
        ).Result()
        if err != nil {
            fmt.Printf("加锁失败,错误信息:%v\n", err)
            return false, err
        }
        if success {
            fmt.Println("加锁成功")
            return true, nil
        }
        fmt.Printf("加锁失败,第 %d 次重试...\n", i+1)
        time.Sleep(rl.retryDelay)
    }
    return false, errors.New("加锁失败,已达到最大重试次数")
}

// Unlock 解锁(通过 Lua 脚本确保原子性)
func (rl *RedisLock) Unlock(ctx context.Context) error {
    // Lua 脚本:先判断锁的 value 是否匹配(避免误解锁他人的锁),再删除 key
    luaScript := `
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        end
        return 0
    `
    result, err := rl.client.Eval(
        ctx,
        luaScript,
        []string{rl.lockKey},
        rl.lockValue,
    ).Result()
    if err != nil {
        return fmt.Errorf("解锁失败:%v", err)
    }
    if res, ok := result.(int64); ok && res == 1 {
        fmt.Println("解锁成功")
        return nil
    }
    return errors.New("解锁失败:锁不存在或已过期")
}

func main() {
    // 初始化 Redis 客户端
    redisClient := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // 无密码
        DB:       0,  // 使用 0 号数据库
    })
    defer redisClient.Close()

    ctx := context.Background()
    // 创建分布式锁实例(锁 key 为 "stock_lock",用于保护库存操作)
    lock := NewRedisLock(redisClient, "stock_lock")
    // 自定义锁过期时间(可选)
    lock.expireTime = 5 * time.Second

    // 加锁
    success, err := lock.Lock(ctx)
    if err != nil || !success {
        fmt.Printf("加锁失败:%v\n", err)
        return
    }
    defer lock.Unlock(ctx) // 延迟解锁,确保临界操作执行完后释放锁

    // 临界操作:模拟扣减库存
    fmt.Println("执行临界操作:扣减库存...")
    time.Sleep(2 * time.Second) // 模拟业务执行时间
    fmt.Println("临界操作执行完成")
}

代码核心说明​

  1. 加锁逻辑:使用 SETNX 命令(NX 表示 “不存在才设置”)+ 过期时间(EX),确保加锁原子性,同时避免锁忘记释放导致的死锁;​

  2. 解锁逻辑:通过 Lua 脚本实现 “判断 value + 删除 key” 的原子操作,避免误解锁他人持有的锁(比如锁过期后被其他进程重新加锁,原进程解锁时不会影响新锁);​

  3. 重试机制:加锁失败后支持重试,适配高并发场景下的锁竞争;​

  4. 资源释放:通过 defer 确保临界操作执行完后解锁,即使程序异常也能释放锁。

常见问题

Q1. Redis 分布式锁的 “锁过期” 问题如何解决?​

锁过期是指持有锁的进程未执行完临界操作,锁就因超时被自动释放,导致其他进程获取锁,引发并发问题。

解决方案:​

  1. 合理设置锁超时时间(根据业务执行时间预估,预留一定缓冲);​

  2. 实现 “锁续约” 机制:在临界操作执行过程中,定期检查锁是否存在,若存在则延长过期时间(比如用 goroutine 定时执行 EXPIRE 命令)。​

Q2. Redis 单点故障会导致锁失效吗?​

会。若使用 Redis 单机部署,当实例宕机时,所有未过期的锁都会丢失。

解决方案:​

  1. 部署 Redis 主从 + 哨兵模式:主节点宕机后,哨兵会自动将从节点提升为主节点,确保锁数据不丢失;​

  2. 使用 Redis Cluster 集群:多节点冗余存储,提升可用性,避免单点故障。​

Q3. Redis 分布式锁支持可重入吗?​

上述基础实现不支持可重入(同一进程再次加锁会失败)。

若需要可重入特性,可修改实现:​

  1. 锁的 value 存储进程标识(如 PID + 线程 ID);​

  2. 加锁时先判断锁是否存在,且 value 为当前进程标识,若是则直接返回成功(重入),并更新锁过期时间;​

  3. 解锁时递减重入计数,计数为 0 时再删除锁。​

Q4. 高并发场景下,Redis 分布式锁会出现 “惊群效应” 吗?​

会。当锁释放时,大量等待加锁的进程会同时竞争锁,导致 Redis 压力增大。

解决方案:​

  1. 给重试机制添加随机延迟(避免所有进程同时重试);​

  2. 使用 Redis 的 BLPOP 命令实现阻塞式等待,锁释放时主动通知等待进程,减少无效重试。​

总结​

Redis 分布式锁凭借高性能、部署简单、可靠性高、灵活性强等核心优势,成为分布式系统中解决并发竞争问题的首选方案。

相比传统的数据库锁和 ZooKeeper 锁,Redis 分布式锁更适配高并发场景,且开发和维护成本更低,能有效保障共享资源的互斥访问,避免数据不一致问题。​

在实际开发中,需注意锁超时、单点故障、可重入性等问题,结合业务场景合理设计锁的过期时间、重试机制和高可用架构。

上述 Go 语言实现可直接用于生产环境,也可根据需求扩展锁续约、可重入等特性。​

另外整理了和 mysql 锁的区别如下:

维度 redis mysql
原理 内存+SETNX(不存在则设置)+RedLock + PX (过期) 事务 、表锁、行锁、for update
性能 高吞吐、低延迟(微秒级响应) 较低吞吐、较高延迟(毫秒级响应)
并发性 高并发 并发性较差
可靠性 依赖 Redis 的可用性 依赖数据库的 ACID 特性
锁释放 手动 、 过期自动 手动(事务提交/回滚)
锁粒度 粒度可控,可以按照业务维度上锁,使用分布式系统 锁单表单行
适用场景 微服务、分布式、高并发、短时锁竞争
(如秒杀、缓存击穿保护)
长事务、强一致性需求(如金融转账、订单支付)
优点 ● 适用分布式系统,可以按业务资源上锁
● 锁粒度可控,
基于 redis 实现,内存实现,性能较高
● 支持自动续期、超时等功能,满足更多业务需求
● 强一致性
缺点 - ● 容易死锁,导致连接占用不释放,占用服务内存
● 锁粒度不可控
● 性能较差
使用场景 分布式系统下可以结合使用,保证数据的强一致性

如果大家对 Redis 分布式锁的实现细节、高可用优化或特殊场景适配还有哪些不清楚的地方,欢迎大家在评论区交流~~~​

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/why-recommend-redis-distributed-lock/

备用原文链接: https://blog.fiveyoboy.com/articles/why-recommend-redis-distributed-lock/