美文网首页JavaJava实战
基于lua将公司系统的性能提升了100倍

基于lua将公司系统的性能提升了100倍

作者: 不孤独的字符串 | 来源:发表于2021-05-13 23:25 被阅读0次

    一、背景

    某天测试的小姐姐突然跑来,说提交一批6w条消息等很久才能提交成功,同时甩来了一张图,好家伙,总耗时150多秒。但想到平时发送接口自己也经常在用,并没有很慢,后来跟测试小姐姐再沟通了下,她用的是自己的账号,我用的是admin账号,而admin是不走管控,极有可能是管控出了问题。

    二、排查

    本地部署了套环境,提交了一批消息,后台提交数据到管控服务的时候,是需要等到管控服务将数据投递到MQ才会返回提交结果,这个过程是串行的。给管控服务的整个过滤链中所有的filter加上日志,查看每个filter的耗时时间。根据打印出来的日志,发现黑名单管控就花了146s,基本占据整个的发送时间。


    1.jpg

    看来瓶颈就在黑名单管控这里。进一步排查发现,对于黑名单管控的处理,将用户的黑名单全部放到了Redis,遍历发送的内容,查询每条发送的终端id是否存在黑名单中,存在则过滤。对于场景来说,发送了6w条消息,那么就需要与Redis交互6w次,网络传输的开销特别大

    三、解决方案

    一开始想到的解决方案,是基于布隆过滤器在前面做一层拦截,对于可能存在黑名单中的终端id再去Redis查一下数据,但如果提交的终端id都存在黑名单中,那其实还是会频繁与Redis交互,并不是很好的解决方法。事实上,问题存在的原因就是控管与Redis的交互是一条一条数据执行的,时间都花在了网络传输上,那么基于lua便可实现批量处理。

    具体的业务就不再细说,因为黑名单管控还分不同的策略,基于业务编写lua脚本:

    local dataStr = KEYS[1]
    local msgTable = loadstring("return "..dataStr)()
    local filterKeys = {}
    for mIndex, entity in ipairs(msgTable) do
        local key = entity[1]
        local value = entity[2]
        local msgType = entity[3]
        local suffix = entity[4]
        -- sms and voice
        if(msgType == 0 or msgType == 3)
        then
            local bit = redis.call("GETBIT", key, value);
            if(bit == 1)
            then
                table.insert(filterKeys, key..suffix..value)
            end
        end
        -- others
        if(msgType == 2 or msgType == 8 or msgType == 6 or msgType == 9 or msgType == 4)
        then
            local exist = redis.call("SISMEMBER", key, '"'..value..'"');
            if(exist == 1)
            then
                table.insert(filterKeys, key..suffix..value)
            end
        end
    end
    return filterKeys
    

    脚本逻辑并不复杂,通过传入参数判断不同的业务类型调用相应的Redis命令,判断是否存在对应集合里,如果存在,则把key和value拼接起来(规则:key+"_"+value),统一返回,提供给到业务进行黑名单过滤的判断,主要是不同业务类型存储黑名单的数据类型不一样。同时项目启动的时候便将lua脚本加载到缓存里,后续通过SHA去调用,避免每次调用都去加载脚本。

    @Autowire
    private RedissonClient redisson;
    
    private static final String BATCH_SCRIPT_PATH = "lua/black_list_batch.lua";
    
    @PostConstruct
    private void init(){
        try {
            ResourceScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource(BATCH_SCRIPT_PATH));
            lua_script = scriptSource.getScriptAsString();
            script = redisson.getScript(StringCodec.INSTANCE);
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(BATCH_SCRIPT_PATH);
            if(inputStream == null){
                throw new FileNotFoundException("lua script not found");
            }
    
            lua_script = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
            lua_script = script.scriptLoad(lua_script);
        } catch (Exception e) {
            logger.error("load lua script {} error:", BATCH_SCRIPT_PATH, e);
        }
    }
    

    同时封装了拼接lua脚本参数和执行lua脚本的方法,luaResult便是执行脚本的返回结果,拿到结果后再在业务层面遍历每条数据,对每条数据进行过滤的判断。

    /**
     * 判断是否在黑名单
     * @param frameBuilder
     * @param msgSingles
     * @param target
     * @param strategyType
     */
    private void isInBlackList(FrameBuilder frameBuilder, List<MsgSingle> msgSingles, int target, StrategyType strategyType){
        if(ListUtil.isBlank(msgSingles)){
            return;
        }
        Set<String> filterSet = executeBlacklistScript(msgSingles, target, strategyType);
        if(CollectionUtils.isEmpty(filterSet)){
            return;
        }
    
        Iterator<MsgSingle> it = msgSingles.iterator();
        while (it.hasNext()){
            MsgSingle msg = it.next();
            MsgType type = msg.getMsgType();
            // 终端id
            String terminalId = msg.getTerminalId();
            RedisKeyAndVal redisKeyAndVal = blacklistFilter.getRedisKeyAndVal(terminalId, type, target, strategyType);
            String filterKey = redisKeyAndVal.getKey() + SUFFIX + redisKeyAndVal.getVal();
            if(filterSet.contains(filterKey)){
                // 执行过滤逻辑
                frameBuilder.append(BizForm.BLACK, type, msg);
                it.remove();
            }
        }
    }
    
    /**
     * redis执行lua脚本
     * @param msgSingles
     * @return
     */
    private Set<String> executeBlacklistScript(List<MsgSingle> msgSingles, int target, StrategyType strategyType){
        List<String> luaResultAll = new ArrayList<>();
        // 分批查询,防止lua参数过长
        List<List<MsgSingle>> msgSinglesList = ListUtil.splitList(msgSingles, MAX_SCRIPT_PARAMS);
        for (List<MsgSingle> msgSingleList : msgSinglesList) {
            List<String> luaResult = null;
            String scriptParam = genLuaScriptParam(msgSingleList, target, strategyType);
            if (scriptParam.length() < SCRIPT_MIN_LEN) {
                return Collections.emptySet();
            }
            try {
                luaResult = script.evalSha(RScript.Mode.READ_WRITE, lua_script, RScript.ReturnType.MAPVALUELIST, Collections.singletonList(scriptParam));
            } catch (RedisException e) {
                logger.warn("execute script error:", e);
            }
            if (ListUtil.isNotBlank(luaResult)) {
                luaResultAll.addAll(luaResult);
            }
        }
        if (ListUtil.isBlank(luaResultAll)) {
            return Collections.emptySet();
        }
        return luaResultAll.stream().collect(Collectors.toSet());
    }
    
    /**
     * 拼装lua脚本
     * @param msgSingles
     * @return
     */
    private String genLuaScriptParam(List<MsgSingle> msgSingles, int target, StrategyType strategyType){
        StringBuilder sb = new StringBuilder(Delimiters.LEFT_BRACH);
        for (MsgSingle msg : msgSingles) {
            MsgType type = msg.getMsgType();
            String terminalId = msg.getTerminalId();
            RedisKeyAndVal keyAndVal = blacklistFilter.getRedisKeyAndVal(terminalId, type, target, strategyType);
            sb.append(Delimiters.LEFT_BRACH);
            sb.append(Delimiters.SINGLE_QUOTE);
            sb.append(keyAndVal.getKey());
            sb.append(Delimiters.SINGLE_QUOTE);
            sb.append(Delimiters.COMMA);
    
            if(type == MsgType.SMS || type == MsgType.VOICE){
                sb.append(Long.parseLong(keyAndVal.getVal()));
            }else{
                sb.append(Delimiters.SINGLE_QUOTE);
                sb.append(keyAndVal.getVal());
                sb.append(Delimiters.SINGLE_QUOTE);
            }
            sb.append(Delimiters.COMMA);
    
            sb.append(type.getIndex());
            sb.append(Delimiters.COMMA);
            sb.append(Delimiters.SINGLE_QUOTE);
            sb.append(SUFFIX);
            sb.append(Delimiters.SINGLE_QUOTE);
            sb.append(Delimiters.RIGHT_BRACH);
            sb.append(Delimiters.COMMA);
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append(Delimiters.RIGHT_BRACH);
        return sb.toString();
    }
    

    四、总结

    同样提交6w条数据,黑名单管控消耗时间从146s降低到了差不多1.2s,提升了接近100倍。


    2.jpg

    期间还发现了一个很有趣的点,Redis执行lua脚本是分批次去执行的,每批现在设置默认1w条消息,之前担心存在单次查询太久,阻塞到Redis,曾经换成每批1k条去执行,发现整体的耗时更长了。后来测了每批1w的环境下,单次执行大概是20ms,对Redis的压力也不大。对于这类问题,其实也是在时间和空间上寻找一个平衡点。

    相关文章

      网友评论

        本文标题:基于lua将公司系统的性能提升了100倍

        本文链接:https://www.haomeiwen.com/subject/bbdfjltx.html