在数据库添加一条记录时碰到了下面情况。
具体经过是,在添加这条数据 时数据库里已经有两条一样的数据了,所以先在前端操作后,失败了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.youkuaiyun.com/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;
}
}