目录一、问题描述二、大key的认定一String 类型二Set、List、Hash、ZSet 等集合类型三、解决策略分析一解决策略一拆分大 Key注意事项二解决策略二分拆集合数据注意事项三解决策略三压缩方案四、总结干货分享感谢您的阅读在高并发场景下缓存作为前置查询机制显著减轻了数据库的压力提高了系统性能。然而这也带来了缓存失效、增加回溯率等风险。常见的问题包括缓存穿透、缓存雪崩、热Key和大Key等。这些问题如果不加以处理会影响系统的稳定性和性能。因此采用有效的缓存策略如缓存空结果、布隆过滤器、缓存过期时间随机化、多级缓存等对于保障系统在高并发情况下的可靠性至关重要。本次我们将详细探讨大Key及其应对策略。一、问题描述大 Key 是指在缓存系统中某些 Key 对应的值Value存储的数据量非常大大 Key 可能会导致一系列性能问题和系统不稳定性响应超时由于 Redis 是单线程的如果某个 Key 的 Value 很大在进行GET或SET操作时会占用 Redis 的单线程导致其他请求被阻塞从而引发响应超时。另外集合类型如 Set、List、Hash、ZSet的元素较多时删除或读取这些大集合的时间复杂度为 O(n)会严重阻塞 Redis 进程导致应用服务的超时和崩溃。数据倾斜大 Key 会导致 Redis 集群中某些节点存储的数据量远大于其他节点从而引起数据分布不均衡的问题。集群负载不均衡会导致某些节点的内存和计算资源紧张降低整体性能。二、大key的认定缓存系统中一般大 Key 的定义如下一String 类型Value 大于 10K 为“大” KeyValue 大于 100K 为“超大” Key二Set、List、Hash、ZSet 等集合类型元素个数超过 1000 为“大” Key元素个数超过 10000 为“超大” Key三、解决策略分析一解决策略一拆分大 Key通过将大 Key 拆分成多个小 Key可以分散数据存储和访问压力避免单点故障和性能瓶颈。拆分大 Key 的具体步骤和实现方法如下确定拆分策略拆分大 Key 的策略通常包括按照数据块大小、按照数据量等方式进行。在实际应用中可以根据业务需求和数据特性来选择合适的拆分方法。定义拆分规则确定如何将大 Key 拆分成多个小 Key需要设计清晰的拆分规则。例如可以按照固定大小的数据块进行拆分或者根据数据内容的逻辑关系进行拆分。实现拆分逻辑实现拆分大 Key 的逻辑将大 Key 拆分成多个小 Key并分别存储在不同的分片或不同的 Redis 节点上。实现将大 Key 拆分为多个小 Key 并存储import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.ArrayList; import java.util.List; /** * program: zyfboot-javabasic * author: zhangyanfeng * create: 2013-03-25 20:39 **/ public class LargeKeySplitter { private static final int CHUNK_SIZE 1024; // 每个小块的大小 private static final ListJedisPool shardPools new ArrayList(); static { // 假设有三个 Redis 分片 shardPools.add(new JedisPool(new JedisPoolConfig(), localhost, 6379)); shardPools.add(new JedisPool(new JedisPoolConfig(), localhost, 6380)); shardPools.add(new JedisPool(new JedisPoolConfig(), localhost, 6381)); } public static void setLargeKey(String key, String value) { int numChunks (int) Math.ceil((double) value.length() / CHUNK_SIZE); for (int i 0; i numChunks; i) { String chunkKey key _chunk_ i; String chunkValue value.substring(i * CHUNK_SIZE, Math.min((i 1) * CHUNK_SIZE, value.length())); int shardIndex Math.abs(chunkKey.hashCode()) % shardPools.size(); // 根据 key 计算分片索引 try (Jedis jedis shardPools.get(shardIndex).getResource()) { jedis.set(chunkKey, chunkValue); } } } public static String getLargeKey(String key, int numChunks) { StringBuilder value new StringBuilder(); for (int i 0; i numChunks; i) { String chunkKey key _chunk_ i; int shardIndex Math.abs(chunkKey.hashCode()) % shardPools.size(); // 根据 key 计算分片索引 try (Jedis jedis shardPools.get(shardIndex).getResource()) { String chunkValue jedis.get(chunkKey); if (chunkValue ! null) { value.append(chunkValue); } } } return value.toString(); } public static void main(String[] args) { String key large_key; String value new String(new char[5000]).replace(\0, a); // 模拟大 Key 的值 setLargeKey(key, value); System.out.println(Cache value: getLargeKey(key, 5)); } }注意事项数据一致性拆分大 Key 后需要确保数据的一致性和完整性。例如可以使用事务或分布式锁来确保数据操作的原子性。分片策略根据业务特性选择合适的分片策略避免某一分片负载过重或过于空闲。二解决策略二分拆集合数据针对集合存储了过多值的问题特别是对于大型数据集合如 hash、set、list、zset 等可以采用分拆的方法来减轻单个集合导致的性能问题。对于 Redis 中的集合类型数据如 hash可以通过分拆的方式将原始的大集合分解为多个小集合。例如将 hash 类型的 field 根据其 hash 值的模运算结果分配到不同的桶新的 hashKey中存储。这样可以减少单个 hashKey 的数据量从而减少单个操作对 Redis 服务的压力。实现在 Redis 中对 hash 类型数据进行分拆存储import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.HashMap; import java.util.Map; /** * program: zyfboot-javabasic * author: zhangyanfeng * create: 2013-03-25 20:56 **/ public class HashSplitter { private static final int NUM_BUCKETS 10000; // 桶的数量 private static final JedisPool jedisPool new JedisPool(new JedisPoolConfig(), localhost, 6379); public static void hset(String hashKey, String field, String value) { int bucket Math.abs(field.hashCode()) % NUM_BUCKETS; String newHashKey hashKey : bucket; try (Jedis jedis jedisPool.getResource()) { jedis.hset(newHashKey, field, value); } } public static String hget(String hashKey, String field) { int bucket Math.abs(field.hashCode()) % NUM_BUCKETS; String newHashKey hashKey : bucket; try (Jedis jedis jedisPool.getResource()) { return jedis.hget(newHashKey, field); } } public static void main(String[] args) { String hashKey myHash; String field1 field1; String value1 value1; String field2 field2; String value2 value2; // 存储数据 hset(hashKey, field1, value1); hset(hashKey, field2, value2); // 获取数据 System.out.println(Field1 value: hget(hashKey, field1)); System.out.println(Field2 value: hget(hashKey, field2)); } }注意事项一致性问题分拆后的数据需要保证一致性可以使用事务或者分布式锁来确保数据操作的原子性。桶的数量选择选择合适的桶数量非常重要需要根据数据分布和业务特性来进行调整避免某些桶过度负载或者浪费空间。三解决策略三压缩方案压缩 Redis 中存储的 value 可以显著减少存储空间并且在网络传输时也能减少带宽消耗。在 Redis 中常用的压缩算法包括但不限于GZIP 压缩使用 GZIP 算法对 value 进行压缩适合对文本数据和可压缩数据进行压缩。LZ4 压缩LZ4 是一种快速压缩算法适合对数据进行实时压缩和解压适用于需要低延迟的场景。Snappy 压缩Snappy 也是一种快速压缩算法比较适合对图片、音频等二进制数据进行压缩。BZIP2 压缩适合于对需要更高压缩率的数据进行处理但压缩速度相对较慢。选择压缩算法时需要考虑数据的特性、压缩率、解压速度和对 CPU 的消耗等因素。如在 Redis 中使用 GZIP 进行 value 的压缩和解压import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * program: zyfboot-javabasic * author: zhangyanfeng * create: 2013-03-25 21:13 **/ public class RedisValueCompression { private static final JedisPool jedisPool new JedisPool(new JedisPoolConfig(), localhost, 6379); // 将字符串压缩为字节数组 public static byte[] compress(String data) throws IOException { ByteArrayOutputStream bos new ByteArrayOutputStream(data.length()); try (GZIPOutputStream gzip new GZIPOutputStream(bos)) { gzip.write(data.getBytes()); } return bos.toByteArray(); } // 将字节数组解压为字符串 public static String decompress(byte[] compressedData) throws IOException { ByteArrayInputStream bis new ByteArrayInputStream(compressedData); StringBuilder sb new StringBuilder(); try (GZIPInputStream gis new GZIPInputStream(bis)) { byte[] buffer new byte[1024]; int bytesRead; while ((bytesRead gis.read(buffer)) ! -1) { sb.append(new String(buffer, 0, bytesRead)); } } return sb.toString(); } // 将压缩后的数据存入 Redis public static void setCompressedValue(String key, String data) throws IOException { try (Jedis jedis jedisPool.getResource()) { byte[] compressedData compress(data); jedis.set(key.getBytes(), compressedData); } } // 从 Redis 中获取压缩后的数据并解压 public static String getDecompressedValue(String key) throws IOException { try (Jedis jedis jedisPool.getResource()) { byte[] compressedData jedis.get(key.getBytes()); if (compressedData ! null) { return decompress(compressedData); } return null; } } public static void main(String[] args) throws IOException { String key compressedKey; String originalValue This is a test string to be compressed and stored in Redis.; // 存储压缩后的数据 setCompressedValue(key, originalValue); // 从 Redis 获取并解压数据 String retrievedValue getDecompressedValue(key); System.out.println(Original value: originalValue); System.out.println(Retrieved value: retrievedValue); } }四、总结在高并发场景下大 Key 问题是缓存系统中常见的挑战之一它可能导致响应超时、数据倾斜等严重性能问题和系统稳定性隐患。为有效解决大 Key 带来的各种挑战可以采取多种策略和技术手段首先拆分大 Key 是一种常见的解决方案。通过将大 Key 拆分成多个小 Key可以分散数据访问压力避免单点故障和性能瓶颈。这需要明确的拆分策略和实现逻辑确保数据的一致性和操作的原子性。其次针对集合类型数据如 Hash、Set、List、ZSet 等可以采用分拆的方法来降低单个集合导致的性能问题。例如将大型 Hash 数据按照哈希值进行分桶存储可以有效减少单个操作对 Redis 的影响。另外压缩 Redis 中存储的大 Value 可以显著减少存储空间和网络传输消耗提升系统整体性能。选择合适的压缩算法如 GZIP、LZ4、Snappy 等需要综合考虑数据特性和压缩效率。有效应对大 Key 问题需要综合考虑业务需求、数据特性以及系统架构选择合适的技术手段和策略进行优化和改进。