Просмотр исходного кода

Merge branch 'master' into pre-master

zhaohaipeng 1 месяц назад
Родитель
Сommit
f200dc8c2f
18 измененных файлов с 321 добавлено и 243 удалено
  1. 2 0
      .gitignore
  2. 1 1
      recommend-feature-client/pom.xml
  3. 95 68
      recommend-feature-client/src/main/java/com/tzld/piaoquan/recommend/feature/client/FeatureV2Client.java
  4. 15 0
      recommend-feature-client/src/main/java/com/tzld/piaoquan/recommend/feature/domain/ad/base/AdRankItem.java
  5. 24 20
      recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/ODPSToRedis.java
  6. 2 0
      recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/model/DTSConfig.java
  7. 9 2
      recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/service/DTSConfigService.java
  8. 1 0
      recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/service/ODPSService.java
  9. 34 13
      recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/service/RedisService.java
  10. 11 39
      recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/util/CompressionUtil.java
  11. 57 93
      recommend-feature-service/src/main/java/com/tzld/piaoquan/recommend/feature/service/FeatureV2Service.java
  12. 13 0
      recommend-feature-service/src/main/java/com/tzld/piaoquan/recommend/feature/util/CompressionUtil.java
  13. 3 3
      recommend-feature-service/src/main/resources/application-dev.yml
  14. 1 1
      recommend-feature-service/src/main/resources/application-pre.yml
  15. 1 1
      recommend-feature-service/src/main/resources/application-prod.yml
  16. 1 1
      recommend-feature-service/src/main/resources/application-test.yml
  17. 7 1
      recommend-feature-service/src/main/resources/application.yml
  18. 44 0
      recommend-feature-service/src/test/java/com/tzld/piaoquan/recommend/feature/service/FeatureV2ServiceTest.java

+ 2 - 0
.gitignore

@@ -49,3 +49,5 @@ build/
 
 ### log ###
 logs/*
+config-cache
+**/**/*.log

+ 1 - 1
recommend-feature-client/pom.xml

@@ -10,7 +10,7 @@
     <modelVersion>4.0.0</modelVersion>
 
     <artifactId>recommend-feature-client</artifactId>
-    <version>1.1.24</version>
+    <version>1.1.36</version>
 
     <dependencies>
         <dependency>

+ 95 - 68
recommend-feature-client/src/main/java/com/tzld/piaoquan/recommend/feature/client/FeatureV2Client.java

@@ -11,44 +11,76 @@ import net.devh.boot.grpc.client.inject.GrpcClient;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * FeatureV2 gRPC 客户端
- * 
+ *
  * 优化说明:
  * 1. 增加异常处理:捕获 StatusRuntimeException,避免错误向上传播
  * 2. 增加重试机制:对 UNAVAILABLE 等可恢复错误自动重试(50ms 延迟)
  * 3. 增加降级处理:重试失败后返回空结果,保证服务稳定
  * 4. 增加详细日志:记录错误详情,便于问题排查
- * 
+ *
  * @author dyp
  */
 @Component
 @Slf4j
 public class FeatureV2Client {
-    
+
     @GrpcClient("recommend-feature")
     private FeatureV2ServiceGrpc.FeatureV2ServiceBlockingStub client;
-    
+
     /**
-     * 最大重试次数
-     * 说明:对于网络连接问题,重试可以触发连接重建
+     * 单次 gRPC 请求的拆包大小(按 key 数量)
+     *
+     * 说明:避免单次响应过大触发 gRPC 默认 4MB 限制/链路取消;用户侧指定默认 500。
      */
-    private static final int MAX_RETRY_ATTEMPTS = 2;
-    
+    private static final int BATCH_SIZE = 500;
+
     /**
-     * 重试延迟(毫秒)
-     * 说明:50ms 快速重试,给连接重建预留时间
+     * 拆包并发线程池
+     *
+     * 说明:机器 4 核时并发建议 4~8,这里按 cpu*2 并限制在 [4, 8]。
      */
-    private static final long RETRY_DELAY_MS = 50;
+    private static final int BATCH_PARALLELISM = Math.max(4,
+            Math.min(8, Runtime.getRuntime().availableProcessors() * 2));
+
+    private static final ExecutorService BATCH_EXECUTOR = new ThreadPoolExecutor(
+            BATCH_PARALLELISM,
+            BATCH_PARALLELISM,
+            60L,
+            TimeUnit.SECONDS,
+            // 有界队列 + CallerRuns:避免极端情况下无界堆积;队列满时回退到调用线程执行。
+            new LinkedBlockingQueue<>(2048),
+            new ThreadFactory() {
+                private final AtomicInteger idx = new AtomicInteger(1);
+
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread t = new Thread(r);
+                    t.setName("feature-v2-client-batch-" + idx.getAndIncrement());
+                    t.setDaemon(true);
+                    return t;
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
 
     /**
      * 批量获取特征数据
-     * 
+     *
      * @param protos 特征请求列表
      * @return 特征数据 Map,key 为 uniqueKey,value 为特征值 JSON 字符串
      */
@@ -59,104 +91,99 @@ public class FeatureV2Client {
         }
 
         long startTime = System.currentTimeMillis();
-        // 从第 0 次尝试开始
-        Map<String, String> result = multiGetFeatureWithRetry(protos, 0);
-        log.info("multiGetFeature: end, result.size={}, cost={}", result != null ? result.size() : 0, System.currentTimeMillis() - startTime);
-        return result;
+
+        // 小请求不拆包:保持原行为
+        if (protos.size() <= BATCH_SIZE) {
+            Map<String, String> result = multiGetFeatureWithRetry(protos, 0);
+            return result;
+        }
+
+        // 大请求拆包并发执行,最终合并结果;单个 batch 失败时返回部分结果
+        int total = protos.size();
+        int batchCount = (total + BATCH_SIZE - 1) / BATCH_SIZE;
+        List<CompletableFuture<Map<String, String>>> futures = new ArrayList<>(batchCount);
+        for (int start = 0; start < total; start += BATCH_SIZE) {
+            int end = Math.min(start + BATCH_SIZE, total);
+            List<FeatureKeyProto> batch = protos.subList(start, end);
+            futures.add(CompletableFuture.supplyAsync(() -> multiGetFeatureWithRetry(batch, 0), BATCH_EXECUTOR));
+        }
+
+        Map<String, String> merged = new HashMap<>();
+        int exceptionBatchCount = 0;
+        for (CompletableFuture<Map<String, String>> f : futures) {
+            try {
+                Map<String, String> part = f.join();
+                if (part != null && !part.isEmpty()) {
+                    merged.putAll(part);
+                }
+            } catch (Exception e) {
+                // 理论上 multiGetFeatureWithRetry 已吞掉异常并返回空;这里兜底,确保其它 batch 不受影响
+                exceptionBatchCount++;
+                log.error("multiGetFeature: batch future failed, ignored for partial result", e);
+            }
+        }
+
+        log.info("multiGetFeature: end, protos.size={}, batchSize={}, batchCount={}, parallelism={}, merged.size={}, exceptionBatchCount={}, cost={}",
+                protos.size(), BATCH_SIZE, batchCount, BATCH_PARALLELISM, merged.size(), exceptionBatchCount,
+                System.currentTimeMillis() - startTime);
+        return merged;
     }
-    
+
     /**
      * 带重试的特征获取方法
-     * 
+     *
      * @param protos 特征请求列表
      * @param attemptCount 当前重试次数(从 0 开始)
      * @return 特征数据 Map
      */
     private Map<String, String> multiGetFeatureWithRetry(List<FeatureKeyProto> protos, int attemptCount) {
-        log.warn("multiGetFeatureWithRetry: start, attempt={}, protos.size={}", attemptCount, protos.size());
-        
+
         MultiGetFeatureRequest request = MultiGetFeatureRequest.newBuilder()
                 .addAllFeatureKey(protos)
                 .build();
-        
+
         try {
             // 调用 gRPC 服务,设置 3 秒超时
             MultiGetFeatureResponse response = client
                     .withDeadlineAfter(3, TimeUnit.SECONDS)
                     .multiGetFeature(request);
-            
+
             // 响应为空或没有结果
             if (response == null || !response.hasResult()) {
                 log.info("multiGetFeature grpc error: response is null or has no result, attempt={}", attemptCount);
                 return Collections.emptyMap();
             }
-            
+
             // 业务错误码检查
             if (response.getResult().getCode() != 1) {
                 log.info("multiGetFeature grpc code={}, msg={}, attempt={}", response.getResult().getCode(),
                         response.getResult().getMessage(), attemptCount);
                 return Collections.emptyMap();
             }
-            
+
             // 特征数据为空
             if (response.getFeatureCount() == 0) {
                 log.info("multiGetFeature no feature, attempt={}", attemptCount);
                 return Collections.emptyMap();
             }
-            
+
             // 成功返回特征数据
             return response.getFeatureMap();
-            
+
         } catch (StatusRuntimeException e) {
             Status.Code code = e.getStatus().getCode();
             String description = e.getStatus().getDescription();
-            
+
             // 记录详细的错误信息(使用 error 级别确保一定会输出)
-            log.error("gRPC call failed: code={}, description={}, attempt={}/{}, protos.size={}, exception={}", 
-                    code, description, attemptCount + 1, MAX_RETRY_ATTEMPTS + 1, protos.size(), e.getClass().getName(), e);
-            
-            // 判断是否应该重试
-            if (shouldRetry(code) && attemptCount < MAX_RETRY_ATTEMPTS) {
-                log.warn("Retrying gRPC call after {}ms, attempt={}/{}, reason={}", RETRY_DELAY_MS, attemptCount + 1, MAX_RETRY_ATTEMPTS, code);
-                
-                // 等待一段时间后重试(给连接重建预留时间)
-                try {
-                    Thread.sleep(RETRY_DELAY_MS);
-                } catch (InterruptedException ie) {
-                    Thread.currentThread().interrupt();
-                    log.warn("Retry sleep interrupted", ie);
-                }
-                
-                // 递归调用,进行重试
-                return multiGetFeatureWithRetry(protos, attemptCount + 1);
-            }
-            
-            // 重试失败或不可重试的错误,降级返回空结果
-            log.error("gRPC call failed after {} attempts, returning empty result for graceful degradation. code={}", 
-                    attemptCount + 1, code);
+            log.error("gRPC call failed: code={}, description={}, protos.size={}, exception={}",
+                    code, description, protos.size(), e.getClass().getName(), e);
             return Collections.emptyMap();
         } catch (Exception e) {
             // 捕获所有其他异常(不应该发生,但为了安全起见)
-            log.error("multiGetFeatureWithRetry: unexpected exception, attempt={}/{}, protos.size={}, exception={}", 
-                    attemptCount + 1, MAX_RETRY_ATTEMPTS + 1, protos.size(), e.getClass().getName(), e);
+            log.error("multiGetFeatureWithRetry: unexpected exception, protos.size={}, exception={}",
+                    attemptCount + 1, protos.size(), e.getClass().getName(), e);
             return Collections.emptyMap();
         }
     }
-    
-    /**
-     * 判断错误是否应该重试
-     * 
-     * @param code gRPC 状态码
-     * @return true 表示应该重试,false 表示不应该重试
-     */
-    private boolean shouldRetry(Status.Code code) {
-        // UNAVAILABLE: 连接不可用(如网络断开、连接关闭)- 应该重试
-        // DEADLINE_EXCEEDED: 超时 - 应该重试
-        // RESOURCE_EXHAUSTED: 资源耗尽(如连接池满)- 应该重试
-        // CANCELLED: 请求被取消(如服务端处理失败、连接中断)- 应该重试
-        return code == Status.Code.UNAVAILABLE 
-                || code == Status.Code.DEADLINE_EXCEEDED 
-                || code == Status.Code.RESOURCE_EXHAUSTED
-                || code == Status.Code.CANCELLED;
-    }
+
 }

+ 15 - 0
recommend-feature-client/src/main/java/com/tzld/piaoquan/recommend/feature/domain/ad/base/AdRankItem.java

@@ -59,11 +59,26 @@ public class AdRankItem implements Comparable<AdRankItem> {
     private Long id;
     // 广告id
     private Long skuId;
+
+    private Long adSkuId;
+    private String adSkuCode;
+    private String adSkuName;
+
     //客户id
     private Long customerId;
+
+    // 代理商id
+    private Long agentId;
     //行业
     private String profession;
 
+    private String adProfessionName;
+
+    private String adCategoryId;
+    private String adCategoryName;
+
+    private Integer landingPageType;
+
     // 特征
     private Map<String, Object> ext = new HashMap<>();
 

+ 24 - 20
recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/ODPSToRedis.java

@@ -1,6 +1,10 @@
 package com.tzld.piaoquan.recommend.feature.produce;
 
 import com.alibaba.fastjson.JSONObject;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
 import com.tzld.piaoquan.recommend.feature.produce.model.DTSConfig;
 import com.tzld.piaoquan.recommend.feature.produce.service.CMDService;
 import com.tzld.piaoquan.recommend.feature.produce.service.DTSConfigService;
@@ -8,7 +12,6 @@ import com.tzld.piaoquan.recommend.feature.produce.service.ODPSService;
 import com.tzld.piaoquan.recommend.feature.produce.service.RedisService;
 import com.tzld.piaoquan.recommend.feature.produce.util.JSONUtils;
 import lombok.extern.slf4j.Slf4j;
-import com.squareup.okhttp.*;
 import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.spark.SparkConf;
@@ -17,6 +20,7 @@ import org.apache.spark.api.java.JavaSparkContext;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * @author dyp
@@ -32,16 +36,6 @@ public class ODPSToRedis {
 
         Map<String, String> argMap = cmdService.parse(args);
 
-//        argMap.put("project", "loghubods");
-//        argMap.put("table", "alg_video_source_feature_day");
-//        argMap.put("dt", "20250610");
-//        argMap.put("hh", "13");
-//        argMap.put("mi", "00");
-//        argMap.put("env", "test");
-//        argMap.put("odpsBatchSize", "300000");
-//        argMap.put("retry", "1");
-
-
         if (MapUtils.isEmpty(argMap)) {
             log.error("args is empty");
             return;
@@ -65,6 +59,8 @@ public class ODPSToRedis {
             return;
         }
 
+        DTSConfig dtsConfigV2 = dtsConfigService.getV2DTSConfig(argMap);
+
         long count = 0;
         int retry = NumberUtils.toInt(argMap.getOrDefault("retry", "50"), 50);
         while (count <= 0 && retry-- >= 0) {
@@ -110,24 +106,32 @@ public class ODPSToRedis {
             }
         }
 
-        log.info("odps count {}", count);
-        long odpsBatchSize = NumberUtils.toLong(argMap.getOrDefault("odpsBatchSize", "200000"));
-        argMap.put("partitionNum", String.valueOf(count / odpsBatchSize + 1));
-
         JavaRDD<Map<String, String>> fieldValues = odpsService.read(jsc, config, argMap);
         if (fieldValues == null) {
             log.info("odps empty");
             return;
         }
-        //log.info("odps count {}", fieldValues.count());
 
         // RDD
         log.info("sync redis");
         RedisService redisService = new RedisService(env);
-        int partitionNum = NumberUtils.toInt(argMap.get("partitionNum"), 10);
-        fieldValues.repartition(partitionNum).foreachPartition(iterator -> {
-            redisService.mSetV2(iterator, config);
-        });
+
+
+        log.info("odps count {}", count);
+
+        long odpsBatchSize = NumberUtils.toLong(argMap.getOrDefault("odpsBatchSize", "200000"));
+        int calcPartationNum = (int) (count / odpsBatchSize + 1);
+
+        int maxPartitionNum = NumberUtils.toInt(argMap.get("maxPartitionNum"), 30);
+        int finalPartationNum = Math.min(maxPartitionNum, calcPartationNum);
+
+        log.info("argMap.maxPartitionNum: {} calcPartationNum: {}, finalPartationNum: {}", maxPartitionNum, calcPartationNum, finalPartationNum);
+        log.info("config: {}, dtsConfigV2: {}", config, dtsConfigV2);
+        JavaRDD<Map<String, String>> repartitionedFieldValues = fieldValues.repartition(finalPartationNum);
+        repartitionedFieldValues.foreachPartition(iterator -> redisService.mSetV2(iterator, config, false));
+        if (Objects.nonNull(dtsConfigV2) && dtsConfigV2.selfCheck()) {
+            repartitionedFieldValues.foreachPartition(iterator -> redisService.mSetV2(iterator, dtsConfigV2, true));
+        }
 
         redisService.setMonitor(config, argMap);
 

+ 2 - 0
recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/model/DTSConfig.java

@@ -26,6 +26,8 @@ public class DTSConfig implements Serializable {
     public static class Redis implements Serializable {
         private String prefix;
         private List<String> key;
+        private List<String> value;
+        private String featureField;
         private long expire;
     }
 

+ 9 - 2
recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/service/DTSConfigService.java

@@ -32,14 +32,21 @@ public class DTSConfigService {
     }
 
     public DTSConfig getDTSConfig(Map<String, String> argMap) {
+        return this.getDTSConfig(argMap, "dts.config");
+    }
+
+    public DTSConfig getV2DTSConfig(Map<String, String> argMap) {
+        return this.getDTSConfig(argMap, "dts.config.v2");
+    }
 
+    private DTSConfig getDTSConfig(Map<String, String> argMap, String apolloConfigKey) {
         List<DTSConfig> dtsConfigs = JSONUtils.fromJson(
-                ConfigService.getAppConfig().getProperty("dts.config", ""),
+                ConfigService.getAppConfig().getProperty(apolloConfigKey, ""),
                 new TypeToken<List<DTSConfig>>() {
                 },
                 Collections.emptyList());
         if (CollectionUtils.isEmpty(dtsConfigs)) {
-            log.error("DTSConfig not config");
+            log.error("DTSConfig not config, apolloConfigKey: {}", apolloConfigKey);
             return null;
         }
         Optional<DTSConfig> optional = dtsConfigs.stream()

+ 1 - 0
recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/service/ODPSService.java

@@ -85,6 +85,7 @@ public class ODPSService {
         private List<String> cols;
 
         public RecordToMap(List<String> cols) {
+            log.info("RecordToMap: {}", cols);
             this.cols = cols;
         }
 

+ 34 - 13
recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/service/RedisService.java

@@ -1,8 +1,10 @@
 package com.tzld.piaoquan.recommend.feature.produce.service;
 
+import com.google.common.reflect.TypeToken;
 import com.tzld.piaoquan.recommend.feature.produce.model.DTSConfig;
 import com.tzld.piaoquan.recommend.feature.produce.util.CompressionUtil;
 import com.tzld.piaoquan.recommend.feature.produce.util.JSONUtils;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.MapUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -10,10 +12,8 @@ import redis.clients.jedis.Jedis;
 import redis.clients.jedis.Pipeline;
 
 import java.io.Serializable;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit;
  *
  * @author dyp
  */
+@Slf4j
 public class RedisService implements Serializable {
     private int port = 6379;
     private String password = "";
@@ -36,7 +37,8 @@ public class RedisService implements Serializable {
         }
     }
 
-    public void mSetV2(Iterator<Map<String, String>> dataIte, DTSConfig config) {
+    public void mSetV2(Iterator<Map<String, String>> dataIte, DTSConfig config, boolean isV3Optimize) {
+        log.info("mSetV2 param: {}, isV3Optimize: {}", config, isV3Optimize);
         Map<String, String> batch = new HashMap<>();
         Jedis jedis = new Jedis(hostName, port);
         jedis.auth(password);
@@ -44,15 +46,32 @@ public class RedisService implements Serializable {
         while (dataIte.hasNext()) {
             Map<String, String> record = dataIte.next();
             String redisKey = redisKey(record, config);
+
+            Map<String, String> valueMap = new HashMap<>();
+            if (StringUtils.isNotBlank(config.getRedis().getFeatureField())) {
+                record = JSONUtils.fromJson(record.get(config.getRedis().getFeatureField()), new TypeToken<Map<String, String>>() {
+                }, Collections.emptyMap());
+            }else if (CollectionUtils.isNotEmpty(config.getRedis().getValue())) {
+                for (String valueKey : config.getRedis().getValue()) {
+                    valueMap.put(valueKey, record.get(valueKey));
+                }
+                record = valueMap;
+            }
+
             String value = JSONUtils.toJson(record);
             batch.put(redisKey, value);
-            if (batch.size() >= 5000) {
-                mSet(jedis, batch, expire, TimeUnit.SECONDS);
+            if (batch.size() >= 1000) {
+                mSet(jedis, batch, expire, TimeUnit.SECONDS, isV3Optimize);
                 batch.clear();
+                try {
+                    TimeUnit.MILLISECONDS.sleep(10);
+                } catch (Exception ignore) {
+
+                }
             }
         }
         if (MapUtils.isNotEmpty(batch)) {
-            mSet(jedis, batch, expire, TimeUnit.SECONDS);
+            mSet(jedis, batch, expire, TimeUnit.SECONDS, isV3Optimize);
         }
         jedis.close();
     }
@@ -65,16 +84,18 @@ public class RedisService implements Serializable {
         jedis.setex(redisKey, expire, "");
     }
 
-    private void mSet(Jedis jedis, Map<String, String> batch, long expire, TimeUnit timeUnit) {
+    private void mSet(Jedis jedis, Map<String, String> batch, long expire, TimeUnit timeUnit, boolean isV3Optimize) {
         long expireSeconds = timeUnit.toSeconds(expire);
 
         Pipeline pipeline = jedis.pipelined();
         for (Map.Entry<String, String> e : batch.entrySet()) {
-            // pipeline.setex(e.getKey(), expireSeconds, e.getValue());
-            // 压缩后的key,升级后进行替换
             try {
-                pipeline.setex(e.getKey(), expireSeconds, CompressionUtil.snappyCompress(e.getValue()));
-            } catch (Exception ex) {
+                String key = e.getKey();
+                if (!isV3Optimize) {
+                    key = String.format("%s:v2", e.getKey());
+                }
+                pipeline.setex(key.getBytes(StandardCharsets.UTF_8), expireSeconds, CompressionUtil.snappyCompressV2(e.getValue()));
+            } catch (Exception ignore) {
             }
         }
         pipeline.sync();

+ 11 - 39
recommend-feature-produce/src/main/java/com/tzld/piaoquan/recommend/feature/produce/util/CompressionUtil.java

@@ -1,7 +1,8 @@
 package com.tzld.piaoquan.recommend.feature.produce.util;
 
-import com.google.common.collect.Lists;
-import net.jpountz.lz4.*;
+import net.jpountz.lz4.LZ4Compressor;
+import net.jpountz.lz4.LZ4Factory;
+import net.jpountz.lz4.LZ4SafeDecompressor;
 import org.xerial.snappy.Snappy;
 
 import java.io.ByteArrayOutputStream;
@@ -65,6 +66,11 @@ public class CompressionUtil {
         return Base64.getEncoder().encodeToString(compressedBytes);
     }
 
+    public static byte[] snappyCompressV2(String input) throws IOException{
+        byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
+        return Snappy.compress(inputBytes);
+    }
+
     // 将Snappy压缩后的String解压缩回String
     public static String snappyDecompress(String compressedInput) throws IOException {
         byte[] compressedBytes = Base64.getDecoder().decode(compressedInput);
@@ -72,43 +78,9 @@ public class CompressionUtil {
         return new String(decompressedBytes, StandardCharsets.UTF_8);
     }
 
-    public static void main(String[] args) {
-        List<String> lz4Strs = Lists.newArrayList(
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuNjY1MjY4XCIsGAAUNxgAYzc3NTg0MhgAFnYwAGUwMTgyMDkYAAUwAGEwMjM3OTMYADdzdHIwAFUyNzM3MRgABjAAUTMwNjY3GAAldm8wAHI4LjI5NDk1SAAAGAADMAB0Ny45NDk0MBgAB5AAVjE1MTA0GAADMACBMC4xODkxMzZgAAHwABU02QBEMjM1MtkARXNfMzAZAEIzOTMxkgAB8gAFMgBSMDI0OTHDAAAZAAYyAGEwMjUyMDRkAAH0AAYyAEIzNDQzMgAAGQAHMgBCMzQwORkARHZpZXcnAXEzNDgxOC4wSgABGAADxwBkMzU1NTMyGQAAJwEDYwBjOC4zNzE3YwAAKAEEYwByOC4zMTc4NpUAASkBAzIAcjAuMjA4NTX5AAAZAAQyAAAZADM5NjQrAURfY250lgBTMi43MjF0AQIZAASXAFMuMTgyNDYCVHNoYXJlMwAzOTUzrgACFwADMQBEMTA5MBkAAeAABJYAYzg4NDA5MDMAARoABJcAZDE1NTAwMfwAAn8ABDYAcjMuMjI3NDP2AQIbAAQ3AGMzLjI1MzERAlVyX3JhdJwAcjAuMDA2NjkCAQMbAAOgAAAbADM3NDcEAVRldHVybtIAMzYzNJ8AAxgAAzMAQzg0NTkZAALrAAWdADQwNDTSAAIaAAScAEk1Mjg00QAZNIgDMzI0OH4CAGwBGTSMAyQzNowDA9MAA0ABAbkAIzcxkAMCHAAEbwACHAATNNwDAr0ABKUASzIyMDLYAAQ3AFMzOTA2N9oAAVwBAKYABW8AYTczMjA4M6kCBB0ABDkAIDAulAETMWABAnAAAdMBA2wEJTUyMgEDSwEBHgADUAFLNjcyNJIAAR8ABLIAVDE4NDM3cgEIQAAEfwBUMzI0OTVCAAE7ADNoYXMpAAOCACkyM4UCBiAAA4QANDI2NYYDAroBBiIAA0IAJDk1GwEMIQADQwA1MTA5MQIMhQADRQElNjh7AQsiAATLAEQxMjAw6wAMaAAEEAFEMzAwOO4ADCQABEgAMjUyNdEA8Al9IiwidmlkZW9fc291cmNlIjoiQUdDIn0=",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuODgzMTE0XCIsGAAUNxgAYzk5Nzg4MhgAFnYwAFUwNDY1NRcABS8AUjA1NDM1RwA3c3RyLwBCNTI3MUcAABgACDAAMTQ2OUgAJXZvMACFNy43MTk5MzYYAAMwAHI3Ljc4ODE2eAAYdo8ARzM1OTMYAAMwAHIwLjQyMzMxMAAB7wASNNgAgTEuMDIwODAxYQAAGQAlMzAZAFMxNjM5MxkAFXYyAADbACI1MJIAABgABDEAABgAIzg2MQAA2wAEYwAAGQBBMzkzOEoAABkACDIAQTc3NDcZAER2aWV3JgGWNzA0NzE1MC4wGgADyACEMzY3ODk3OTMbAAAqAQNnAHI3Ljk0MjM2cwEAQwEEZwBkNy44ODY1WwEIyQBTNDM3MzB/AAnKAFI0NjI4ODIAVHJfY250mQBjMy41NDUwjwECGgAEmQBiLjY2NzA5FwFVc2hhcmU0AFM3MTQ2NrEAAhoAAzQAczIwMDM5MDUbAAIBAQPNAIg1NTU3ODUzNxwABdAAczYwNTk3MzgcAAKHAARtAWQzLjYzNTmoAgG8AAQ4AGMzLjUxMDgDAkVfcmF0owCBMC4wMTMxMzFvAQMbAAOkAAAbADI0ODKxAmVyZXR1cm7ZAEQyODA0ogADGwADNgBkMTk5OTY2wQEC9QAEogAQMkkDIzAxvgACHAAEowBzNDM5MjE4ORwAAdoAGTSbAzIwOTiwAQQcAANxAACfAzo1NzDdAANMAQDDADM1MTTKAgLeAARxAAAcADI2NzGcAgPEAASpAGUzMDYwMTWNAAL8AAQ5AGQ0NDY0MTniAASOAAQ7AABWAzQ2OTRRAQLHAAQ7AAAdACQwMG4BAlcAAv0BAnoDdDI1MzI1MDIFAQchAAPtAHQxNTU3MzY4BQMDtwACQwAD9AB0MjQzMDQ4OXcCCCMABIcAeTM1MjA3MjN/AjNoYXMrAAMPBSs5MoUCBiIAA40AVDU0NTI5jAICzgEGJAADRgBUMzcyMzavAAwkAANIAGQyMDA4MjixAAHNAAdIAAPXAFQ4NDE2MxoBCyQABNgAajEyNzE1NGACDkoAZDI5Nzk4N90BDJQABEwAYTQzNzY1MiMB8AB9IiwidmlkZW9fc291cmM6BuBVR0MtYXV0b3BpY2sifQ==",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuNzI1OTA4XCIsGAAUNxgAYzk4ODA5NRgAFnYwAGUwMzIxNzMYAAUwAEIwNDU2FwA3c3RyLwBRNDQzMjEvAAAYAAcvACI2MXYAJXZvLwCBOC41ODM0NDQvAAAYAAMvAHQ3LjczNjE3GAAHjgBWMjc2MTUYAAMwAHIwLjM1MzAwjwAB7gAWNNcAQTE3ODdhAAAZAC8zMBkAAhd2MgBSMDQ1MTnCAAAZAAYyAAcZAADbAAZkADMwNDZ8AAAYAAgxAAQYAER2aWV3JAGBNDY4OTY3LjCUAAEZAAPGAHQzMjM1ODg0GgAAJwEDYwAAEAFCNDIyOTMAFm9kAAsZAAjHAEMzNDk1iQEAGQAGlgAHGQBUcl9jbnSYAHIzLjIxOTgwXQECGgADmQBiNC4yMTM3HAJUc2hhcmUzAFMyMDc4NbAAAhkAAzIAYzE0OTQzMxoAAeQABC4BeDM3NTc1MjkbAASbAAobAAKCAAQ3ACA0LrcCITQyAgECGwAENwALGwA1cmF0oAB0MC4wMDk5OTYAARsAA6IAYzAuMDEwOFMBZHJldHVybtYAUzE1MDg4oQADGgADNQBUMTQ3NjWhAQLxAAShAFQxNzI5NTYAAhsABKEACRsAAbwAGTSQAzM0MDmhAgCPARk0lAM5NDg3vgAEbwABvwAjNjGnAgLaAARwAAocAAPbAANKAlQxNjk4MKcAA90ABDgAChwAA4wABnEAQjk3ODAEAgTEAAQ5AAkcAAJwAAL0AQJsA1QxMjk1MPECA3QAASAABFYBXzE0MjI3IQABBH0AZDEzMTMzM3kBCEMABIMACiIAAV4AM2hhcyoAA/wEQzQ2ODbXAQshAAOJAFMzNTA0MSIAAsMBDUQARDIwODOqAAwjAANGAGMxNDk5MjJHAAtpAATRAF8zOTg3MCMAAwTSAAgjAAxqAARHABAxJwUETAMMJQAESQAGJQDwAH0iLCJ2aWRlb19zb3VyYx4G4FVHQy1oYW5kcGljayJ9",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuOTA4NTM1XCIsGAAUNxgAYzg5Mzk4OBgAFnYwAGUwMzk5NjcYAAUwAGEwNDA5NjkYADdzdHIwAFU0Mzk5MRgABzAAMjU4MkgAJXZvMABjNy43MTcyMAAAGAADMAByNy43NDAwMUgAGHaQAFEzMDg0NF8AABcAAy8AcjAuMzE3MDlfAAHvABQ02ABUOTAzNjPYAERzXzMwGQBSODUyNDXBAAHxAAUyAEUwNDE3MgAXdjIAYTA0MDU4NnwAAfMABzIAMjYxOEsAABkABmQAQzA0NzbEADRpZXclAaExMTExNTI0Ni4wTAABGwADyQCUMTAxNTU4NjQyHAAAKwEDaAByNy45MzU5OYEAACwBBGgAgjcuODMzOTMzTgAALQEDMgBmMC4zMzEyGAAEMQABLwEjOTQGAkRfY250mwCBMy44MTMyMjJLAAIaAAOaAHIzLjk2NDM3TQBUc2hhcmU0AFQ0ODg5N7EAAhoAAzQAdDQ2NTQxMDHMAADoAASbADAxODFBAhk3BQEEoACTMzg4MTk0NTY1OgACiQAEOwBjNC4wMzAx7wACGwAFOQBjLjg3NjM0CAJFX3JhdKYAcjAuMDEwNDiOAQMbAAOnAAEbACIzM1YCZXJldHVybtwAVDQ0MjQ4hwACGwADNgBzNDE2MDcwORwAAvgABKMAVTgzNzkw4QECHAAEpAAwMTg03gATNDkAAcEAGTShAzM1MjeMAACBARk0pQMjMDlFAwPDAARyAALEABI1HQIDHAAEcgABHAAEFQECxAAENwBrNzU3MTYy4QAEOAB9MTU3NTUwOaoAA8MBZTAuOTAzN3MAAqsABDwAAGADMjA4OS8CA1QBAekCA4kEZDM0MjgzORsCCCEAA18BdTMyMjAzOTMDAgK4AAJDAAOBAIQ2MDA4ODQwN0oBByMABIcAkzEyMzQyNDM0MyQAAUAAM2hhc3EAA40AVDExNjUwwgILIwADjwBkMTA0OTUyjgAC0gENSABUNDkwNzNrAAwkAANJAH80NjcwMDkwkAAEA9oAZDE4Nzg3NXkBC5IABNwAZDQwNjQ0MbwBDG8ABOIBZDgzNzc5NWcDDCYABEwAcTE4NTE1OTaXAPAAfSIsInZpZGVvX3NvdXJjRQbgb2xkVmlkZW9QdXNoIn0=",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuOTIyMzIzXCIsGAAUNxgAYzg2MTY1NxgAFnYwAGUwNTAwNTgYAAUwAGEwNDcwNTIYADhzdHIwAEU0Mjc0GAAGMABRNTQ2MDYYACV2bzAAdTcuNjU4NjkXAAMvAHQ3LjgyNjU1GAAHjwBSMzgzMzh3AAAYAAMwAIEwLjM2ODI1NUgAAe8AFDTYAFI5MzAwMUkAABkAJDMwGQBSODk2NjWpAAHxAAUyAFYwNTE0MfIABjIAYTA0ODg5MWQAAfMABzIAMjUyODwBABkABzIAQzU0NTL1ADRpZXcmAaEzNTUxMjA3MC4wTQABGwADygCUMjQyMzQ3MTg5HAAALAEDaQB0Ny42NzU4NP4ABmkAdDcuNzU3MzkZABV2MgByMC4zOTQ2N5sAABkABpsAUjM3OTI2mwBVcl9jbnScAGMuODQwOTkAAQEaAAObAGMzLjYxODlNAFRzaGFyZTQAczE5MjczOTSzAAIbAAM1AHkxMzIzMzY56wAE0ACTNTgwOTY2MzE1OQABCAEEogCjMTA3OTkyMzk5Nx4AA6YAAzwAczMuODY3NTnJAgGnAAQ6AHMzLjcxNjUwDQJFX3JhdKkAdDAuMDEzMDM2AAEbAAOpAAIbABIw3wFlcmV0dXJu3wBUNzc3NjeSAQMcAAM3AIMxMTQwMjkwMaUAAvwABOEAejMyMTE5OTUdAASnAIM1ODg4MzgwNDoAAd8AGTSpAzcwMDShARg0rQNDNTk1MqsAA+EAAxgBAccAKTI5/QAEcgABHAAUMSYDA+MAAzgAdTI5ODcyMTeOAAIeAAQ6AIw1Mjc5ODQyNqwAAz0AdzAuOTY3Njg0Ahg0agNDOTE0OTgDAz0BAeoBA5MEhDEzNjE0NjkyXAACegABIgADZAF/ODkyNDU0OCIAAQRqAZ8yMjkyOTQwNzNGAAAExQCENDA5NTc4MDbJAQFjADNoYXOUAAOPAFQ0NjI4MXQCCyMAA5AAZDMxNTA4NmsAAvMBDUgAXzE5MzIxJQAGA0oAdDEzMjY2NTdYAQtvAATdAGQ3NzIzNzFGAgslAATeAH8xNDIwNjQ3lgAGA6sBdDMwODY5NjKQAwy9AAROAHE1NzcwMzc1JwDwAH0iLCJ2aWRlb19zb3VyY1UGsHRyYW5zcG9ydCJ9",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuNDkzMDIzXCIsGAAUNxgAYzc5NTI3ORgAFnYwAGUwMjI2NzQYAAUwAGEwMzIyODUYADdzdHIwAEI0NTk4SAAAGAAGMABRNDA1OTYwACV2bzAAcjguOTI5MjRIAAAYAAMwAIM3Ljk2MDgyNzAAB5AARDIwMjRHAAePAFQyNTcwMacARXNfMTTYAEY4MjAzGQAkMzAZAFQ2NzI2NwkBF3YyAFQwMzEzOTIAF3YyAFIwMjc1NqsAANsABmQAUjA0MDE0MgAAGQAHMgAzNDA5VQFEdmlldyYBYjkzNTAuMNwAABcAA/UAZDI1NDYwORkAACUBA8YAcjcuODc4NjBiAAAmAQRiAHI4LjI1NTkwxgAAEAEGlABSMjQ3MzOIAQAZAAQyAHIwLjIyNzU3WQFUcl9jbnSVAHIyLjcxNzk0MwACGgADmACBNC4yMzA1NzGxAFRzaGFyZTQAJDQzyAACFwADMQBUMTAzMzbIAADhAASXAGgzNDEzNzMaAASYAHMxMDI5MzM1GwACfwAENgByNC4xOTk0NYAAAhsABDcAdDMuNDQ1NTPPADVyYXScAIEwLjAwODM0MrYAAxsAA6AAABsAMzc2M1EAVGV0dXJu0gA0MjEyhAACGAADMwA6ODIy6wAEnQBTMTM3MDQzAAIFAQScAFM0MjE4MRoAAbYAHzSHAwQAhgEZNIsDMjMwNe0BA7gABG0AAbkAIjQ3AAMDHAAEbwAAHAASOO0AAroABDUAVDEwNzE3iAACGwAENABEMjgzN70ABaMAA3UCdDAuNzgwMTVsAAKkAAQ5AAA/AzMxMjKlAAJVAAjiAjQxODnnAQgeAANNAVo2NTQzOJIAAj0AA3sANjg0NAsCBz8ABH4AVDIzNDI1YAABOgAzaGFzaAAD6QQUN2AACx8AA4IANDE5NEAAAtECDUEADNsCBiEABEMANDAzNrACC2UABHoBNDI1NU4CCyIABMoAJTgy6QANiAADDQEAMgMEvwEMJAAERwBBNDIyN2oA8AB9IiwidmlkZW9fc291cmMBBpB1bmtub3duIn0=",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuNTk3NDY0XCIsGAAUNxgAVDc0OTM1GAAWdjAAYzAyMTkxMjAAFnYwAGEwMzMxODkYADdzdHIwAFUzNjY3NRgABjAAMjQ0Mi8AJXZvLwBxOC4yNTY3OC4AABcAAy4AZTguMTAzMBgAB44AYzE4MDkyMTAAB44AUjI2ODkzGAAB7gAUNNcAYTg0MDU4NjEAABkAJDMwGQBSNzU3OTSoAADYAAYyAGMwMzg1MzcyABd2MgBSMDMzNTbDAAHyAAYyADM0NThLAAAZAAcyAAb0ADRpZXckAZEyMzg0NDI4LjBkAAEaAAP4AIQyMzY3MDUyNRsAACkBA2YAcjcuOTIyMzfKAAArAQRmAHI3Ljk0ODg0/AAAFAEGygBiMzA1MzAzZgAJygBSMjY2ODNLAFVyX2NudJoAYi43NzA1NJABAhoAA5oAcjMuMjUyMjAgAlRzaGFyZTQARDg3NDTMAAIZAAMzAGUxMDQ4MzbMAADnAASbAIQ1OTA0OTI2MegAABwABNAAkzEyMjM0Nzc1Mh0AAocABDoAczMuNDYxMTi4AQG8AAQ5AHMzLjMzMjc1ugFFX3JhdKQAcjAuMDA3OTC8AQMbAAOmAHMwLjAxMDIwUQBUZXR1cm7aAFQ1MjI0N4YAAhoAAzUAYzc4NTU5NhsAAvUABKEAeTI3MDcxMjQcAASiAGQ1NDE4NzUSAQHZABk0mgMoNjKWARk0ngM0NzcymAEC2wADGQIAwQAyMTEzmgEDHAAEcQAAHAAyMDA3AwIDwwAFqQBjMjc1NTcwqQADHQAEOQAgNDHGAAUdAABoAQCqAAVzAFc4NTg4MkECGTRaA0M2NDg3bAECdAAB5AIDggRVNDMxMzndAQcgAANcAWQ2MzY1NzRdAQOYAAJBAAN/AHQxODAyNzkyPgIIIwAEwACDMzI2NDY4NDPeAAE/ADNoYXMrAAOKAEQxODg1ugILIgADjABFMjQxNSMAAugBDUYANTg3NswDDCMAA0cAZDEwNTA2NBQCC2sABJABAPYAHzUkAAQE1wBzMTIzMjM0OdYAAocDB5EAAyABTzI2NDmUAAcETABhNTM2OTYziQHwAH0iLCJ2aWRlb19zb3VyYzYGwHVzZXJ1cGxvYWQifQ==",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuMzk3OTczXCIsGAAUNxgAYzQ0NTY4NRgAFnYwAGUwMjA5NDgYAAUwAGEwMTk2NzEYADdzdHIwAFU1MjYzNxgABjAAQjQ0MTNIACV2bzAAcjguNDUxODlIAAAYAAMwACA4LhcAITU2SAAYdpAAUjE3NzA1wAAAGAADMABiMC4xNjc1XwAB7wAVNNgAMzcyN2AAABkAJDMwGQBSODYzMjlhAAHxAAUyAGEwMTgzNDl6AAAZAAYyAGEwMzYwNjQZAAHzAAYyAEIzODgwMgAAGQAHMgBCNDE3N1UBRHZpZXcmAYE5NTgwMjUuMEsAARkAA8cAdDQ0OTE4ODcaAAAoAQNlAHI4LjY5MjgyMgAWb2QAdjguMDQyOTARAQQxAHQwLjE1OTUwWgEIyABDMjkwMPkARF9jbnSWAHMyLjUwMDQ5RQEBGgADlwAgMi4sAQOOAVRzaGFyZTQAUzUwNDI4rwACGQADMwBjMTk4MjYxGgAB4wAEmQB4ODg1NzkwORsABDEBgzM1Nzk5ODc2HAAChAAEOAAQMgIBIjYz6gACGwAFOABiLjkxNDExmwFVcl9yYXShAHQwLjAwODM31QABGwADowAAGwAjNzTEAmRyZXR1cm7XAEQyMDA2ogADGgADNQBTODgzNjKgAALxAASgAFQzNDM3NrsBAhsABKAAaTE0OTU1M9cAGTSTAzI2ODEzAwHYABk0lwMyNDQ1WwIDvgAEcAAAvwAqNjj1AARxAAAcACQ5MhwAA9sAAxkCYzE2MjUzMMMAA90ABDgAZTEyOTEwOR0AAIABAKkAAzoAcjAuNDc3MTDQAgSqAAQ7AABRAzMyMDSfAQJzAAL4AQJxA1QxNjk2MhECA3cAASAAA1kBVDc1Mjc1WgEIIAAE8ABzMTQxMjg0NDYDCCIABIMAdDEwMzg0MTFlAAFeADNoYXMrAAMCBTQ4MDKJAQshAAOJAEQzMzU2AgECxwENRABENTA1N78DDCMAA0YAVDE5ODgwGgMLaQAE0gA6NjAzIwMGaQAE0wBfMzI5ODVrAAUESABUMzQwNjWvAgyQAARKAGExNDk3NzAmAPAAfSIsInZpZGVvX3NvdXJjJgbg5YWo6Z2ic3BpZGVyIn0=",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuODA2MjEzXCIsGAAVNxgARDY2NDAYABZ2MABjMDUwNTA2MAAWdjAAYTA1Mzg1MhgAN3N0cjAAQjYyNjQwAAAYAAYwAEI2MjE1GAAldm8wAIE3LjQzNjY4NEgAABgAAzAAUzcuMjc4jwAYdo8AVDM3NTU5RwAHjwBhMzkxOTM5RwAB7wAWNNgARTU3NDEZACQzMBkAVzk3NzY18QAFMgBSMDU0NjHaAADyAAYyADMwNTgKAQDaAAcxAEI2MzA4YwAAGQAIMQBBOTM4N3wARHZpZXclAZYyNDczMDc5LjAaAAP3AHUxMzMxMDA2GwAAKQEDZwBUNy40NjC6AQAqAQRnAABDATYwNjgUAQaZAFQ0MDc0MVwBCMoAUjQzMTQymQBUcl9jbnSaAGgzLjg5MTkZAAOZAIEzLjg2MzI4OLIAVHNoYXJlMwBjMTU0OTI4zAACGgADNABjODI3MzA0GgACAAEDzACIMjAwNzM1ODUcAATPAHQzNTYwMTU3HAADnwADOQByMy44ODM3M9MAAhsABDgAVDQuMjA0YgJVcl9yYXSjAHQwLjAxMjk31wABGwADpAAAGwAjMzlQAFVldHVybtgARTI0OTCGAAIbAAM1AEU3MTY3vwEC8wAEdAFzMTI2NjI1NtkAAhwABKIAIDIxdQEE3AEBvgAZNJkDIjM3YAEEGwADbwBlMC44NjQxHAABvwAEcAAAwABBNDA2MpgBAxwABHAAABwAEzOYAgPBAAQ3AHMxMDk2MjUwpwADHQAEOAB0MjA2NzAzMh0AACACAKgAA4MBAI4ANDM1NU0BAh0ABDsAAFYDOTQ0NlABAvoBAnYDRTkyODg6AQN3AAEgAAPrAGU1MjE2NzMjAQK1AAEhAAS6AG84MTc4MjUiAAEEhAB0MTUzNTk0NoQBAT4AM2hhc24AAwgFUzMyMDkz4gALIgADiwBUMTg1NTNFAALJAQ1GAGMxNTUzOTdHAAwkAANIAFk4Mjk0MwYDB0cAA1QBVDI4MjI2SAALjwAE1wBUNDkxNjZIAAxsAAQeAVUxMjY5NB4BDCYABEsAYTIxMjEyMP4B8AB9IiwidmlkZW9fc291cmMxBvAI5YWo6Z2ic3BpZGVyLWF1dG9waWNrIn0=",
-                "8hZ7ImZlYXR1cmUiOiJ7XCJyb3NfMWRcIjpcIjAuODk5NTQ3XCIsGAAVNxgAUzM3MTIzGAAWdjAAZTA0MzgyNhgABTAAYTA0NzE5MRgAOHN0cjAANTg3MhcABi8AQjU2MzdfACV2by8AgTcuNzYzMzE1MAAAGAADMAB0Ny44NTk3MzAAB48AUjM0MDIzjwAAGAADMACBMC4zNzA5MDhIAAHvABQ02ABlOTAzNTY5GQAkMzAZAFQ4NTE5MAkBF3YyADUwNTExABd2MQBXMDQ2OTfyAAVjAEMwNTcySgAACwEHMgA0NTUxrQA0aWV3JQGhMTk0MTkxMjYuMJcAARsAA8kAlDE3NzU2ODA2MxwAACwBA2kAcjcuNTAyNDflAAAtAQRpAHY3LjYwNzA3FgEEMgB0MC4zODc4MF8BCM0AUjM1NzMxYAFUcl9jbnScAGkzLjY0NjgaAAObAGMzLjU3MjSsAVRzaGFyZTQAVDk0NjEwsgACGgADNACEMTAwMDk5ODnOAADqAASdAJg0NDUxOTE3NTEdAAXTAIM2MzA4MTY0MB0AA6QAA9gAgTMuODI4MjY0PwECGwAEOQBjMy42NTc5ggJVcl9yYXSnAHIwLjAxMjAxQAEDGwADqAAAGwAiMzLcAWRyZXR1cm7cAGQ4NTEwNjSGAAIbAAM1AHM4Mzc5NTkyHAAC+AAE3QBrMjU0Njc5HQAEpAB0NDIwNzM2MDoAAcEAGTSjAzc3MzDdABk0pwM5NTM0wwAEcwABxAAiNTB8AwPfAARyAAAcADoyODThAANNASAyMxcBFDmHAQP/AAQ6AIMzNTg0Mjc5OOYABawAAz0AczAuOTYyNTdgAwBJAhk0ZANDODM5M64AAjwBAe0CA40EETa3AgVbAAchAANiAYQ2NTg2MTM1N30AByIABPYAjzE3MjY0NzcwRgABBMQAdTI3MjY1ODlxAQGEADNoYXPrAAOOAFQyMzMzN5QBCyMAA5AAZDIzNDU2M2sAAgQDDUgAXzk0ODQ2JAAFA0kAdDEwMDMxMjbeAgtuAATcAGQ2MDExMTB9AgslAATdAGo5Nzk4NTR+AweUAAOoAYMyMzkwNjcwNUoBDLsABE0AcTQwNTQ4OTlxAfAAfSIsInZpZGVvX3NvdXJjTAbg5Z6C55u0c3BpZGVyIn0="
-        );
-
-        List<String> snappyStrs = Lists.newArrayList(
-                "mQyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjY2NTI2OFwiLAkYADcRGBg3NzU4NDJcCRgAdhkwFDAxODIwORUYFTAUMDIzNzkzBRgIc3RyHTAQMjczNzEVGBkwEDMwNjY3BRgEdm8VMBg4LjI5NDk1CUgBGA0wGDcuOTQ5NDARGB2QEDE1MTA0GRgNMBwwLjE4OTEzNgVgBfAANBXZDDIzNTIR2QxzXzMwFRkMMzkzMQmSAHIBYhUyEDAyNDkxCcMBGRkyFDAyNTIwNAVkBfQZMgwzNDQzCTIBGR0yDDM0MDkJGQx2aWV3MScYMzQ4MTguMAVKBRgNxxQzNTU1MzIRGSEnDWMUOC4zNzE3DWMBGRFjGDguMzE3ODYJlQB2MscAEDIwODU1CfkBGREyGDAuMjA5NjQtKwxfY250EZYQMi43MjEtdAkZEZcQLjE4MjRNNhBzaGFyZREzCDk1Mw2uCRcNMQwxMDkwERkF4BXIEDg0MDkwDTMFGhGXFDE1NTAwMRH8CX8RNhgzLjIyNzQzKfYJGxE3FDMuMjUzMU0REHJfcmF0FZwYMC4wMDY2OSkCDRsNoAEbCDc0Ny0EEGV0dXJuEdIINjM0DZ8NGA0zDDg0NTkNGQnrFZ0IMDQ0EdIJGhGcDDUyODQy0QAANDKIAwgyNDhNfiGFBDRzEWoUMC43NzM2cYwAXwnTTQgFuQQ3MW2QCRwRbwkcADRt3A3VDTgMMjIwMjrYABE3EDM5MDY3DdoRihE3GDAuNzMyMDg9eQA0MkIDDDQzMzEtYAlwJdONbAQ1MjUyLh4ALVAMNjcyNDqSAAk9BDRkhaoQMTg0Mzcxci5AABW4DDI0OTURQgU7CGhhcw0pDYIEMjMyhQIZIAA3CWQIMjY1cYYpuhkiDUIEOTUxGz4hAA1DCDEwOVUxPoUADcoENjg1ezoiABHLDDEyMDAR6z5oABFGEDMwMDg0Lac+JAARSAg1MjUJ0Vx9IiwidmlkZW9fc291cmNlIjoiQUdDIn0=",
-                "1AyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjg4MzExNFwiLAkYBDdkDRgYOTk3ODgyXAkYAHYZMBAwNDY1NRUXFS8UMDU0MzU0BRgIc3RyHS8MNTI3MQlHARguMAAINDY5BTAEdm8VMBw3LjcxOTkzNhUYDTAYNy43ODgxNgl4AHYujwAMMzU5Mx0YDTAYMC40MjMzMQkwBe8ENGQF2BwxLjAyMDgwMQVhARkEMzAVGRAxNjM5Mw0ZBHZfETIUMC4wNTUwCZIIcm92FTEBGAQ4Ng0xAdsuMQAMMzkzOAVKARkRYwEyDDc3NDcFGQx2aWV3MSYgNzA0NzE1MC4wGRoNyBwzNjc4OTc5MxEbISoNyhw3Ljk0MjM2NAk0BG9yFWcUNy44ODY1MVsuyQAQNDM3MzANfzLKABA0NjI4OAkyFHJfY250X00eFDMuNTQ1MC2PCRoRmRQuNjY3MDkpFxBzaGFyZRU0EDcxNDY2DbEJGg00GDIwMDM5MDUNGykBDc0cNTU1Nzg1MzcuHAAxNxw3NjA1OTczOA0cCYcRORQzLjYzNTlRqAW8ETgUMy41MTA4TQMMX3JhdBWjGDAuMDEzMTNNBQkbDaQBGwg0ODJJsRRyZXR1cm4V2QwyODA0EaINGw02FDE5OTk2NjHBCfURogAyYUkEMDENvgkcEaMYNDM5MjE4OQ0cBb8ANDKbAwgwOTgpsBEcDXEYMC45OTU3MDbdAC1MAcMINTE0TcoJ3hFxARwINjcxSZwNxBE4FDMwNjAxNRWNCfwRORQ0NDY0MTkR4hGOETthVgg2OTQxUQRjbgHHETsBHQQwMDFuCVcp/QBkZZMcMjUzMjUwMi5J/i4hAA3tGDE1NTczNjhxBS4iABGBGDI0MzA0ODlRdy4jABGHGDM1MjA3MjMyfwIIaGFzDSsNjAQ5MjqFAhkiDY0UNTQ1Mjk4Da0pzhkkDUYQMzcyMzYRrz4kAA1IFDIwMDgyOBGxJRAdSC3LEDg0MTYzMRo6JAAR2BQxMjcxNTQ2YAJGSgAUMjk3OTg3Md0+lAARTBQ0Mzc2NTIlIzh9IiwidmlkZW9fc291cmPBOjRVR0MtYXV0b3BpY2sifQ==",
-                "uAyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjcyNTkwOFwiLAkYADcRGBg5ODgwOTVcCRgAdhkwFDAzMjE3MxUYFTAMMDQ1NgkXCHN0ch0vEDQ0MzIxBS8BGB0vBDYxCXYEdm8VLxw4LjU4MzQ0NAUvARgNLxg3LjczNjE3ERgdjhAyNzYxNRkYDTAYMC4zNTMwMAmPBe4ENGQN7xQ5ODE3ODcFYQEZBDMwUhkABHZfGTIQMDQ1MTkJwghyb3YdMh0ZAdsuMgAANg18ARgZYwAwFRgMdmlldzEkHDQ2ODk2Ny4wBZQFGQA3CZQYMzIzNTg4NBEaIScANAkaIRAMNDIyOQkzBG9yFWQ6GQAuxwAMMzQ5NS2JAHYyxwAdGRByX2NudBGYHDMuMjE5ODA0BWUJGg2ZFDQuMjEzN0kcEHNoYXJlETMQMjA3ODUNsAkZDTIUMTQ5NDMzDRoF5BHLGDM3NTc1MjkuGwAxMTYbAAByCZwtAgQ0LkG3BDQyHZ0RNzobAAhyYXQVoBgwLjAwOTk5ETYFGw2iFDAuMDEwOC1TFHJldHVybhHWEDE1MDg4DaENGg01EDE0NzY1MaEJ8RGhEDE3Mjk1ETYJGxGhMhsABdcANDKQAwg0MDlNoQHzBDRzEW4YMC45ODQ4NzK+ABFvBb8ENjFNpwnaEXA2HAAN2y1IEDE2OTgwEacNHBE4NhwADYwRORQwLjk3ODBJBBEcETkyHAAJcCn0AGRlhRAxMjk1MFHxLiAAMVYQMTQyMjdOIQAAMRG2FDMxMzMzNDaUAQVjEYM2IgAFGwhoYXMNbQ2IDDQ2ODYNQzohAA2JEDM1MDQxDSIpw0JEABAyMDgzN1ojAA1GFDE0OTkyMg0kOmkAEdEQMzk4NzBWIwAR0i4jAEKNADEYoSdxTD4lABFJGSU4fSIsInZpZGVvX3NvdXJjwR40VUdDLWhhbmRwaWNrIn0=",
-                "3wyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjkwODUzNVwiLAkYADcRGBg4OTM5ODhcCRgAdhkwFDAzOTk2NxUYFTAUMDQwOTY5BRgIc3RyHTAQNDM5OTEVGB0wCDU4MglIBHZvFTAUNy43MTcyDTABGA0wGDcuNzQwMDEJSAB2LpAAEDMwODQ0BV8BFw0vGDAuMzE3MDkJXwXvBDRkFfAIMzYzEdgMc18zMBEZEDg1MjQ1CcEAcgFhFTIMMDQxNxUyAHYdMhQwNDA1ODYFfAXzHTIINjE4CUsBGRlkDDA0NzYNxAhpZXcxJSQxMTExNTI0Ni4wBUwFGw3JIDEwMTU1ODY0MhEcISsNaBg3LjkzNTk5CYEBGRFoHDcuODMzOTMzCU4yzAAMMzMxMhkYETElLwQ5NE0GDF9jbnQRmxwzLjgxMzIyMgVLCRoNmhgzLjk2NDM3CU0Qc2hhcmURNBA0ODg5NxGxCRoNNBg0NjU0MTAxEcwlAw3NCDE4MUFBADcyBQERoCAzODgxOTQ1NjUNOgmJETsUNC4wMzAxDe8JGxU5FC44NzYzNE0IDF9yYXQVphgwLjAxMDQ4KY4NGw2nBRsIMzM0CfUQZXR1cm4V3BA0NDI0OBGHCRsNNhg0MTYwNzA5DRwJ+BGjEDgzNzkwNeEJHBGkCDE4NAHeADQNOQXBBDRzEXFhoQg1MjcNjCGbBDRzEXIUMC44OTA5bUUNwxFyCcQANUkdDRwRcgUcMRUJxBE3FDc1NzE2MjrhABE4GDE1NzU1MDlCqgAtwwGrBDM3FXMEY24BqxE8GDAuODUwODlJLw11SQEAZGWZFDM0MjgzOVEbLiEALV8YMzIyMDM5M1UDAGUllwlDDYEcNjAwODg0MDcxSh0jEYcgMTIzNDI0MzQzDSQFQAhoYXMNLA2NEDExNjUwUcI6IwANjxQxMDQ5NTIRjinSQkgAEDQ5MDczEWs+JAANSRg0NjcwMDkwWpAADdoUMTg3ODc1MXk6kgAR3BQ0MDY0NDExvD5vABFLFDgzNzc5NXFnPiYAEUwYMTg1MTU5NgWXOH0iLCJ2aWRlb19zb3VyY8FFNG9sZFZpZGVvUHVzaCJ9",
-                "7AyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjkyMjMyM1wiLAkYADcRGBg4NjE2NTdcCRgAdhkwFDA1MDA1OBUYFTAUMDQ3MDUyBRgIc3RyLjAADDQyNzQVGBkwEDU0NjA2BRgEdm8VMBg3LjY1ODY5FRcNLxg3LjgyNjU1ERgdjxAzODMzOAl3CHZvdhEwHDAuMzY4MjU1BUgF7wQ0ZBHwDDMwMDEJSQEZBDMwERkUODk2NjU0DTIEdl8ZMhAwNTE0MRnyGTIUMDQ4ODkxBTIF8xVkEDA1NTI4KTwBGR0yDDU0NTIN9QhpZXcxJiQzNTUxMjA3MC4wBU0FGw36IDI0MjM0NzE4OREcISwENGQFzRg3LjY3NTg0Ef4AchVpGDcuNzU3MzkRGS7NABQzOTQ2NzcJZwRvdhUyGDAuMzc5MjYJmxByX2NudBWcFC44NDA5OS0ABRoNmxQzLjYxODkNTRBzaGFyZRE0GDE5MjczOTQNswkbDTUYMTMyMzM2OTLrABHQIDU4MDk2NjMxNQ05JQgxPSQxMDc5OTIzOTk3DR4JjBE8GDMuODY3NTlNyQWnEToYMy43MTY1ME0NDF9yYXQVqRgwLjAxMzAzETYFGwA3KV0JGwAwKd8UcmV0dXJuFd8QNzc3Njcxkg0cDTccMTE0MDI5MDENpQn8FaUUMjExOTk1Nh0AEacYNTg4ODM4MDE2BcQANDKpAwgwMDQ9oQQ0cxF0GDAuODU5NTINqwnGEXMFxwQyOTL9ABFyBRwAMXEmDeNNXBgyOTg3MjE3FY4JHhE6HDUyNzk4NDI2DeYVrA09GDAuOTY3NjhdNAQ0cxU8FDAuOTE0OW04DVgl6o2THDEzNjE0NjkyEVwJegUiLWQYODkyNDU0OE4iABGCIDIyOTI5NDA3M0pGABHFHDQwOTU3ODA2MckFYwhoYXMNcg2PEDQ2MjgxUXQ6IwANkBQzMTUwODYRaynzQkgAEDE5MzIxYiUADUoYMTMyNjY1NzFYOm8AEd0YNzcyMzcxMQ3bOiUAEd4UMTQyMDY0NQFClgAANEnnGDMwODY5NjJxkD4nABFOGDU3NzAzNzUFJ3R9IiwidmlkZW9fc291cmNlIjoidHJhbnNwb3J0In0=",
-                "lgyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjQ5MzAyM1wiLAkYADcRGBg3OTUyNzlcCRgAdhkwFDAyMjY3NBUYFTAUMDMyMjg1BRgIc3RyHTAMNDU5OAlIARgZMBA0MDU5NgUwBHZvFTAYOC45MjkyNAlIARgNMBw3Ljk2MDgyNw0wHZAMMjAyNBFHAHYRLxgwLjI1NzAxEacQc18xNGQN8BQ3ODIwMzQFSCEICDMwZA0ZEDY3MjY3MQkAdh0yFDAzMTM5NA0yAHYdMhAwMjc1NgmrAdsZZBAwNDAxNAkyARkZZAwwNDA5LVUMdmlldzEmFDkzNTAuMAViBRcN9RQyNTQ2MDkRGSElDcYYNy44Nzg2MAliARkRYhg4LjI1NTkwCcYAdjLGABAyNDczMymIARkRMhgwLjIyNzU3KVkQcl9jbnQRlRgyLjcxNzk0CTMJGg2YHDQuMjMwNTcxBbEQc2hhcmURNAQ0MxHICRcNMRAxMDMzNhHIBfgNyRQzNDEzNzMuGgARmBgxMDI5MzM1DRsJfxE2GDQuMTk5NDUJgAkbETcYMy40NDU1MxHPCHJhdBWcHDAuMDA4MzQyBbYAcgkbDaABGwg3NjMNURBldHVybhHSCDIxMhGECRgNMwg4MjI26wARnRAxMzcwNA0zKQURnBA0MjE4MQ0aBbYANFqHAwhfY24BHA1qGDAuNzkzMDUp7Q24EW0FuQQ0N2kADRwRbwEcADgJ7Qm6ETUQMTA3MTcRiAkbETQMMjgzNxG9EYcRNxgwLjc4MDE1EWwJpBE5GDAuNjcxMjINpQlVKeoEZFyBaAgxODkx5y4eAA3jEDY1NDM4NpIACT1NJwg4NDRZCwBlJZ8FIBF+EDIzNDI1EWAFGghoYXMNKQ2BADcRYDofAA2CCDE5NBFAKbVCQQA+2wIZIRFDCDAzNlGwOmUAEcgIMjU1UU46IgARygQ4MhXpQogALQ1hMjG/PiQAEUcMNDIyNwVqbH0iLCJ2aWRlb19zb3VyY2UiOiJ1bmtub3duIn0=",
-                "zgyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjU5NzQ2NFwiLAkYBDdkDRgYNzQ5MzU0XAkYAHYZMBQwMjE5MTIVGBUwFDAzMzE4OQUYCHN0ch0wEDM2Njc1FRgZMAg0NDIJLwR2bxUvGDguMjU2NzgFLgEXDS4UOC4xMDMwFRgdjhQxODA5MjENMAB2ETAYMC4yNjg5MwkYBe4ENGQN1xQ4NDA1ODYFMQEZBDMwERkQNzU3OTQJqAByAWIVMhQwMzg1MzcNMgB2HTIQMDMzNTYJwwXyGTIINDU4DUsBGRlkHfQIaWV3MSQgMjM4NDQyOC4wBWQFGg34HDIzNjcwNTI1ERshKQ1mGDcuOTIyMzcJygEZEWYYNy45NDg4NAn8AHYyygAUMzA1MzAzCWYyygAQMjY2ODMJSxByX2NudBWaFC43NzA1NCmQCRoNmhwzLjI1MjIwNAVNEHNoYXJlETQMODc0NBHMCRkNMxQxMDQ4MzYVzABpIQENzRw1OTA0OTI2MRHoARwR0CAxMjIzNDc3NTINHQmHEToYMy40NjExOC24AF8BvBE5HDMuMzMyNzU3BaMUcl9yYXRlEaQYMC4wMDc5MCm8DRsNphgwLjAxMDIwDVEQZXR1cm4RNhA1MjI0NxGGCRoNNRQ3ODU1OTYNGwn1EaEYMjcwNzEyNDIcABGiFDU0MTg3NTESBb4ANDKaAwQ2Mi6WAQQ0cxFwGDAuNzQ3NzIxmAnbLUwBwQgxMTMpmg3cEXEBHAgwMDdJAw3DETgYMjI3NTU3MA2pDR0ROQQ0MQHGFR0haAGqFXMQODU4ODJdQQQ0cxU7GDAuNzY0ODctbCk3JeEIMWRcgYIQNDMxMzk13R0gLVwUNjM2NTc0MV0uIQARfxwxODAyNzkyMTa7AAVkEcAcMzI2NDY4NDMNIwUcCGhhcw1ODYoQMTg4NThWIgANjAwyNDE1FSMp6EJGAAg4NzZ1zD4jAA1HFDEwNTA2NFEUPo0ALVUB9gA1WiQAEdcYMTIzMjM0OQ20Pm4AEUoMMjY0OWaUABFMFDUzNjk2MyWJOH0iLCJ2aWRlb19zb3VyY8E2LHVzZXJ1cGxvYWQifQ==",
-                "wAyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjM5Nzk3M1wiLAkYADcRGBg0NDU2ODVcCRgAdhkwFDAyMDk0OBUYFTAUMDE5NjcxBRgIc3RyHTAQNTI2MzcVGBkwDDQ0MTMJSAR2bxUwGDguNDUxODkJSAEYDTAEOC4BFwQ1NgVIAHYukAAQMTc3MDUJwAEYDTAUMC4xNjc1CV8F7wA0FdgINzI3DWABGQQzMBEZEDg2MzI5CWEAcgFhFTIUMDE4MzQ5BXoBGRkyFDAzNjA2NAUZBfMZMgwzODgwCTIBGR0yDDQxNzcpVQx2aWV3MSYcOTU4MDI1LjAFSwB2ARkEN2Qlnxg0NDkxODg3ERohKA1lGDguNjkyODIJMgRvchVkGDguMDQyOTA5ERExGDAuMTU5NTAxWgB2FTIUMC4yOTAwDfkMX2NudBGWGDIuNTAwNDktRQUaDZcEMi4hLC2OEHNoYXJlETQQNTA0MjgNrwkZDTMUMTk4MjYxDRoF4xGZGDg4NTc5MDkuGwAxMRwzNTc5OTg3Ng0cCYQROAAyIQIENjMJ6gkbFTgYLjkxNDExNCUeEHJfcmF0FaEYMC4wMDgzNxHVBRsNowEbBDc0TcQUcmV0dXJuEdcMMjAwNhGiDRoNNRA4ODM2Mg2gCfERoBAzNDM3NjG7CRsRoBQxNDk1NTMy1wAANDKTAwg2ODFpMwXYBDRzEW4YMC40NDQ0NUlbEdkENGRFLAG/BDY4NvUAEXEBHAQ5MhEcDdsNOBQxNjI1MzANww0cETgUMTI5MTA5FR1BBAGpDToYMC40NzcxMEnQER0ROxgwLjg2MjA0LZ8Jcyn4Ca8QMTY5NjJRES4gAC1ZFDc1Mjc1MjaXAAlADX4YMTQxMjg0NEoiABGDGDEwMzg0MTERZQU+CGhhcw1tDYgIODAyMYk6IQANiQwzMzU2MQIpx0JEAAw1MDU3cb8+IwANRhAxOTg4MHEaOmkAEdIINjAzNiMDGWkR0xAzMjk4NV5rABFIEDM0MDY1Ua8+kAARShAxNDk3N2l3OH0iLCJ2aWRlb19zb3VyY8EmNOWFqOmdonNwaWRlciJ9",
-                "1AyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjgwNjIxM1wiLAkYADcVGAw2NjQwERgAdhkwGDA1MDUwNlwJMAB2GTAUMDUzODUyBRgIc3RyHTAMNjI2NAkwARgVYAF3ADUJGAR2bxUwHDcuNDM2Njg0BUgBGA0wEDcuMjc4DY8Adi6PABAzNzU1ORFHHY8UMzkxOTM5BUcF7wQ0ZBHwEDY1NzQxFRkEMzARGRA5Nzc2NR3xFTIQMDU0NjEJ2ghyb3YdMggwNTgtCgHaHTEMNjMwOAljARkZYxQwNTkzODcFfAx2aWV3MSUgMjQ3MzA3OS4wGRoN9xgxMzMxMDA2FRshKQ2YEDcuNDYwMboBGRFnIUMIMDY4ORQRMhwwLjQwNzQxNAlmMsoAEDQzMTQyCZkQcl9jbnQRmhQzLjg5MTkuGQANmRwzLjg2MzI4OAVMEHNoYXJlETMUMTU0OTI4DcwJGg00FDgyNzMwNA0aKQANzBwyMDA3MzU4NS4cABHPGDM1NjAxNTcRHA2fDTkYMy44ODM3MwnTCRsROBA0LjIwNFFiEHJfcmF0FaMYMC4wMTI5NxHXBRsNpAEbBDM5DVAQZXR1cm4V2AwyNDkwFYYJGw01DDcxNjc1vwnzEaEYMTI2NjI1Ng3ZCRwRogQyMSF1MdwFvgA0MpkDBDM3KWARGw1vFDAuODY0MRUcBb8RcAHACDQwNk2uCRwRcAEcADNNmA3BETcYMTA5NjI1MA2nDR0ROBgyMDY3MDMyER1BIAGoLYMBjggzNTUxTQRjbgEdETsYMC45NzQ0NjJQASn6AGRljww5Mjg4OQIpVQUgDesUNTIxNjczNSMdIRF/FDgxNzgyNU4iABGEGDE1MzU5NDYxhAVfCGhhcw1NDYkQMzIwOTMN4joiAA2LEDE4NTUzEUUpyUJGABQxNTUzOTcNRz4kAAA3CfMUODI5NDM0DSQ6awAR1RAyODIyNhFIOiQAEdcQNDkxNjYRSD5sABFJEDEyNjk0NR4+JgARSxQyMTIxMjAl/jh9IiwidmlkZW9fc291cmPBMVjlhajpnaJzcGlkZXItYXV0b3BpY2sifQ==",
-                "5gyQeyJmZWF0dXJlIjoie1wicm9zXzFkXCI6XCIwLjg5OTU0N1wiLAkYADcVGBQzNzEyM1wJGAB2GTAUMDQzODI2FRgVMBQwNDcxOTEFGAhzdHIuMAAIODcyFRcZLww1NjM3CV8Edm8VLxw3Ljc2MzMxNQUwARgNMBg3Ljg1OTczETAdjxAzNDAyMwmPCHZvdhEwHDAuMzcwOTA4BUgF7wA0EdgUOTAzNTY5FRkEMzARGRA4NTE5MDEJBHZfGTIIMDUxFTEAdh0xEDA0Njk3HfIVYwwwNTcyDUohCxljDDA1NTERrQhpZXcxJSQxOTQxOTEyNi4wBZcFGw35IDE3NzU2ODA2MxEcISwNaRg3LjUwMjQ3CeUBGRFpGDcuNjA3MDc5FhEyGDAuMzg3ODAxXy7NABAzNTczMSlgFHJfY250X00hFDMuNjQ2ODIaAA2bFDMuNTcyNC2sEHNoYXJlETQQOTQ2MTARsgkaDTQcMTAwMDk5ODkRziUFDc8gNDQ1MTkxNzUxLh0AFdMcNjMwODE2NDANHQ2kDTscMy44MjgyNjQlPwkbETkUMy42NTc5TYIQcl9yYXQVpxgwLjAxMjAxKUANGw2oARsEMzIp3BRyZXR1cm4R3BQ4NTEwNjQRhgkbDTUYODM3OTU5Mg0cCfgRohQyNTQ2Nzk6HQARpBg0MjA3MzYwEToFwQA0MqMDCDczMB3dBDRzEXMYMC44MzUzNDLDABFzBcQENTBpfA3fEXIBHAgyODQ24QAtTQQyMyEXADkxhw3kETocMzU4NDI3OTgN5hWsDT0YMC45NjI1N21gQUkENHMVPBgwLjg4MzkzDa4pPEkDAGSFjQA2RbcVWx0hLWIcNjU4NjEzNTcRfR0iEYEcMTcyNjQ3NzBORgARxBgyNzI2NTg5NXEFhAhoYXMNUA2OEDIzMzM3MZQ6IwANkBQyMzQ1NjMRaynyQkgAEDk0ODQ2XiQADUkYMTAwMzEyNlHeOm4AEdwYNjAxMTEwNA3+OiUAEd0UOTc5ODU0Nn4DHZQtqBwyMzkwNjcwNQ1MPrsAEU0YNDA1NDg5OSVxOH0iLCJ2aWRlb19zb3VyY8FMNOWeguebtHNwaWRlciJ9"
-        );
-        testSnappyDecompress(snappyStrs, 1);
-//        testSnappyDecompress(snappyStrs, 10);
-//        testSnappyDecompress(snappyStrs, 100);
-//        testSnappyDecompress(snappyStrs, 1000);
-//        testSnappyDecompress(snappyStrs, 10000);
-
-//        testLz4Decompress(lz4Strs, 1);
-//        testLz4Decompress(lz4Strs, 10);
-//        testLz4Decompress(lz4Strs, 100);
-//        testLz4Decompress(lz4Strs, 1000);
-//        testLz4Decompress(lz4Strs, 10000);
+    public static void main(String[] args) throws IOException {
+        String str = "gBR8eyJmZWF0dXJlIjoie1wicm9zXzFoXCI6XCIwLjBcIiwJEwAyRhMAADNGEwAANEYTAAA2RhMABDdkOl8AAHZGcgAAdkZyAAB2RnIAAHZGcgAAdkZyAAB2PnIACHN0cj5yAAETOuQAARM65AABEzrkAAETOuQAARM65AAEdm9CcgABEzpyAAETOnIAARM6cgABEzpyAAETQnIAQlYBAHZGVgEBE0JyAEJWAQEmQnIAQlYBRTo60gABFAAyOsAAAHIBmkIoAAB2QigAIUc+KAABFD5QAAx2aWV3Ps8BBRQ6ZAAFFD7tAAE8OqAABRQ+7wABKEJiAQByQqAAIYlCoABK8AABFD4oABByX2NudD7JAAkVOsoACRU6ywAJFTrMAAkVOs0ACRU6zgAQc2hhcmU+fgAJFTp+AAkVOn4ACRU6fgAJFTp+AAkVOn4AAHYhYD7tAQUVWiYBOpQACdNGLAAIcmF0QtUAAHIJFkJCAAUsOtcADSwANDrFBAksOtkADRY62gAUcmV0dXJuPlkBDRY+hAAJLD6EAAkWADQ6hAAJFj6EAAkWOoQAKXM+NAEJFkY0AQxjbnQ0RgQGDRc+sgANFz6zAA0XPrQADRc+tQANFzq2AC1QPrcADRdCuAAJ+kIuAAkXQi4ADXNCLwANGEIwAAlHAHZFp1KJAQUbPh4BHTY+IgEdGwA0ZlEAPioBHTY+LgEdG0LWAB0cPtoAAHYB0whoYXMNJDrdADoeADrgADoeADrjAAB2NloAOuYAOh4AOukAOh4AOuwASbpitQA+HwA6tgA+HwA6twA+HwA6uAA+HwA6uQA+HwA6ugAAdjYUAT6pATofAD6sAT5dAD4/AD4gADJAAFx9IiwidmlkZW9pZCI6IjEwMDAwMTYwIn0=";
+        System.out.println(CompressionUtil.snappyDecompress(str));
     }
 
     private static void testSnappyDecompress(List<String> strs, int num) {

+ 57 - 93
recommend-feature-service/src/main/java/com/tzld/piaoquan/recommend/feature/service/FeatureV2Service.java

@@ -1,9 +1,5 @@
 package com.tzld.piaoquan.recommend.feature.service;
 
-import com.alibaba.fastjson.JSON;
-import com.ctrip.framework.apollo.model.ConfigChange;
-import com.ctrip.framework.apollo.model.ConfigChangeEvent;
-import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
 import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
 import com.google.common.base.Strings;
 import com.tzld.piaoquan.recommend.feature.common.ThreadPoolFactory;
@@ -21,9 +17,14 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
-import javax.annotation.PostConstruct;
-import java.util.*;
-import java.util.concurrent.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
 
 /**
  * @author dyp
@@ -35,78 +36,24 @@ public class FeatureV2Service {
     @Autowired
     private RedisTemplate<String, String> redisTemplate;
 
+    @Qualifier("byteRedisTemplate")
+    @Autowired
+    private RedisTemplate<String, byte[]> byteRedisTemplate;
+
     @ApolloJsonValue("${dts.config:}")
     private List<DTSConfig> newDtsConfigs;
 
-    @Value("${feature.mget.batch.size:200}")
-    private Integer batchSize;
-
-    /**
-     * DTSConfig 缓存,按 tableName 索引,避免每次 redisKey() 都遍历列表
-     * 启动时通过 @PostConstruct 初始化,配置变更时通过 @ApolloConfigChangeListener 更新
-     */
-    private volatile Map<String, DTSConfig> dtsConfigCache = Collections.emptyMap();
+    @ApolloJsonValue("${dts.config.v2:}")
+    private List<DTSConfig> dtsConfigsV2;
 
-    @PostConstruct
-    public void init() {
-        rebuildDtsConfigCache();
-    }
-
-    /**
-     * 重建 DTSConfig 缓存(从 newDtsConfigs 字段)
-     */
-    private void rebuildDtsConfigCache() {
-        rebuildDtsConfigCacheFromList(newDtsConfigs);
-    }
-
-    /**
-     * 重建 DTSConfig 缓存
-     * @param configs 配置列表
-     */
-    private void rebuildDtsConfigCacheFromList(List<DTSConfig> configs) {
-        if (configs == null || configs.isEmpty()) {
-            dtsConfigCache = Collections.emptyMap();
-            log.info("DTSConfig cache rebuilt, size=0");
-            return;
-        }
-        Map<String, DTSConfig> cache = new HashMap<>(configs.size());
-        for (DTSConfig config : configs) {
-            if (config.getOdps() != null && StringUtils.isNotBlank(config.getOdps().getTable())) {
-                cache.put(config.getOdps().getTable(), config);
-            }
-        }
-        dtsConfigCache = cache;
-        log.info("DTSConfig cache rebuilt, size={}", cache.size());
-    }
+    @Value("${feature.mget.batch.size:300}")
+    private Integer batchSize;
 
-    /**
-     * 监听 Apollo 配置变更,自动重建缓存
-     * 手动解析新配置值,避免与 @ApolloJsonValue 自动注入产生竞态条件
-     */
-    @ApolloConfigChangeListener
-    public void onConfigChange(ConfigChangeEvent changeEvent) {
-        if (changeEvent.isChanged("dts.config")) {
-            ConfigChange change = changeEvent.getChange("dts.config");
-            String newValue = change.getNewValue();
-            log.info("dts.config changed, old={}, new={}", change.getOldValue(), newValue);
-
-            if (StringUtils.isNotBlank(newValue)) {
-                try {
-                    List<DTSConfig> newConfigs = JSON.parseArray(newValue, DTSConfig.class);
-                    rebuildDtsConfigCacheFromList(newConfigs);
-                } catch (Exception e) {
-                    log.error("Failed to parse dts.config: {}", newValue, e);
-                }
-            } else {
-                dtsConfigCache = Collections.emptyMap();
-                log.info("dts.config is empty, cache cleared");
-            }
-        }
-    }
+    @Value("${feature.service.optimize.switch:false}")
+    private Boolean optimizeV3Switch;
 
     public MultiGetFeatureResponse multiGetFeature(MultiGetFeatureRequest request) {
         int keyCount = request.getFeatureKeyCount();
-
         if (keyCount == 0) {
             return MultiGetFeatureResponse.newBuilder()
                     .setResult(Result.newBuilder().setCode(1))
@@ -115,32 +62,41 @@ public class FeatureV2Service {
 
         long startTime = System.currentTimeMillis();
 
-        // 1. 生成Redis Key列表
-        List<String> redisKeys = CommonCollectionUtils.toList(request.getFeatureKeyList(), this::redisKey);
+        List<String> redisKeys;
+        if (Boolean.TRUE.equals(optimizeV3Switch)) {
+            redisKeys = request.getFeatureKeyList().stream()
+                    .map(this::buildRedisKeyV2)
+                    .collect(Collectors.toList());
+        } else {
+            redisKeys = request.getFeatureKeyList().stream()
+                    .map(this::redisKey)
+                    .map(s -> String.format("%s:v2", s))
+                    .collect(Collectors.toList());
+        }
 
         int safeBatchSize = (batchSize == null || batchSize <= 0) ? 300 : batchSize;
 
         // 2. 分批并行查询(必须保证返回值与 key 顺序一致)
-        List<BatchFuture> futures = new ArrayList<>();
+        List<BatchFuture<byte[]>> futures = new ArrayList<>();
         for (int start = 0; start < redisKeys.size(); start += safeBatchSize) {
             int end = Math.min(start + safeBatchSize, redisKeys.size());
             List<String> batchKeys = redisKeys.subList(start, end);
-            Future<List<String>> future = ThreadPoolFactory.multiGetFeaturePool()
-                    .submit(() -> redisTemplate.opsForValue().multiGet(batchKeys));
-            futures.add(new BatchFuture(start, end - start, future));
+            Future<List<byte[]>> future = ThreadPoolFactory.multiGetFeaturePool()
+                    .submit(() -> byteRedisTemplate.opsForValue().multiGet(batchKeys));
+            futures.add(new BatchFuture<byte[]>(start, end - start, future));
         }
 
         // 3. 收集结果(整体最多等待约 2s;超时/异常的批次用 null 填充,避免错位)
-        List<String> allValues = new ArrayList<>(Collections.nCopies(keyCount, null));
+        List<byte[]> allValues = new ArrayList<>(Collections.nCopies(keyCount, null));
         long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(2000);
-        for (BatchFuture bf : futures) {
+        for (BatchFuture<byte[]> bf : futures) {
             long remainingNanos = deadlineNanos - System.nanoTime();
             if (remainingNanos <= 0) {
                 bf.future.cancel(true);
                 continue;
             }
             try {
-                List<String> batchValues = bf.future.get(remainingNanos, TimeUnit.NANOSECONDS);
+                List<byte[]> batchValues = bf.future.get(remainingNanos, TimeUnit.NANOSECONDS);
                 if (batchValues == null) {
                     continue;
                 }
@@ -150,14 +106,14 @@ public class FeatureV2Service {
                 }
             } catch (TimeoutException e) {
                 bf.future.cancel(true);
-                log.warn("Batch mGet timeout, startIndex={}, size={}", bf.startIndex, bf.size);
+                log.warn("Batch mGetV2 timeout, startIndex={}, size={}", bf.startIndex, bf.size);
             } catch (Exception e) {
-                log.error("Batch mGet failed, startIndex={}, size={}", bf.startIndex, bf.size, e);
+                log.error("Batch mGetV2 failed, startIndex={}, size={}", bf.startIndex, bf.size, e);
             }
         }
 
         // 4. 解压缩
-        List<String> values = CommonCollectionUtils.toList(allValues, CompressionUtil::snappyDecompress);
+        List<String> values = CommonCollectionUtils.toList(allValues, CompressionUtil::snappyDecompressV2);
 
         // 5. 构建响应
         MultiGetFeatureResponse.Builder builder = MultiGetFeatureResponse.newBuilder();
@@ -167,31 +123,40 @@ public class FeatureV2Service {
                     Strings.nullToEmpty(values.get(i)));
         }
         MultiGetFeatureResponse build = builder.build();
-        log.info("multiGetFeature, cost={}ms", System.currentTimeMillis() - startTime);
+        log.info("multiGetFeature, cost={}ms, keyCount={}", System.currentTimeMillis() - startTime, keyCount);
         return build;
     }
 
-    private static final class BatchFuture {
+    private static final class BatchFuture<T> {
         private final int startIndex;
         private final int size;
-        private final Future<List<String>> future;
+        private final Future<List<T>> future;
 
-        private BatchFuture(int startIndex, int size, Future<List<String>> future) {
+        private BatchFuture(int startIndex, int size, Future<List<T>> future) {
             this.startIndex = startIndex;
             this.size = size;
             this.future = future;
         }
     }
 
-    // Note:写入和读取的key生成规则应保持一致
     private String redisKey(FeatureKeyProto fk) {
-        // 使用缓存查找配置,O(1) 复杂度
-        // 缓存在启动时初始化,配置变更时通过 Apollo 监听器更新
-        DTSConfig config = dtsConfigCache.get(fk.getTableName());
-        if (config == null) {
+        return this.buildRedisKey(fk, newDtsConfigs);
+    }
+
+    private String buildRedisKeyV2(FeatureKeyProto fk) {
+        return this.buildRedisKey(fk, dtsConfigsV2);
+    }
+
+    // Note:写入和读取的key生成规则应保持一致
+    private String buildRedisKey(FeatureKeyProto fk, List<DTSConfig> dtsConfigs){
+        Optional<DTSConfig> optional = dtsConfigs.stream()
+                .filter(c -> c.getOdps() != null && StringUtils.equals(c.getOdps().getTable(), fk.getTableName()))
+                .findFirst();
+        if (!optional.isPresent()) {
             log.error("table {} not config", fk.getTableName());
             return "";
         }
+        DTSConfig config = optional.get();
 
         // Note:写入和读取的key生成规则应保持一致
         List<String> fields = config.getRedis().getKey();
@@ -208,5 +173,4 @@ public class FeatureV2Service {
         }
         return sb.toString();
     }
-
 }

+ 13 - 0
recommend-feature-service/src/main/java/com/tzld/piaoquan/recommend/feature/util/CompressionUtil.java

@@ -12,6 +12,7 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
+import java.util.Objects;
 
 
 /**
@@ -83,4 +84,16 @@ public class CompressionUtil {
         }
     }
 
+    public static String snappyDecompressV2(byte[] compressedInput) {
+        if (Objects.isNull(compressedInput)) {
+            return "";
+        }
+        try {
+            byte[] decompressedBytes = Snappy.uncompress(compressedInput);
+            return new String(decompressedBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            log.error("snappyDecompressV2 error compressedInput {}", compressedInput, e);
+            return "";
+        }
+    }
 }

+ 3 - 3
recommend-feature-service/src/main/resources/application-dev.yml

@@ -8,7 +8,7 @@ grpc:
 eureka:
   instance:
     prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false
-    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} #注册到eureka上的唯一实例ID
+    instance-id: ${spring.application.name}:${POD_NAME}:${server.port} #注册到eureka上的唯一实例ID
     lease-renewal-interval-in-seconds: 10 #表示eureka client发送心跳给server端的频率,默认30
     lease-expiration-duration-in-seconds: 30 #表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认90
   client:
@@ -18,7 +18,7 @@ eureka:
 
 spring:
   redis:
-    hostName: r-bp1pi8wyv6lzvgjy5z.redis.rds.aliyuncs.com
+    hostName: r-bp1wwqqkjqwwkxgbup.redis.rds.aliyuncs.com
     port: 6379
     password: Wqsd@2019
     timeout: 1000
@@ -29,7 +29,7 @@ spring:
         max-idle: 8
         min-idle: 0
   tair:
-    hostName: r-bp1pi8wyv6lzvgjy5z.redis.rds.aliyuncs.com
+    hostName: r-bp1wwqqkjqwwkxgbup.redis.rds.aliyuncs.com
     port: 6379
     password: Wqsd@2019
     timeout: 1000

+ 1 - 1
recommend-feature-service/src/main/resources/application-pre.yml

@@ -4,7 +4,7 @@ server:
 eureka:
   instance:
     prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false
-    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} #注册到eureka上的唯一实例ID
+    instance-id: ${spring.application.name}:${POD_NAME}:${server.port} #注册到eureka上的唯一实例ID
     lease-renewal-interval-in-seconds: 10 #表示eureka client发送心跳给server端的频率,默认30
     lease-expiration-duration-in-seconds: 30 #表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认90
   client:

+ 1 - 1
recommend-feature-service/src/main/resources/application-prod.yml

@@ -28,7 +28,7 @@ thread:
 eureka:
   instance:
     prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false
-    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} #注册到eureka上的唯一实例ID
+    instance-id: ${spring.application.name}:${POD_NAME}:${server.port} #注册到eureka上的唯一实例ID
     lease-renewal-interval-in-seconds: 10 #表示eureka client发送心跳给server端的频率,默认30
     lease-expiration-duration-in-seconds: 30 #表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认90
   client:

+ 1 - 1
recommend-feature-service/src/main/resources/application-test.yml

@@ -4,7 +4,7 @@ server:
 eureka:
   instance:
     prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false
-    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} #注册到eureka上的唯一实例ID
+    instance-id: ${spring.application.name}:${POD_NAME}:${server.port} #注册到eureka上的唯一实例ID
     lease-renewal-interval-in-seconds: 10 #表示eureka client发送心跳给server端的频率,默认30
     lease-expiration-duration-in-seconds: 30 #表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认90
   client:

+ 7 - 1
recommend-feature-service/src/main/resources/application.yml

@@ -1,8 +1,14 @@
 spring:
+  lifecycle:
+    timeout-per-shutdown-phase: 30s
   profiles:
     active: dev
   application:
     name: recommend-feature
+
+server:
+  shutdown: graceful
+
 logging:
   file:
     path: /datalog/weblog/${spring.application.name}/
@@ -12,7 +18,7 @@ app:
 apollo:
   bootstrap:
     enabled: true
-    namespaces: application
+    namespaces: application,RD.Experiment
   cacheDir: /datalog/apollo-cache-dir
 
 # Actuator 监控配置

+ 44 - 0
recommend-feature-service/src/test/java/com/tzld/piaoquan/recommend/feature/service/FeatureV2ServiceTest.java

@@ -0,0 +1,44 @@
+package com.tzld.piaoquan.recommend.feature.service;
+
+import com.tzld.piaoquan.recommend.feature.util.CompressionUtil;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.Arrays;
+import java.util.List;
+
+@SpringBootTest
+public class FeatureV2ServiceTest {
+
+    @Autowired
+    @Qualifier("byteRedisTemplate")
+    private RedisTemplate<String, byte[]> byteRedisTemplate;
+
+    @Qualifier("redisTemplate")
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Test
+    public void featureV2Test() {
+
+        List<String> byteArraykeys = Arrays.asList(
+                "snappy:alg_user_network_seq_feature:ohlZ7xLtnpqmeqJaBHejIeX2BZiY:v2"
+        );
+
+        for (byte[] bytes : byteRedisTemplate.opsForValue().multiGet(byteArraykeys)) {
+            System.out.println(CompressionUtil.snappyDecompressV2(bytes));
+        }
+
+        List<String> keys = Arrays.asList(
+                "snappy:alg_user_network_seq_feature:ohlZ7xLtnpqmeqJaBHejIeX2BZiY"
+        );
+
+        for (String value : redisTemplate.opsForValue().multiGet(keys)) {
+            System.out.println(CompressionUtil.snappyDecompress(value));
+        }
+
+    }
+}