【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:只写对自己和他人有用的文字。

相关推荐
七夜zippoe17 分钟前
深入解析CANN仓库中的HCCL分布式通信库
pytorch·分布式·cann·hccl·通信库
富士康质检员张全蛋29 分钟前
Kafka的操作 消费者组 消费位置查看
分布式·kafka
运维行者_40 分钟前
如何查看每个IP的带宽使用情况?NetFlow 技术实战指南
开发语言·网络·分布式·后端·架构·带宽
YDS8293 小时前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
霸道流氓气质4 小时前
Kiro 中配置 RabbitMQ MCP Server 指南
分布式·rabbitmq·ruby
国科安芯4 小时前
AS32S601型抗辐射MCU在分布式太空算力架构中的技术演进与应用前景
人工智能·分布式·单片机·嵌入式硬件·架构·边缘计算
lindexi4 小时前
WPF 笔迹延迟优化从硬件到软件的全链路分析
wpf
不会C语言的菜鸟5 小时前
分布式数据一致性:从工程实践到理论基石
分布式
爬也要爬着前进17 小时前
redis主从搭建
数据库·redis·缓存
Leo.yuan18 小时前
数据仓库建设怎么做?从源数据到分析报表全流程讲清
大数据·分布式·spark