キャッシュスペースの最適化の実践

ガイド

Redis のキャッシュは最も一般的に使用されているサービスであり、幅広いシナリオに適用でき、さまざまなビジネス シナリオで広く使用されています。このため、キャッシュはハードウェア コストの重要な原因となっており、コストを削減し、パフォーマンスを向上させるためにスペースの最適化を行う必要があります。

私たちのケースで、キャッシュ領域を 70% 削減する方法を説明してみましょう。

シーン設定

1. POJO をキャッシュに保存する必要があります。このクラスの定義は次のとおりです。

public class TestPOJO implements Serializable {
    private String testStatus;
    private String userPin;
    private String investor;
    private Date testQueryTime;
    private Date createTime;
    private String bizInfo;
    private Date otherTime;
    private BigDecimal userAmount;
    private BigDecimal userRate;
    private BigDecimal applyAmount;
    private String type;
    private String checkTime;
    private String preTestStatus;
    
    public Object[] toValueArray(){
        Object[] array = {testStatus, userPin, investor, testQueryTime,
                createTime, bizInfo, otherTime, userAmount,
                userRate, applyAmount, type, checkTime, preTestStatus};
        return array;
    }
    
    public CreditRecord fromValueArray(Object[] valueArray){         
        //具体的数据类型会丢失,需要做处理
    }
}

2. 以下の例をテストデータとして使用します

TestPOJO pojo = new TestPOJO();
pojo.setApplyAmount(new BigDecimal("200.11"));
pojo.setBizInfo("XX");
pojo.setUserAmount(new BigDecimal("1000.00"));
pojo.setTestStatus("SUCCESS");
pojo.setCheckTime("2023-02-02");
pojo.setInvestor("ABCD");
pojo.setUserRate(new BigDecimal("0.002"));
pojo.setTestQueryTime(new Date());
pojo.setOtherTime(new Date());
pojo.setPreTestStatus("PROCESSING");
pojo.setUserPin("ABCDEFGHIJ");
pojo.setType("Y");

従来の慣習

System.out.println(JSON.toJSONString(pojo).length());

JSON を使用して、  length=284 を直接シリアル化して出力しますこの方法が最も単純で、最も一般的に使用されます。具体的なデータは次のとおりです。

{"applyAmount":200.11,"bizInfo":"XX","checkTime":"2023-02-02","investor":"ABCD","otherTime":"2023-04-10 17:45:17.717 ","preCheckStatus":"PROCESSING","testQueryTime":"2023-04-10 17:45:17.717","testStatus":"SUCCESS","type":"Y","userAmount":1000.00," userPin":"ABCDEFGHIJ","userRate":0.002}

上記には、属性名を保存する必要のない無駄なデータが多く含まれていることがわかりました。

改善 1 - 属性名を削除する

System.out.println(JSON.toJSONString(pojo.toValueArray()).length());

オブジェクト構造の代わりに配列構造を選択し、属性名を削除し、  length=144を出力し、データ サイズを 50% 削減すると、具体的なデータは次のようになります。

["成功","ABCDEFGHIJ","ABCD","2023-04-10 17:45:17.717",null,"XX","2023-04-10 17:45:17.717",1000.00,0.002,200.11 ,"Y","2023-02-02","処理中"]

null を格納する必要がなく、時刻の形式が文字列としてシリアル化されることがわかりました。無理なシリアル化の結果はデー​​タの拡張につながるため、より適切なシリアル化ツールを選択する必要があります。

改善 2 - より優れたシリアル化ツールを使用する

//我们仍然选取JSON格式,但使用了第三方序列化工具
System.out.println(new ObjectMapper(new MessagePackFactory()).writeValueAsBytes(pojo.toValueArray()).length);

より優れたシリアル化ツールを選択し、フィールド圧縮と適切なデータ形式を実現し、印刷 長 = 92 で、スペースは前のステップと比較して 40% 削減されます。

これはバイナリ データです。Redis はバイナリで操作する必要があります。バイナリが文字列に変換された後の出力は次のようになります。

��成功��ABCDEFGHIJ��ABCD��j��6����XX��j��6����?`bM����@i��Q�Y�2023-02-02�処理中

このアイデアに従ってさらに深く掘り下げたところ、より極端な最適化効果を達成するにはデータ タイプを手動で選択し、さらなる改善を達成するにはより小さいデータ タイプの使用を選択できることがわかりました。

改善 3 - データ型の最適化

上記の使用例では、testStatus、preCheckStatus、investor の 3 つのフィールドは実際には列挙文字列型ですが、文字列の代わりにより単純なデータ型 (byte や int など) を使用できる場合は、スペースをさらに節約できます。このうち、checkTime は Long 型に置き換えることができ、シリアル化ツールで出力されるバイト数が少なくなります。

public Object[] toValueArray(){
    Object[] array = {toInt(testStatus), userPin, toInt(investor), testQueryTime,
    createTime, bizInfo, otherTime, userAmount,
    userRate, applyAmount, type, toLong(checkTime), toInt(preTestStatus)};
    return array;
}

手動で調整した後は、文字列型の代わりに小さいデータ型を使用し、 長さ = 69を印刷します。

改善 4 - ZIP 圧縮を検討する

上記の点に加えて、より小さい容量を取得するために ZIP 圧縮の使用を検討することもできます。コンテンツが大きい場合や繰り返しが多い場合、ZIP 圧縮の効果は明ら​​かです。保存されたコンテンツが TestPOJO の配列である場合は、ZIP 圧縮が適している可能性がありますZIP圧縮の場合。

ただし、ZIP 圧縮しても必ずしもサイズが小さくなるわけではなく、30 バイト未満の場合はサイズが大きくなる可能性があります。反復コンテンツが少ない場合は、大幅な改善は得られません。そしてCPUのオーバーヘッドも発生します。

上記の最適化後、ZIP 圧縮は必須ではなくなり、ZIP の圧縮効果を区別するには実際のデータに基づいてテストする必要があります。

ついに上陸

上記のいくつかの改善手順は最適化の考え方を反映していますが、逆シリアル化のプロセスでは型の損失が発生し、これに対処するのが面倒なので、逆シリアル化の問題も考慮する必要があります。

キャッシュ オブジェクトが事前定義されている場合は、各フィールドを手動で処理できるため、実際の戦闘では、上記の目標を達成し、きめ細かい制御を実現し、最高の圧縮効果と最小のパフォーマンス オーバーヘッドを実現するために、手動シリアル化を使用することをお勧めします。

以下の msgpack の実装コードを参照してください。以下はテストコードです。Packer や UnPacker などのより良いツールを自分でパックしてください。

<dependency>    
    <groupId>org.msgpack</groupId>    
    <artifactId>msgpack-core</artifactId>    
    <version>0.9.3</version>
</dependency>
    public byte[] toByteArray() throws Exception {
        MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
        toByteArray(packer);
        packer.close();
        return packer.toByteArray();
    }

    public void toByteArray(MessageBufferPacker packer) throws Exception {
        if (testStatus == null) {
            packer.packNil();
        }else{
            packer.packString(testStatus);
        }

        if (userPin == null) {
            packer.packNil();
        }else{
            packer.packString(userPin);
        }

        if (investor == null) {
            packer.packNil();
        }else{
            packer.packString(investor);
        }

        if (testQueryTime == null) {
            packer.packNil();
        }else{
            packer.packLong(testQueryTime.getTime());
        }

        if (createTime == null) {
            packer.packNil();
        }else{
            packer.packLong(createTime.getTime());
        }

        if (bizInfo == null) {
            packer.packNil();
        }else{
            packer.packString(bizInfo);
        }

        if (otherTime == null) {
            packer.packNil();
        }else{
            packer.packLong(otherTime.getTime());
        }

        if (userAmount == null) {
            packer.packNil();
        }else{
            packer.packString(userAmount.toString());
        }

        if (userRate == null) {
            packer.packNil();
        }else{
            packer.packString(userRate.toString());
        }

        if (applyAmount == null) {
            packer.packNil();
        }else{
            packer.packString(applyAmount.toString());
        }

        if (type == null) {
            packer.packNil();
        }else{
            packer.packString(type);
        }

        if (checkTime == null) {
            packer.packNil();
        }else{
            packer.packString(checkTime);
        }

        if (preTestStatus == null) {
            packer.packNil();
        }else{
            packer.packString(preTestStatus);
        }
    }


    public void fromByteArray(byte[] byteArray) throws Exception {
        MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(byteArray);
        fromByteArray(unpacker);
        unpacker.close();
    }

    public void fromByteArray(MessageUnpacker unpacker) throws Exception {
        if (!unpacker.tryUnpackNil()){
            this.setTestStatus(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setUserPin(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setInvestor(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setTestQueryTime(new Date(unpacker.unpackLong()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setCreateTime(new Date(unpacker.unpackLong()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setBizInfo(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setOtherTime(new Date(unpacker.unpackLong()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setUserAmount(new BigDecimal(unpacker.unpackString()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setUserRate(new BigDecimal(unpacker.unpackString()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setApplyAmount(new BigDecimal(unpacker.unpackString()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setType(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setCheckTime(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setPreTestStatus(unpacker.unpackString());
        }
    }

シーン拡張

2 億人のユーザーのデータを保存し、各ユーザーには 40 個のフィールドが含まれ、フィールド キーの長さは 6 バイトで、フィールドは個別に管理されているとします。

通常、ハッシュ構造を考えると、ハッシュ構造には追加のリソースを占有するキー情報が格納され、フィールドキーは不要なデータになりますが、上記の考え方によれば、ハッシュ構造の代わりにリストを使用することができます。

公式 Redis ツールでテストしたところ、リスト構造の使用には 144G のスペースが必要ですが、ハッシュ構造の使用には 245G のスペースが必要です(属性の 50% 以上が空の場合は、それがまだ適用可能かどうかをテストする必要があります)。

上記のケースでは、数行の簡単なコードだけでスペースを 70% 以上削減できる、非常に簡単な対策をいくつか講じました。これは、大量のデータ量と高いパフォーマンス要件があるシナリオで強く推奨されます。:

• オブジェクトの代わりに配列を使用します (多数のフィールドが空の場合は、シリアル化ツールと連携して null を圧縮する必要があります)。

• より優れたシリアル化ツールを使用する

• より小さいデータ型を使用する

• ZIP 圧縮の使用を検討する

• ハッシュ構造の代わりにリストを使用します (多数のフィールドが空の場合は、テストの比較が必要です)。

 

おすすめ

転載: blog.csdn.net/APItesterCris/article/details/131164219