欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于Java實現(xiàn)簡單的時序數(shù)據(jù)壓縮算法

 更新時間:2022年06月27日 08:14:08   作者:buttercup  
這篇文章主要為大家詳細(xì)介紹了如何利用Java語言實現(xiàn)簡單易懂的時序數(shù)據(jù)壓縮算法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下

背景

今年在公司內(nèi)部主導(dǎo)了兩個的行情數(shù)據(jù)系統(tǒng)的構(gòu)建,兩者均使用到了常見的時序數(shù)據(jù)壓縮算法。

這里簡單總結(jié)一下過程中積累的一些經(jīng)驗。

讓我們先來思考一個問題:壓縮算法生效的前提是什么?

數(shù)據(jù)本身至少要符合以下兩種特性其一:

  • 數(shù)據(jù)存在冗余
  • 數(shù)據(jù)符合特定的概率分布

在時序數(shù)據(jù)領(lǐng)域,數(shù)據(jù)冗余度與相似度較高,因此天生適合進(jìn)行壓縮。

但對于不同類型的數(shù)據(jù),其所適用的壓縮算法也大相徑庭。

下面我們逐一介紹這些數(shù)據(jù)相應(yīng)的壓縮算法。

整數(shù)

整型數(shù)據(jù)是構(gòu)建各種應(yīng)用的基石,時序型應(yīng)用也不例外。
在行情數(shù)據(jù)中,存在大量的整型數(shù)據(jù),例如:逐筆成交中的時間戳、成交量。

根據(jù)壓縮算法的不同,可以將整型數(shù)據(jù)分為以下 3 類:

  • 無符號整型 —— Varint
  • 有符號整型 —— ZigZag
  • 時間戳 —— Delta2 + Simple8b

Varint

一個 32 位的無符號整型能表達(dá) 0 - 4294967295 之間的任意數(shù)字
但這些數(shù)字在日常生活中出現(xiàn)的概率并不是均勻分布的,一個著名的例子是本福特定律,該定律常被用于辨別數(shù)據(jù)的真?zhèn)巍?/p>

通常情況下,較小的數(shù)字出現(xiàn)的概率會高于極大的數(shù)據(jù)。
以年齡為例,無論人口如何分布,大部分人的年齡都位于 0 ~ 100 之間。

表示 128 僅需要 7bit 足矣,如果使用 32bit 的無符號整型進(jìn)行存儲,意味著至少浪費了 24bit。

幸運的是,我們能通過一種自適應(yīng)編碼方式來減少這種浪費 —— Varint。

public class VarIntCodec {

    static int encodeInt(int v, byte[] bytes, int offset) {
        if (v < 0) {
            throw new IllegalStateException();
        } else if (v < 128) {
            bytes[offset++] = (byte) v;
        } else if (v < 16384) {
            bytes[offset++] = (byte) (v | 0x80);
            bytes[offset++] = (byte) ((v >>> 7) & 0x7F);
        } else if (v < 2097152) {
            bytes[offset++] = (byte) (v | 0x80);
            bytes[offset++] = (byte) ((v >>> 7) | 0x80);
            bytes[offset++] = (byte) (v >>> 14);
        } else if (v < 268435456) {
            bytes[offset++] = (byte) (v | 0x80);
            bytes[offset++] = (byte) ((v >>> 7) | 0x80);
            bytes[offset++] = (byte) ((v >>> 14) | 0x80);
            bytes[offset++] = (byte) (v >>> 21);
        } else {
            bytes[offset++] = (byte) (v | 0x80);
            bytes[offset++] = (byte) ((v >>> 7) | 0x80);
            bytes[offset++] = (byte) ((v >>> 14) | 0x80);
            bytes[offset++] = (byte) ((v >>> 21) | 0x80);
            bytes[offset++] = (byte) (v >>> 28);
        }
        return offset;
    }

    static int decodeInt(byte[] bytes, int[] offset) {
        int val;
        int off = offset[0];
        byte b0, b1, b2, b3;
        if ((b0 = bytes[off++]) >= 0) {
            val = b0;
        } else if ((b1 = bytes[off++]) >= 0) {
            val = (b0 & 0x7F) + (b1 << 7);
        } else if ((b2 = bytes[off++]) >= 0) {
            val = (b0 & 0x7F) + ((b1 & 0x7F) << 7) + (b2 << 14);
        } else if ((b3 = bytes[off++]) >= 0) {
            val = (b0 & 0x7F) + ((b1 & 0x7F) << 7) + ((b2 & 0x7F) << 14) + (b3 << 21);;
        } else {
            val = (b0 & 0x7F) + ((b1 & 0x7F) << 7) + ((b2 & 0x7F) << 14) + ((b3 & 0x7F) << 21) + (bytes[off++] << 28);
        }
        offset[0] = off;
        return val;
    }

}

通過觀察代碼可以得知,Varint 編碼并不是沒有代價的:每 8bit 需要需要犧牲 1bit 作為標(biāo)記位。

對于值較大的數(shù)據(jù),Varint 需要額外的空間進(jìn)行編碼,從而導(dǎo)致更大的空間消耗。

因此使用 Varint 的前提有兩個:

  • 數(shù)據(jù)較小
  • 沒有負(fù)數(shù)

ZigZag

Varint 編碼負(fù)數(shù)的效率很低:

對于 big-endian 數(shù)據(jù)來說,Varint 是通過削減 leading-zero 來實現(xiàn)的壓縮。

而負(fù)數(shù)的首個 bit 永遠(yuǎn)非零,因此不但無法壓縮數(shù)據(jù),反而會引入不必要的空間開銷。

ZigZag 通過以下方式來彌補這一缺陷:

增加小負(fù)數(shù)的 leading-zero 數(shù)量,然后再進(jìn)行 Varint 編碼。

其原理很簡單,增加一個 ZigZag 映射:

  • Varint 編碼前增加映射 (n << 1) ^ (n >> 31)
  • Varint 解碼后增加映射 (n >>> 1) ^ -(n & 1)

當(dāng) n = -1 時,Varint 編碼前映射:

n = -1      -> 11111111111111111111111111111111
a = n << 1  -> 11111111111111111111111111111110
b = n >> 31 -> 11111111111111111111111111111111
a ^ b       -> 00000000000000000000000000000001

當(dāng) n = -1 時,Varint 解碼后映射:

m = a ^ b    -> 00000000000000000000000000000001
a = m >>> 1  -> 00000000000000000000000000000000
b = -(m & 1) -> 11111111111111111111111111111111
a ^ b        -> 11111111111111111111111111111111

ZigZag 映射能夠有效增加小負(fù)數(shù)的 leading-zero 數(shù)量,進(jìn)而提高編碼效率。

ZigZag 本身也并不是完美的的:

  • 占用了部分非負(fù)數(shù)的編碼空間
  • 對于大負(fù)數(shù)沒有優(yōu)化效果

Delta2

時間戳是時序數(shù)據(jù)中的一類特殊的數(shù)據(jù),其值往往比較大,因此不適用于 Varint 編碼。
但是時間戳本身具有以下兩個特性:

  • 非負(fù)且單調(diào)遞增
  • 數(shù)據(jù)間隔較為固定

前面提到過:提高整型數(shù)據(jù)壓縮率的一個重要手段是增加 leading-zero 數(shù)量。
說人話就是:盡可能存儲較小的數(shù)字。

因此,相較于存儲時間戳,存儲前后兩條數(shù)據(jù)的時間間隔是一個更好的選擇。

第一種方式是使用 Delta 編碼:

  • 第一個數(shù)據(jù)點,直接存儲 t0t0
  • 第 n 個數(shù)據(jù)點,則存儲 tn−t0tn−t0

但是 Delta 編碼的數(shù)據(jù)冗余仍然較多,因此可以改進(jìn)為 Delta2 編碼:

  • 第一個數(shù)據(jù)點,直接存儲 t0t0
  • 第 n 個數(shù)據(jù)點,則存儲 tn−tn−1tn−tn−1

編碼后的 int64 的時間戳,可以用 int32 甚至更小的數(shù)據(jù)類型進(jìn)行存儲。

Simple8b

朋友,你覺得 Varint 與 Delta2 已經(jīng)是整型壓縮的極限了嗎?

某些特殊的時序事件,可能以很高的頻率出現(xiàn),比如行情盤口報價。

這類數(shù)據(jù)的時間戳進(jìn)行 Delta2 編碼后,可能會出現(xiàn)以下兩種情況:

  • 場景1:很多連續(xù)的 0 或 1 區(qū)間
  • 場景2:大部分?jǐn)?shù)據(jù)分布在 0 ~ 63 的范圍內(nèi)

對于場景1,游程編碼(RLE)是個不錯的選擇,但普適性較差。

對于場景2,每個值僅需 6bit 存儲即可,使用 Varint 編碼仍有空間浪費。

Simple8b 算法能較好的處理這一問題,其核心思想是:

將盡可能多數(shù)字打包在一個 64bit 長整型中。

Simple8b 將 64 位整數(shù)分為兩部分:

  • selector(4bit) 用于指定剩余 60bit 中存儲的整數(shù)的個數(shù)與有效位長度
  • payload(60bit) 則是用于存儲多個定長的整數(shù)
    根據(jù)一個查找表,將數(shù)據(jù)模式匹配到最優(yōu)的 selector,然后將多個數(shù)據(jù)編碼至 payload

Simple8b 維護(hù)了一個查找表,可以將數(shù)據(jù)模式匹配到最優(yōu)的 selector,然后將多個數(shù)據(jù)編碼至 payload。編碼算法可以參考這個Go實現(xiàn)。

需要指出的是,這個開源實現(xiàn)使用回溯法進(jìn)行編碼,其復(fù)雜度為 O(n2)O(n2)( n 為輸入數(shù)據(jù)長度)。該實現(xiàn)對于離線壓縮來說已經(jīng)足夠,但對于實時壓縮來說稍顯不足。

我們在此代碼的基礎(chǔ)上,使用查表法維護(hù)了一個狀態(tài)轉(zhuǎn)移數(shù)組,將編碼速度提升至 O(n)O(n),使其能夠應(yīng)用于實時壓縮。

浮點數(shù)

浮點數(shù)是一類較難壓縮的數(shù)據(jù),IEEE 705 在編碼上幾乎沒有浪費任何一個 bit,因此無法使用類似 Varint 的方式進(jìn)行壓縮:

0131230signexponent (8-bit)fraction (23-bit)= 0.15625

目前常用的壓縮方式可以分為兩類:

  • 有損壓縮 —— 丟棄不必要的精度后,使用整數(shù)進(jìn)行表示
  • 無損壓縮 —— XOR 編碼

有損壓縮

有損壓縮需要配合業(yè)務(wù)領(lǐng)域知識來使用,因為首先要確定需要保留的精度。

當(dāng)精度確定之后,就可以將浮點數(shù)轉(zhuǎn)換為整數(shù),然后使用 Varint 進(jìn)行編碼。

public static ScaledResult scaleDecimal(float[] floats, final int scaleLimit) throws CodecException {

    BigDecimal[] decimals = new BigDecimal[floats.length];
    for (int i=0; i<floats.length; i++) {
        decimals[i] = new BigDecimal(Float.toString(floats[i]));
    }

    BigDecimal base = null;
    int maxScale = 0;
    for (BigDecimal decimal : decimals) {
        Preconditions.checkState(decimal.signum() >= 0);
        int scale = decimal.scale();
        if (scale == 1 && maxScale == 0 && decimal.stripTrailingZeros().scale() <= 0) {
            scale = 0;
        }
        maxScale = Math.max(maxScale, scale);
        base = base == null || decimal.compareTo(base) < 0 ? decimal : base;
    }
    final int scale = Math.min(maxScale, scaleLimit);
    final long scaledBase = base.scaleByPowerOfTen(scale).setScale(0, RoundingMode.HALF_UP).longValueExact();
    long[] data = new long[decimals.length];
    for (int i=0; i<decimals.length; i++) {
        long scaledValue = decimals[i].scaleByPowerOfTen(scale).setScale(0, RoundingMode.HALF_UP).longValueExact();
        data[i] = scaledValue - scaledBase;
    }
    return new ScaledResult(scale, scaledBase, data);
}

這個壓縮算法的優(yōu)點主要是直觀,并且天然跨語言,比較適合前后端交互。

不過也存在以下局限性:

  • 只能壓縮精度已知的數(shù)據(jù)
  • 數(shù)據(jù)中不能同時出現(xiàn)正數(shù)與負(fù)數(shù)
  • 編碼速度較慢

編碼速度慢很大的原因,是在 float 轉(zhuǎn)換為 BigDecimal 這一步使用Float.toString 保留數(shù)據(jù)精度。該方法不僅復(fù)雜,還生成了許多中間對象,因此效率自然不高。

一個 workaround 方式是使用高效的解析庫 Ryu ,并將其返回值改為 BigDecimal,減少中間對象的生成。這一優(yōu)化能夠?qū)⒕幋a速度提高約 5 倍。

無損壓縮

有損壓縮的一個缺點是泛用性較差,并且無法處理小數(shù)位較大的情況。

然而高冗余的浮點數(shù)據(jù),在時序數(shù)據(jù)中比比皆是,比如某些機(jī)器監(jiān)控指標(biāo),其值往往只在 0 ~ 1 之間波動。

2015 年 Fackbook 發(fā)表了一篇關(guān)于內(nèi)存時序數(shù)據(jù)庫的 論文,其中提出了一種基于異或算法的浮點數(shù)壓縮算法。

Facebook 的研究人員在時序浮點數(shù)中發(fā)現(xiàn)個規(guī)律:

在同個時間序列中,大部分浮點數(shù)的占用的有效 bit 位是類似的,并且往往只有中間的一個連續(xù)區(qū)塊存儲著不同的數(shù)據(jù)。

因此他們想了個辦法提取并只保存這部分?jǐn)?shù)據(jù):

1.將 float 轉(zhuǎn)換為 bits

2.計算兩個相鄰 bits 的異或值 xor

3.記錄 xor 的 leading-zero 與連續(xù)區(qū)塊長度 block-size

4.記錄 1 bit 的標(biāo)記位 flag:

  • 如果 leading-zero 與 block-size 與之前相同,記為 false
  • 如果 leading-zero 與 block-size 與之前不同,記為 true

5.當(dāng) flag 為 true 時,記錄 leading-zero 與 block-size

6.記錄連續(xù)區(qū)塊 block 數(shù)據(jù),然后進(jìn)入下個循環(huán)

由于大部分?jǐn)?shù)據(jù)的 leading-zero 與連續(xù)區(qū)塊長度 block-size 均相等,往往只需要存儲 flag 與 block 數(shù)據(jù),因此大多數(shù)情況都能有效的減少數(shù)據(jù)存儲空間。

這個算法主要的難點是實現(xiàn)高效的 BitWriter,這個可以通過繼承 ByteArrayOutputStream 并增加一個 long 類型的 buffer 實現(xiàn)。

abstract class BlockMeta<T extends BlockMeta<T>> {

    short leadingZero;
    short tailingZero;
    int blockSize;

    BlockMeta<T> withMeta(int leadingZero, int blockSize) {
        this.leadingZero = (short) leadingZero;
        this.tailingZero = (short) (length() - leadingZero - blockSize);
        this.blockSize = blockSize;
        return this;
    }

    abstract long value();
    abstract int length();

    boolean fallInSameBlock(BlockMeta<? extends BlockMeta<?>> block) {
        return block != null && block.leadingZero == leadingZero && block.tailingZero == tailingZero;
    }
}

static void encodeBlock(BitsWriter buffer, BlockMeta<?> block, BlockMeta<?> prevBlock) {
    if (block.value() == 0) {
        buffer.writeBit(false);
    } else {
        boolean ctrlBit;
        buffer.writeBit(true);
        buffer.writeBit(ctrlBit = ! block.fallInSameBlock(prevBlock));
        if (ctrlBit) {
            buffer.writeBits(block.leadingZero, 6);
            buffer.writeBits(block.blockSize, 7);
        }
        buffer.writeBits(block.value(), block.blockSize);
    }
}

static long decodeBlock(BitsReader reader, BlockMeta<?> blockMeta) {
    if (reader.readBit()) {
        boolean ctrlBit = reader.readBit();
        if (ctrlBit) {
            int leadingZero = (int) reader.readBits(6);
            int blockSize = (int) reader.readBits(7);
            blockMeta.withMeta(leadingZero, blockSize);
        }
        CodecException.malformedData(blockMeta == null);
        long value = reader.readBits(blockMeta.blockSize);
        return value << blockMeta.tailingZero;
    }
    return 0;
}

Facebook 聲稱,使用該算法壓縮 2 小時的時序數(shù)據(jù),每個數(shù)據(jù)點僅僅需 1.37 byte:

A two-hour block allows us to achieve a compression ratio of
1.37 bytes per data point.

經(jīng)測試,該算法能夠?qū)⒎謺r數(shù)據(jù)壓縮為原來的 33%,壓縮率可達(dá) 60% 以上,效果顯著。

字符串

時序數(shù)據(jù)中的字符串大致可以分為兩類:

  • 標(biāo)簽型 —— 數(shù)據(jù)字典
  • 非標(biāo)簽型 —— Snappy

標(biāo)簽型

標(biāo)簽型字符串在時時序數(shù)據(jù)中更為常見,比如監(jiān)控數(shù)據(jù)中的 IP 與 Metric 名稱便是此類數(shù)據(jù)。

該類數(shù)據(jù)通常作為查詢索引使用,其值也比較固定。因此通常使用數(shù)據(jù)字典進(jìn)行壓縮,將變長字符串轉(zhuǎn)換為定長的整型索引。

  • 一方面減少了空間占用
  • 另一方面有利于構(gòu)建高效的索引

當(dāng)轉(zhuǎn)換成整型后,還能進(jìn)一步的利用 BloomFilter 與 Bitmap 等數(shù)據(jù)結(jié)構(gòu),進(jìn)一步提升查詢性能。

非標(biāo)簽型

非標(biāo)簽型的字符串較為少見,較少時序數(shù)據(jù)引擎支持該類型。

這類數(shù)據(jù)的值較為不可控,因此只能使用通用的壓縮算法進(jìn)行壓縮。

為了兼顧效率,通常情況下會使用 snappy 或 lz4,兩者不相伯仲。

通常情況下兩者的壓縮比較為接近,這方面 snappy 有微弱的優(yōu)勢。

不夠 lz4 提供了更為靈活的壓縮策略:

  • 可調(diào)的壓縮等級,允許使用者自行調(diào)配速度與壓縮率
  • 可以在壓縮速度與解壓速度上進(jìn)行權(quán)衡,當(dāng)解壓與壓縮機(jī)器性能不對稱時較為有用

以上就是基于Java實現(xiàn)簡單的時序數(shù)據(jù)壓縮算法的詳細(xì)內(nèi)容,更多關(guān)于Java時序數(shù)據(jù)壓縮算法的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java中StringUtils工具類進(jìn)行String為空的判斷解析

    Java中StringUtils工具類進(jìn)行String為空的判斷解析

    這篇文章主要介紹了Java中StringUtils工具類進(jìn)行String為空的判斷解析,具有一定借鑒價值,需要的朋友可以參考下
    2018-01-01
  • java.exe和javaw.exe的區(qū)別及使用方法

    java.exe和javaw.exe的區(qū)別及使用方法

    這篇文章主要介紹了java.exe和javaw.exe的區(qū)別及使用方法,需要的朋友可以參考下
    2014-04-04
  • SpringBoot解決同名類導(dǎo)致的bean名沖突bean name conflicts問題

    SpringBoot解決同名類導(dǎo)致的bean名沖突bean name conflicts問題

    這篇文章主要介紹了SpringBoot解決同名類導(dǎo)致的bean名沖突bean name conflicts問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • Java 基礎(chǔ)語法 異常處理

    Java 基礎(chǔ)語法 異常處理

    這篇我們就來介紹Java 基礎(chǔ)語法中長遇到的那些異常及處理方法的一下相關(guān)資料,感興趣的小伙伴可以參考一下下面文章的詳細(xì)內(nèi)容,希望對你有所幫助
    2021-10-10
  • 解析Java定時任務(wù)的選型及改造問題

    解析Java定時任務(wù)的選型及改造問題

    這篇文章主要介紹了Java定時任務(wù)的選型及改造問題,本文給大家提到了Java主流三大定時任務(wù)框架優(yōu)缺點,通過實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2022-02-02
  • 從架構(gòu)思維角度分析高并發(fā)下冪等性解決方案

    從架構(gòu)思維角度分析高并發(fā)下冪等性解決方案

    冪等(idempotent、idempotence)是一個數(shù)學(xué)與計算機(jī)學(xué)概念,常見于抽象代數(shù)中。?在編程中.一個冪等操作的特點是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復(fù)執(zhí)行,并能獲得相同結(jié)果的函數(shù)
    2022-01-01
  • 詳解常用的Spring Bean擴(kuò)展接口

    詳解常用的Spring Bean擴(kuò)展接口

    本篇文章主要介紹了一些常用的Spring Bean擴(kuò)展接口以及它們的簡單用法,具有很好的參考價值。下面跟著小編一起來看下吧
    2017-05-05
  • Java?實戰(zhàn)范例之校園二手市場系統(tǒng)的實現(xiàn)

    Java?實戰(zhàn)范例之校園二手市場系統(tǒng)的實現(xiàn)

    讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+mysql+maven+tomcat實現(xiàn)一個校園二手市場系統(tǒng),大家可以在過程中查缺補漏,提升水平
    2021-11-11
  • 淺談Spring Boot 2.0遷移指南主要注意點

    淺談Spring Boot 2.0遷移指南主要注意點

    Spring官方的Spring Boot 2變動指南,主要是幫助您將應(yīng)用程序遷移到Spring Boot 2.0,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-10-10
  • java基于UDP實現(xiàn)圖片群發(fā)功能

    java基于UDP實現(xiàn)圖片群發(fā)功能

    這篇文章主要為大家詳細(xì)介紹了java基于UDP實現(xiàn)圖片群發(fā)功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-01-01

最新評論