├── .gitignore ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── ssedano │ └── hash │ └── JumpConsistentHash.java └── test └── java └── com └── github └── ssedano └── hash └── JumpConsistentHashTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | target/ 4 | .settings 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jump Consistent Hash 2 | 3 | A Java implementation of the jump consistent hash from Lamping and Veach. 4 | 5 | Paper: [http://arxiv.org/ftp/arxiv/papers/1406/1406.2294.pdf](http://arxiv.org/ftp/arxiv/papers/1406/1406.2294.pdf) 6 | 7 | ## Abstract 8 | 9 | Extracted from the paper 10 | 11 | > We present jump consistent hash, a fast, minimal memory, consistent hash algorithm that can 12 | be expressed in about 5 lines of code. In comparison to the algorithm of Karger et al., jump 13 | consistent hash requires no storage, is faster, and does a better job of evenly dividing the key 14 | space among the buckets and of evenly dividing the workload when the number of buckets 15 | changes. Its main limitation is that the buckets must be numbered sequentially, which makes it 16 | more suitable for data storage applications than for distributed web caching. 17 | 18 | # Usage 19 | 20 | Add the dependency to your pom.xml 21 | 22 | 23 | com.github.ssedano 24 | jump-consistent-hash 25 | 1.0.0 26 | 27 | 28 | Or download the `jar` from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cjump-consistent-hash) and add it to your classpath. 29 | 30 | Clone the repo and build it with maven and add the `jar` to your classpath. 31 | 32 | $ git clone https://github.com/ssedano/jump-consistent-hash 33 | $ cd jump-consistent-hash 34 | $ mvn clean install 35 | 36 | Or copy the class and add it to your code base. 37 | 38 | Then import it and use it. 39 | 40 | int jumpConsistentHash = JumpConsistentHash.jumpConsistenHash(key, buckets); 41 | 42 | Additional info in the `javadoc`. 43 | 44 | # License 45 | 46 | Licensed under [Apache 2](http://www.apache.org/licenses/LICENSE-2.0). 47 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.github.ssedano 4 | jump-consistent-hash 5 | 1.0.0 6 | jar 7 | Jump consistent hash 8 | 9 | Java implementation of the jump consistent hashing algorithm from Lamping and Veach. Paper http://arxiv.org/ftp/arxiv/papers/1406/1406.2294.pdf. 10 | 11 | https://github.com/ssedano/jump-consistent-hash 12 | 2014 13 | 14 | 15 | Apache License, Version 2.0 16 | http://www.apache.org/licenses/LICENSE-2.0.txt 17 | repo 18 | 19 | 20 | 21 | https://github.com/ssedano/jump-consistent-hash 22 | scm:git:https://github.com/ssedano/jump-consistent-hash.git 23 | scm:git:git@github.com:ssedano/jump-consistent-hash.git 24 | 25 | 26 | 27 | Serafin Sedano 28 | serafin.sedano@gmail.com 29 | http://github.com/ssedano 30 | 31 | 32 | 33 | 34 | ossrh 35 | https://oss.sonatype.org/content/repositories/snapshots 36 | 37 | 38 | ossrh 39 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 40 | 41 | 42 | 43 | UTF-8 44 | 45 | 46 | 47 | junit 48 | junit 49 | [4.13.1,) 50 | test 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-compiler-plugin 58 | 3.1 59 | 60 | 1.8 61 | 1.8 62 | 63 | 64 | 65 | 66 | 67 | 68 | release 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-source-plugin 74 | 2.1.2 75 | 76 | 77 | attach-sources 78 | 79 | jar-no-fork 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-javadoc-plugin 87 | 2.9.1 88 | 89 | 90 | attach-javadocs 91 | 92 | jar 93 | 94 | 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-gpg-plugin 100 | 1.5 101 | 102 | 103 | sign-artifacts 104 | verify 105 | 106 | sign 107 | 108 | 109 | 110 | 111 | 112 | org.sonatype.plugins 113 | nexus-staging-maven-plugin 114 | 1.6.2 115 | true 116 | 117 | ossrh 118 | https://oss.sonatype.org/ 119 | true 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/ssedano/hash/JumpConsistentHash.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 ssedano 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | */ 17 | package com.github.ssedano.hash; 18 | 19 | /** 20 | * {@link #jumpConsistentHash(long, int)} accepts "a 64-bit key and the number 21 | * of buckets. It outputs a number in the range [0, buckets)." Paper. 23 | *

24 | * The C++ implementation they provide is as follows: 25 | * 26 | *

 27 |  * {@code
 28 |  *  int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
 29 |  *   int64_t b = ­-1, j = 0;
 30 |  *     while (j < num_buckets) {
 31 |  *       b = j;
 32 |  *       key = key * 2862933555777941757ULL + 1;
 33 |  *       j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
 34 |  *     }
 35 |  *     return b;
 36 |  *   }}
 37 |  * 
38 | * 39 | * @author Serafin Sedano 40 | */ 41 | public final class JumpConsistentHash { 42 | private static final long UNSIGNED_MASK = 0x7fffffffffffffffL; 43 | 44 | private static final long JUMP = 1L << 31; 45 | 46 | private static final long CONSTANT = Long 47 | .parseUnsignedLong("2862933555777941757"); 48 | 49 | private JumpConsistentHash() { 50 | throw new AssertionError( 51 | "No com.github.ssedano.hash.JumpConsistentHash instances for you!"); 52 | } 53 | 54 | /** 55 | * Accepts "a 64-bit key and the number of buckets. It outputs a number in 56 | * the range [0, buckets].". This implementation uses as a key the 57 | * {@link Object#hashCode()} of the supplied argument. 58 | * 59 | * @param o 60 | * object to store 61 | * @param buckets 62 | * number of available buckets 63 | * @return the hash of the object o 64 | */ 65 | public static int jumpConsistentHash(final Object o, final int buckets) { 66 | return jumpConsistentHash(o.hashCode(), buckets); 67 | } 68 | 69 | /** 70 | * Accepts "a 64-bit key and the number of buckets. It outputs a number in 71 | * the range [0, buckets]." 72 | * 73 | * @param key 74 | * key to store 75 | * @param buckets 76 | * number of available buckets 77 | * @return the hash of the key 78 | * @throws IllegalArgumentException 79 | * if buckets is less than 0 80 | */ 81 | public static int jumpConsistentHash(final long key, final int buckets) { 82 | checkBuckets(buckets); 83 | long k = key; 84 | long b = -1; 85 | long j = 0; 86 | 87 | while (j < buckets) { 88 | b = j; 89 | k = k * CONSTANT + 1L; 90 | 91 | j = (long) ((b + 1L) * (JUMP / toDouble((k >>> 33) + 1L))); 92 | } 93 | return (int) b; 94 | } 95 | 96 | private static void checkBuckets(final int buckets) { 97 | if (buckets < 0) { 98 | throw new IllegalArgumentException("Buckets cannot be less than 0"); 99 | } 100 | } 101 | 102 | private static double toDouble(final long n) { 103 | double d = n & UNSIGNED_MASK; 104 | if (n < 0) { 105 | d += 0x1.0p63; 106 | } 107 | return d; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/github/ssedano/hash/JumpConsistentHashTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 ssedano 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | */ 17 | package com.github.ssedano.hash; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertTrue; 21 | 22 | import java.util.IntSummaryStatistics; 23 | import java.util.Map; 24 | import java.util.TreeMap; 25 | import java.util.concurrent.ThreadLocalRandom; 26 | 27 | import org.junit.Test; 28 | 29 | import com.github.ssedano.hash.JumpConsistentHash; 30 | 31 | /** 32 | * @author Serafin Sedano 33 | */ 34 | public class JumpConsistentHashTest { 35 | 36 | @Test 37 | public void hashInRange() { 38 | int bucket = JumpConsistentHash.jumpConsistentHash(ThreadLocalRandom 39 | .current().nextLong(), 10); 40 | assertTrue("Expected bucket in range [0, 10) but was " + bucket, 41 | bucket > -1 && bucket < 10); 42 | } 43 | 44 | @Test(expected = IllegalArgumentException.class) 45 | public void negativeBucketsEqualsMinusOne() { 46 | JumpConsistentHash.jumpConsistentHash(1L, -1); 47 | } 48 | 49 | @Test 50 | public void hashInRangeForObject() { 51 | int bucket = JumpConsistentHash.jumpConsistentHash(new Object(), 10); 52 | assertTrue("Expected bucket in range [0, 10) but was " + bucket, 53 | bucket > -1 && bucket < 10); 54 | } 55 | 56 | @Test(expected = IllegalArgumentException.class) 57 | public void negativeBucketsEqualsMinusOnForObjecte() { 58 | JumpConsistentHash.jumpConsistentHash(new Object(), -1); 59 | } 60 | 61 | @Test 62 | public void assignsToSameBucket() { 63 | int bucket1 = JumpConsistentHash.jumpConsistentHash(1L, 10); 64 | int bucket2 = JumpConsistentHash.jumpConsistentHash(1L, 11); 65 | 66 | assertEquals(bucket1, bucket2); 67 | } 68 | 69 | @Test 70 | public void assignsObjectToSameBucket() { 71 | int bucket1 = JumpConsistentHash.jumpConsistentHash("object", 10); 72 | int bucket2 = JumpConsistentHash.jumpConsistentHash("object", 11); 73 | 74 | assertEquals(bucket1, bucket2); 75 | } 76 | 77 | /** 78 | * Paper states a standar error of 0.00000000764 and (0.99999998, 79 | * 1.00000002) 99% confidence interval. 80 | * 81 | */ 82 | @Test 83 | public void keyDistribution() { 84 | Map sizes = new TreeMap<>(); 85 | 86 | ThreadLocalRandom r = ThreadLocalRandom.current(); 87 | int keys = 100_000; 88 | int buckets = 10; 89 | 90 | for (int i = 0; i < keys; i++) { 91 | int b = JumpConsistentHash.jumpConsistentHash(r.nextInt(keys), 92 | buckets); 93 | sizes.compute(b, (k, v) -> v == null ? 0 : v + 1); 94 | } 95 | assertEquals(buckets, sizes.size()); 96 | IntSummaryStatistics stats = sizes.values().stream().mapToInt(i -> i) 97 | .summaryStatistics(); 98 | 99 | double percent99 = (double) keys / (double) buckets * 0.01d; 100 | 101 | assertTrue( 102 | "Expected over 99% avg (" + percent99 + ") but was " 103 | + stats.getAverage(), stats.getAverage() > percent99); 104 | } 105 | } 106 | --------------------------------------------------------------------------------