项目点滴:强行让“策略模式“在日志编号生成中应用

在近期参与开发的项目中,遇到了一个数据查询性能问题。本文对优化过程做简单的描述。

组件:时许数据库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);
    }
}

额,结束了。。。。。

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值