如何正确使用 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 日 版权声明:本文可自由转载,注明出处即可。

相关推荐
shuair1 小时前
redis大key问题-生成大key-生成100万条测试数据
redis
n***33352 小时前
linux redis简单操作
linux·运维·redis
T***74254 小时前
redis连接服务
数据库·redis·bootstrap
m***92385 小时前
Window下Redis的安装和部署详细图文教程(Redis的安装和可视化工具的使用)
数据库·redis·缓存
程序员皮皮林6 小时前
Redis:大数据中如何抗住2000W的QPS
大数据·数据库·redis
n***s9096 小时前
Redis如何设置密码
数据库·redis·缓存
y***61316 小时前
redis 使用
数据库·redis·缓存
n***78686 小时前
Redis-配置文件
数据库·redis·oracle
r***11336 小时前
如何实现Redis安装与使用的详细教程
数据库·redis·缓存