在近期参与开发的项目中,遇到了一个数据查询性能问题。本文对优化过程做简单的描述。
组件:时许数据库TDengine
场景:我们使用时序数据库TDengine存储源源不断进入的日志数据,这些日志数据是和业务挂钩的,需要进行业务处理,所以每条日志都有一个日志编号,在业务系统中,有通过日志编号查询日志详情的场景。
设计:数据库选型选了TDengine,设计了一个日志超级表,多个日志子表。(超级表、子表均是TDengine的概念,这里不做过多阐述)。日志超级表的主键为时间戳类型的ts,用于记录日志时间,有一个日志编号,通过雪花算法生成,用作唯一标识。以及其他字段。。。
问题:由于日志数据源源不断,很快日志超级表(超级表是TDengine的概念,这里不做过多阐述)的数据量就能达到千万级,于是通过一个日志标号(非索引字段)查询日志信息就会很慢,有点类似于MySQL的全表扫描。
问题思考:雪花算法生成的日志标号虽然具有唯一性,但不能为非索引查询带来性能。TDengine在千万级数据量是通过主键索引查询很快,如果缩小查询范围,比如将查询范围具体到子表,查询速度更快。
解决思路:重新设计日志编号,除了保证唯一,还要使其具有主键信息(时间戳),还可附加其他信息(比如子表后缀名)。在页面通过编号查询时,解析出编号的主键信息等一并作为查询条件,便可以极大提升查询速率。
最简单的就是做简单拼接:雪花算法唯一ID + 时间戳 + 子表后缀。但是简单拼接的数据太过于直白,有数据安全风险,且编号长度会比较长(目前系统就是简单拼接,日志编号长度约40个字符)。因此应该提供一种机制,在后期可以自定义添加一些装饰对“拼接后的数据”做一定装饰的方法,可以是长度优化,也可以是关键信息掩盖等。而且要能识别到以有的日志编号用的是哪一种装饰方法,这样就能逆向还原。
这还说啥,策略模式可以走一波了。
直接上代码了。。
开始 =====================================================
定义一个日志编号生成器:NumberGenerator
@Slf4j
public class WarningNumberGenerator {
private static final NumberDecorator defNumberDecorator = new SimpleNumberDecorator(); // 默认编号装饰器,可更改
private static final int MODE_BIT = 1; // 日志编号模式标志位, 限制1位,用于识别预警装饰器
private static final int ID_OFFSET = MODE_BIT;
private static final int ID_LENGTH = 19;
private static final int TIME_OFFSET = ID_OFFSET + ID_LENGTH;
private static final int TIME_LENGTH = 13;
private static final Map<String, NumberDecorator> DECORATORS = new HashMap<>();
static {
putDecorator(new SimpleNumberDecorator());
putDecorator(new CryptNumberDecorator());
}
private static void putDecorator(NumberDecorator decorator) {
if (decorator.mode().length() == 1) { // 预警标号模式标志位, 限制1位
DECORATORS.put(decorator.mode(), decorator);
}
}
// =============================== 生成 =================================
/**
* 生成编号
*
* @return 唯一编号
*/
public static String generateNumber() {
return generateNumber(new Date(), "");
}
/**
* 生成编号
*
* @return 唯一编号
*/
public static String generateNumber(Date date) {
return generateNumber(date, "");
}
/**
* 生成编号
*
* @return 唯一编号
*/
public static String generateNumber(String identifier) {
return generateNumber(new Date(), identifier);
}
/**
* 生成编号,生成规则:装饰标志位 + 19位雪花算法 + 当前时间戳 + 标识符
*
* @return 唯一编号
*/
public static String generateNumber(Date date, String identifier) {
long time = date.getTime();
String plainNumber = String.valueOf(SnowFlakeUtil.nextId()) + time + identifier;
return defNumberDecorator.mode() + defNumberDecorator.decorate(plainNumber);
}
// =============================== 解析 =================================
private static NumberDecorator getDecorator(String logNumber) {
for (String mode : DECORATORS.keySet()) {
if (logNumber.startsWith(mode)) {
return DECORATORS.get(mode);
}
}
return defNumberDecorator;
}
private static String getPlainNumber(String logNumber) {
NumberDecorator decorator = getDecorator(logNumber);
logNumber= StrUtil.subSuf(logNumber, 1); // 去除标志位
return decorator.undecorate(logNumber); //去除装饰
}
/**
* 解析时间戳
*
* @param logNumber 日志编号
* @return
*/
public static Date getDate(String logNumber) {
try {
String plainNumber = getPlainNumber(logNumber);
String timeStr = StrUtil.sub(plainNumber, ID_LENGTH, ID_LENGTH + TIME_LENGTH);
return new Date(Long.parseLong(timeStr));
} catch (Exception e) {
log.error(StrUtil.format("解析日志编号[{}]时间戳错误:", logNumber), e);
return null;
}
}
/**
* 解析标识
*
* @param logNumber 日志编号
* @return
*/
public static String getIdentifier(String logNumber) {
try {
String plainNumber = getPlainNumber(logNumber);
return StrUtil.subSuf(plainNumber, ID_LENGTH + TIME_LENGTH);
} catch (Exception e) {
log.error(StrUtil.format("解析日志编号[{}]时间戳错误:", logNumber), e);
return StrUtil.EMPTY;
}
}
}
定义一个日志编号装饰器:NumberDecorator
public interface NumberDecorator {
/**
* 限制为1位数字字符
*
* @return 模式标志
*/
String mode();
/**
* 装饰
*
* @param plainNumber
* @return
*/
String decorate(String plainNumber);
/**
* 去装饰
*
* @param decorated
* @return
*/
String undecorate(String decorated);
}
实现一个日志编号装饰器:SimpleNumberDecorator(仅简单拼接)
public class SimpleNumberDecorator implements NumberDecorator {
@Override
public String mode() {
return "1";
}
@Override
public String decorate(String plainNumber) {
return plainNumber;
}
@Override
public String undecorate(String decorated) {
return decorated;
}
}
实现一个日志编号装饰器:CryptNumberDecorator(仅简单拼接)
public class CryptNumberDecorator implements WarningNumberDecorator {
String secret = SecureUtil.md5().digestHex16("LogNumberCryptKey");
@Override
public String mode() {
return "2";
}
@Override
public String decorate(String plainNumber) {
SM4 sm4 = SmUtil.sm4(secret.getBytes(StandardCharsets.UTF_8));
return sm4.encryptHex(plainNumber);
}
@Override
public String undecorate(String decorated) {
SM4 sm4 = SmUtil.sm4(secret.getBytes(StandardCharsets.UTF_8));
return sm4.decryptStr(decorated);
}
}
额,结束了。。。。。