本文目录导读:

我来详细讲解如何使用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使用,这样可以非侵入式地实现防重逻辑,便于维护和扩展。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播