Understanding Redis Hotkeys, Bigkeys, and Other Performance Bottlenecks: Optimization Strategies in Spring Boot Applications

Master Spring Ter
6 min readNov 16, 2024

Redis is a high-performance in-memory data store widely used for caching, messaging, and real-time analytics. However, certain patterns like hotkeys, bigkeys, and other performance bottlenecks can degrade its efficiency. This article explores these concepts, their impact on your Spring Boot 3.x applications, and strategies to handle them effectively.

Introduction

Redis excels in speed and simplicity, making it a popular choice for various applications. However, improper usage can lead to performance degradation. Understanding and addressing performance bottlenecks like hotkeys, bigkeys, slow commands, and others is crucial for maintaining optimal performance in your Spring Boot applications.

This article provides practical solutions and code examples to help you detect and mitigate these issues effectively.

Redis Performance Bottlenecks

Hotkeys

Hotkeys are keys that receive a disproportionately high number of requests compared to others. They can cause uneven load distribution and strain specific nodes in a Redis cluster.

Common Causes:

  • Global configuration data accessed frequently
  • Popular user sessions
  • Trending content caches
  • Counters and rate limiters
  • Distributed locks

Bigkeys

Bigkeys are keys that hold large amounts of data, such as:

  • Large strings (>10KB)
  • Lists with thousands of elements
  • Hashes with numerous fields
  • Sets and sorted sets with many members

They can consume significant memory and processing time.

Slow Commands

Slow commands are operations that take longer to execute due to their time complexity or the size of the data they operate on.

Examples:

  • KEYS * (O(N) complexity)
  • SORT on large datasets
  • ZRANGE with large ranges
  • Blocking commands like BLPOP when misused

Blocking Operations

Blocking operations can halt the Redis event loop, affecting all other operations.

Examples:

  • Long-running Lua scripts
  • Transactions (MULTI/EXEC) with slow commands
  • Misconfigured BLPOP or BRPOP

Need help with Spring Framework? Master Spring TER, a ChatGPT model, offers real-time troubleshooting, problem-solving, and up-to-date Spring Boot info. Click master-spring-ter for free expert support!

Memory Fragmentation

Memory fragmentation occurs when Redis’s memory allocator can’t reuse freed memory efficiently, leading to increased memory usage.

Network Latency

Network latency can significantly affect Redis performance, especially in distributed environments.

Detecting Performance Issues

Monitoring Hotkeys

Implement an access monitor to track key usage.

@Component
@Slf4j
public class RedisKeyAccessMonitor {

private final ConcurrentHashMap<String, AtomicInteger> accessCount = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

@PostConstruct
public void init() {
scheduler.scheduleAtFixedRate(this::analyzeAndResetCounts, 1, 1, TimeUnit.MINUTES);
}

public void recordAccess(String key) {
accessCount.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet();
}

private void analyzeAndResetCounts() {
accessCount.forEach((key, count) -> {
if (count.get() > 1_000) {
log.warn("Hotkey detected: {} (access count: {})", key, count.get());
}
});
accessCount.clear();
}

@PreDestroy
public void cleanup() {
scheduler.shutdown();
}
}

Usage: Inject RedisKeyAccessMonitor into your service classes and call recordAccess(key) whenever you access a Redis key.

Identifying Bigkeys

Use the SCAN command to iterate over keys and analyze their sizes.

@Service
@Slf4j
public class RedisBigKeyAnalyzer {

@Autowired
private StringRedisTemplate redisTemplate;

public void analyzeBigKeys(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build();
try (Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(
redisConnection -> redisConnection.scan(options))) {

while (cursor.hasNext()) {
String key = new String(cursor.next(), StandardCharsets.UTF_8);
DataType type = redisTemplate.type(key);

switch (type.code()) {
case "string":
Long size = redisTemplate.execute(
(RedisCallback<Long>) conn -> conn.stringCommands().strLen(key.getBytes()));
if (size != null && size > 10_240) { // 10KB threshold
log.warn("Bigkey detected - String: {} (size: {} bytes)", key, size);
}
break;
// Handle other data types similarly
}
}
} catch (Exception e) {
log.error("Error during bigkey analysis", e);
}
}
}

Analyzing Slow Commands

Use Redis’s built-in slow log feature.

CONFIG SET slowlog-log-slower-than 10000  # Log commands slower than 10ms
SLOWLOG GET # Retrieve slow log entries

Alternatively, monitor slow commands via Spring Boot’s metrics.

management:
endpoints:
web:
exposure:
include: 'redis'
metrics:
enable.redis: true

Optimization Strategies

Mitigating Hotkeys

Key Sharding

Distribute load by spreading access across multiple keys.

@Service
public class ShardedService {

private static final int SHARD_COUNT = 10;

public void set(String key, String value) {
for (int i = 0; i < SHARD_COUNT; i++) {
String shardedKey = key + ":" + i;
redisTemplate.opsForValue().set(shardedKey, value);
}
}

public String get(String key) {
int shard = ThreadLocalRandom.current().nextInt(SHARD_COUNT);
String shardedKey = key + ":" + shard;
return redisTemplate.opsForValue().get(shardedKey);
}
}

Considerations:

  • Consistency: Ensure all shards are updated atomically.
  • Memory Overhead: Data duplication increases memory usage.

Local Caching with Caffeine

Reduce Redis load by caching frequently accessed data locally.

@Configuration
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("localCache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10_000));
return cacheManager;
}
}

@Service
public class CachedService {

@Autowired
private StringRedisTemplate redisTemplate;

@Cacheable(cacheNames = "localCache", key = "#key")
public String getCachedValue(String key) {
return redisTemplate.opsForValue().get(key);
}
}

Managing Bigkeys

Chunking Large Strings

Split large strings into smaller chunks.

@Service
public class ChunkedStringService {

private static final int CHUNK_SIZE = 1_048_576; // 1MB

public void setLargeString(String key, String value) {
int chunks = (value.length() + CHUNK_SIZE - 1) / CHUNK_SIZE;

redisTemplate.opsForValue().set(key + ":meta", String.valueOf(chunks));

for (int i = 0; i < chunks; i++) {
int start = i * CHUNK_SIZE;
int end = Math.min(start + CHUNK_SIZE, value.length());
String chunk = value.substring(start, end);
redisTemplate.opsForValue().set(key + ":chunk:" + i, chunk);
}
}

public String getLargeString(String key) {
String meta = redisTemplate.opsForValue().get(key + ":meta");
if (meta == null) return null;

int chunks = Integer.parseInt(meta);
StringBuilder sb = new StringBuilder();

for (int i = 0; i < chunks; i++) {
String chunk = redisTemplate.opsForValue().get(key + ":chunk:" + i);
if (chunk != null) {
sb.append(chunk);
} else {
log.error("Missing chunk {} for key {}", i, key);
return null;
}
}

return sb.toString();
}
}

Splitting Large Hashes

Divide large hashes into smaller, manageable parts.

@Service
public class SplitHashService {

private static final int MAX_FIELDS = 1_000;

public void setLargeHash(String key, Map<String, String> fields) {
int part = 0;
Map<String, String> batch = new HashMap<>();

for (Map.Entry<String, String> entry : fields.entrySet()) {
batch.put(entry.getKey(), entry.getValue());
if (batch.size() >= MAX_FIELDS) {
redisTemplate.opsForHash().putAll(key + ":part:" + part++, batch);
batch.clear();
}
}

if (!batch.isEmpty()) {
redisTemplate.opsForHash().putAll(key + ":part:" + part++, batch);
}

redisTemplate.opsForValue().set(key + ":parts", String.valueOf(part));
}

public Map<String, String> getLargeHash(String key) {
String partsStr = redisTemplate.opsForValue().get(key + ":parts");
if (partsStr == null) return Collections.emptyMap();

int parts = Integer.parseInt(partsStr);
Map<String, String> result = new HashMap<>();

for (int i = 0; i < parts; i++) {
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key + ":part:" + i);
entries.forEach((k, v) -> result.put(k.toString(), v.toString()));
}

return result;
}
}

Avoiding Slow Commands

  • Replace KEYS with SCAN: KEYS is O(N), whereas SCAN is incremental and non-blocking.
  • Limit Range Operations: Avoid large ranges in commands like ZRANGE or LRANGE.
  • Optimize Lua Scripts: Keep scripts short and efficient.

Reducing Blocking Operations

  • Use Pipelines: Batch multiple commands to reduce round trips.
  • Avoid Long Transactions: Keep transactions short to prevent blocking.
  • Asynchronous Operations: Use async methods where possible.

Handling Memory Fragmentation

  • Use Appropriate Memory Allocator: Redis supports jemalloc, which is optimized for long-running applications.
  • Redis Configurations: Tune maxmemory and maxmemory-policy settings.
  • Periodic Restarts: In some cases, scheduled restarts can help reclaim fragmented memory.

Minimizing Network Latency

  • Deploy Close to Application: Host Redis instances near your application servers.
  • Use Connection Pooling: Reuse connections to reduce overhead.
  • Optimize Serialization: Use efficient serialization methods like Protocol Buffers or MessagePack.

Best Practices and Recommendations

Regular Monitoring

  • Use Redis Monitoring Tools: Leverage redis-cli, Redis Sentinel, or third-party tools.
  • Set Up Alerts: Monitor key metrics and set thresholds for alerts.
  • Logging: Implement logging for slow queries and unusual patterns.

Data Modeling

  • Efficient Data Structures: Choose the right data type for your use case.
  • Data Partitioning: Design your data model to distribute load evenly.
  • Avoid Global Keys: Minimize the use of keys accessed by all clients.

Configuration Tuning

  • Adjust slowlog-log-slower-than: Set appropriate thresholds for your environment.
  • Tune maxmemory-policy: Choose a policy that suits your application's needs.
  • Optimize timeout Settings: Configure client timeouts to prevent hanging connections.

Application Design

  • Graceful Degradation: Implement fallback mechanisms for Redis failures.
  • Timeouts and Retries: Set appropriate timeouts and retry policies.
  • Circuit Breakers: Use patterns to prevent cascading failures.

Security and Stability

  • Use AUTH: Secure your Redis instance with authentication.
  • Limit Command Access: Disable dangerous commands using rename-command.
  • Isolation: Run Redis instances in isolated environments if necessary.

Conclusion

Understanding and managing Redis performance bottlenecks is essential for building robust and efficient applications. By proactively monitoring for issues like hotkeys, bigkeys, slow commands, and others, you can implement strategies to mitigate them effectively.

Key Takeaways:

  • Monitor Regularly: Early detection is crucial for timely intervention.
  • Optimize Proactively: Implement optimization strategies before issues escalate.
  • Test Thoroughly: Validate changes in a controlled environment before production deployment.
  • Stay Informed: Keep up with Redis updates and best practices.

By following these guidelines, you can ensure that your Spring Boot applications leverage Redis efficiently, maintaining high performance and scalability.

References:

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Master Spring Ter
Master Spring Ter

Written by Master Spring Ter

https://chatgpt.com/g/g-dHq8Bxx92-master-spring-ter Specialized ChatGPT expert in Spring Boot, offering insights and guidance for developers.

No responses yet

Write a response