Práctica de optimización del espacio de caché

guía

El almacenamiento en caché de Redis es nuestro servicio más utilizado, es aplicable a una amplia gama de escenarios y se usa ampliamente en varios escenarios comerciales. Debido a esto, la memoria caché se ha convertido en una fuente importante de costos de hardware y es necesario que optimicemos un poco el espacio para reducir costos y mejorar el rendimiento.

Tomemos nuestro caso para ilustrar cómo reducir el espacio de caché en un 70%.

escenario

1. Necesitamos almacenar el POJO en el caché, la definición de esta clase es la siguiente

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. Utilice los siguientes ejemplos como datos de prueba.

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");

práctica convencional

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

Utilice JSON para serializar e imprimir directamente  length=284 . Este método es el más simple y el más utilizado. Los datos específicos son los siguientes:

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

Descubrimos que lo anterior contiene una gran cantidad de datos inútiles y no es necesario almacenar el nombre del atributo.

Mejora 1: eliminar el nombre del atributo

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

Al elegir una estructura de matriz en lugar de una estructura de objeto, eliminar el nombre del atributo, imprimir  length=144 y reducir el tamaño de los datos en un 50%, los datos específicos son los siguientes:

["ÉXITO","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","PROCESAMIENTO"]

Descubrimos que no es necesario almacenar nulos y el formato de hora se serializa como una cadena. Los resultados de serialización irrazonables provocarán la expansión de los datos, por lo que debemos elegir una mejor herramienta de serialización.

Mejora 2: utilice mejores herramientas de serialización

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

Elija una mejor herramienta de serialización, realice la compresión de campos y un formato de datos razonable, la longitud de impresión  = 92 y el espacio se reducirá en un 40% en comparación con el paso anterior.

Este es un dato binario. Redis debe operarse en binario. Después de convertir el binario en una cadena, la impresión es la siguiente:

��ÉXITO�ABCDEFGHIJ�ABCD��j�6���XX��j�6����?`bM����@i��Q�Y�2023-02-02�PROCESAMIENTO

Siguiendo esta idea y profundizando, descubrimos que podemos seleccionar manualmente el tipo de datos para lograr un efecto de optimización más extremo y optar por utilizar un tipo de datos más pequeño para lograr una mejora adicional.

Mejora 3: optimización de tipos de datos

En el caso de uso anterior, los tres campos testStatus, preCheckStatus e inversor son en realidad tipos de cadenas de enumeración. Si puede utilizar tipos de datos más simples (como byte o int) en lugar de cadenas, puede ahorrar aún más espacio. Entre ellos, checkTime se puede reemplazar por el tipo Long, que la herramienta de serialización generará con menos bytes.

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

Después del ajuste manual, utilice un tipo de datos más pequeño en lugar del tipo Cadena,  longitud de impresión = 69

Mejora 4: considere la compresión ZIP

Además de los puntos anteriores, también puede considerar usar la compresión ZIP para obtener un volumen menor. Cuando el contenido es grande o repetitivo, el efecto de la compresión ZIP es obvio. Si el contenido almacenado es una matriz de TestPOJO, puede ser adecuado para Compresión ZIP.

Pero la compresión ZIP no necesariamente reduce el tamaño y puede aumentarlo si es inferior a 30 bytes. En el caso de contenidos poco repetitivos no se puede obtener ninguna mejora significativa. Y hay una sobrecarga de CPU.

Después de la optimización anterior, la compresión ZIP ya no es obligatoria y debe probarse de acuerdo con datos reales para distinguir el efecto de compresión de ZIP.

Finalmente aterrizó

Los varios pasos de mejora anteriores reflejan la idea de optimización, pero el proceso de deserialización provocará la pérdida de tipos, lo cual es engorroso de manejar, por lo que también debemos considerar el tema de la deserialización.

Cuando el objeto de caché está predefinido, podemos procesar manualmente cada campo, por lo que en el combate real, se recomienda utilizar la serialización manual para lograr los objetivos anteriores, lograr un control detallado, lograr el mejor efecto de compresión y la menor sobrecarga de rendimiento.

Puede consultar el código de implementación de msgpack a continuación. El siguiente es el código de prueba. Empaque mejores herramientas como Packer y UnPacker usted mismo:

<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());
        }
    }

extensión de escena

Supongamos que almacenamos datos para 200 millones de usuarios, cada usuario contiene 40 campos, la longitud de la clave de campo es de 6 bytes y los campos se administran por separado.

En circunstancias normales, pensamos en la estructura hash, y la estructura hash almacena información clave, lo que ocupará recursos adicionales, y la clave de campo son datos innecesarios. De acuerdo con las ideas anteriores, puede usar listas en lugar de estructuras hash.

Probado por la herramienta oficial de Redis, el uso de la estructura de lista requiere 144 G de espacio, mientras que el uso de la estructura hash requiere 245 G de espacio (cuando más del 50% de los atributos están vacíos, debe probar si aún es aplicable)

En el caso anterior, hemos tomado varias medidas muy simples: solo unas pocas líneas de código simple pueden reducir el espacio en más del 70%, lo cual es muy recomendable en escenarios con un gran volumen de datos y altos requisitos de rendimiento. :

• Utilice una matriz en lugar de un objeto (si una gran cantidad de campos están vacíos, debe cooperar con la herramienta de serialización para comprimir el valor nulo)

• Utilice mejores herramientas de serialización

• utilizar tipos de datos más pequeños

• Considere usar compresión ZIP

• Utilice una lista en lugar de una estructura hash (si una gran cantidad de campos están vacíos, se requiere una comparación de prueba)

 

Supongo que te gusta

Origin blog.csdn.net/APItesterCris/article/details/131164219
Recomendado
Clasificación