如何正确使用 Redis 分布式锁(完整技术分享文档)
目录
- 基础概念
- 实现方案与代码实现
- [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")
- 优缺点对比
- 失效情况分析与解决方案
- 最佳实践案例
- 总结
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 框架(推荐方案)
步骤:
- 添加 Maven 依赖:
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>
- 配置 Redis(application.yml):
yaml
spring:
redis:
host: localhost
port: 6379
timeout: 2000ms
- 使用 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 日 版权声明:本文可自由转载,注明出处即可。