基于lua将公司系统的性能提升了100倍
Posted 我的糖给娴宝
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于lua将公司系统的性能提升了100倍相关的知识,希望对你有一定的参考价值。
某天测试的小姐姐突然跑来,说提交一批6w条的消息等很久才能提交成功,同时甩来了一张图,好家伙,总耗时150多秒。但想到平时发送接口自己也经常在用,并没有很慢,后来跟测试小姐姐再沟通了下,她用的是自己的账号,我用的是admin账号,而admin是不走管控,极有可能是管控出了问题。
本地部署了套环境,提交了一批消息,后台提交数据到管控服务的时候,是需要等到管控服务将数据投递到MQ才会返回提交结果,这个过程是串行的。给管控服务的整个过滤链中所有的filter加上日志,查看每个filter的耗时时间。根据打印出来的日志,发现黑名单管控就花了146s,基本占据整个的发送时间。
看来瓶颈就在黑名单管控这里。进一步排查发现,对于黑名单管控的处理,将用户的黑名单全部放到了Redis,遍历发送的内容,查询每条发送的终端id是否存在黑名单中,存在则过滤。对于场景来说,发送了6w条消息,那么就需要与Redis交互6w次,网络传输的开销特别大
一开始想到的解决方案,是基于布隆过滤器在前面做一层拦截,对于可能存在黑名单中的终端id再去Redis查一下数据,但如果提交的终端id都存在黑名单中,那其实还是会频繁与Redis交互,并不是很好的解决方法。事实上,问题存在的原因就是控管与Redis的交互是一条一条数据执行的,时间都花在了网络传输上,那么基于lua便可实现批量处理。
具体的业务就不再细说,因为黑名单管控还分不同的策略,基于业务编写lua脚本:
private RedissonClient redisson;
private static final String BATCH_SCRIPT_PATH = "lua/black_list_batch.lua";
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倍。
期间还发现了一个很有趣的点,Redis执行lua脚本是分批次去执行的,每批现在设置默认1w条消息,之前担心存在单次查询太久,阻塞到Redis,曾经换成每批1k条去执行,发现整体的耗时更长了。后来测了每批1w的环境下,单次执行大概是20ms,对Redis的压力也不大。对于这类问题,其实也是在时间和空间上寻找一个平衡点。
以上是关于基于lua将公司系统的性能提升了100倍的主要内容,如果未能解决你的问题,请参考以下文章