【Redis实战进阶篇1】Redis 分布式锁:从手写实现到 Redisson 最佳实践

💻 Hello World, 我是 予枫。

代码不止,折腾不息。作为一个正在升级打怪的 Java 后端练习生,我喜欢把踩过的坑和学到的招式记录下来。 保持空杯心态,让我们开始今天的技术分享。

在分布式系统中,单机锁(如 synchronizedReentrantLock)只能保证单个 JVM 内的线程安全,而跨服务、跨节点的并发场景(如秒杀库存扣减、分布式任务调度、订单幂等处理)则需要分布式锁 来保证数据一致性。Redis 凭借高性能、高可用的特性,成为实现分布式锁的首选方案。本文将从最基础的 setnx 手写实现出发,剖析死锁、集群失效等核心问题,最终落地 Redisson 分布式锁的最佳实践。

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

先看一个典型的业务场景:电商平台的库存扣减。

  • 单机部署时,用 synchronized 修饰扣减方法即可保证同一时刻只有一个线程修改库存;
  • 集群部署时(多节点 / 多服务实例),每个实例有独立的 JVM,本地锁无法跨实例生效,会出现多个线程同时扣减库存,导致超卖 (库存为负)或重复扣减(库存数据不一致)。

分布式锁的核心目标:在分布式环境下,保证同一时刻只有一个线程执行临界区代码。Redis 实现分布式锁的核心思路是:利用 Redis 的原子性命令,将 "锁" 存储为 Redis 中的一个 Key,线程获取锁即创建该 Key,释放锁即删除该 Key。

二、基础版实现:基于 SETNX 命令

2.1 核心命令:SETNX

SETNX(SET if Not Exists):当 Key 不存在时才设置值,返回 1;若 Key 已存在则不操作,返回 0。该命令是原子性的,这是实现分布式锁的基础。

复制代码
# 语法:SETNX key value
127.0.0.1:6379> SETNX lock:stock 1  # 锁Key:lock:stock,值:1(可自定义)
(integer) 1  # 返回1,获取锁成功
127.0.0.1:6379> SETNX lock:stock 1  # 再次执行,Key已存在,获取锁失败
(integer) 0

2.2 手写基础版分布式锁(Java + Jedis)

首先引入 Jedis 依赖(Maven):

复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.4.3</version>
</dependency>

基础版实现代码:

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * 基于 SETNX 的基础版分布式锁
 */
public class SimpleRedisLock {
    // Redis 连接池
    private final JedisPool jedisPool;
    // 锁Key前缀
    private static final String LOCK_PREFIX = "lock:";
    // 锁过期时间(默认10秒,防止死锁)
    private static final int DEFAULT_EXPIRE_SECONDS = 10;

    public SimpleRedisLock() {
        // 初始化Jedis连接池(实际项目中建议配置化)
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        this.jedisPool = new JedisPool(config, "127.0.0.1", 6379);
    }

    /**
     * 获取锁
     * @param lockKey 业务锁Key(如:stock_1001)
     * @return 是否获取成功
     */
    public boolean lock(String lockKey) {
        try (Jedis jedis = jedisPool.getResource()) {
            // 核心:SETNX 命令
            Long result = jedis.setnx(LOCK_PREFIX + lockKey, "1");
            // 设置过期时间(避免死锁)
            if (result == 1) {
                jedis.expire(LOCK_PREFIX + lockKey, DEFAULT_EXPIRE_SECONDS);
                return true;
            }
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey 业务锁Key
     */
    public void unlock(String lockKey) {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.del(LOCK_PREFIX + lockKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 测试方法
    public static void main(String[] args) {
        SimpleRedisLock redisLock = new SimpleRedisLock();
        String lockKey = "stock_1001";

        // 模拟线程1获取锁
        new Thread(() -> {
            if (redisLock.lock(lockKey)) {
                try {
                    System.out.println("线程1获取锁成功,执行库存扣减...");
                    Thread.sleep(5000); // 模拟业务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    redisLock.unlock(lockKey);
                    System.out.println("线程1释放锁");
                }
            } else {
                System.out.println("线程1获取锁失败");
            }
        }).start();

        // 模拟线程2竞争锁
        new Thread(() -> {
            if (redisLock.lock(lockKey)) {
                try {
                    System.out.println("线程2获取锁成功,执行库存扣减...");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    redisLock.unlock(lockKey);
                    System.out.println("线程2释放锁");
                }
            } else {
                System.out.println("线程2获取锁失败");
            }
        }).start();
    }
}

2.3 基础版的核心问题

看似能工作,但存在 3 个致命问题:

  1. 死锁风险setnxexpire 是两个独立命令,若执行 setnx 后程序崩溃(如 JVM 宕机),expire 未执行,锁 Key 会永久存在,导致死锁;
  2. 误删锁 :若线程 A 的业务执行时间超过锁过期时间,锁自动释放,此时线程 B 获取锁,线程 A 执行完业务后调用 unlock,会误删线程 B 的锁;
  3. 过期时间难设置:设置太短,业务没执行完锁就释放;设置太长,若线程异常,锁释放慢,影响并发效率。

三、进阶优化:解决死锁与误删问题

3.1 核心优化点

  1. 原子化设置锁 + 过期时间 :使用 Redis 的 SET key value NX EX seconds 命令,将 setnxexpire 合并为一个原子命令;
  2. 防误删:给锁 Value 设置唯一标识(如 UUID + 线程 ID),释放锁时先校验标识,再删除;
  3. 看门狗机制:若业务未执行完,自动续期锁的过期时间(避免锁提前释放)。

3.2 优化版实现(解决死锁 + 防误删)

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.UUID;

/**
 * 优化版:原子设置锁+过期时间 + 防误删 + 简易看门狗
 */
public class OptimizedRedisLock {
    private final JedisPool jedisPool;
    private static final String LOCK_PREFIX = "lock:";
    private static final int DEFAULT_EXPIRE_SECONDS = 10;
    // 唯一标识(每个线程的锁Value唯一)
    private String lockValue;

    public OptimizedRedisLock() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        this.jedisPool = new JedisPool(config, "127.0.0.1", 6379);
        // 生成唯一标识:UUID + 线程ID
        this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
    }

    /**
     * 获取锁:原子化 SET NX EX
     * @param lockKey 业务锁Key
     * @return 是否获取成功
     */
    public boolean lock(String lockKey) {
        try (Jedis jedis = jedisPool.getResource()) {
            // SET key value NX(仅Key不存在时设置) EX(过期时间)
            String result = jedis.set(LOCK_PREFIX + lockKey, lockValue, "NX", "EX", DEFAULT_EXPIRE_SECONDS);
            // "OK" 表示设置成功,获取锁成功
            return "OK".equals(result);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 释放锁:先校验标识,再删除(Lua脚本保证原子性)
     * @param lockKey 业务锁Key
     * @return 是否释放成功
     */
    public boolean unlock(String lockKey) {
        // Lua脚本:先判断Value是否匹配,匹配则删除
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) " +
                "else " +
                "return 0 " +
                "end";
        try (Jedis jedis = jedisPool.getResource()) {
            Object result = jedis.eval(luaScript, 1, LOCK_PREFIX + lockKey, lockValue);
            // 返回1表示删除成功
            return "1".equals(result.toString());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 简易看门狗:定时续期锁的过期时间
     * @param lockKey 业务锁Key
     * @param delay 续期间隔(如3秒)
     */
    public void watchDog(String lockKey, long delay) {
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(delay * 1000);
                    // 校验锁是否还属于当前线程,是则续期
                    try (Jedis jedis = jedisPool.getResource()) {
                        String currentValue = jedis.get(LOCK_PREFIX + lockKey);
                        if (lockValue.equals(currentValue)) {
                            // 续期:重置过期时间为10秒
                            jedis.expire(LOCK_PREFIX + lockKey, DEFAULT_EXPIRE_SECONDS);
                            System.out.println("看门狗续期成功,锁Key:" + lockKey);
                        } else {
                            // 锁已释放,退出看门狗
                            break;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }).start();
    }

    // 测试方法
    public static void main(String[] args) {
        OptimizedRedisLock redisLock = new OptimizedRedisLock();
        String lockKey = "stock_1001";

        // 模拟线程1获取锁(业务执行时间超过默认过期时间)
        new Thread(() -> {
            if (redisLock.lock(lockKey)) {
                try {
                    System.out.println("线程1获取锁成功,执行库存扣减...");
                    // 启动看门狗,每3秒续期一次
                    redisLock.watchDog(lockKey, 3);
                    Thread.sleep(15000); // 业务执行15秒(超过默认10秒过期时间)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    boolean unlockResult = redisLock.unlock(lockKey);
                    System.out.println("线程1释放锁结果:" + unlockResult);
                }
            } else {
                System.out.println("线程1获取锁失败");
            }
        }).start();

        // 模拟线程2竞争锁
        new Thread(() -> {
            // 等待5秒,确保线程1先获取锁
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (redisLock.lock(lockKey)) {
                try {
                    System.out.println("线程2获取锁成功,执行库存扣减...");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    boolean unlockResult = redisLock.unlock(lockKey);
                    System.out.println("线程2释放锁结果:" + unlockResult);
                }
            } else {
                System.out.println("线程2获取锁失败(线程1的看门狗续期了锁)");
            }
        }).start();
    }
}

3.3 关键优化点解释

  1. 原子化设置锁jedis.set(key, value, "NX", "EX", seconds) 是原子操作,避免了 setnxexpire 分离导致的死锁;
  2. 防误删锁 :释放锁时使用 Lua 脚本,先校验 get(key) 的值是否等于当前线程的唯一标识,再删除,Lua 脚本在 Redis 中是原子执行的,避免 "校验 - 删除" 过程中锁被其他线程修改;
  3. 简易看门狗:启动一个后台线程,每隔一段时间(如锁过期时间的 1/3)检查锁是否还属于当前线程,若是则重置过期时间,保证业务执行完前锁不释放。

3.4 仍存在的问题

尽管做了优化,但手写实现仍有短板:

  • 看门狗实现简陋(如未处理线程中断、异常),生产环境需考虑更多边界;
  • 集群环境下,Redis 主从复制存在延迟,若主节点宕机,从节点未同步锁 Key,会导致锁失效;
  • 需手动处理锁超时、重试、释放等逻辑,开发效率低。

四、最佳实践:Redisson 分布式锁

Redisson 是 Redis 官方推荐的 Java 客户端,内置了分布式锁的完整实现,解决了手写实现的所有痛点,是生产环境的首选。

4.1 Redisson 核心特性

  • 基于 Lua 脚本保证锁操作的原子性;
  • 内置自动看门狗机制(默认 30 秒过期,每 10 秒续期一次);
  • 支持可重入锁、公平锁、读写锁等多种锁类型;
  • 提供 RedLock 算法解决集群环境下的锁失效问题;
  • 自动处理锁释放、超时、重试等边界情况。

4.2 Redisson 集成与使用(Spring Boot)

步骤 1:引入依赖(Maven)
复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>
步骤 2:配置 Redisson(application.yml)
复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: ""
    database: 0

# Redisson 配置(简化版)
redisson:
  config: |
    singleServerConfig:
      address: "redis://127.0.0.1:6379"
      password: ""
      database: 0
    threads: 10
    nettyThreads: 10
步骤 3:Redisson 分布式锁实现代码
java 复制代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Redisson 分布式锁最佳实践
 */
@Component
public class RedissonDistributedLock {
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 获取可重入分布式锁
     * @param lockKey 业务锁Key
     * @param waitTime 最大等待时间(秒):获取锁的超时时间
     * @param leaseTime 锁持有时间(秒):0表示使用看门狗自动续期
     * @return 是否获取成功
     */
    public boolean lock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // tryLock:尝试获取锁,超时返回false
            // 参数:waitTime(等待时间), leaseTime(持有时间), 时间单位
            return lock.tryLock(waitTime, leaseTime, java.util.concurrent.TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey 业务锁Key
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        // 校验锁是否属于当前线程,避免误删
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }

    // 业务层使用示例
    @Component
    static class StockService {
        @Autowired
        private RedissonDistributedLock redissonLock;

        public void deductStock(Long stockId) {
            String lockKey = "stock:" + stockId;
            // 获取锁:最大等待3秒,持有时间0(开启看门狗)
            if (redissonLock.lock(lockKey, 3, 0)) {
                try {
                    System.out.println("线程" + Thread.currentThread().getId() + "获取锁成功,扣减库存...");
                    // 模拟业务执行(超过30秒,看门狗会自动续期)
                    Thread.sleep(40000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    redissonLock.unlock(lockKey);
                    System.out.println("线程" + Thread.currentThread().getId() + "释放锁");
                }
            } else {
                System.out.println("线程" + Thread.currentThread().getId() + "获取锁失败,超时");
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        // Spring Boot 环境下可通过ApplicationContext获取Bean
        // 此处简化,模拟业务调用
        StockService stockService = new StockService();
        // 模拟多线程扣减库存
        new Thread(() -> stockService.deductStock(1001L)).start();
        new Thread(() -> stockService.deductStock(1001L)).start();
    }
}

4.3 Redisson 分布式锁原理

Redisson 实现分布式锁的核心是 Lua 脚本,以 tryLock 为例,核心逻辑如下:

复制代码
-- 1. 检查锁是否存在,若不存在则设置锁(支持可重入)
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 2. 若锁已存在,检查是否是当前线程持有(可重入)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
-- 3. 锁被其他线程持有,返回剩余过期时间
return redis.call('pttl', KEYS[1]);
  • 可重入:使用 Hash 结构存储锁,Key 是锁标识,Field 是线程 ID,Value 是重入次数;
  • 看门狗 :当 leaseTime=0 时,Redisson 会启动一个定时任务(TimeoutTask),每隔 lockWatchdogTimeout/3(默认 10 秒)执行一次续期,将锁过期时间重置为 30 秒;若线程正常释放锁,看门狗自动停止;若线程异常,看门狗也会停止,锁到期自动释放。

五、集群环境下的锁失效:RedLock 算法

5.1 集群环境的锁失效问题

Redis 主从集群中,主节点负责写操作,从节点同步数据。若主节点宕机,从节点升级为主节点,但此时主节点的锁 Key 尚未同步到从节点,导致新主节点中无锁 Key,其他线程可重新获取锁,引发并发问题。

5.2 RedLock 原理

RedLock 是 Redis 作者提出的分布式锁算法,核心思路:

  1. 部署多个独立的 Redis 节点(至少 3 个,无主从关系);
  2. 线程依次向所有节点请求获取锁,只有当超过半数节点获取锁成功,且总耗时小于锁过期时间,才认为锁获取成功;
  3. 释放锁时,向所有节点发送释放请求。

5.3 Redisson 实现 RedLock

java 复制代码
import org.redisson.api.RedissonClient;
import org.redisson.api.RedissonRedLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * RedLock 解决集群锁失效问题
 */
@Component
public class RedissonRedLockDemo {
    // 假设配置了3个独立的Redis节点客户端
    @Autowired
    private RedissonClient redissonClient1;
    @Autowired
    private RedissonClient redissonClient2;
    @Autowired
    private RedissonClient redissonClient3;

    public void redLockDemo(String lockKey) {
        // 获取3个节点的锁
        RLock lock1 = redissonClient1.getLock(lockKey);
        RLock lock2 = redissonClient2.getLock(lockKey);
        RLock lock3 = redissonClient3.getLock(lockKey);

        // 组合为RedLock
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

        try {
            // 尝试获取锁:等待3秒,持有10秒
            boolean isLock = redLock.tryLock(3, 10, java.util.concurrent.TimeUnit.SECONDS);
            if (isLock) {
                System.out.println("RedLock获取成功,执行临界区业务...");
                Thread.sleep(5000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁(会向所有节点释放)
            redLock.unlock();
        }
    }
}

注意:RedLock 性能略低于普通分布式锁(需访问多个节点),仅在对数据一致性要求极高的场景(如金融交易)使用,普通业务场景使用单节点 Redisson 锁即可。

六、基础版 vs Redisson 版对比

特性 手写基础版 Redisson 版
原子性 需手动保证(SET NX EX) 内置 Lua 脚本,天然原子性
死锁问题 需手动处理过期时间 + 看门狗 内置看门狗,自动续期 / 释放
防误删锁 需手动写 Lua 脚本校验标识 内置校验逻辑,支持 isHeldByCurrentThread
可重入性 需手动实现 Hash 存储重入次数 原生支持可重入锁
集群适配 无,主从切换易失效 支持 RedLock,解决集群锁失效问题
边界处理(超时 / 重试) 需手动编写 内置完善的超时、重试、异常处理逻辑
开发效率 低(需处理大量边界) 高(一行代码调用)
生产可用性 低(易踩坑) 高(经过生产验证)

七、总结

关键点回顾

  1. 基础实现 :基于 SETNX 的分布式锁需解决原子性(SET NX EX)、死锁(过期时间)、误删(唯一标识 + Lua 脚本)三大问题;
  2. 最佳实践:生产环境优先使用 Redisson 分布式锁,其内置看门狗、可重入、集群适配等特性,能规避手写实现的所有痛点;
  3. 集群场景:普通业务用单节点 Redisson 锁,高一致性场景(如金融)使用 RedLock 算法。

落地建议

  • 非核心业务(如缓存更新):可使用手写基础版,但需严格校验原子性和过期时间;
  • 核心业务(如库存、订单、支付):必须使用 Redisson 分布式锁,优先选择 tryLock(waitTime, 0, TimeUnit.SECONDS)(开启看门狗);
  • 集群部署:若 Redis 为主从架构,且对数据一致性要求高,升级为 RedLock 或使用 Redis Cluster + Redisson。

Redis 分布式锁的核心是原子性高可用,手写实现适合学习原理,而 Redisson 是生产环境的最优解,既能保证正确性,又能提升开发效率。

🌟 关注【予枫】,获取更多技术干货

  • 📅 身份:一名热爱技术的研二学生

  • 🏷️ 标签:Java / 算法 / 个人成长

  • 💬 Slogan:只写对自己和他人有用的文字。

相关推荐
瑶山2 小时前
Spring Cloud微服务搭建二、分布式定时任务Quartz+MySQL接入
分布式·mysql·spring cloud·微服务·quartz
小北方城市网2 小时前
Spring Cloud Gateway 生产问题排查与性能调优全攻略
redis·分布式·缓存·性能优化·mybatis
【赫兹威客】浩哥3 小时前
【赫兹威客】完全分布式Hadoop测试教程
大数据·hadoop·分布式
Dobby_053 小时前
【kafka】初学者指南:从零看懂Kafka
分布式·kafka
Gary董3 小时前
Kafka速度快的原因
分布式·kafka
数据与后端架构提升之路12 小时前
Seata 全景拆解:AT、TCC、Saga 该怎么选?告别“一把梭”的架构误区
分布式·架构
预立科技12 小时前
Redis 中 Lua 与 Pipeline 的相同点,区别,使用场景
redis·pipeline·lua
曲幽12 小时前
FastAPI多进程部署:定时任务重复执行?手把手教你用锁搞定
redis·python·fastapi·web·lock·works
wWYy.14 小时前
详解redis(15):缓存雪崩
数据库·redis·缓存