操作日志记录及防重提交

起因

某次测试发截图说好像因为网络卡的原因,导致添加一条数据,点了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请求测试
可以看到当第一个请求未完成时,其他请求全部被拒绝

总结

防重提交在一定程度上避免了脏数据的出现,后续需要完善分布式应用的并发处理。

发布了4 篇原创文章 · 获赞 0 · 访问量 36

猜你喜欢

转载自blog.csdn.net/m0_37453314/article/details/105409513