一些表单重复提交和并发问题的记录

在数据库添加一条记录时碰到了下面情况。

具体经过是,在添加这条数据 时数据库里已经有两条一样的数据了,所以先在前端操作后,失败了3次。在手动删除数据库的重复数据后。再一次添加,数据库就插入了4条相同数据,再重复这流程2变之后还是如此。

因为在多次正常流程测试下是不会发生这种情况的。所以可能前端记录到了开始失败的状态,然后一次发了N次请求。所以从这引申出了2个问题,表单重复提交和并发。虽然这种错误发生不常见。但发生了这个错误后,之后请求都会在持久层报查询数据不唯一的异常。

这是一张统计表,年份+月份+区域 应该唯一。这导致下次添加任务时持久层会报查询不为一的异常。这张表的插入逻辑的是:如果数据库没有这个年月的统计数据,则添加一条数据。否则在这行中的count字段+1;所以可能发生情况1:一次插入几行;情况2:count+N的状况。

 

并发:

表单重复提交也可能导致并发问题。这里的并发问题举例来说就是:

线程A对一行读了再写,线程B对一行读了再写,写数据是基于读出的数据的。问题就出在线程A读了还没写回去,线程B就读了。这样会产生数据覆盖的问题。通常做法是乐观锁,基于CAS原理,通过编程实现,并在数据表添加一个表示版本的字段 。或者悲观锁,基于数据库的实现。但这里对于情况1,添加数据库的唯一键就行了。对于情况2,只要修改下sql语句就行了,把读和写并为一句sql。

//            这里不是原子操作,并发线程会肯能会覆盖typeDaily.Count的数据,应改进sql语句,把查再改并为一句sql,下同
//            taskStatisticsTypeDailyRepository.updateCount(typeDaily.getId(), typeDaily.getCount()+1);
            taskStatisticsTypeDailyRepository.updateCount(typeDaily.getId());

    /**
     * 更新统计信息
     * @param id 主键id
     * @param count 计数
     * @return 影响行
     */
//    @Modifying
//    @Query("update TaskStatisticsTypeDaily t set t.count = ?2 where t.id = ?1")
//    Integer updateCount(Integer id, Integer count);

    /**
     * 更新统计信息+1
     * @param id 主键id
     * @return 影响行
     */
    @Modifying
    @Query("update TaskStatisticsTypeDaily t set t.count = t.count+1 where t.id = ?1")
    Integer updateCount(Integer id);

 

表单重复提交:

解决方案是:过滤器+redis.每次把当前访问的标识存redis中,访问结束后删除。

package com.wisdomwater.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 防表单重复提交pre过滤器,存token到redis。跳过find,get,query请求
 * 因为是分布式,所以只能在zuul写过滤器,不好写拦截器
 * 参考https://blog.csdn.net/u013600907/article/details/82629824
 */
@Component
public class FormSubmitPreFilter extends ZuulFilter {

    @Autowired
    RedisTemplate redisTemplate;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (!ctx.sendZuulResponse()) {
            return false;
        }
        return true;
    }

    @Override
    public Object run() throws ZuulException {

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        //token包含客户端地址,访问路径访问参数
        String addr = request.getRemoteAddr();
        String URI = request.getRequestURI();
        if (URI.contains("query") || URI.contains("find")|| URI.contains("get")) {//查询
            return null;
        }
        String submitToken = "formsubmit:" + addr + "," + URI + "&";//token还要加上请求参数的信息,因为有时候打开页面可能请求接口2次以上。可能以后还要加body的信息
        Map<String, String[]> map = request.getParameterMap();
        for (String key : map.keySet()) {
            submitToken += key + ":";
            for (String val : map.get(key)) {
                submitToken += val + ",";
            }
        }
//        System.out.println("yyyyyy--------------->" + submitToken);
        if (redisTemplate.opsForValue().get(submitToken) != null) {
            System.out.println("还在访问!!!!!!");
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(901);
            return null;
        }
        redisTemplate.opsForValue().set(submitToken, "1234", 2, TimeUnit.SECONDS);//设置过期时间,万一post过滤器没删除token,也能自动删除
        request.setAttribute("submitToken", submitToken);
        return null;
    }
}

 

package com.wisdomwater.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 防表单重复提交post过滤器,移除redis的token。跳过find,get,query请求
 */
@Component
public class FormSubmitPostFilter extends ZuulFilter {

    @Autowired
    RedisTemplate redisTemplate;

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (!ctx.sendZuulResponse()) {
            return false;
        }
        return true;
    }

    @Override
    public Object run() throws ZuulException {

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        String URI = request.getRequestURI();
        if (URI.contains("query") || URI.contains("find") || URI.contains("get")) {//查询
            return null;
        }

        String value = (String) request.getAttribute("submitToken");
//        System.out.println("结束!!!!?????:" + value);
        redisTemplate.delete(value);
        return null;
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值