欢迎光临
我们一直在努力

Redis 分布式锁实战:从手写实现到Redisson优化

Redis 分布式锁实战:从手写实现到 Redisson 优化

引言

在分布式系统中,并发资源竞争是高频问题 —— 比如库存扣减、订单创建、缓存更新等场景,若缺乏有效的同步机制,可能导致数据不一致、重复操作等严重问题。Redis 凭借高性能、高可用的特性,成为实现分布式锁的主流方案之一。本文将从原生 Redis 分布式锁的手写实现入手,分析其潜在缺陷,再引入 Redisson 框架进行优化,最终提供可直接复用的生产级模板,帮助开发者快速解决分布式并发问题。

一、分布式锁的核心设计要求

一个可靠的分布式锁必须满足以下 5 个核心条件,否则可能引发业务异常:

  • 互斥性:同一时刻只能有一个线程持有锁,避免资源竞争;
  • 高可用:Redis 集群环境下,锁服务不能因单点故障失效;
  • 防死锁:持有锁的线程崩溃后,锁能自动释放,避免资源永久阻塞;
  • 可重入:同一线程可多次获取同一把锁,支持嵌套调用场景;
  • 一致性:锁的释放必须是持有锁的线程本人,防止误释放他人锁。
  • 二、原生 Redis 分布式锁手写实现(基础版)

    2.1 核心原理

    利用 Redis 的 SET key value NX EX timeout 命令实现锁的获取:

    • NX:仅当 key 不存在时才设置成功(保证互斥性);
    • EX timeout:自动设置过期时间(防死锁);
    • 释放锁时通过 Lua 脚本原子化删除 key(避免误释放)。

    2.2 基础实现代码(Java)

    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import java.util.Collections;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;

    /**
    * 原生 Redis 分布式锁基础实现
    */

    public class RedisLockBasic {
    // Redis 客户端(Spring 注入)
    private final StringRedisTemplate stringRedisTemplate;
    // 锁的前缀(避免 key 冲突)
    private static final String LOCK_PREFIX = "distributed:lock:";
    // 解锁 Lua 脚本(原子操作,避免误释放)
    private static final String UNLOCK_SCRIPT = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
    else
    return 0
    end
    """
    ;

    public RedisLockBasic(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
    * 获取锁
    * @param lockKey 锁的业务标识(如:stock:1001)
    * @param expireTime 过期时间(秒)
    * @param retryTimes 重试次数(获取锁失败时重试)
    * @param retryInterval 重试间隔(毫秒)
    * @return 锁的唯一标识(解锁时需验证),获取失败返回 null
    */

    public String tryLock(String lockKey, int expireTime, int retryTimes, long retryInterval) {
    // 生成锁的唯一标识(避免误释放其他线程的锁)
    String lockValue = UUID.randomUUID().toString();
    String realLockKey = LOCK_PREFIX + lockKey;

    // 循环重试获取锁
    for (int i = 0; i <= retryTimes; i++) {
    // 执行 SET NX EX 命令获取锁
    Boolean success = stringRedisTemplate.opsForValue()
    .setIfAbsent(realLockKey, lockValue, expireTime, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(success)) {
    return lockValue; // 获取锁成功,返回唯一标识
    }
    // 重试间隔
    try {
    Thread.sleep(retryInterval);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return null;
    }
    }
    return null; // 重试次数耗尽,获取锁失败
    }

    /**
    * 释放锁(原子操作)
    * @param lockKey 锁的业务标识
    * @param lockValue 锁的唯一标识(与获取时返回值一致)
    * @return 释放成功返回 true,失败返回 false
    */

    public boolean unlock(String lockKey, String lockValue) {
    String realLockKey = LOCK_PREFIX + lockKey;
    // 执行 Lua 脚本解锁
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
    Long result = stringRedisTemplate.execute(
    redisScript,
    Collections.singletonList(realLockKey),
    lockValue
    );
    return result != null && result > 0;
    }
    }

    2.3 基础版的潜在缺陷

  • 锁超时释放问题:若业务执行时间超过锁的过期时间,锁会自动释放,可能导致并发问题;
  • 不支持重入:同一线程多次获取锁会失败,无法满足嵌套调用场景(如:方法 A 调用方法 B,两者都需要获取同一把锁);
  • 主从切换风险:Redis 主从架构下,主节点锁未同步到从节点时主节点宕机,从节点升级为主节点后,可能出现重复加锁;
  • 重试机制简陋:固定重试次数和间隔,缺乏动态适配能力。
  • 三、Redisson 优化实现(生产级方案)

    Redisson 是 Redis 官方推荐的 Java 客户端,内置了分布式锁的完整实现,解决了原生方案的诸多缺陷,且支持多种锁类型(可重入锁、公平锁、红锁等)。

    3.1 核心优化点

  • 自动续命机制:通过后台线程定期延长锁的过期时间,避免业务未执行完锁被释放;
  • 天然可重入:基于 Redis 的 Hash 结构存储线程标识和重入次数,支持嵌套调用;
  • 高可用保障:支持 Redis 单机、主从、哨兵、集群多种部署模式,结合红锁算法解决主从切换问题;
  • 丰富的锁类型:除可重入锁外,还支持公平锁、读写锁、联锁等,适配不同业务场景;
  • 优雅的 API 设计:支持 try-with-resources 语法,自动释放锁,减少手动操作失误。
  • 3.2 生产级集成模板(Spring Boot)

    3.2.1 第一步:引入依赖

    <!– Redisson 核心依赖 –>
    <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version> <!– 建议使用最新稳定版 –>
    </dependency>

    3.2.2 第二步:配置 Redisson

    # application.yml
    spring:
    redis:
    host: 127.0.0.1
    port: 6379
    password: yourredispassword # 无密码可省略
    database: 0

    redisson:
    # 单机模式配置(集群/哨兵模式参考官方文档)
    singleServerConfig:
    address: redis://${spring.redis.host}:${spring.redis.port}
    password: ${spring.redis.password}
    database: ${spring.redis.database}
    # 连接池配置(根据业务调整)
    connectionPoolSize: 16
    connectionMinimumIdleSize: 8
    idleConnectionTimeout: 30000
    connectTimeout: 10000
    timeout: 3000

    3.2.3 第三步:封装分布式锁工具类

    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.util.concurrent.TimeUnit;

    /**
    * Redisson 分布式锁工具类(生产级可直接复用)
    */

    @Component
    public class RedissonLockUtil {
    @Resource
    private RedissonClient redissonClient;
    // 锁的前缀(统一命名规范,便于排查)
    private static final String LOCK_PREFIX = "distributed:lock:";

    /**
    * 获取可重入锁(默认非公平锁)
    * @param lockKey 锁的业务标识(如:order:create:1001)
    * @param waitTime 最大等待时间(秒):获取锁失败时,最多等待多久
    * @param leaseTime 锁的持有时间(秒):0 表示自动续命(默认 30 秒续期一次)
    * @return 锁实例(需手动释放或使用 try-with-resources)
    */

    public RLock getLock(String lockKey, long waitTime, long leaseTime) {
    String realLockKey = LOCK_PREFIX + lockKey;
    RLock lock = redissonClient.getLock(realLockKey);
    // 尝试获取锁:waitTime 内获取不到则返回失败
    try {
    boolean success = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
    return success ? lock : null;
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return null;
    }
    }

    /**
    * 获取公平锁(按请求顺序获取锁,避免饥饿)
    * @param lockKey 锁的业务标识
    * @param waitTime 最大等待时间(秒)
    * @param leaseTime 锁的持有时间(秒)
    * @return 公平锁实例
    */

    public RLock getFairLock(String lockKey, long waitTime, long leaseTime) {
    String realLockKey = LOCK_PREFIX + lockKey;
    RLock fairLock = redissonClient.getFairLock(realLockKey);
    try {
    boolean success = fairLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
    return success ? fairLock : null;
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return null;
    }
    }

    /**
    * 释放锁(推荐使用 try-with-resources 自动释放)
    * @param lock 锁实例
    */

    public void unlock(RLock lock) {
    if (lock != null && lock.isHeldByCurrentThread()) {
    lock.unlock();
    }
    }
    }

    3.2.4 第四步:业务场景使用示例

    import org.redisson.api.RLock;
    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;

    @Service
    public class OrderService {
    @Resource
    private RedissonLockUtil redissonLockUtil;
    @Resource
    private StockMapper stockMapper; // 库存 DAO 层

    /**
    * 下单扣减库存(分布式锁保证原子性)
    * @param productId 商品 ID
    * @param quantity 购买数量
    * @return 下单结果
    */

    public String createOrder(Long productId, Integer quantity) {
    // 锁的业务标识:按商品 ID 粒度加锁,避免大面积阻塞
    String lockKey = "product:stock:" + productId;
    RLock lock = null;

    try {
    // 获取锁:最大等待 3 秒,持有锁 10 秒(10 秒内未完成自动释放,避免死锁)
    lock = redissonLockUtil.getLock(lockKey, 3, 10);
    if (lock == null) {
    return "下单失败,请稍后重试";
    }

    // 核心业务逻辑:查询库存 -> 扣减库存
    Integer stock = stockMapper.selectStockByProductId(productId);
    if (stock < quantity) {
    return "库存不足";
    }
    stockMapper.decreaseStock(productId, quantity);
    return "下单成功";

    } finally {
    // 释放锁(确保无论业务成功与否都释放锁)
    redissonLockUtil.unlock(lock);
    }
    }
    }

    3.3 高级特性:红锁(RedLock)

    若 Redis 集群存在主从切换风险(如主节点宕机后从节点未同步锁数据),可使用红锁算法,需部署多个独立的 Redis 节点(至少 3 个):

    import org.redisson.api.RedissonClient;
    import org.redisson.api.RedissonRedLock;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;

    @Component
    public class RedLockUtil {
    @Resource
    private RedissonClient redissonClient1; // 第一个 Redis 节点客户端
    @Resource
    private RedissonClient redissonClient2; // 第二个 Redis 节点客户端
    @Resource
    private RedissonClient redissonClient3; // 第三个 Redis 节点客户端

    /**
    * 获取红锁(多节点锁,提升可用性)
    * @param lockKey 锁的业务标识
    * @param waitTime 最大等待时间(秒)
    * @param leaseTime 持有时间(秒)
    * @return 红锁实例
    */

    public RedissonRedLock getRedLock(String lockKey, long waitTime, long leaseTime) {
    String realLockKey = LOCK_PREFIX + lockKey;
    // 获取多个节点的锁
    List<RLock> locks = new ArrayList<>();
    locks.add(redissonClient1.getLock(realLockKey));
    locks.add(redissonClient2.getLock(realLockKey));
    locks.add(redissonClient3.getLock(realLockKey));

    // 构建红锁(需超过半数节点获取锁成功才算整体成功)
    RedissonRedLock redLock = new RedissonRedLock(locks.toArray(new RLock[0]));
    try {
    boolean success = redLock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
    return success ? redLock : null;
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return null;
    }
    }
    }

    四、使用注意事项

  • 锁粒度设计:尽量按业务最小粒度加锁(如商品 ID、用户 ID),避免使用全局锁(如 lock:all)导致并发性能下降;
  • 过期时间设置: leaseTime 需大于业务最大执行时间,避免锁提前释放;若业务执行时间不确定,设置 leaseTime=0 开启自动续命;
  • 异常处理:必须在 finally 中释放锁,或使用 try-with-resources 语法(RLock 实现了 AutoCloseable 接口);
  • 集群部署:生产环境建议使用 Redis 集群 + 哨兵模式,配合 Redisson 自动重连机制,提升锁服务可用性;
  • 避免滥用:分布式锁是重量级操作,非并发场景无需使用;优先考虑本地锁(synchronized、ReentrantLock)或乐观锁(CAS)。
  • 五、总结

    本文从原生 Redis 分布式锁的手写实现出发,分析了基础版的缺陷,进而引入 Redisson 框架提供生产级解决方案。Redisson 不仅解决了锁超时、可重入、高可用等问题,还提供了丰富的锁类型和优雅的 API,极大降低了分布式锁的使用门槛。

    文中提供的 Redisson 工具类和业务示例可直接复用,开发者可根据实际场景(单机 / 集群、普通锁 / 公平锁 / 红锁)灵活调整。在分布式系统中,合理使用 Redis 分布式锁能有效保障数据一致性,提升系统并发能力。

    赞(0)
    未经允许不得转载:171主机测评 » Redis 分布式锁实战:从手写实现到Redisson优化
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址