场景演示
假设有一个录入学生信息的功能,为了便于演示,要求不能有重名的学生,并且数据库对应字段没有做唯一限制.
@GetMapping("/student/{name}")
public Object reSubmitTest(@PathVariable String name){
List<Student> allByName = repository.findByName(name);
if (allByName != null && allByName.size() > 0) {
return "姓名重复";
}
//便于测试假设都是18岁
return repository.save(new Student(name, 18));
}
学生表
Field | Type | Comment |
---|---|---|
id | int(11) | 自增 |
name | varchar(20) | 姓名 |
age | int(5) | 年龄 |
上面这段代码,如果什么都不做,100个请求同时进来会发生什么呢
-
模拟同时100个请求
-
发现重复插入了很多条数据
如何解决
在网上有很多处理重复提交的方案,大部分的逻辑都是利用redis的key的过期时间,让请求在一点时间内重复进入方法,直达key过期.这种方法有一个缺点,需要固定一个时间,这个时间设置长了浪费性能,设置短了起不到防止重复提交的作用,我觉得有更好的方案
单服务
- 定义一个注解,拦截需要处理的方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resubmit {
}
- 拦截处理
@Aspect
@Component
public class SubmissionAop {
@Autowired
private HttpServletRequest request;
private static ConcurrentHashMap<String,Integer> concurrentHashMap = new ConcurrentHashMap<>();
@Pointcut("@annotation(Resubmit)")
public void needCheckMethod() {
}
@Before("needCheckMethod()")
public void checkMethod() {
//requestId 的获取,确保同一个用户,同一个url
String requestId = request.getMethod() + request.getServletPath() + request.getHeader("token");
try {
check(requestId);
request.setAttribute("__request_resubmit_need_release","need");
} catch (Exception e) {
//抛出异常后被统一异常处理,转化为返回信息返回给前端
throw new RuntimeException("重复提交-上一个请求还未处理完");
}
}
@After("needCheckMethod()")
public void release(){
if ("need".equals(String.valueOf(request.getAttribute("__request_resubmit_need_release")))) {
String requestId = request.getMethod() + request.getServletPath() + request.getHeader("token");
concurrentHashMap.remove(requestId);
}
}
/**同步保证查询和设置是原子操作
* @param requestId
* @throws Exception
*/
private static synchronized void check(String requestId) throws Exception {
if (concurrentHashMap.get(requestId) != null) {
throw new Exception();
}
concurrentHashMap.put(requestId, 1);
}
}
在请求进入方法前,加锁,往后的同一个请求(requestId相同)无法获取锁,就被判定为重复请求,抛出异常,等第一个请求调用完毕后再释放锁,这样一来,就不需要设定时间来限制访问
多服务/或redis
上面的aop方法,无法适用于多服务/集群,总体逻辑不变,适用redis来做分布式锁就行了,改造比较简单,如下
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private HttpServletRequest request;
@Pointcut("@annotation(Resubmit)")
public void needCheckMethod() {
}
@Before("needCheckMethod()")
public void checkMethod() {
String requestId = request.getServletPath() + request.getHeader("token");
Boolean absent = redisTemplate.opsForValue()
.setIfAbsent(requestId, "1",60, TimeUnit.SECONDS);
Assert.notNull(absent,"");
if (!absent) {
throw new RuntimeException("重复提交-上一个请求还未处理完");
}
request.setAttribute("__request_resubmit_need_release","need");
}
@After("needCheckMethod()")
public void release(){
if ("need".equals(String.valueOf(request.getAttribute("__request_resubmit_need_release")))) {
String requestId = request.getServletPath() + request.getHeader("token");
redisTemplate.delete(requestId);
}
}
}
上面的 redisTemplate.opsForValue().setIfAbsent(requestId, "1",60, TimeUnit.SECONDS);是原子操作,这也是为什么使用它做分布式锁的原因, 其次这个方法需要redis版本在2.1以上,否则只能在同步方法中先setIfAbsent 再设置过期时间了. 这里设置过期时间的原因是,有可能第一个请求设置完锁后,redis出现问题,导致后面的请求一直无法获取锁,从而所有请求都被判定为重复请求.
最后 如果发现本文有需要改进的地方,或者你有更好的方案,欢迎留言交流
网友评论