如何正确使用 Redis 分布式锁(完整技术分享文档)

如何正确使用 Redis 分布式锁(完整技术分享文档)


目录

  1. 基础概念
  2. 实现方案与代码实现
    • [2.1 SET 命令实现](#2.1 SET 命令实现 "#21-set-%E5%91%BD%E4%BB%A4%E5%AE%9E%E7%8E%B0")
    • [2.2 StringRedisTemplate 实现](#2.2 StringRedisTemplate 实现 "#22-stringredistemplate-%E5%AE%9E%E7%8E%B0")
    • [2.3 Redlock 算法](#2.3 Redlock 算法 "#23-redlock-%E7%AE%97%E6%B3%95")
    • [2.4 Redisson 框架](#2.4 Redisson 框架 "#24-redisson-%E6%A1%86%E6%9E%B6")
    • [2.5 Spring Boot 注解方式实现](#2.5 Spring Boot 注解方式实现 "#25-spring-boot-%E6%B3%A8%E8%A7%A3%E6%96%B9%E5%BC%8F%E5%AE%9E%E7%8E%B0")
  3. 优缺点对比
  4. 失效情况分析与解决方案
  5. 最佳实践案例
  6. 总结

1. 基础概念

分布式锁 用于协调多个节点对共享资源的互斥访问,常用于秒杀、抢红包、库存扣减等场景。Redis 因其高性能、原子性操作(如 SET NX PX)成为分布式锁的常用实现工具。

核心要求:

  • 互斥性:任意时刻只有一个客户端持有锁。
  • 锁超时释放:避免死锁。
  • 安全性:只能被持有者删除。
  • 高性能与高可用:低开销、支持多节点容错。

2. 实现方案与代码实现

2.1 SET 命令实现(基础方案)

原理 :

使用 SET key value NX PX milliseconds 原子命令实现加锁,解锁通过 Lua 脚本校验持有者身份。

代码示例(Java + Jedis):

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

public class RedisLock {
    private static final String LOCK_KEY = "resource_lock";
    private static final int EXPIRE_TIME = 30000; // 30s

    public static boolean tryLock(Jedis jedis, String lockKey, String uniqueId, int expireTime) {
        String result = jedis.set(lockKey, uniqueId, "NX", "PX", expireTime);
        return "OK".equals(result);
    }

    public static void unlock(Jedis jedis, String lockKey, String uniqueId) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(luaScript, 1, lockKey, uniqueId);
    }

    // 使用示例
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        String uniqueId = UUID.randomUUID().toString();
        if (tryLock(jedis, LOCK_KEY, uniqueId, EXPIRE_TIME)) {
            try {
                // 业务逻辑
            } finally {
                unlock(jedis, LOCK_KEY, uniqueId);
            }
        }
    }
}

2.2 StringRedisTemplate 实现(Spring Boot 推荐)

原理 :

使用 Spring Data Redis 提供的 StringRedisTemplate 实现加锁和解锁,结合 Lua 脚本确保安全性。

代码示例(Spring Boot):

java 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.UUID;

@Service
public class RedisLockService {

    private final StringRedisTemplate redisTemplate;

    public RedisLockService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock(String lockKey, long expireTime) {
        String uniqueId = UUID.randomUUID().toString();
        Boolean result = redisTemplate.opsForValue().setIfAbsent(
            lockKey, uniqueId, expireTime, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(result);
    }

    public void unlock(String lockKey, String uniqueId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        redisTemplate.execute(redisScript, Collections.singletonList(lockKey), uniqueId);
    }

    // 使用示例
    public void processResource() {
        String lockKey = "resource_lock";
        if (tryLock(lockKey, 30000)) {
            try {
                // 业务逻辑
            } finally {
                unlock(lockKey, redisTemplate.opsForValue().get(lockKey));
            }
        }
    }
}

2.3 Redlock 算法(多实例方案)

原理 :

通过多个独立 Redis 实例实现高可用锁。

代码示例(伪代码):

python 复制代码
def acquire_redlock(lock_name, expire_time):
    instances = [redis.Redis(host=f'redis{i}') for i in range(5)]
    success_count = 0
    for instance in instances:
        if instance.set(lock_name, "locked", nx=True, px=expire_time):
            success_count += 1
    return success_count >= 3  # 多数成功

2.4 Redisson 框架(推荐方案)

步骤:

  1. 添加 Maven 依赖:
xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>
  1. 配置 Redis(application.yml):
yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
  1. 使用 RLock:
java 复制代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private RedissonClient redissonClient;

    public void processOrder(String orderId) {
        RLock lock = redissonClient.getLock("order_lock:" + orderId);
        try {
            boolean isLocked = lock.tryLock(30, 30, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("获取锁失败");
            }
            // 业务逻辑
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

2.5 Spring Boot 注解方式实现

2.5.1 实现原理

通过 自定义注解 + AOP 切面 实现分布式锁的自动加锁/解锁,结合 Redisson 的 RLock 提供可重入锁、自动续期等功能。


2.5.2 实现步骤
1. 定义自定义注解
java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
    String value(); // 锁 key 的 SpEL 表达式(如 #userId)
    int waitTime() default 30; // 等待加锁时间(秒)
    int leaseTime() default 30; // 锁持有时间(秒)
}

2. 实现 AOP 切面
java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class DistributedLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    private final ExpressionParser parser = new SpelExpressionParser();

    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint pjp, DistributedLock distributedLock) throws Throwable {
        // 解析 SpEL 表达式获取锁 key
        String lockKey = parseSpEL(distributedLock.value(), pjp.getArgs());
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 尝试加锁
            boolean isLocked = lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("获取分布式锁超时");
            }
            return pjp.proceed(); // 执行目标方法
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 解析 SpEL 表达式
    private String parseSpEL(String expression, Object[] args) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < args.length; i++) {
            context.setVariable("arg" + i, args[i]);
        }
        return parser.parseExpression(expression).getValue(context, String.class);
    }
}

3. 配置 Spring Boot 启用 AOP
java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 必须启用 exposeProxy
public class AopConfig {
}

4. 使用示例
java 复制代码
@Service
public class OrderService {

    @DistributedLock(value = "#userId", waitTime = 10, leaseTime = 30)
    public void createOrder(String userId) {
        // 业务逻辑(无需手动加锁)
    }
}

2.5.3 注解失效的常见场景
场景 原因 解决方案
1. 同类方法调用 AOP 代理失效(Spring 无法拦截内部调用) 使用 ((OrderService) AopContext.currentProxy()).createOrder(userId)
2. 未启用 AOP 未配置 @EnableAspectJAutoProxy 或未扫描切面类 添加 @EnableAspectJAutoProxy 并确保切面类被 Spring 扫描
3. 事务传播行为不匹配 @Transactional 方法嵌套调用导致锁提前释放 为分布式锁方法单独开启新事务(@Transactional(propagation = Propagation.REQUIRES_NEW))
4. 异常未捕获 方法抛出异常后未正确释放锁 在 finally 块中释放锁(Redisson 已自动处理)
5. SpEL 表达式错误 value 中的 SpEL 表达式解析失败(如 #userId 不存在) 检查方法参数名是否匹配,或使用 args[0] 代替 #userId

3. 优缺点对比

方案 互斥性 自动续期 可重入 高可用 性能 适用场景
SET 命令 ✅ ❌ ❌ ❌ ⭐⭐⭐⭐⭐ 低并发、简单业务
StringRedisTemplate ✅ ❌ ❌ ❌ ⭐⭐⭐⭐ Spring Boot 项目
Redlock ✅ ❌ ❌ ✅ ⭐⭐⭐ 高并发、强一致性要求
Redisson ✅ ✅ ✅ ✅ ⭐⭐⭐⭐ 复杂业务、Java 项目
注解方式 ✅ ✅ ✅ ✅ ⭐⭐⭐⭐ 快速开发、Spring Boot

4. 失效情况分析与解决方案

4.1 死锁(Lock Not Released)

  • 原因:客户端异常崩溃未释放锁。
  • 解决方案:设置合理的 TTL(如 30s),或使用 Redisson 的 Watchdog 自动续期。

4.2 误删他人锁(Wrong Unlock)

  • 原因 :未校验锁持有者身份直接 DEL。
  • 解决方案:加锁时写入唯一标识,解锁时用 Lua 脚本校验。

4.3 主从切换丢锁

  • 原因:主节点宕机后从节点未同步锁信息。
  • 解决方案:使用 Redlock 或改用 CP 系统(如 ZooKeeper)。

4.4 业务执行时间 > TTL

  • 原因:锁自动释放导致重复执行。
  • 解决方案:引入 Watchdog 自动续期(Redisson 默认支持)。

4.5 注解失效的典型场景

场景 1:同类方法调用导致锁失效
java 复制代码
@Service
public class OrderService {

    @DistributedLock(value = "#userId")
    public void createOrder(String userId) {
        // 业务逻辑
    }

    public void batchCreateOrders(String userId) {
        createOrder(userId); // ❌ 同类调用,AOP 代理失效
    }
}

解决方案:

java 复制代码
public void batchCreateOrders(String userId) {
    ((OrderService) AopContext.currentProxy()).createOrder(userId); // ✅ 通过代理调用
}
场景 2:事务传播行为导致锁提前释放
java 复制代码
@Service
public class OrderService {

    @DistributedLock(value = "#userId")
    @Transactional
    public void createOrder(String userId) {
        // 业务逻辑
    }

    @Transactional
    public void processOrder(String userId) {
        createOrder(userId); // ❌ 同事务中调用,锁可能提前释放
    }
}

解决方案:

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(String userId) {
    // 独立事务
}

5. 最佳实践案例

场景:秒杀系统扣库存(Redisson + Spring Boot)

java 复制代码
@Service
public class SeckillService {

    @Autowired
    private RedissonClient redissonClient;

    public String seckill(String productId, String userId) {
        RLock lock = redissonClient.getLock("seckill_lock:" + productId);
        try {
            boolean isLocked = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!isLocked) {
                return "系统繁忙,请稍后";
            }
            // 双重检查库存
            Integer stock = stockService.getStock(productId);
            if (stock <= 0) {
                return "库存不足";
            }
            stockService.deduct(productId);
            createOrder(productId, userId);
            return "秒杀成功";
        } catch (Exception e) {
            return "秒杀失败";
        } finally {
            lock.unlock();
        }
    }
}

6. 总结

  • 优先选择 Redisson:内置自动续期、可重入、高可用,适合复杂业务。
  • Spring Boot 项目推荐 StringRedisTemplate:简化配置,兼容性强。
  • 注解方式实现:通过 AOP 切面 + Redisson 提供开箱即用的分布式锁能力,简化代码逻辑。
  • 避免手写锁:非原子操作、未校验持有者等常见误区易导致严重问题。
  • 锁粒度要细:如按商品 ID、用户 ID 维度加锁,提升并发度。
  • 结合幂等性设计:即使锁失效,业务逻辑也能通过数据库约束、唯一索引兜底。

一句话原则 :
"能不用锁就不用锁,能用数据库约束就不用分布式锁。"


作者:Beata - 后端服务架构自由人

最后更新:2025 年 11 月 30 日 版权声明:本文可自由转载,注明出处即可。

相关推荐
IT大白鼠3 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙3 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除3 天前
redis--018
redis
两锭子3 天前
Redis基础
redis
烟沙九洲3 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索3 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞3 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路3 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog3 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis