项目为单机项目 接口中是有重复验证的

  • 当前功能的重复验证是在功能开头进行的
  • 验证重复后,做了一大堆的其他工作在进行保存的

问题叙述

甲方在视图展示页面发现了不该有的重复。 我方检查接口和测试功能,并未复现问题。

问题产生原因

测试发现无问题之后,删除重复数据,不久后甲方有反应相同的问题。 这次阴差阳错的由于远程处理卡顿问题,连续点击测试N次功能点,错误数据由此产生

问题排除

已经确认为并发问题,但是具体造成原因还不明朗 最后通过反复测试,最终发现两个可疑点

  1. 功能中验证重复是在一开始进行的,途中做了很多很多无关的工作后,才进行了最终的save,可能期间过程有些延迟导致的
  • 其中使用到了图片上传,但是不是前端调用的文件服务而是选择让后端处理
  • 其中使用到了查询第三方远程库,且数据量优点大,同时 sql已经无法在进行优化了
  1. 由于各种原因,对各系统的数据库为同一个且为单机数据库,可能存储的延迟造成的。

最后通过模拟线上数据库的环境进行测试,发现疑点二可以消除。

疑点一的环境比较复杂只能通过 睡眠等待 来模拟环境

问题确认跟处理办法

  1. 通过 睡眠等待 来模拟 数据重复验证 的系类操作,发现就是这里出的问题
  2. 最终处理
    1. 用最终的存储数据库直接兜底推荐,在不可重复字段上加入了唯一索引
  3. 其他处理方式参考
    1. 对当前功能方法加锁推荐
      1. 可能会影响功能效率
    2. 保证第三方接口/数据库查询 响应的通畅且迅速
    3. 在最终保存时进行重复验证

      总结

  • 对于一些数据的强验证,一定注意数据录入和数据验证之间的延迟。
  • 对唯一行的数据一定要进行数据库兜底
  • 对增改的操作,中需要大量验证的时候,考虑下锁synchronized
  • 分布式下问题参考( 极客时间~每日一课
    • Screenshot_2021-06-21-06-46-25-731_极客时间.png

      测试demo仿源码功能

      ```java

@RestController @RequestMapping(“/front”) public class BookController { @Autowired private BookService bookService; @RequestMapping(value=”/formBuySubmit”,method=RequestMethod.POST) public ResultUtil formBuy(BookBean book){ return bookService.formBuy(book); } }

public interface BookService { ResultUtil formBuy(BookBean book); }

@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao;

  1. /**
  2. * 初步怀疑是判断重复之后接下来的其他操作导致了延迟的增加
  3. * @param bean
  4. * @return
  5. */
  6. @SuppressWarnings("unlikely-arg-type")
  7. @Override
  8. public synchronized ResultUtil<BookBean> formBuy(BookBean bean) {
  9. ResultUtil<BookBean> res = new ResultUtil<BookBean>();

// BookDao.sleep(1000); if(BookDao.verify(bean.getBookName())){ res.setResult(false); res.setResultInfo(“重复了”); // throw new RuntimeException(“存在重复”); }else { // 进行延时 - 模拟期间的 上传文件,查询第三方库等操作 BookDao.sleep(5000); // BookDao.sleep(1000); BookBean book = bookDao.save(bean); res.setResult(true); res.setResultInfo(“荐购成功,请等待管理员审核”); res.setResponseData(book); } return res; } }

@Component @Slf4j public class BookDao { static List names = new ArrayList<>(); public BookBean save(BookBean bean){ bean.setCreateTime(TimeFormat.currentTime()); // 模拟保存数据延时 // sleep(5000); sleep(1000); names.add(bean.getBookName()); log.info(bean.toString()); return bean; }

  1. public static Boolean verify(String name){
  2. if(names.contains(name)){
  3. log.info("name:"+name);
  4. log.info("nameList"+ StringUtils.join(names.toArray(),","));
  5. return true;
  6. }
  7. return false;
  8. }
  9. public static void sleep(long millis ){
  10. try {
  11. Thread.sleep(millis);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }

} public class ResultUtil {

  1. private boolean result; //正确与否
  2. private String resultInfo;//回调信息
  3. private T responseData; //回调数据
  4. public boolean isResult() {
  5. return result;
  6. }
  7. public void setResult(boolean result) {
  8. this.result = result;
  9. }
  10. public T getResponseData() {
  11. return responseData;
  12. }
  13. public void setResponseData(T responseData) {
  14. this.responseData = responseData;
  15. }
  16. public String getResultInfo() {
  17. return resultInfo;
  18. }
  19. public void setResultInfo(String resultInfo) {
  20. this.resultInfo = resultInfo;
  21. }

}

public class TimeFormat { /**

  1. * yyyy-MM-dd HH:mm:ss
  2. */
  3. public static final SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  4. public static String currentTime(){
  5. String str="";
  6. Date date = new Date();
  7. try {
  8. str = format1.format(date);
  9. } catch (Exception e) {
  10. System.out.println("转换时间格式异常(yyyy-MM-dd):"+str);
  11. str="";
  12. e.printStackTrace();
  13. }
  14. return str;
  15. }

}

@Data public class BookBean implements Serializable{ private static final long serialVersionUID = 1L; private String bookName; //名称 private String createTime; //创建时间

}

  1. - 模拟测试并发
  2. ```http
  3. ###
  4. POST http://localhost:8080/front/formBuySubmit
  5. Content-Type: application/x-www-form-urlencoded
  6. bookName=1231
  7. ###
  8. POST http://localhost:8080/front/formBuySubmit
  9. Content-Type: application/x-www-form-urlencoded
  10. bookName=1231

小知识

  • 测试并发的过程中发现postMan的压测不是并发执行,他是串行的气死个人