本文目录导读:

我来详细讲解如何使用Redis分布式锁实现接口防重,这是企业级项目中非常常见的解决方案。
核心原理
为什么需要接口防重?
- 用户重复提交(快速点击、网络重试)
- 接口幂等性要求
- 防止数据重复处理
Redis分布式锁优势
- 原子性操作
- 自动过期(避免死锁)
- 高性能
- 支持分布式场景
完整实现方案
1 基础依赖(Spring Boot + Redis)
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2 Redis配置
# application.yml
spring:
redis:
host: localhost
port: 6379
password:
timeout: 5000
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
3 核心锁工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLockUtil {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取分布式锁(最简实现)
*/
public boolean tryLock(String key, String value, long expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁(使用Lua脚本保证原子性)
*/
public void unlock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
);
}
}
4 注解方式(推荐)
自定义注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreventDuplicateSubmit {
// 锁的key前缀
String prefix() default "";
// 过期时间(秒)
long expireTime() default 10;
// 提示信息
String message() default "请求处理中,请勿重复提交";
}
AOP切面实现
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.UUID;
@Aspect
@Component
@Order(1) // 高优先级
public class PreventDuplicateAspect {
@Autowired
private RedisLockUtil redisLockUtil;
@Around("@annotation(com.example.annotation.PreventDuplicateSubmit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
PreventDuplicateSubmit annotation = method.getAnnotation(PreventDuplicateSubmit.class);
// 生成唯一锁Key
String lockKey = generateLockKey(joinPoint, annotation);
String lockValue = UUID.randomUUID().toString();
try {
// 尝试获取锁
boolean locked = redisLockUtil.tryLock(lockKey, lockValue, annotation.expireTime());
if (!locked) {
// 重复提交,返回提示信息
throw new RuntimeException(annotation.message());
}
// 执行业务逻辑
return joinPoint.proceed();
} finally {
// 业务执行完毕后释放锁(注意:不是简单删除,要校验value)
redisLockUtil.unlock(lockKey, lockValue);
}
}
/**
* 生成锁Key(结合请求参数和用户信息)
*/
private String generateLockKey(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit annotation) {
StringBuilder sb = new StringBuilder("prevent:duplicate:");
// 1. 添加自定义前缀
if (!annotation.prefix().isEmpty()) {
sb.append(annotation.prefix()).append(":");
}
// 2. 添加用户ID(从请求中获取)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String userId = request.getHeader("userId");
if (userId != null) {
sb.append("user:").append(userId).append(":");
}
}
// 3. 添加请求参数(建议只取关键参数)
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
sb.append("params:").append(JSON.toJSONString(args));
}
return sb.toString();
}
}
5 业务使用示例
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/create")
@PreventDuplicateSubmit(prefix = "order_create", expireTime = 5, message = "订单正在处理中")
public Result createOrder(@RequestBody OrderDTO orderDTO) {
// 业务逻辑
return Result.success("订单创建成功");
}
}
高级实现(Redisson)
如果希望使用更成熟的方案,推荐Redisson:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.22.0</version>
</dependency>
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedissonDistributedLock {
@Autowired
private RedissonClient redissonClient;
/**
* 使用Redisson实现分布式锁
*/
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime,
TimeUnit unit, LockSupplier<T> supplier) {
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
try {
// 尝试获取锁(等待时间,释放时间,时间单位)
locked = lock.tryLock(waitTime, leaseTime, unit);
if (!locked) {
throw new RuntimeException("系统繁忙,请稍后再试");
}
return supplier.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@FunctionalInterface
public interface LockSupplier<T> {
T get();
}
}
使用示例:
@PostMapping("/payment")
public Result payment(@RequestBody PaymentDTO paymentDTO) {
String lockKey = "payment:" + paymentDTO.getOrderId();
return redissonDistributedLock.executeWithLock(
lockKey,
3, // 等待时间
10, // 释放时间
TimeUnit.SECONDS,
() -> {
// 业务逻辑
return Result.success("支付成功");
}
);
}
关键设计要点
1 锁Key设计原则
完整结构:prefix:userId:methodName:params[:uniqueId]
示例:
- order:create:user123:{"orderId":"ORD001"}
- payment:user456:payment:{"paymentId":"PAY001","amount":100}
- submit:user789:submit:{"formId":"FORM001"}
2 常见问题处理
// 1. Redis锁续期问题(复杂业务)
public boolean tryLockWithRenew(String key, String value, long expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
// 启动一个后台线程定期续期
renewExpire(key, value, expireTime);
}
return Boolean.TRUE.equals(result);
}
// 2. 锁粒度控制(尽可能细)
// 错误:全局锁
String badKey = "submit:all";
// 正确:细粒度锁
String goodKey = "submit:user:" + userId + ":order:" + orderId;
3 异常情况处理
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
if (e.getMessage().contains("请勿重复提交")) {
return Result.error(400, e.getMessage());
}
// 其他异常处理
return Result.error(500, "系统异常");
}
性能优化建议
-
锁过期时间要合理
- 设置太短:业务未完成锁就释放
- 设置太长:影响并发性能
- 建议:平均业务处理时间的3倍
-
使用连接池
- 避免频繁创建/销毁连接
- 设置合理的连接池参数
-
异步释放锁
- 对于异步处理的业务
- 使用MQ等机制在业务完成后释放
监控和运维
// 添加监控指标
@Component
public class LockMonitor {
@Autowired
private RedisTemplate redisTemplate;
// 定期清理过期锁(兜底方案)
@Scheduled(cron = "0 0/1 * * * ?") // 每分钟执行一次
public void cleanupExpiredLocks() {
Set<String> keys = redisTemplate.keys("prevent:duplicate:*");
if (keys != null && !keys.isEmpty()) {
for (String key : keys) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl != null && ttl <= 0) {
redisTemplate.delete(key); // 清理已过期但未释放的锁
}
}
}
}
}
Redis分布式锁实现接口防重是成熟且高效的方案,关键点在于:
- 原子性:加锁和释放锁要保证原子操作
- 安全性:只能释放自己加的锁
- 可用性:考虑Redis宕机等异常情况
- 性能:合理设置过期时间,控制锁粒度
建议配合注解和AOP使用,这样可以非侵入式地实现防重逻辑,便于维护和扩展。