Commit 339eb5eb authored by drnull03's avatar drnull03

Added The project code

parent 4159b6ea
This source diff could not be displayed because it is too large. You can view the blob instead.
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
\ No newline at end of file
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17 (2)" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Consistent Hashing - Quick Reference Guide
## Quick Start
```java
// 1. Create a consistent hash instance
ConsistentHash<String> hash = new ConsistentHash<>(150); // 150 virtual nodes
// 2. Add nodes (servers, caches, etc.)
hash.addNode("server1");
hash.addNode("server2");
hash.addNode("server3");
// 3. Route keys to nodes
String node = hash.getNode("user:12345");
// 4. Remove nodes when needed
hash.removeNode("server1");
```
## Core API Reference
### Constructor
```java
// Default: 150 virtual nodes, MD5 hash function
ConsistentHash<T> hash = new ConsistentHash<>();
// Custom virtual nodes
ConsistentHash<T> hash = new ConsistentHash<>(500);
// Custom hash function
ConsistentHash<T> hash = new ConsistentHash<>(150, new MD5HashFunction());
```
### Node Management
```java
// Add a node
hash.addNode(node);
// Remove a node
hash.removeNode(node);
// Check if node exists
boolean exists = hash.containsNode(node);
// Get all nodes
Set<T> nodes = hash.getNodes();
// Count physical nodes
int count = hash.getNodeCount();
// Count virtual nodes
int ringSize = hash.getRingSize();
// Clear all nodes
hash.clear();
```
### Key Routing
```java
// Get responsible node for a key
T node = hash.getNode(key); // Returns null if ring is empty
// Get keys assigned to a node (for analysis)
List<String> keys = hash.getKeysForNode(node, sampleSize);
// Get preceding node in ring
T predecessor = hash.getPrecedingNode(node);
```
### Analytics & Distribution
```java
// Get distribution across nodes
Map<T, Integer> distribution = hash.getDistribution(sampleSize);
// Returns: {node1: 3000, node2: 3100, node3: 2900} for 10000 keys
// Get formatted statistics
String stats = hash.getStatistics(sampleSize);
System.out.println(stats);
```
## Common Patterns
### Pattern 1: Cache Routing
```java
ConsistentHash<String> cache = new ConsistentHash<>(150);
cache.addNode("cache-01");
cache.addNode("cache-02");
cache.addNode("cache-03");
String cacheServer = cache.getNode("user:" + userId);
Value value = cacheServer.get(key);
if (value == null) {
value = database.get(key);
cacheServer.put(key, value); // Cache miss handling
}
```
### Pattern 2: Load Balancer
```java
ConsistentHash<String> lb = new ConsistentHash<>(150);
lb.addNode("web-server-1");
lb.addNode("web-server-2");
lb.addNode("web-server-3");
// Route request based on session ID
String sessionKey = "session:" + request.getSessionId();
String server = lb.getNode(sessionKey);
request.forward(server);
// Same session always goes to same server
```
### Pattern 3: Database Sharding
```java
ConsistentHash<Integer> sharding = new ConsistentHash<>(150);
sharding.addNode(1); // Shard 1
sharding.addNode(2); // Shard 2
sharding.addNode(3); // Shard 3
Integer shard = sharding.getNode("user:" + userId);
User user = database.shard(shard).get(userId);
```
### Pattern 4: Failure Recovery
```java
ConsistentHash<String> cache = new ConsistentHash<>(150);
cache.addNode("cache-01");
cache.addNode("cache-02");
cache.addNode("cache-03");
// Node fails
cache.removeNode("cache-02"); // ~1/3 of cache-02's keys rehash to others
// Node recovers
cache.addNode("cache-02"); // Automatically rebalances
// Node upgrade/migration
cache.removeNode("cache-01");
cache.addNode("cache-01-v2"); // Minimal disruption
```
## Hash Functions
### MD5HashFunction (Recommended)
```java
ConsistentHash<String> hash = new ConsistentHash<>(150,
new ConsistentHash.MD5HashFunction());
// Benefits:
// - Cryptographically strong
// - Uniform distribution
// - No collisions in practice
// - Industry standard
```
### SimpleHashFunction (Fast Alternative)
```java
ConsistentHash<String> hash = new ConsistentHash<>(150,
new ConsistentHash.SimpleHashFunction());
// Benefits:
// - Very fast
// - Suitable for non-critical applications
// - Less uniform distribution
```
### Custom Hash Function
```java
ConsistentHash<String> hash = new ConsistentHash<>(150,
new ConsistentHash.HashFunction() {
@Override
public long hash(String key) {
// Your implementation
return Math.abs((long) key.hashCode());
}
});
```
## Configuration Recommendations
### Virtual Nodes Per Node Count
| Nodes | Recommended VN | Max Imbalance |
|---|---|---|
| 1-3 | 150 | ~5-10% |
| 3-10 | 150 | ~3-5% |
| 10-50 | 100 | ~3-4% |
| 50-100 | 50 | ~2-3% |
| 100+ | 30 | ~1-2% |
**Rule of Thumb**: Total virtual nodes should be 150-500 for most use cases.
## Performance Tips
### 1. Reduce Virtual Node Count for Large Clusters
```java
// For 100+ nodes
ConsistentHash<String> hash = new ConsistentHash<>(30); // Still balanced
```
### 2. Cache Node Lookups
```java
// Instead of:
String node = hash.getNode(key); // O(log n) lookup
// Cache results for bulk operations
Map<String, String> nodeCache = new HashMap<>();
for (String key : keys) {
nodeCache.put(key, hash.getNode(key));
}
```
### 3. Batch Distribution Queries
```java
// Instead of querying one key at a time
Map<String, Integer> dist = hash.getDistribution(10000);
// This is more efficient than
for (int i = 0; i < 10000; i++) {
hash.getNode("key:" + i);
}
```
## Troubleshooting
### Problem: Uneven Distribution
**Symptom**: One node gets significantly more keys than others
**Causes**:
1. Too few virtual nodes (< 50)
2. Poor hash function
3. Biased key distribution
**Solutions**:
```java
// Increase virtual nodes
ConsistentHash<String> hash = new ConsistentHash<>(500);
// Use better hash function
hash = new ConsistentHash<>(150, new ConsistentHash.MD5HashFunction());
// Analyze with larger sample
Map<String, Integer> dist = hash.getDistribution(100000);
```
### Problem: Too Many Keys Rehashed
**Symptom**: Most keys move when nodes are added/removed
**Causes**:
1. Poor hash function causing clustering
2. Node ID collision
3. Inconsistent node names
**Solutions**:
```java
// Use MD5 hash function
hash = new ConsistentHash<>(150, new ConsistentHash.MD5HashFunction());
// Ensure unique, consistent node names
hash.addNode("server-1-prod-datacenter-a"); // Consistent identifier
// Test redistribution
Map<String, String> before = captureAssignments(hash, 10000);
hash.addNode("new-server");
Map<String, String> after = captureAssignments(hash, 10000);
int movedCount = countMoved(before, after);
double percentage = (movedCount * 100.0) / 10000;
// Should be ~25-35% for adding 1 node to 3-4 nodes
```
## Testing Checklist
```java
// ✓ Test node addition
hash.addNode("node1");
hash.addNode("node2");
assertEquals(2, hash.getNodeCount());
// ✓ Test node removal
hash.removeNode("node1");
assertEquals(1, hash.getNodeCount());
// ✓ Test key consistency
String node1 = hash.getNode("key");
String node2 = hash.getNode("key");
assertEquals(node1, node2);
// ✓ Test load distribution
Map<String, Integer> dist = hash.getDistribution(10000);
dist.values().forEach(count -> {
assertTrue(count > 2500 * 0.6); // At least 60% of ideal
assertTrue(count < 2500 * 1.4); // At most 140% of ideal
});
// ✓ Test key redistribution
Map<String, String> before = getAssignments(hash, 5000);
hash.addNode("node3");
Map<String, String> after = getAssignments(hash, 5000);
int moved = countMoved(before, after);
assertTrue(moved > 5000 * 0.2); // At least 20% moved
assertTrue(moved < 5000 * 0.5); // Less than 50% moved
// ✓ Test edge cases
hash.addNode("node1");
assertNotNull(hash.getNode(""));
assertNotNull(hash.getNode("a".repeat(10000)));
```
## Common Use Cases
| Use Case | Virtual Nodes | Key Type |
|---|---|---|
| Cache Layer | 150 | Cache Key |
| Load Balancer | 150 | Session ID |
| Database Sharding | 100 | User ID or Partition Key |
| CDN | 200 | Content ID |
| Task Distribution | 50 | Task ID |
| Message Queue | 100 | Message Key |
## Advanced Topics
### Weighted Nodes (Custom Implementation)
```java
// Custom implementation for weighted nodes
public class WeightedConsistentHash<T> {
private ConsistentHash<T> hash;
private Map<T, Integer> weights;
public void addWeightedNode(T node, int weight) {
for (int i = 0; i < weight; i++) {
hash.addNode(node); // Add multiple times
}
}
}
```
### Replica Placement (For Fault Tolerance)
```java
// Find replicas for a key
public List<String> getReplicaNodes(String key, int replicaCount) {
List<String> replicas = new ArrayList<>();
String currentNode = hash.getNode(key);
replicas.add(currentNode);
for (int i = 1; i < replicaCount; i++) {
currentNode = hash.getPrecedingNode(currentNode);
replicas.add(currentNode);
}
return replicas;
}
```
## References
- **Original Paper**: "Consistent Hashing and Random Trees" - Karger et al.
- **Memcached**: Uses consistent hashing for node distribution
- **Redis Cluster**: Uses hash slots (similar concept)
- **Cassandra**: Uses token-based consistent hashing
- **DynamoDB**: Uses consistent hashing for partition assignment
---
**Last Updated**: March 2026
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ds</groupId>
<artifactId>ConsistentHashing</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package org.ds;
import java.util.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
/**
* Consistent Hashing implementation with virtual nodes for better load distribution.
*
* Key features:
* - MD5-based hash function for uniform distribution
* - Virtual nodes to improve load balance
* - Support for dynamic node addition/removal
* - Minimal key redistribution on topology changes
*/
public class ConsistentHash<T> {
private static final int DEFAULT_VIRTUAL_NODES = 150;
private final SortedMap<Long, T> ring = new TreeMap<>();
private final int virtualNodes;
private final HashFunction hashFunction;
/**
* Create a ConsistentHash with default number of virtual nodes
*/
public ConsistentHash() {
this(DEFAULT_VIRTUAL_NODES);
}
/**
* Create a ConsistentHash with specified number of virtual nodes
*
* @param virtualNodes number of virtual nodes per physical node
*/
public ConsistentHash(int virtualNodes) {
this(virtualNodes, new MD5HashFunction());
}
/**
* Create a ConsistentHash with custom hash function
*
* @param virtualNodes number of virtual nodes per physical node
* @param hashFunction custom hash function implementation
*/
public ConsistentHash(int virtualNodes, HashFunction hashFunction) {
this.virtualNodes = virtualNodes;
this.hashFunction = hashFunction;
}
/**
* Add a node to the ring
*
* @param node the node to add
*/
public void addNode(T node) {
for (int i = 0; i < virtualNodes; i++) {
long hash = hashFunction.hash(node.toString() + ":" + i);
ring.put(hash, node);
}
}
/**
* Remove a node from the ring
*
* @param node the node to remove
*/
public void removeNode(T node) {
for (int i = 0; i < virtualNodes; i++) {
long hash = hashFunction.hash(node.toString() + ":" + i);
ring.remove(hash);
}
}
/**
* Get the node responsible for the given key
*
* @param key the key to look up
* @return the responsible node, or null if ring is empty
*/
public T getNode(String key) {
if (ring.isEmpty()) {
return null;
}
long hash = hashFunction.hash(key);
// Find the first node hash that is greater than or equal to key hash
SortedMap<Long, T> tailMap = ring.tailMap(hash);
if (!tailMap.isEmpty()) {
return tailMap.get(tailMap.firstKey());
}
// Wrap around: return the first node in the ring
return ring.get(ring.firstKey());
}
/**
* Get all nodes in the ring
*
* @return set of unique nodes
*/
public Set<T> getNodes() {
return new HashSet<>(ring.values());
}
/**
* Get the number of unique physical nodes
*
* @return number of unique nodes
*/
public int getNodeCount() {
return getNodes().size();
}
/**
* Check if the ring contains a node
*
* @param node the node to check
* @return true if node exists in ring
*/
public boolean containsNode(T node) {
return getNodes().contains(node);
}
/**
* Get the ring size (total virtual nodes)
*
* @return number of virtual nodes in ring
*/
public int getRingSize() {
return ring.size();
}
/**
* Get statistics about key distribution across nodes
*
* @param sampleSize number of keys to sample for distribution analysis
* @return map of node to count of keys assigned to it
*/
public Map<T, Integer> getDistribution(int sampleSize) {
Map<T, Integer> distribution = new HashMap<>();
for (T node : getNodes()) {
distribution.put(node, 0);
}
for (int i = 0; i < sampleSize; i++) {
T node = getNode("key:" + i);
distribution.put(node, distribution.get(node) + 1);
}
return distribution;
}
/**
* Get keys assigned to a specific node
*
* @param node the target node
* @param sampleSize number of sample keys to check
* @return list of keys assigned to this node
*/
public List<String> getKeysForNode(T node, int sampleSize) {
List<String> keys = new ArrayList<>();
for (int i = 0; i < sampleSize; i++) {
String key = "key:" + i;
if (getNode(key).equals(node)) {
keys.add(key);
}
}
return keys;
}
/**
* Get the node that precedes the given node in the ring
*
* @param node the target node
* @return the preceding node
*/
public T getPrecedingNode(T node) {
Set<T> nodes = getNodes();
if (nodes.size() <= 1) {
return null;
}
// Find the hash positions of all instances of this node
long lastHash = Long.MIN_VALUE;
for (Map.Entry<Long, T> entry : ring.entrySet()) {
if (entry.getValue().equals(node)) {
lastHash = entry.getKey();
}
}
// Find the preceding hash position
SortedMap<Long, T> headMap = ring.headMap(lastHash);
if (!headMap.isEmpty()) {
return headMap.get(headMap.lastKey());
}
return ring.get(ring.lastKey());
}
/**
* Clear all nodes from the ring
*/
public void clear() {
ring.clear();
}
/**
* Interface for hash functions
*/
public interface HashFunction {
long hash(String key);
}
/**
* MD5-based hash function for consistent hashing
*/
public static class MD5HashFunction implements HashFunction {
@Override
public long hash(String key) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(key.getBytes(StandardCharsets.UTF_8));
// Convert first 8 bytes of MD5 to long (unsigned)
long hash = 0;
for (int i = 0; i < 8; i++) {
hash = (hash << 8) | (messageDigest[i] & 0xFF);
}
return Math.abs(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not available", e);
}
}
}
/**
* Simple hash function based on String.hashCode() - less uniform but faster
*/
public static class SimpleHashFunction implements HashFunction {
@Override
public long hash(String key) {
return Math.abs((long) key.hashCode());
}
}
/**
* Get detailed statistics about the hash ring
*
* @return formatted statistics string
*/
public String getStatistics(int sampleSize) {
StringBuilder sb = new StringBuilder();
sb.append("Consistent Hash Statistics:\n");
sb.append("============================\n");
sb.append("Physical Nodes: ").append(getNodeCount()).append("\n");
sb.append("Virtual Nodes: ").append(getRingSize()).append("\n");
sb.append("Virtual Nodes per Physical Node: ").append(virtualNodes).append("\n");
sb.append("\nKey Distribution (based on ").append(sampleSize).append(" samples):\n");
Map<T, Integer> distribution = getDistribution(sampleSize);
distribution.entrySet().stream()
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
.forEach(entry -> {
double percentage = (entry.getValue() * 100.0) / sampleSize;
sb.append(" ").append(entry.getKey()).append(": ")
.append(entry.getValue()).append(" (").append(String.format("%.2f%%", percentage)).append(")\n");
});
return sb.toString();
}
}
This diff is collapsed.
https://git.hiast.edu.sy/diaa.hanna/conshashing
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment