2020-03-21:问题记录:防伪码生成

背景:公司自主产品,想在系统内嵌套防伪码导出和验证功能,防止产品被假冒

     阶段--目前还处于起步阶段,自主品牌名气也还不太大,所以对于防伪码量级上要求不大,在10万级别上

1.产品内容

    产品的需求可以分成2部分:

  1. 防伪码的批量导出
    1. 单次导出防伪码数量预估在万级左右
    2. 导出的防伪码必须是全系统唯一
  2. 防伪码校验
    1. 防伪码格式校验前端做,减少http请求消耗
    2. 单账号次数拦截 - 用手机号做请求次数限制
    3. 防伪码  首次校验记录时间,之后查询返回第一次校验时间和手机号(这里可以关联系统中)

2.初步设计

第一版:    

  1. DB中预先生成100万条数据
  2. 需要分配的时候从里面获取

然后二话不错开整,出设计文档,开发文档:

  1. DB预先生成------增加定时任务定时检查DB中未使用的防伪码储备是否低于阈值40万条,少于就开始创建创建直到储备量到100万
  2. 分配防伪码-------判断需要分配的数量,先判断是否在  0~ 100万之间 ,然后与数据库储备进行比较
    1. 需求量小于等于储备量:随机更新防伪码状态为占用,并且加上本次操作的标记----触发一次生成校验
    2. 需求量大于储备量:触发生成防伪码接口,直到生成100万在释放接口

这一套操作下来的结果如何呢,当单次分配防伪码量大的时候,返回结果极慢,而且这个接口我还做了分布式锁,单次只能有一个请求能分配防伪码,20万数据平均时间在13秒左右,有时候还不止

当碰到这种接口请求时间很长的时候,处理方案有很多

  • 通过界面优化加异步处理:点击分配防伪码按钮之后,后端同步返回一个触发成功的提示,然后异步创建任务不断分配,并且记录返回给前端已经分配的数量,让前端能够以进度条完成情况的形式给用户
  • 通过对接口进行优化,提高返回速度,保证在同步情况下返回时间能缩短到用户能忍受的2~3秒,当然在难度和挑战性上这种方式会更高

 第二版

很显然,男程序员必须迎男而上,gang一波,看能够将这个方法做优化,需求我们前面已经介绍

先分析第一版为什么耗时的原因:

  1. 生成20万个防伪码:数据库更新20万条数据,这其实是一个耗时的操作,平均执行下来需要7~8秒
  2. 将分配的20万防伪码查询出来,20万条数据  100*25ms ,大约在2~3秒内
  3. 防伪码整理到excel中,也是需要3~5秒左右
  4. 上传到oss上也需要1~2秒,整体算下来就是15秒左右

既然我们已经查到出问题的点,那怎么针对呢:

  1. 更新20万的防伪码既然如此耗时,那为什么不将这个时间 与 主要流程 异步
    1. DB中生成防伪码是一个定时任务
    2. DB中更新需要的防伪码 也是一个任务(该功能必须提前锁定哪一批防伪码,这里就要使用到redis缓存做一个能快速存取的中间件)
  2. 查询20万数据数据耗时,优化方向:直接通过redis获取,速度能在1~2秒之内
  3. 防伪码整理到excel 03版本中,每个sheet也最多能保存6万出头,可以通过fork/join思想,6万数据一组,每个sheet单独操作
  4. 上传oss上可以作成异步,从接口返回结果到去oss上点击下载文件,这中间足够文件上传好了

-------------------------------------------------------------------------------

话不多说--设计思路出来了,写开发文档.......进入开发。。。。

来了,来了,他来了!!!!

测试的时候上限值设置成30万:代码如下


    /**
     * 防伪码默认最多预先生成 30万
     */
    private static final int MAX_NEW_SECURITY_CODES = 300000;

    /**
     * 防伪码库存中少于 10万 触发生成操作
     */
    private static final int CAPACITY_NEW_SECURITY_CODES = 100000;

    /**
     * 防伪码在redis中默认分成100个key存储
     */
    private static final int MAX_SECURITY_CODES_KEYS_SPLIT_NUM = 100;

    /**
     * db单次入库数据步长
     */
    private static final int STEP = 2000;

    private static final String SECURITY_CODE_CREATE_LOCK = "security_code_create_lock";
    private static final String SECURITY_CODE_CACHE = "security_code_cache_%d";
    @Override
    @Transactional
    public TSecurityCodeInfoDO createSecurityCode(SecurityCodeRequest request) {
        Long needCreateCodeNum = request.getCodeNum();
        //参数校验
        Assert.isTrue(request.getTitle().length() > 0 && request.getTitle().length() < 31, "防伪码标题长度异常");

        Assert.isTrue(needCreateCodeNum > 0 && needCreateCodeNum <= MAX_NEW_SECURITY_CODES, "防伪码生成数量异常(单次最多导出 "+ MAX_NEW_SECURITY_CODES +" 条)");

        //判断redis 中存储的防伪码数量是否足够
        if (!checkCacheCodeNum(needCreateCodeNum)) {
            //异步触发生成防伪码操作
            threadPoolTaskExecutor.execute(() -> {
                createSecurityCodesTask(true);
            });
            throw new FaceServiceException(10000L, "系统正在生成防伪码,请3分钟后重试");
        }

        ArrayList<List<String>> codeLists = new ArrayList<>();
        //循环redis取
        for (int i = 0; i < MAX_SECURITY_CODES_KEYS_SPLIT_NUM; i++) {
            String cacheKey = String.format(SECURITY_CODE_CACHE, i);
            needCreateCodeNum = getSecurityCodesByKey(codeLists, cacheKey, needCreateCodeNum);
            if (needCreateCodeNum <= 0) {
                break;
            }
        }
        String ossUrl = createExcelAndUploadOss(request.getTitle(), codeLists);

        //4.将防伪码创建信息入库到security_code_info表中
        TSecurityCodeInfoDO securityCodeInfoDO = new TSecurityCodeInfoDO();
        securityCodeInfoDO.setTitle(request.getTitle());
        securityCodeInfoDO.setCodeNum(request.getCodeNum());
        securityCodeInfoDO.setOssFileUrl(ossUrl);
        securityCodeInfoDO.setOperateUserId(1L);
        securityCodeInfoDO.setOperateUserName("哈哈");
        securityCodeInfoDO.setCreateTime(LocalDateTime.now());
        securityCodeInfoMapper.insertSelective(securityCodeInfoDO);

        //将分配的防伪码状态变为 1
        codeLists.forEach(codeList -> {
//            threadPoolTaskExecutor.execute(() -> {
            //批量更新防伪码状态与对应的code
            securityCodeMapper.batchUpdate(codeList, securityCodeInfoDO.getId(), SecurityCodeStatusEnum.EXPORT_CODE.getCode());
//            });
        });
        return securityCodeInfoDO;
    }

    /**
     * 将防伪码打印到excel中,上传到oss上
     * @param sheetTitle
     * @param codeLists
     * @return
     */
    private String createExcelAndUploadOss(String sheetTitle, ArrayList<List<String>> codeLists){
        List<String> headerList = new ArrayList<>();
        headerList.add(0, "序号");
        headerList.add(1, "防伪码");


        String ossUrl = null;
        try (InputStream is = ExcelUtil.createExcel(sheetTitle, headerList, codeLists)) {
            String key =  "dev/security/code/" + DigestUtils.md5Hex(LocalDateTime.now().toString()) + ".xls";
            ossFaceClientManager.uploadFile(key, is);
            ossUrl = String.format(PIC_OSS_NGINX_IP, key);
        } catch (Exception e) {
            LoggerUtil.error(logger, e, "上传oss异常");
        }
        return ossUrl;
    }


    @Override
    public void updateSecurityCode(SecurityCodeRequest request) {
        Assert.isTrue(request.getTitle().length() > 0 && request.getTitle().length() < 31, "防伪码标题长度异常");
        TSecurityCodeInfoDO securityCodeInfoDO = new TSecurityCodeInfoDO();
        securityCodeInfoDO.setId(request.getId());
        securityCodeInfoDO.setTitle(request.getTitle());
        Assert.isTrue(securityCodeInfoMapper.updateByPrimaryKeySelective(securityCodeInfoDO) > 0, "更新标题失败");
    }


    @Override
    public int getTotal() {
        return securityCodeInfoMapper.countTotal();
    }

    @Override
    public List<TSecurityCodeInfoDO> getSecurityCodeInfos(PageQuery query) {
        return securityCodeInfoMapper.selectPage(query);
    }

    @Override
//    @Scheduled(cron = "0 0/30 * * * ?") 半小时触发一次
    public void createSecurityCodesTask(Boolean autoFlag) {
        String lock = (String) redisCacheManager.get(SECURITY_CODE_CREATE_LOCK);
        if (StringUtils.isNotBlank(lock)) {
            logger.info("已经有生成程序在跑了,不用重复发起");
            return;
        }
        //3分钟内允许允许一次,一次只能有一个在运行
        redisCacheManager.set(SECURITY_CODE_CREATE_LOCK, "1", 60 * 3);


        Long count = securityCodeMapper.countStatus(SecurityCodeStatusEnum.NEW_CODE.getCode());
        Long cacheCodeNum = countCacheCodes();

        Long startTime = System.currentTimeMillis();
        // 检查缓存数量
//        synchronizeCodeCache(STEP, count, cacheCodeNum);

        Long partFinishTime = System.currentTimeMillis();
        if (!Objects.equals(cacheCodeNum, count)){
            System.out.println("【时间统计】- 同步DB数据到缓存中花费时间: " +  (partFinishTime - startTime));
        }
        // 请求来源 &&  阈值
        if ( !autoFlag &&  count >= CAPACITY_NEW_SECURITY_CODES) {
            return;
        }
        Long needCreateCounts = MAX_NEW_SECURITY_CODES - count;

        //2.数据生成
        int insertNum = Math.toIntExact(needCreateCounts);
        ArrayList<HashSet<String>> codeSets = new ArrayList<>();
        while (insertNum > 0) {
            if (insertNum >= STEP) {
                codeSets.add(getRandomCodes(STEP));
            } else {
                codeSets.add(getRandomCodes(insertNum));
            }
            insertNum -= STEP;
        }
        System.out.println("【时间统计】- 生成"+ insertNum + " 条数据 花费时间: " +  (System.currentTimeMillis() - partFinishTime));

        //3.数据入DB, 然后存缓存
        for (HashSet<String> codeSet : codeSets) {
            Long beforeLastId = securityCodeMapper.selectLastNewCode(SecurityCodeStatusEnum.NEW_CODE.getCode());
            securityCodeMapper.batchInsert(codeSet, null);
            //查询最新的那条记录的id(sql优化)
            List<String> codesTemp = securityCodeMapper.selectCodesFromOldLastId(SecurityCodeStatusEnum.NEW_CODE.getCode(), beforeLastId, STEP);
            redisCacheManager.redisStringTemplate.opsForList().rightPushAll(getEmptySecurityCodeKey(), codesTemp);
        }
        System.out.println("【时间统计】- 定时任务总 花费时间: " +  (System.currentTimeMillis() - startTime));
    }

    @Override
    public void cleanSecurityCodeCache() {
        for (int i = 0; i < MAX_SECURITY_CODES_KEYS_SPLIT_NUM; i++) {
            String cacheKey = String.format(SECURITY_CODE_CACHE, i);
            if (redisCacheManager.redisTemplate.opsForList().size(cacheKey) == 0) {
                continue;
            }
            redisCacheManager.remove(cacheKey);
        }
    }

    /**
     * 获取缓存中防伪码数量
     * @return
     */
    private Long countCacheCodes() {
        Long cacheCodeNum = 0L;
        for (int i = 0; i < MAX_SECURITY_CODES_KEYS_SPLIT_NUM; i++) {
            String cacheKey = String.format(SECURITY_CODE_CACHE, i);
            cacheCodeNum += redisCacheManager.redisTemplate.opsForList().size(cacheKey);
        }
        return cacheCodeNum;
    }

    /**
     * 判断 缓存中 防伪码数量是否足够
     * @param needCreateCodeNum
     * @return true 表示足够
     */
    private Boolean checkCacheCodeNum(Long needCreateCodeNum) {
        Long cacheCodeNum = 0L;
        for (int i = 0; i < MAX_SECURITY_CODES_KEYS_SPLIT_NUM; i++) {
            String cacheKey = String.format(SECURITY_CODE_CACHE, i);
            cacheCodeNum += redisCacheManager.redisTemplate.opsForList().size(cacheKey);
            if (cacheCodeNum >= needCreateCodeNum) {
                return true;
            }
        }
        return false;
    }
    /**
     * 查询存防伪码最少的list的key返回
     *
     * @return
     */
    private String getEmptySecurityCodeKey() {
        String minNumCacheKey = String.format(SECURITY_CODE_CACHE, 0);
        if (redisCacheManager.redisTemplate.opsForList().size(minNumCacheKey) == 0) {
            return minNumCacheKey;
        }

        for (int i = 1; i < MAX_SECURITY_CODES_KEYS_SPLIT_NUM; i++) {
            String cacheKey = String.format(SECURITY_CODE_CACHE, i);
            if (redisCacheManager.redisTemplate.opsForList().size(cacheKey) == 0) {
                return cacheKey;
            }

            if (redisCacheManager.redisTemplate.opsForList().size(cacheKey) <
                    redisCacheManager.redisTemplate.opsForList().size(minNumCacheKey)) {
                minNumCacheKey = cacheKey;
            }
        }
        return minNumCacheKey;
    }

    /**
     * 从传入的key中取出 需要的数量为 needCodeNum 的数据,如果不够,返回不够多少
     *
     * @param codeLists
     * @param key
     * @param needCodeNum
     * @return
     */
    private long getSecurityCodesByKey(ArrayList<List<String>> codeLists, String key, Long needCodeNum) {
        Long size = redisCacheManager.redisTemplate.opsForList().size(key);
        if (size == null || size == 0) {
            return needCodeNum;
        }
        //从key对应的组中查询出需要数量的needCodeNum
        Long searchSize = 0L;
        long nextNeedSearchNum = 0;
        if (size <= needCodeNum) {
            searchSize = -1L;
            nextNeedSearchNum = needCodeNum - size;
        } else {
            searchSize = needCodeNum;
        }
        List<String> codeCache = redisCacheManager.redisStringTemplate.opsForList().range(key, 0, searchSize - 1);

        if (searchSize > 0) {
            redisCacheManager.redisStringTemplate.opsForList().trim(key, searchSize, -1);
        } else {
            redisCacheManager.remove(key);
        }
        codeLists.add(codeCache);
        return nextNeedSearchNum;
    }

    /**
     * 随机生成16位数值
     *
     * @param codeNum
     * @return
     */
    private HashSet<String> getRandomCodes(int codeNum) {
        HashSet<String> codes = new HashSet<String>(Math.toIntExact(codeNum));
        IntStream.rangeClosed(1, codeNum).forEach(value -> {
            codes.add(new StringBuffer()
                    .append(leftPad(RandomUtils.nextInt(0, 99999999), 8, '0'))
                    .append(leftPad(RandomUtils.nextInt(0, 99999999), 8, '0')).toString());
        });
        return codes;
    }

    /**
     * 向左补全数据
     *
     * @param num
     * @param maxLen
     * @param filledChar
     * @return
     */
    private static String leftPad(int num, final int maxLen, char filledChar) {
        StringBuffer sb = new StringBuffer();
        String str = String.valueOf(num);
        for (int i = str.length(); i < maxLen; i++) {
            sb.append(filledChar);
        }
        return sb.append(str).toString();
    }

    /**
     * 同步:redis缓存中的数量与 DB
     * @param step 每次查询db的步长
     * @param count 数据库中记录数量
     * @param cacheCodeNum 缓存中记录数量
     */
    private void synchronizeCodeCache(Integer step, Long count, Long cacheCodeNum){
        if (count != cacheCodeNum) {
            Integer pageNo = 0;
            while (count > 0) {
                List<Object> dbCodes = securityCodeMapper.selectCodesPage(SecurityCodeStatusEnum.NEW_CODE.getCode(), pageNo, step);
                String cacheKey = String.format(SECURITY_CODE_CACHE, pageNo);
                redisCacheManager.redisTemplate.opsForList().rightPushAll(cacheKey, dbCodes);
                count -= step;
                pageNo++;
            }
        }
    }


    enum SecurityCodeStatusEnum {

        NEW_CODE(0, "新防伪码"),
        EXPORT_CODE(1, "已经导出的防伪码"),
        CHECKED_CODE(2,"被检验过的防伪码");

        private int code;

        private String msg;

        SecurityCodeStatusEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }
    }

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值