起因
某次测试发截图说好像因为网络卡的原因,导致添加一条数据,点了N次,结果在列表中就已经添加了N条。
分析
查看代码,在插入数据之前,有根据数据的字段内容查询是否已经存在,存在则更新,否则添加。依然出现这种情况,多种情况都可能导致:
1、并发请求进来时,并发查询都是不存在,最后并发执行插入,导致脏数据;
2、分布式应用,在执行插入相关操作时,没有进行数据库锁表的操作。
第2点比较复杂,后面再处理,先处理第1点,同一用户的操作(相同请求)必须等操作结果返回后才允许第二次请求,从而避操作请求并发。
需求
1、前端在发送请求后,没有返回响应之前,disable掉操作按钮
2、后端记录用户操作日志,新增、修改、删除等操作进行防重提交
方案设计
通过网上查找资料,记录操作日志及防重新提交,通常使用AOP切面进行处理,并通过redis锁作为标识
过程
1、定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAndNoRepeatSubmit {
String value() default "";
int lockTime() default 10;
}
2、增加一个切面方法
@Aspect
@Component
@Slf4j
public class LogAspect {
@Resource
private AsyncTaskService asyncService;
@Pointcut("@annotation(logAndNoRepeatSubmit)")
public void pointcut(LogAndNoRepeatSubmit logAndNoRepeatSubmit) {
}
@Around(value = "pointcut(logAndNoRepeatSubmit)")
public Object around(ProceedingJoinPoint point, LogAndNoRepeatSubmit logAndNoRepeatSubmit) throws Throwable {
long beginTime = System.currentTimeMillis();
int lockSeconds = logAndNoRepeatSubmit.lockTime();
String key = LogAspectUtil.getKeyFromRequest();
String clientId = UUID.randomUUID().toString();
boolean isSuccess=false;
//在并发情况下,redis请求会有大概10ms的请求时长,因此加锁,避免redis还没有执行加锁,其他请求已经进来而导致与预期不一致的情况
synchronized (this) {
isSuccess = RedisUtil.tryLock(CacheConst.IDEMPOTENCE + key, clientId, lockSeconds);
}
if (isSuccess) {
Object result = null;
try {
result = point.proceed();
} catch (Throwable e) {
log.error("不重复提交日志", e);
throw e;
} finally {
RedisUtil.del(CacheConst.IDEMPOTENCE + key);
}
long time = System.currentTimeMillis() - beginTime;
Long uid = UserCacheUtil.getUserInfo().getId();
String userName = UserCacheUtil.getUserInfo().getName();
String ip = WebUtil.getIP();
//记录操作日志
OperationLogDTO dto = LogAspectUtil.handleLog(point, time, result, uid, userName, "COM-PC", ip);
saveLog(dto);
return result;
} else {
return R.fail("重复请求,请稍后再试");
}
}
private void saveLog(OperationLogDTO dto) {
// 保存系统日志,异步方法保存,或者放到MQ去执行
asyncService.saveLog(dto);
}
}
3、设计操作日志记录内容:
CREATE TABLE `operation_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`uid` bigint(20) DEFAULT NULL,
`user_name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
`operation` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
`response_time` bigint(5) DEFAULT NULL,
`method` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
`params` text CHARACTER SET utf8mb4,
`ip` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
`ctime` datetime DEFAULT NULL,
`status` bit(1) DEFAULT NULL,
`response_code` int(4) DEFAULT NULL,
`response_msg` varchar(1000) CHARACTER SET utf8mb4 DEFAULT NULL,
`request_side` varchar(20) CHARACTER SET utf8mb4 DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2690 DEFAULT CHARSET=utf-8;
4、测试
使用jmeter做并发5请求测试
可以看到当第一个请求未完成时,其他请求全部被拒绝
总结
防重提交在一定程度上避免了脏数据的出现,后续需要完善分布式应用的并发处理。