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

相关推荐
二宝哥28 分钟前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
ShiXZ2136 小时前
Redis docker-compose 部署指南
redis·docker·eureka
青山木9 小时前
Redis 缓存一致性全解:从 Cache Aside 到延迟双删、Canal+MQ 的进阶之路
java·数据库·redis·后端·缓存
YDS82910 小时前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
爬也要爬着前进1 天前
redis主从搭建
数据库·redis·缓存
用户095367515831 天前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌1 天前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆1 天前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰1 天前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_727535621 天前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf