在分布式系统中,并发操作共享资源(如库存扣减、订单创建)时,单机锁(synchronized/ReentrantLock)无法跨服务实例生效,而 Redis 分布式锁是解决该问题的主流方案。本文基于前文 SpringBoot+Redis 缓存实践,不仅实现生产级 Redis 分布式锁核心逻辑 ,还封装了注解式分布式锁(@DLock),让业务代码与锁逻辑解耦,兼顾性能与开发优雅性。
一、核心设计思路
本文实现的分布式锁包含两大核心部分:
- 底层核心实现 :基于 Redis
SET NX PX原子命令 + Lua 脚本,保证锁的互斥性、安全性; - 上层优雅封装 :通过自定义注解
@DLock+AOP 切面,实现 "注解即加锁",无需手动编写加锁 / 释放锁代码; - 复用缓存配置:直接复用缓存实践中定义的序列化规则,保证全局 Redis 操作的一致性。
二、环境准备
- 依赖 :
spring-boot-starter-aop(AOP 切面依赖);
三、核心代码实现
1. 分布式锁通用接口
定义标准化接口,隔离锁的底层实现,便于后续扩展(如 Zookeeper 锁):
java
package com.demo.redis.lock;
/**
* 分布式锁通用接口
*/
public interface DistributedLock {
/**
* 阻塞式获取锁(获取不到则一直等待)
* @param lockKey 锁的唯一标识
* @param expireTime 锁过期时间(毫秒)
* @return 是否获取成功
*/
boolean lock(String lockKey, long expireTime);
/**
* 释放锁
* @param lockKey 锁的唯一标识
* @return 是否释放成功
*/
boolean unlock(String lockKey);
/**
* 带超时的非阻塞获取锁
* @param lockKey 锁的唯一标识
* @param expireTime 锁过期时间(毫秒)
* @param waitTime 最大等待时间(毫秒,-1表示无限等待)
* @return 是否获取成功
*/
boolean tryLock(String lockKey, long expireTime, long waitTime);
}
2. Redis 分布式锁核心实现(生产级)
解决线程安全、多锁兼容、异常处理等核心问题,是分布式锁的底层核心:
java
package com.demo.redis.lock.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.demo.redis.config.RedisConfigure;
import com.demo.redis.lock.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis分布式锁实现类
* 核心特性:
* 1. SET NX PX 原子加锁 + Lua脚本原子释放锁
* 2. ThreadLocal<Map>存储锁标识,支持同一线程持有多把锁
* 3. 复用缓存配置的序列化规则,保证全局一致性
*/
@Slf4j
@Component
public class RedisDistributedLock implements DistributedLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 线程本地存储:key=锁标识,value=唯一UUID(避免线程间锁标识冲突)
private ThreadLocal<Map<String, String>> lockFlag = new ThreadLocal<>();
// 释放锁Lua脚本:保证"判断锁归属+删除锁"原子性,防止误删其他客户端锁
private static final String UNLOCK_LUA_SCRIPT =
"if redis.call("get",KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call("del",KEYS[1]) " +
"else " +
" return 0 " +
"end ";
/**
* 阻塞式获取锁:复用tryLock逻辑,无限等待
*/
@Override
public boolean lock(String lockKey, long expireTime) {
return tryLock(lockKey, expireTime, -1);
}
/**
* 释放锁:核心保证仅持有者可释放
*/
@Override
public boolean unlock(String lockKey) {
// 1. 前置校验:当前线程未持有该锁,直接返回失败
Map<String, String> map = lockFlag.get();
if (CollectionUtil.isEmpty(map) || !map.containsKey(lockKey)) {
log.warn("释放锁失败:当前线程未持有锁,lockKey={}", lockKey);
return false;
}
try {
// 2. 执行Lua脚本释放锁(原子操作)
Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
byte[] scriptByte = RedisSerializer.string().serialize(UNLOCK_LUA_SCRIPT);
return connection.eval(
scriptByte,
ReturnType.BOOLEAN,
1,
RedisConfigure.KEY_SERIALIZER.serialize(lockKey), // 复用缓存的key序列化器
RedisConfigure.VALUE_SERIALIZER.serialize(map.get(lockKey)) // 复用缓存的value序列化器
);
});
if (!Boolean.TRUE.equals(success)) {
log.warn("释放锁失败:锁已过期或被其他客户端持有,lockKey={}", lockKey);
}
return Boolean.TRUE.equals(success);
} catch (Exception e) {
log.error("释放锁异常,lockKey={}", lockKey, e);
} finally {
// 3. 清理线程本地存储,避免内存泄漏
map.remove(lockKey);
if (map.isEmpty()) {
lockFlag.remove(); // 无锁时清空ThreadLocal
}
}
return false;
}
/**
* 带超时的非阻塞获取锁:核心原子加锁逻辑
*/
@Override
public boolean tryLock(String lockKey, long expireTime, long waitTime) {
// 1. 参数校验:过期时间必须>0,防止死锁
if (expireTime <= 0) {
throw new IllegalArgumentException("锁过期时间必须大于0");
}
long startTime = System.currentTimeMillis();
String uuid = UUID.randomUUID().toString(); // 每个锁的唯一标识
// 2. 循环重试获取锁
while (true) {
try {
// 3. 执行Redis原子加锁命令(SET NX PX)
Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
// 初始化线程本地存储
Map<String, String> map = lockFlag.get();
if (map == null) {
map = new HashMap<>();
}
map.put(lockKey, uuid);
lockFlag.set(map);
// 核心:Redis底层原子操作(NX=仅当key不存在时设置,PX=设置过期时间)
return connection.set(
RedisConfigure.KEY_SERIALIZER.serialize(lockKey),
RedisConfigure.VALUE_SERIALIZER.serialize(uuid),
Expiration.from(expireTime, TimeUnit.MILLISECONDS),
RedisStringCommands.SetOption.ifAbsent()
);
});
// 4. 加锁成功则返回
if (Boolean.TRUE.equals(success)) {
log.info("获取锁成功,lockKey={}, uuid={}", lockKey, uuid);
return true;
}
// 5. 校验是否超时,超时则放弃
if (waitTime != -1 && (System.currentTimeMillis() - startTime) > waitTime) {
log.info("获取锁超时,lockKey={}, waitTime={}ms", lockKey, waitTime);
return false;
}
} catch (Exception e) {
log.error("获取锁异常,lockKey={}", lockKey, e);
}
// 6. 未超时则短暂休眠后重试(避免CPU空转)
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
return false;
}
}
}
}
3. 自定义分布式锁注解(@DLock)
通过注解简化加锁逻辑,支持自定义锁前缀、锁标识、过期时间:
java
package com.demo.redis.lock;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 分布式锁注解
* 作用于方法上,实现"注解即加锁"
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DLock {
/**
* 锁前缀(用于区分业务)
*/
String prefix() default "";
/**
* 锁标识的SpEL表达式(支持方法参数解析)
* 示例:[#productId, #user.id]
*/
String[] keys() default {};
/**
* 锁过期时间(毫秒)
* 默认使用切面中的默认值
*/
long lockExpire() default 0;
}
4. 分布式锁切面(AOP)
解析@DLock注解,自动完成加锁 / 释放锁逻辑,是注解式锁的核心:
java
package com.demo.redis.lock;
import cn.hutool.core.util.StrUtil;
import com.demo.redis.utils.SpelUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 分布式锁切面
* 解析@DLock注解,自动加锁/释放锁
*/
@Aspect
@Component
@EnableAspectJAutoProxy
@Slf4j
public class DLockAspect {
public static final String SPLICE_STR = ":";
/**
* 默认锁过期时间(ms)
*/
private static final long DEFAULT_LOCK_EXPIRE_MS = 2000L;
@Autowired
private DistributedLock distributedLock;
/**
* 切点:匹配所有标注@DLock的方法
*/
@Pointcut("@annotation(dLock)")
public void pointCut(DLock dLock) {
}
/**
* 环绕通知:执行加锁→执行业务→释放锁
*/
@Around("pointCut(dLock)")
public Object invoke(ProceedingJoinPoint joinPoint, DLock dLock) throws Throwable {
long startTime = System.currentTimeMillis();
// 1. 构建锁标识
String lockKey = dLock.prefix();
final String[] keys = dLock.keys();
if (keys.length > 0) {
String[] paramValues = new String[keys.length];
for (int i = 0; i < keys.length; i++) {
// 解析SpEL表达式,获取方法参数值
paramValues[i] = SpelUtils.parse(keys[i], joinPoint);
}
log.info("分布式锁参数解析结果:{}", Arrays.toString(paramValues));
lockKey += String.join(SPLICE_STR, paramValues);
}
// 兜底:未配置锁标识时,使用类名+方法名
if (StrUtil.isBlank(lockKey)) {
lockKey = joinPoint.getTarget().getClass().getSimpleName() + SPLICE_STR + joinPoint.getSignature().getName();
}
lockKey = lockKey.toLowerCase(); // 统一小写,避免大小写冲突
try {
// 2. 获取锁(使用注解配置的过期时间,无则用默认值)
long lockExpire = dLock.lockExpire() > 0 ? dLock.lockExpire() : DEFAULT_LOCK_EXPIRE_MS;
boolean lockSuccess = distributedLock.lock(lockKey, lockExpire);
if (lockSuccess) {
log.info("获取分布式锁成功,lockKey={},耗时(ms):{}", lockKey, System.currentTimeMillis() - startTime);
// 3. 执行业务方法
return joinPoint.proceed();
} else {
throw new RuntimeException("获取分布式锁失败:" + lockKey);
}
} finally {
// 4. 释放锁(finally保证必释放)
try {
distributedLock.unlock(lockKey);
log.info("释放分布式锁成功,lockKey={}", lockKey);
} catch (Exception e) {
log.error("释放分布式锁异常,lockKey={}", lockKey, e);
}
}
}
}
5. SpEL 表达式解析工具
用于解析@DLock注解中的keys参数,支持从方法参数中提取锁标识:
java
package com.demo.redis.utils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
/**
* SpEL表达式解析工具
* 解析@DLock注解中的keys参数
*/
@Slf4j
public class SpelUtils {
/**
* SpEL表达式解析器
*/
private static final ExpressionParser PARSER = new SpelExpressionParser();
/**
* 方法参数名发现器
*/
private static final ParameterNameDiscoverer NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
/**
* 解析SpEL表达式,获取实际参数值
* @param eps SpEL表达式(如#productId)
* @param joinPoint 切面连接点
* @return 解析后的参数值
*/
public static String parse(String eps, ProceedingJoinPoint joinPoint) {
try {
// 1. 获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 2. 获取方法参数名
String[] paramNames = NAME_DISCOVERER.getParameterNames(method);
// 3. 解析SpEL表达式
Expression expression = PARSER.parseExpression(eps);
// 4. 构建表达式上下文(绑定方法参数)
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 5. 计算表达式值
final Object value = expression.getValue(context);
log.info("解析SpEL表达式:{} → {}", eps, value);
return value == null ? "" : value.toString();
} catch (Exception e) {
log.error("解析SpEL表达式失败,表达式:{}", eps, e);
}
return "";
}
}
四、实战场景:库存扣减
以电商核心场景 "库存扣减" 为例,分别演示手动加锁 和注解式加锁两种方式,验证分布式锁有效性。
1. 库存业务接口
java
package com.demo.redis.service;
/**
* 库存业务接口
*/
public interface IStockService {
/**
* 手动加锁扣减库存
* @param productId 商品ID
* @param deductNum 扣减数量
* @return 是否扣减成功
*/
boolean deductStock(Long productId, Integer deductNum);
/**
* 注解式加锁扣减库存
* @param productId 商品ID
* @param deductNum 扣减数量
* @return 是否扣减成功
*/
boolean deductStockV2(Long productId, Integer deductNum);
}
2. 库存业务实现类
java
package com.demo.redis.service.impl;
import com.demo.redis.lock.DLock;
import com.demo.redis.lock.DistributedLock;
import com.demo.redis.service.IStockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 库存业务实现:分布式锁解决并发扣减问题
*/
@Slf4j
@Service
public class StockServiceImpl implements IStockService {
// 模拟库存存储(实际为数据库/Redis)
private volatile Long stock = 100L;
@Resource
private DistributedLock distributedLock;
/**
* 方式1:手动加锁扣减库存
*/
@Override
public boolean deductStock(Long productId, Integer deductNum) {
// 1. 定义锁标识(按商品粒度加锁,提升并发性能)
String lockKey = "stock:lock:" + productId;
// 2. 锁过期时间(30秒,需大于业务执行时间)
long expireTime = TimeUnit.SECONDS.toMillis(30);
try {
// 3. 获取分布式锁
boolean lockSuccess = distributedLock.lock(lockKey, expireTime);
if (!lockSuccess) {
log.error("库存扣减失败:获取锁超时,productId={}", productId);
return false;
}
// 4. 核心业务:扣减库存(互斥区,仅一个线程执行)
if (stock >= deductNum) {
log.info("扣减库存前:{},扣减数量:{}", stock, deductNum);
stock -= deductNum;
log.info("扣减库存后:{}", stock);
return true;
} else {
log.error("库存不足:productId={},剩余库存={},扣减数量={}", productId, stock, deductNum);
return false;
}
} finally {
// 5. 释放锁:finally块保证锁必释放,防止死锁
distributedLock.unlock(lockKey);
}
}
/**
* 方式2:注解式加锁扣减库存(优雅版)
* @DLock注解说明:
* - prefix:锁前缀,区分业务
* - keys:锁标识(SpEL表达式,提取productId)
* - lockExpire:锁过期时间(30秒)
*/
@DLock(prefix = "stock:lock:", keys = "#productId", lockExpire = 30_000L)
@Override
public boolean deductStockV2(Long productId, Integer deductNum) {
// 核心业务:无需编写任何加锁/释放锁代码
if (stock >= deductNum) {
log.info("扣减库存前:{},扣减数量:{}", stock, deductNum);
stock -= deductNum;
log.info("扣减库存后:{}", stock);
return true;
} else {
log.error("库存不足:productId={},剩余库存={},扣减数量={}", productId, stock, deductNum);
return false;
}
}
// 供测试获取库存
public Long getStock() {
return stock;
}
}
五、测试验证
编写并发测试用例,模拟 100 个线程并发扣减库存,验证两种加锁方式的有效性:
java
package com.demo.redis;
import com.demo.redis.service.IStockService;
import com.demo.redis.service.impl.StockServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@SpringBootTest(classes = DemoRedisApplication.class)
public class StockServiceTest {
@Autowired
private IStockService stockService;
// 并发线程数
private static final int THREAD_COUNT = 100;
// 每个线程扣减数量
private static final int DEDUCT_NUM = 1;
/**
* 测试手动加锁扣减库存
*/
@Test
public void testDeductStock() throws InterruptedException {
// 1. 创建线程池(模拟分布式环境多实例并发)
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
// 2. 模拟并发扣减
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(() -> {
try {
stockService.deductStock(1L, DEDUCT_NUM);
} finally {
countDownLatch.countDown();
}
});
}
// 3. 等待所有线程执行完成
countDownLatch.await();
executorService.shutdown();
// 4. 验证库存结果(预期:100 - 100*1 = 0)
Long finalStock = ((StockServiceImpl) stockService).getStock();
log.info("手动加锁-并发扣减完成,最终库存:{}", finalStock);
}
/**
* 测试注解式加锁扣减库存
*/
@Test
public void testDeductStockV2() throws InterruptedException {
// 1. 重置库存(避免前一个测试影响)
((StockServiceImpl) stockService).stock = 100L;
// 2. 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
// 3. 模拟并发扣减
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(() -> {
try {
stockService.deductStockV2(1L, DEDUCT_NUM);
} finally {
countDownLatch.countDown();
}
});
}
// 4. 等待所有线程执行完成
countDownLatch.await();
executorService.shutdown();
// 5. 验证库存结果
Long finalStock = ((StockServiceImpl) stockService).getStock();
log.info("注解式加锁-并发扣减完成,最终库存:{}", finalStock);
}
}
六、生产级考虑问题
业务执行时间超过锁过期时间,锁提前释放导致并发问题、同一线程多次获取同一把锁,往往续期锁、可重入锁等方面扩展必不可少。