核心思路:
当用户发出提交请求时,在 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 {@Resourceprivate 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 {@Resourceprivate 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();}}}