重复提交
一、背景与问题
很多时候需要限制用户的并发请求次数,比如对于某个接口,不能请求太频繁,限制 一个用户每秒钟只能请求一次;
举例场景:
- 短信验证码,对于同一个用户,60秒才能请求一次;
- 登录,对于一个用户,10秒才能请求一次
二、架构与思想
分析如上需求,能很清楚的知道这是一个 请求信息记录
的问题,核心在于:
- 以什么为 凭证 作为记录
- 如果已经请求过,记录在哪里
- 请求的时候需要记录请求时间
几个专有名词:
java
凭证:ticket
重复提交:repeat submit
三、具体使用
系统提供了注解@RepeatSubmit
用于解决此问题。
3.1、配置
首先需要决策是使用redis
还是caffine
来存储凭证 ticket? 这里建议:
- 如果是 集群部署,建议用 redis
- 如果是 单体部署,建议用 内存
caffine
如下: 使用caffine
作为存储,
具体凭证使用的是 [url + userid]
java
@Configuration
public class RepeatSubmitConfig {
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
RepeatSubmitCaffeineTicket caffeineTicket = new RepeatSubmitCaffeineTicket(this::ticket);
return new RepeatSubmitAspect(caffeineTicket);
}
/**
* 获取指明某个用户的凭证
*/
private String ticket(String servletPath) {
Long userId = SmartRequestUtil.getRequestUserId();
if (null == userId) {
return StringConst.EMPTY;
}
return servletPath + "_" + userId;
}
}
3.2、@RepeatSubmit注解
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatSubmit {
int value() default 300;//重复提交间隔时间/毫秒
int MAX_INTERVAL = 30000;//最长间隔30s
}
默认重复提交为 300毫秒,具体按照实际业务需要
接下来可以将此注解加到 方法上,如下
java
@RestController
@Api(tags = {SwaggerTagConst.Support.TABLE_COLUMN})
public class TableColumnController extends SupportBaseController {
@Autowired
private TableColumnService tableColumnService;
@ApiOperation("修改表格列 @author 卓大")
@PostMapping("/tableColumn/update")
@RepeatSubmit
public ResponseDTO<String> updateTableColumn(@RequestBody @Valid TableColumnUpdateForm updateForm) {
return tableColumnService.updateTableColumns(SmartRequestUtil.getRequestUser(), updateForm);
}
@ApiOperation("恢复默认(删除) @author 卓大")
@GetMapping("/tableColumn/delete/{tableId}")
@RepeatSubmit
public ResponseDTO<String> deleteTableColumn(@PathVariable Integer tableId) {
return tableColumnService.deleteTableColumn(SmartRequestUtil.getRequestUser(), tableId);
}
@ApiOperation("查询表格列 @author 卓大")
@GetMapping("/tableColumn/getColumns/{tableId}")
public ResponseDTO<String> getColumns(@PathVariable Integer tableId) {
return ResponseDTO.ok(tableColumnService.getTableColumns(SmartRequestUtil.getRequestUser(), tableId));
}
}
四、实现原理
4.1、切面
因为是使用注解来解决问题,所以离不开 AOP,这里使用RepeatSubmitAspect.java
来实现的;核心代码如下:
java
@Around("@annotation(net.lab1024.sa.base.module.support.repeatsubmit.annoation.RepeatSubmit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ticketToken = attributes.getRequest().getServletPath();
String ticket = this.repeatSubmitTicket.getTicket(ticketToken);
if (StringUtils.isEmpty(ticket)) {
return point.proceed();
}
Long timeStamp = this.repeatSubmitTicket.getTicketTimestamp(ticket);
if (timeStamp != null) {
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 说明注解去掉了
if (annotation != null) {
return point.proceed();
}
int interval = Math.min(annotation.value(), RepeatSubmit.MAX_INTERVAL);
if (System.currentTimeMillis() < timeStamp + interval) {
// 提交频繁
return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT);
}
}
Object obj = null;
try {
// 先给 ticket 设置在执行中
this.repeatSubmitTicket.putTicket(ticket);
obj = point.proceed();
} catch (Throwable throwable) {
log.error("", throwable);
throw throwable;
} finally {
this.repeatSubmitTicket.removeTicket(ticket);
}
return obj;
}
4.2、存储
存储到redis: RepeatSubmitRedisTicket.java
java
@Override
public Long getTicketTimestamp(String ticket) {
Long timeStamp = System.currentTimeMillis();
boolean setFlag = redisValueOperations.setIfAbsent(ticket, String.valueOf(timeStamp), RepeatSubmit.MAX_INTERVAL, TimeUnit.MILLISECONDS);
if (!setFlag) {
timeStamp = Long.valueOf(redisValueOperations.get(ticket));
}
return timeStamp;
}
存储到 caffine: RepeatSubmitCaffeineTicket.java
java
/**
* 限制缓存最大数量 超过后先放入的会自动移除
* 默认缓存时间
* 初始大小为:100万
*/
private static Cache<String, Long> cache = Caffeine.newBuilder()
.maximumSize(100 * 10000)
.expireAfterWrite(RepeatSubmit.MAX_INTERVAL, TimeUnit.MILLISECONDS).build();
public RepeatSubmitCaffeineTicket(Function<String, String> ticketFunction) {
super(ticketFunction);
}
@Override
public Long getTicketTimestamp(String ticket) {
return cache.getIfPresent(ticket);
}
联系我们
1024创新实验室-主任:卓大,混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室 致力于成为中原领先、国内一流的技术团队, 以AI+数字化为驱动,用技术为产业互联网提供无限可能, 业务如下:
- 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
- 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
- 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
- AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大 拉你入群,一起学习 | 公众号 :六边形工程师 分享:赚钱、代码、生活 | 请 “1024创新实验室” 烩面里加肉 咖啡配胡辣汤,提神又饱腹 | 抖音 : 六边形工程师 直播:赚钱、代码、中医 |