目录
- 基础概念
- 实现方案与代码实现
- 2.1 SET 命令实现
- 2.2 StringRedisTemplate 实现
- 2.3 Redlock 算法
- 2.4 Redisson 框架
- 2.5 Spring Boot 注解方式实现
- 优缺点对比
- 失效情况分析与解决方案
- 最佳实践案例
- 总结
1. 基础概念
分布式锁用于协调多个节点对共享资源的互斥访问,常用于秒杀、抢红包、库存扣减等高并发场景。Redis 因其高性能、支持原子性操作(如SET NX PX)而成为实现分布式锁的常用工具。
一个可靠的分布式锁需满足以下核心要求:
- 互斥性:任意时刻只有一个客户端能持有锁。
- 锁超时释放:设置合理的过期时间,避免因客户端宕机导致死锁。
- 安全性:锁只能被其持有者释放,防止误删。
- 高性能与高可用:加解锁操作应低延迟,且在部分节点故障时仍能正常工作。
2. 实现方案与代码实现
2.1 SET 命令实现(基础方案)
原理:
利用 Redis 的 SET key value NX PX milliseconds 命令实现原子性加锁。解锁时,需通过 Lua 脚本验证持有者身份后再删除,确保安全。
代码示例(Java + Jedis):
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 Boot 项目中,利用 Spring Data Redis 提供的 StringRedisTemplate 实现加解锁,同样结合 Lua 脚本来保证解锁操作的安全性。
代码示例(Spring Boot):
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;
import java.util.concurrent.TimeUnit;
@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 算法(多实例方案)
原理:
为了提升可用性,Redlock 算法要求在多个独立的 Redis 实例上同时获取锁,当从大多数(N/2+1)实例上成功获取锁时,才算加锁成功。这可以有效应对单点故障。
代码示例(伪代码):
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 框架(推荐方案)
Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格客户端,它提供了丰富的数据结构和分布式服务,其分布式锁实现非常成熟,支持可重入、自动续期、多种锁类型等特性。
使用步骤:
- 添加 Maven 依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>
- 配置 Redis 连接(application.yml):
spring:
redis:
host: localhost
port: 6379
timeout: 2000ms
- 使用 RLock 接口:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@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,从而获得可重入、自动续期等高阶功能。
2.5.2 实现步骤
1. 定义自定义注解
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 切面
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
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 必须启用 exposeProxy
public class AopConfig {}
4. 使用示例
@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 或未扫描切面类 |
添加配置并确保切面类被 Spring 扫描 |
| 3. 事务传播行为不匹配 |
@Transactional 方法嵌套调用可能导致锁提前释放 |
为分布式锁方法单独开启新事务(@Transactional(propagation = Propagation.REQUIRES_NEW)) |
| 4. 异常未捕获 |
方法抛出异常后未正确释放锁 |
Redisson 已自动处理,但需注意避免在 finally 块前退出 |
| 5. SpEL 表达式错误 |
value 中的表达式解析失败(如 #userId 不存在) |
检查方法参数名是否匹配,或使用 args[0] 替代 |
3. 优缺点对比
| 方案 |
互斥性 |
自动续期 |
可重入 |
高可用 |
性能 |
适用场景 |
| SET 命令 |
✅ |
❌ |
❌ |
❌ |
⭐⭐⭐⭐⭐ |
低并发、简单的单点业务 |
| StringRedisTemplate |
✅ |
❌ |
❌ |
❌ |
⭐⭐⭐⭐ |
基于 Spring Boot 的轻量级项目 |
| Redlock |
✅ |
❌ |
❌ |
✅ |
⭐⭐⭐ |
对强一致性和高可用性要求极高的场景 |
| Redisson |
✅ |
✅ |
✅ |
✅ |
⭐⭐⭐⭐ |
复杂的 Java/Spring Boot 业务系统 |
| 注解方式 |
✅ |
✅ |
✅ |
✅ |
⭐⭐⭐⭐ |
追求快速开发、代码简洁的 Spring Boot 项目 |
4. 失效情况分析与解决方案
4.1 死锁(Lock Not Released)
- 原因:持有锁的客户端异常崩溃,未能执行解锁指令。
- 解决方案:务必设置合理的锁超时时间(TTL)。对于执行时间不确定的任务,可使用 Redisson 的 Watchdog 机制自动续期。
4.2 误删他人锁(Wrong Unlock)
- 原因:客户端A的锁超时释放后,客户端B获取了锁,此时客户端A又尝试执行删除操作,导致B的锁被误删。
- 解决方案:加锁时设置唯一值(如 UUID),解锁时使用 Lua 脚本先校验该值再删除。
4.3 主从切换丢锁
- 原因:在 Redis 主从架构中,锁信息写入主节点后,在同步到从节点前主节点宕机,发生故障转移,新主节点上无此锁数据。
- 解决方案:对于一致性要求极高的场景,考虑使用 Redlock 算法或改用 ZooKeeper、etcd 等 CP 型系统。
4.4 业务执行时间超过锁 TTL
- 原因:业务逻辑执行时间过长,超过了锁的过期时间,锁自动释放,导致其他客户端可进入临界区。
- 解决方案:合理评估并设置足够长的 TTL,或使用具备自动续期(Watchdog)功能的客户端(如 Redisson)。
4.5 注解失效的典型场景分析
场景 1:同类方法调用导致锁失效
@Service
public class OrderService {
@DistributedLock(value = "#userId")
public void createOrder(String userId) {
// 业务逻辑
}
public void batchCreateOrders(String userId) {
createOrder(userId); // ❌ 同类调用,AOP 代理失效,锁不生效
}
}
解决方案:
public void batchCreateOrders(String userId) {
((OrderService) AopContext.currentProxy()).createOrder(userId); // ✅ 通过代理调用
}
场景 2:事务传播行为导致锁提前释放
@Service
public class OrderService {
@DistributedLock(value = "#userId")
@Transactional
public void createOrder(String userId) {
// 业务逻辑
}
@Transactional
public void processOrder(String userId) {
createOrder(userId); // ❌ 同事务中调用,事务提交前锁可能因连接关闭而提前释放
}
}
解决方案:让分布式锁方法运行在独立事务中。
@DistributedLock(value = "#userId")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(String userId) {
// 独立事务
}
5. 最佳实践案例
场景:秒杀系统扣减库存(Redisson + Spring Boot)
@Service
public class SeckillService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StockService stockService;
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 + Lua 是轻量可靠的备选。
- 注解化封装:通过 自定义注解 + AOP 的方式,可以极大简化代码,使业务逻辑更清晰,是团队协作和快速开发的利器。
- 避免手写低级错误:切勿手动组合
SETNX 和 EXPIRE 等非原子命令,务必进行持有者身份校验。
- 锁粒度精细化:根据业务按用户ID、商品ID等维度加锁,而非使用全局大锁,以提升系统并发能力。
- 设计兜底方案:分布式锁并非银弹。在业务层结合数据库唯一约束、版本号乐观锁或幂等性设计,即使锁偶尔失效,系统也能保持最终正确性。
核心原则:“能不用锁就不用锁,能用数据库约束等轻量级方案就不用分布式锁。” 分布式锁会引入复杂度并降低性能,应在确有必要时谨慎使用。