基于Redis实现幂等判断
核心思路:
当用户发出提交请求时,在 Redis 中创建一个带有过期时间的唯一标识,表示这个请求已经提交过了。如果 Redis 中已经存在这个标识,则拒绝本次提交,避免重复操作。
基本准备:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.10</version>
</dependency>
大体思路:在业务侧进行加锁的幂等判断,在规定时间内操作只能算一次成功的请求
import com.sa.config.RedissonManager;
import org.redisson.api.RLock;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class RepeatedSubmitService {
@Resource
private RedisTemplate redisTemplate;
private static final long EXPIRE_TIME = 5; // 过期时间,单位秒
/**
* 防止重复提交操作
* @param userId userId 用户ID
* @param actionId actionId 操作标识(可以是业务类型或者表单ID等)
* @return true 表示操作允许,false 表示重复提交
*/
public boolean check(String userId,String actionId){
// 生成redisKey,作为唯一标识
String redisKey = "submitLock:"+userId+":"+actionId;
// 尝试使用 SETNX 来防止重复提交,返回 true 表示设置成功(没有重复提交)
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(redisKey,"LOCKED",EXPIRE_TIME, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)){
return true;
}else{
// Redis 中已经存在锁键,说明是重复提交
return false;
}
}
public void submit(String userId,String actionId){
if (check(userId,actionId)){
System.out.println("业务操作成功");
}else {
System.out.println("重复提交");
}
}
}
package com.sa.controller;
import com.sa.pojo.Order;
import com.sa.service.RepeatedSubmitService;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Log4j2
@RestController
public class OrderController {
@Resource
private RepeatedSubmitService repeatedSubmitService;
@GetMapping("/submit")
public void submit(@RequestBody Order order){
String userId = order.getUserId();
String actionId = order.getActionId();
log.info("userId:{},actionId:{}",userId,actionId);
repeatedSubmitService.submit(userId,actionId);
}
}
在5秒内的重复提交记录,只能是一条生效,剩余的请求在业务侧进行失效处理
改进
此处还可以基于本地缓存实现,这里采用Map模拟,也可以使用Caffine本地缓存
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 幂等性校验
*/
@Service
public class IdempotencyService {
private Map<String, Boolean> requestCache = new HashMap<>();
/**
* 检查是否是重复的请求
*
* @param requestId
* @return
*/
private synchronized boolean check(String requestId) {
if (requestCache.containsKey(requestId)) {
return false;
}
requestCache.put(requestId, true);
return true;
}
/**
* 模拟业务操作
*/
public void processRequest(String requestId) {
if (check(requestId)) {
// 处理业务逻辑
System.out.println("处理请求: " + requestId);
} else {
System.out.println("请求重复: " + requestId);
}
}
}
或是使用Redisson来进行实现:
-
RLock 替代
setIfAbsent
:Redisson 的RLock
封装了 Redis 分布式锁的功能,简化了操作。通过lock.tryLock
来尝试获取锁,获取成功则继续执行操作,获取失败则表示重复提交。 -
自动续期和过期时间:Redisson 内置了看门狗机制,会自动续期锁,防止长时间业务执行时锁提前释放。通过
tryLock(100, EXPIRE_TIME, TimeUnit.SECONDS)
可以设置锁的最大等待时间和最大存活时间,超时后锁自动释放。 -
锁的释放:Redisson 自动确保锁的释放在
finally
块中进行,避免因异常导致锁未被释放。
public boolean checkForRedisson(String userId,String actionId){
// 生成redisKey,作为唯一标识
String redisKey = "submitLock:"+userId+":"+actionId;
// 获取分布式锁对象
RLock lock = RedissonManager.getClient().getLock(redisKey);
try{
Boolean success = lock.tryLock(100,EXPIRE_TIME, TimeUnit.SECONDS);
if (success){
return true;
}else{
return false;
}
}catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}