diff --git a/pom.xml b/pom.xml index 89935e3..212ef91 100644 --- a/pom.xml +++ b/pom.xml @@ -15,11 +15,32 @@ junit 4.12 + + org.powermock + powermock-core + ${powermock.version} + test + + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + moe.cnkirito.sample consistent-hash 1.0-SNAPSHOT + + 2.0.9 + @@ -34,4 +55,4 @@ - \ No newline at end of file + diff --git a/src/main/java/moe/cnkirito/consistenthash/AbstractHashStrategy.java b/src/main/java/moe/cnkirito/consistenthash/AbstractHashStrategy.java new file mode 100644 index 0000000..de21702 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/AbstractHashStrategy.java @@ -0,0 +1,17 @@ +package moe.cnkirito.consistenthash; + +/** + * @author daofeng.xjf + * @date 2019/2/15 + */ +public interface AbstractHashStrategy { + + /** + * 根据服务器地址标识符生成在一致性hash环上的hash值 + * + * @param origin 服务器地址标识字符串 + * @return 一致性hash环上的hash值 + */ + int getHashCode(String origin); + +} diff --git a/src/main/java/moe/cnkirito/consistenthash/ConsistentHashLoadBalancer.java b/src/main/java/moe/cnkirito/consistenthash/ConsistentHashLoadBalancer.java deleted file mode 100644 index b88e132..0000000 --- a/src/main/java/moe/cnkirito/consistenthash/ConsistentHashLoadBalancer.java +++ /dev/null @@ -1,48 +0,0 @@ -package moe.cnkirito.consistenthash; - - -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** - * @author daofeng.xjf - * @date 2019/2/15 - */ -public class ConsistentHashLoadBalancer implements LoadBalancer{ - - private HashStrategy hashStrategy = new FnvHashStrategy(); - - private final static int VIRTUAL_NODE_SIZE = 10; - private final static String VIRTUAL_NODE_SUFFIX = "&&"; - - @Override - public Server select(List servers, Invocation invocation) { - int invocationHashCode = hashStrategy.getHashCode(invocation.getHashKey()); - TreeMap ring = buildConsistentHashRing(servers); - Server server = locate(ring, invocationHashCode); - return server; - } - - private Server locate(TreeMap ring, int invocationHashCode) { - // 向右找到第一个 key - Map.Entry locateEntry = ring.ceilingEntry(invocationHashCode); - if (locateEntry == null) { - // 想象成一个环,超过尾部则取第一个 key - locateEntry = ring.firstEntry(); - } - return locateEntry.getValue(); - } - - private TreeMap buildConsistentHashRing(List servers) { - TreeMap virtualNodeRing = new TreeMap<>(); - for (Server server : servers) { - for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) { - // 新增虚拟节点的方式如果有影响,也可以抽象出一个由物理节点扩展虚拟节点的类 - virtualNodeRing.put(hashStrategy.getHashCode(server.getUrl() + VIRTUAL_NODE_SUFFIX + i), server); - } - } - return virtualNodeRing; - } - -} diff --git a/src/main/java/moe/cnkirito/consistenthash/HashStrategy.java b/src/main/java/moe/cnkirito/consistenthash/HashStrategy.java deleted file mode 100644 index b451260..0000000 --- a/src/main/java/moe/cnkirito/consistenthash/HashStrategy.java +++ /dev/null @@ -1,11 +0,0 @@ -package moe.cnkirito.consistenthash; - -/** - * @author daofeng.xjf - * @date 2019/2/15 - */ -public interface HashStrategy { - - int getHashCode(String origin); - -} diff --git a/src/main/java/moe/cnkirito/consistenthash/JdkHashCodeStrategy.java b/src/main/java/moe/cnkirito/consistenthash/JdkHashCodeStrategy.java deleted file mode 100644 index f6cdfa3..0000000 --- a/src/main/java/moe/cnkirito/consistenthash/JdkHashCodeStrategy.java +++ /dev/null @@ -1,14 +0,0 @@ -package moe.cnkirito.consistenthash; - -/** - * @author daofeng.xjf - * @date 2019/2/15 - */ -public class JdkHashCodeStrategy implements HashStrategy { - - @Override - public int getHashCode(String origin) { - return origin.hashCode(); - } - -} diff --git a/src/main/java/moe/cnkirito/consistenthash/LoadBalanceable.java b/src/main/java/moe/cnkirito/consistenthash/LoadBalanceable.java new file mode 100644 index 0000000..21bf3c9 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/LoadBalanceable.java @@ -0,0 +1,22 @@ +package moe.cnkirito.consistenthash; + +import moe.cnkirito.consistenthash.bo.Invocation; +import moe.cnkirito.consistenthash.bo.Server; + +import java.util.List; + +/** + * @author daofeng.xjf + * @date 2019/2/16 + */ +public interface LoadBalanceable { + + /** + * 根据数据的hash值在一致性hash环上选择第一个服务器 + * + * @param servers 服务器列表 + * @param invocation 包含数据hash值的请求 + * @return 选择到的服务器 + */ + Server select(List servers, Invocation invocation); +} diff --git a/src/main/java/moe/cnkirito/consistenthash/LoadBalancer.java b/src/main/java/moe/cnkirito/consistenthash/LoadBalancer.java deleted file mode 100644 index 86edd02..0000000 --- a/src/main/java/moe/cnkirito/consistenthash/LoadBalancer.java +++ /dev/null @@ -1,12 +0,0 @@ -package moe.cnkirito.consistenthash; - -import java.util.List; - -/** - * @author daofeng.xjf - * @date 2019/2/16 - */ -public interface LoadBalancer { - - Server select(List servers, Invocation invocation); -} diff --git a/src/main/java/moe/cnkirito/consistenthash/StatisticsUtil.java b/src/main/java/moe/cnkirito/consistenthash/StatisticsUtil.java deleted file mode 100644 index c9b6c0b..0000000 --- a/src/main/java/moe/cnkirito/consistenthash/StatisticsUtil.java +++ /dev/null @@ -1,39 +0,0 @@ -package moe.cnkirito.consistenthash; - -/** - * @author daofeng.xjf - * @date 2019/2/16 - */ -public class StatisticsUtil { - - //方差s^2=[(x1-x)^2 +...(xn-x)^2]/n - public static double variance(Long[] x) { - int m = x.length; - double sum = 0; - for (int i = 0; i < m; i++) {//求和 - sum += x[i]; - } - double dAve = sum / m;//求平均值 - double dVar = 0; - for (int i = 0; i < m; i++) {//求方差 - dVar += (x[i] - dAve) * (x[i] - dAve); - } - return dVar / m; - } - - //标准差σ=sqrt(s^2) - public static double standardDeviation(Long[] x) { - int m = x.length; - double sum = 0; - for (int i = 0; i < m; i++) {//求和 - sum += x[i]; - } - double dAve = sum / m;//求平均值 - double dVar = 0; - for (int i = 0; i < m; i++) {//求方差 - dVar += (x[i] - dAve) * (x[i] - dAve); - } - return Math.sqrt(dVar / m); - } - -} diff --git a/src/main/java/moe/cnkirito/consistenthash/Invocation.java b/src/main/java/moe/cnkirito/consistenthash/bo/Invocation.java similarity index 82% rename from src/main/java/moe/cnkirito/consistenthash/Invocation.java rename to src/main/java/moe/cnkirito/consistenthash/bo/Invocation.java index 6a01ca5..bf90acc 100644 --- a/src/main/java/moe/cnkirito/consistenthash/Invocation.java +++ b/src/main/java/moe/cnkirito/consistenthash/bo/Invocation.java @@ -1,19 +1,18 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.bo; /** * @author daofeng.xjf * @date 2019/2/15 */ public class Invocation { - public Invocation() { - } + private String hashKey; + + public Invocation() {} public Invocation(String hashKey) { this.hashKey = hashKey; } - private String hashKey; - public String getHashKey() { return hashKey; } diff --git a/src/main/java/moe/cnkirito/consistenthash/Server.java b/src/main/java/moe/cnkirito/consistenthash/bo/Server.java similarity index 81% rename from src/main/java/moe/cnkirito/consistenthash/Server.java rename to src/main/java/moe/cnkirito/consistenthash/bo/Server.java index 219749a..b5a5a35 100644 --- a/src/main/java/moe/cnkirito/consistenthash/Server.java +++ b/src/main/java/moe/cnkirito/consistenthash/bo/Server.java @@ -1,4 +1,4 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.bo; /** * @author daofeng.xjf @@ -6,15 +6,14 @@ */ public class Server { - public Server() { - } + private String url; + + public Server() {} public Server(String url) { this.url = url; } - private String url; - public String getUrl() { return url; } diff --git a/src/main/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashLoadBalancer.java b/src/main/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashLoadBalancer.java new file mode 100644 index 0000000..0fc3893 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashLoadBalancer.java @@ -0,0 +1,30 @@ +package moe.cnkirito.consistenthash.load.balancer; + + +import moe.cnkirito.consistenthash.AbstractHashStrategy; +import moe.cnkirito.consistenthash.bo.Invocation; +import moe.cnkirito.consistenthash.LoadBalanceable; +import moe.cnkirito.consistenthash.bo.Server; +import moe.cnkirito.consistenthash.strategy.FnvHashStrategy; +import moe.cnkirito.consistenthash.strategy.HashStratetyFactory; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * @author daofeng.xjf + * @date 2019/2/15 + */ +public class ConsistentHashLoadBalancer implements LoadBalanceable { + + private static final AbstractHashStrategy hashStrategy = HashStratetyFactory.getHashStrategy(); + + @Override + public Server select(List servers, Invocation invocation) { + int invocationHashCode = hashStrategy.getHashCode(invocation.getHashKey()); + ConsistentHashRing consistentHashRing = ConsistentHashRing.getInstance(servers); + return consistentHashRing.locate(invocationHashCode); + } + +} diff --git a/src/main/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashRing.java b/src/main/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashRing.java new file mode 100644 index 0000000..44e9320 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashRing.java @@ -0,0 +1,68 @@ +package moe.cnkirito.consistenthash.load.balancer; + +import com.google.common.annotations.VisibleForTesting; +import moe.cnkirito.consistenthash.AbstractHashStrategy; +import moe.cnkirito.consistenthash.bo.Server; +import moe.cnkirito.consistenthash.strategy.FnvHashStrategy; +import moe.cnkirito.consistenthash.strategy.HashStratetyFactory; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static moe.cnkirito.consistenthash.load.balancer.constant.HashRingConstant.VIRTUAL_NODE_SIZE; +import static moe.cnkirito.consistenthash.load.balancer.constant.HashRingConstant.VIRTUAL_NODE_SUFFIX; + +/** + * 一致性hash环 + * + * @author dailj + * @date 2022/11/25 15:54 + */ +public class ConsistentHashRing { + private static ConsistentHashRing instance; + private static TreeMap virtualNodeRing; + private static AbstractHashStrategy hashStrategy = HashStratetyFactory.getHashStrategy(); + + public static ConsistentHashRing getInstance(List servers) { + if (instance == null) { + synchronized (ConsistentHashRing.class) { + if (instance == null) { + instance = new ConsistentHashRing(servers); + } + } + } + return instance; + } + + public Server locate(Integer invocationHashCode) { + // 向右找到第一个 key + Map.Entry locateEntry = virtualNodeRing.ceilingEntry(invocationHashCode); + if (locateEntry == null) { + // 想象成一个环,超过尾部则取第一个 key + locateEntry = virtualNodeRing.firstEntry(); + } + return locateEntry.getValue(); + } + + private ConsistentHashRing(List servers) { + init(servers); + } + + private ConsistentHashRing() {} + + private void init(List servers) { + virtualNodeRing = buildConsistentHashRing(servers); + } + + private TreeMap buildConsistentHashRing(List servers) { + TreeMap virtualNodeRing = new TreeMap<>(); + for (Server server : servers) { + for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) { + // 新增虚拟节点的方式如果有影响,也可以抽象出一个由物理节点扩展虚拟节点的类 + virtualNodeRing.put(hashStrategy.getHashCode(server.getUrl() + VIRTUAL_NODE_SUFFIX + i), server); + } + } + return virtualNodeRing; + } +} diff --git a/src/main/java/moe/cnkirito/consistenthash/KetamaConsistentHashLoadBalancer.java b/src/main/java/moe/cnkirito/consistenthash/load/balancer/KetamaConsistentHashLoadBalancer.java similarity index 65% rename from src/main/java/moe/cnkirito/consistenthash/KetamaConsistentHashLoadBalancer.java rename to src/main/java/moe/cnkirito/consistenthash/load/balancer/KetamaConsistentHashLoadBalancer.java index c307800..98e266d 100644 --- a/src/main/java/moe/cnkirito/consistenthash/KetamaConsistentHashLoadBalancer.java +++ b/src/main/java/moe/cnkirito/consistenthash/load/balancer/KetamaConsistentHashLoadBalancer.java @@ -1,8 +1,12 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.load.balancer; +import moe.cnkirito.consistenthash.bo.Invocation; +import moe.cnkirito.consistenthash.LoadBalanceable; +import moe.cnkirito.consistenthash.bo.Server; +import moe.cnkirito.consistenthash.util.Md5Util; + import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -11,27 +15,16 @@ * @author daofeng.xjf * @date 2019/2/15 */ -public class KetamaConsistentHashLoadBalancer implements LoadBalancer { - - private static MessageDigest md5Digest; - - static { - try { - md5Digest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("MD5 not supported", e); - } - } - - private final static int VIRTUAL_NODE_SIZE = 12; - private final static String VIRTUAL_NODE_SUFFIX = "-"; +public class KetamaConsistentHashLoadBalancer implements LoadBalanceable { + private static final Integer BASE_SIZE = 4; + private static final int VIRTUAL_NODE_SIZE = 12; + private static final String VIRTUAL_NODE_SUFFIX = "-"; @Override public Server select(List servers, Invocation invocation) { long invocationHashCode = getHashCode(invocation.getHashKey()); TreeMap ring = buildConsistentHashRing(servers); - Server server = locate(ring, invocationHashCode); - return server; + return locate(ring, invocationHashCode); } private Server locate(TreeMap ring, Long invocationHashCode) { @@ -47,12 +40,12 @@ private Server locate(TreeMap ring, Long invocationHashCode) { private TreeMap buildConsistentHashRing(List servers) { TreeMap virtualNodeRing = new TreeMap<>(); for (Server server : servers) { - for (int i = 0; i < VIRTUAL_NODE_SIZE / 4; i++) { + for (int i = 0; i < VIRTUAL_NODE_SIZE / BASE_SIZE; i++) { byte[] digest = computeMd5(server.getUrl() + VIRTUAL_NODE_SUFFIX + i); - for (int h = 0; h < 4; h++) { - Long k = ((long) (digest[3 + h * 4] & 0xFF) << 24) - | ((long) (digest[2 + h * 4] & 0xFF) << 16) - | ((long) (digest[1 + h * 4] & 0xFF) << 8) + for (int h = 0; h < BASE_SIZE; h++) { + Long k = ((long) (digest[3 + h * BASE_SIZE] & 0xFF) << 24) + | ((long) (digest[2 + h * BASE_SIZE] & 0xFF) << 16) + | ((long) (digest[1 + h * BASE_SIZE] & 0xFF) << 8) | (digest[h * 4] & 0xFF); virtualNodeRing.put(k, server); @@ -64,17 +57,16 @@ private TreeMap buildConsistentHashRing(List servers) { private long getHashCode(String origin) { byte[] bKey = computeMd5(origin); - long rv = ((long) (bKey[3] & 0xFF) << 24) + return ((long) (bKey[3] & 0xFF) << 24) | ((long) (bKey[2] & 0xFF) << 16) | ((long) (bKey[1] & 0xFF) << 8) | (bKey[0] & 0xFF); - return rv; } private static byte[] computeMd5(String k) { MessageDigest md5; try { - md5 = (MessageDigest) md5Digest.clone(); + md5 = (MessageDigest) Md5Util.MD5_DIGEST.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException("clone of MD5 not supported", e); } diff --git a/src/main/java/moe/cnkirito/consistenthash/load/balancer/constant/HashRingConstant.java b/src/main/java/moe/cnkirito/consistenthash/load/balancer/constant/HashRingConstant.java new file mode 100644 index 0000000..db65993 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/load/balancer/constant/HashRingConstant.java @@ -0,0 +1,10 @@ +package moe.cnkirito.consistenthash.load.balancer.constant; + +/** + * @author dailj + * @date 2022/11/25 16:20 + */ +public class HashRingConstant { + public static final int VIRTUAL_NODE_SIZE = 10; + public static final String VIRTUAL_NODE_SUFFIX = "&&"; +} diff --git a/src/main/java/moe/cnkirito/consistenthash/CRCHashStrategy.java b/src/main/java/moe/cnkirito/consistenthash/strategy/CrcHashStrategy.java similarity index 82% rename from src/main/java/moe/cnkirito/consistenthash/CRCHashStrategy.java rename to src/main/java/moe/cnkirito/consistenthash/strategy/CrcHashStrategy.java index 5ead2fd..899609f 100644 --- a/src/main/java/moe/cnkirito/consistenthash/CRCHashStrategy.java +++ b/src/main/java/moe/cnkirito/consistenthash/strategy/CrcHashStrategy.java @@ -1,14 +1,16 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.strategy; -import java.nio.charset.Charset; +import moe.cnkirito.consistenthash.AbstractHashStrategy; + +import java.nio.charset.StandardCharsets; /** * @author daofeng.xjf * @date 2019/2/16 */ -public class CRCHashStrategy implements HashStrategy { +public class CrcHashStrategy implements AbstractHashStrategy { - private static final int LOOKUP_TABLE[] = {0x0000, 0x1021, 0x2042, 0x3063, + private static final int[] LOOKUP_TABLE = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, @@ -46,10 +48,10 @@ public class CRCHashStrategy implements HashStrategy { * Create a CRC16 checksum from the bytes. implementation is from * mp911de/lettuce, modified with some more optimizations * - * @param bytes + * @param bytes 字节数组 * @return CRC16 as integer value */ - public static int getCRC16(byte[] bytes) { + public static int getCrc16(byte[] bytes) { int crc = 0x0000; for (byte b : bytes) { @@ -58,14 +60,16 @@ public static int getCRC16(byte[] bytes) { return crc & 0xFFFF; } - public static int getCRC16(String key) { - return getCRC16(key.getBytes(Charset.forName("UTF-8"))); + public static int getCrc16(String key) { + return getCrc16(key.getBytes(StandardCharsets.UTF_8)); } @Override public int getHashCode(String origin) { - // optimization with modulo operator with power of 2 - // equivalent to getCRC16(key) % 16384 - return getCRC16(origin) & (16384 - 1); + /** + * optimization with modulo operator with power of + * equivalent to getCRC16(key) % 16384 + */ + return getCrc16(origin) & (16384 - 1); } } diff --git a/src/main/java/moe/cnkirito/consistenthash/FnvHashStrategy.java b/src/main/java/moe/cnkirito/consistenthash/strategy/FnvHashStrategy.java similarity index 62% rename from src/main/java/moe/cnkirito/consistenthash/FnvHashStrategy.java rename to src/main/java/moe/cnkirito/consistenthash/strategy/FnvHashStrategy.java index 42871e9..6ab1457 100644 --- a/src/main/java/moe/cnkirito/consistenthash/FnvHashStrategy.java +++ b/src/main/java/moe/cnkirito/consistenthash/strategy/FnvHashStrategy.java @@ -1,4 +1,6 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.strategy; + +import moe.cnkirito.consistenthash.AbstractHashStrategy; /** * @author daofeng.xjf @@ -6,17 +8,17 @@ *

* FNV1_32_HASH 算法 */ -public class FnvHashStrategy implements HashStrategy { +public class FnvHashStrategy implements AbstractHashStrategy { private static final long FNV_32_INIT = 2166136261L; private static final int FNV_32_PRIME = 16777619; @Override public int getHashCode(String origin) { - final int p = FNV_32_PRIME; int hash = (int) FNV_32_INIT; - for (int i = 0; i < origin.length(); i++) - hash = (hash ^ origin.charAt(i)) * p; + for (int i = 0; i < origin.length(); i++) { + hash = (hash ^ origin.charAt(i)) * FNV_32_PRIME; + } hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; diff --git a/src/main/java/moe/cnkirito/consistenthash/strategy/HashStratetyFactory.java b/src/main/java/moe/cnkirito/consistenthash/strategy/HashStratetyFactory.java new file mode 100644 index 0000000..100d5a7 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/strategy/HashStratetyFactory.java @@ -0,0 +1,27 @@ +package moe.cnkirito.consistenthash.strategy; + +import moe.cnkirito.consistenthash.AbstractHashStrategy; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author dailj + * @date 2022/11/25 17:47 + */ +public class HashStratetyFactory { + private static String type = "fnv"; + private static ConcurrentHashMap hashStrategyMap = new ConcurrentHashMap(); + + static { + hashStrategyMap.put("crc", new CrcHashStrategy()); + hashStrategyMap.put("fnv", new FnvHashStrategy()); + hashStrategyMap.put("jdk", new JdkHashCodeStrategy()); + hashStrategyMap.put("ket", new KetamaHashStrategy()); + hashStrategyMap.put("mur", new MurmurHashStrategy()); + } + + public static AbstractHashStrategy getHashStrategy() { + return hashStrategyMap.get(type); + } + +} diff --git a/src/main/java/moe/cnkirito/consistenthash/strategy/JdkHashCodeStrategy.java b/src/main/java/moe/cnkirito/consistenthash/strategy/JdkHashCodeStrategy.java new file mode 100644 index 0000000..9f3e3ec --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/strategy/JdkHashCodeStrategy.java @@ -0,0 +1,16 @@ +package moe.cnkirito.consistenthash.strategy; + +import moe.cnkirito.consistenthash.AbstractHashStrategy; + +/** + * @author daofeng.xjf + * @date 2019/2/15 + */ +public class JdkHashCodeStrategy implements AbstractHashStrategy { + + @Override + public int getHashCode(String origin) { + return origin.hashCode(); + } + +} diff --git a/src/main/java/moe/cnkirito/consistenthash/KetamaHashStrategy.java b/src/main/java/moe/cnkirito/consistenthash/strategy/KetamaHashStrategy.java similarity index 62% rename from src/main/java/moe/cnkirito/consistenthash/KetamaHashStrategy.java rename to src/main/java/moe/cnkirito/consistenthash/strategy/KetamaHashStrategy.java index 7283d1c..b27660e 100644 --- a/src/main/java/moe/cnkirito/consistenthash/KetamaHashStrategy.java +++ b/src/main/java/moe/cnkirito/consistenthash/strategy/KetamaHashStrategy.java @@ -1,23 +1,15 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.strategy; + +import moe.cnkirito.consistenthash.AbstractHashStrategy; +import moe.cnkirito.consistenthash.util.Md5Util; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; /** * @author daofeng.xjf * @date 2019/2/16 */ -public class KetamaHashStrategy implements HashStrategy { - - private static MessageDigest md5Digest; - - static { - try { - md5Digest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("MD5 not supported", e); - } - } +public class KetamaHashStrategy implements AbstractHashStrategy { @Override public int getHashCode(String origin) { @@ -35,7 +27,7 @@ public int getHashCode(String origin) { public static byte[] computeMd5(String k) { MessageDigest md5; try { - md5 = (MessageDigest) md5Digest.clone(); + md5 = (MessageDigest) Md5Util.MD5_DIGEST.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException("clone of MD5 not supported", e); } diff --git a/src/main/java/moe/cnkirito/consistenthash/MurmurHashStrategy.java b/src/main/java/moe/cnkirito/consistenthash/strategy/MurmurHashStrategy.java similarity index 72% rename from src/main/java/moe/cnkirito/consistenthash/MurmurHashStrategy.java rename to src/main/java/moe/cnkirito/consistenthash/strategy/MurmurHashStrategy.java index 4380f3e..b21945c 100644 --- a/src/main/java/moe/cnkirito/consistenthash/MurmurHashStrategy.java +++ b/src/main/java/moe/cnkirito/consistenthash/strategy/MurmurHashStrategy.java @@ -1,4 +1,6 @@ -package moe.cnkirito.consistenthash; +package moe.cnkirito.consistenthash.strategy; + +import moe.cnkirito.consistenthash.AbstractHashStrategy; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -7,7 +9,9 @@ * @author daofeng.xjf * @date 2019/2/16 */ -public class MurmurHashStrategy implements HashStrategy { +public class MurmurHashStrategy implements AbstractHashStrategy { + private static final Integer BUFF_SIZE = 8; + @Override public int getHashCode(String origin) { @@ -23,7 +27,7 @@ public int getHashCode(String origin) { long h = seed ^ (buf.remaining() * m); long k; - while (buf.remaining() >= 8) { + while (buf.remaining() >= BUFF_SIZE) { k = buf.getLong(); k *= m; @@ -37,8 +41,8 @@ public int getHashCode(String origin) { if (buf.remaining() > 0) { ByteBuffer finish = ByteBuffer.allocate(8).order( ByteOrder.LITTLE_ENDIAN); - // for big-endian version, do this first: - // finish.position(8-buf.remaining()); + /// for big-endian version, do this first: + // finish.position(8-buf.remaining()); finish.put(buf).rewind(); h ^= finish.getLong(); h *= m; diff --git a/src/main/java/moe/cnkirito/consistenthash/util/Md5Util.java b/src/main/java/moe/cnkirito/consistenthash/util/Md5Util.java new file mode 100644 index 0000000..0f0dc35 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/util/Md5Util.java @@ -0,0 +1,21 @@ +package moe.cnkirito.consistenthash.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author dailj + * @date 2022/11/25 14:21 + */ +public class Md5Util { + + public static final MessageDigest MD5_DIGEST; + + static { + try { + MD5_DIGEST = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + } +} diff --git a/src/main/java/moe/cnkirito/consistenthash/util/StatisticsUtil.java b/src/main/java/moe/cnkirito/consistenthash/util/StatisticsUtil.java new file mode 100644 index 0000000..6b9c6c0 --- /dev/null +++ b/src/main/java/moe/cnkirito/consistenthash/util/StatisticsUtil.java @@ -0,0 +1,41 @@ +package moe.cnkirito.consistenthash.util; + +/** + * @author daofeng.xjf + * @date 2019/2/16 + */ +public class StatisticsUtil { + + /** + * 计算方差 s^2=[(x1-x)^2 +...(xn-x)^2]/n + * + * @param x 参数列表 + * @return 方差 + */ + public static double variance(Long[] x) { + int m = x.length; + double sum = 0; + for (int i = 0; i < m; i++) { + sum += x[i]; + } + // 求平均值 + double dAve = sum / m; + double dVar = 0; + // 求方差 + for (int i = 0; i < m; i++) { + dVar += (x[i] - dAve) * (x[i] - dAve); + } + return dVar / m; + } + + /** + * 计算标准差 σ=sqrt(s^2) + * @param x 参数列表 + * @return 标准差 + */ + public static double standardDeviation(Long[] x) { + double varianceValue = variance(x); + return Math.sqrt(varianceValue); + } + +} diff --git a/src/test/java/moe/cnkirito/consistenthash/LoadBalanceTest.java b/src/test/java/moe/cnkirito/consistenthash/LoadBalanceTest.java index 62b81c0..37d18ef 100644 --- a/src/test/java/moe/cnkirito/consistenthash/LoadBalanceTest.java +++ b/src/test/java/moe/cnkirito/consistenthash/LoadBalanceTest.java @@ -1,6 +1,13 @@ package moe.cnkirito.consistenthash; import com.google.common.util.concurrent.AtomicLongMap; +import moe.cnkirito.consistenthash.bo.Invocation; +import moe.cnkirito.consistenthash.bo.Server; +import moe.cnkirito.consistenthash.load.balancer.ConsistentHashLoadBalancer; +import moe.cnkirito.consistenthash.load.balancer.ConsistentHashRing; +import moe.cnkirito.consistenthash.strategy.FnvHashStrategy; +import moe.cnkirito.consistenthash.util.StatisticsUtil; +import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; @@ -22,7 +29,7 @@ public void testDistribution() { for (String ip : ips) { servers.add(new Server(ip+":8080")); } - LoadBalancer chloadBalance = new ConsistentHashLoadBalancer(); + LoadBalanceable chloadBalance = new ConsistentHashLoadBalancer(); // 构造 10000 随机请求 List invocations = new ArrayList<>(); for (int i = 0; i < 10000; i++) { @@ -51,7 +58,7 @@ public void testNodeAddAndRemove() { servers.add(new Server(ip)); } List serverChanged = servers.subList(0, 80); - LoadBalancer chloadBalance = new ConsistentHashLoadBalancer(); + LoadBalanceable chloadBalance = new ConsistentHashLoadBalancer(); // 构造 10000 随机请求 List invocations = new ArrayList<>(); for (int i = 0; i < 10000; i++) { @@ -170,3 +177,4 @@ public void testNodeAddAndRemove() { "11.10.192.118" }; } + diff --git a/src/test/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashRingTest.java b/src/test/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashRingTest.java new file mode 100644 index 0000000..81a1a94 --- /dev/null +++ b/src/test/java/moe/cnkirito/consistenthash/load/balancer/ConsistentHashRingTest.java @@ -0,0 +1,83 @@ +package moe.cnkirito.consistenthash.load.balancer; + +import moe.cnkirito.consistenthash.AbstractHashStrategy; +import moe.cnkirito.consistenthash.bo.Server; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.TreeMap; + +import static org.mockito.Mockito.*; + +/** + * @author dailj + * @date 2022/11/26 14:51 + */ +// 配合@Mock注解使用 +@RunWith(PowerMockRunner.class) +public class ConsistentHashRingTest { + @Mock + ConsistentHashRing instance; + @Mock + AbstractHashStrategy hashStrategy; + ConsistentHashRing consistentHashRing; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + /// 模拟私有方法调用,不会被单元测试覆盖率统计 + // @PrepareForTest({ConsistentHashRing.class}) + public void testGetInstance() { + when(hashStrategy.getHashCode(anyString())).thenReturn(0); + /// 模拟私有方法调用 + /** + * @Spy + * private ConsistentHashRing consistentHashRing = Mockito.spy(ConsistentHashRing.class); + * + * PowerMockito.doReturn(new TreeMap(new HashMap() {{ + * put(Integer.valueOf(0), new Server("url"));}})) + * .when(consistentHashRing, "buildConsistentHashRing", Arrays.asList(new Server("url"))); + */ + + ConsistentHashRing result = ConsistentHashRing.getInstance(Arrays.asList(new Server("url"))); + Assert.assertSame(ConsistentHashRing.getInstance(Arrays.asList(new Server("url"))), result); + /// 验证方法被调用过一次 + /** + * Mockito.verify(hashStrategy).getHashCode("url"); + * Mockito.verifyNoMoreInteractions(hashStrategy); + */ + } + + @Test + public void testBuildConsistentHashRing() throws Exception { + // when(hashStrategy.getHashCode(anyString())).thenReturn(0); + + // 调用无权限的构造方法 + consistentHashRing = (ConsistentHashRing)Whitebox.invokeConstructor(ConsistentHashRing.class); + // 调用无权限的方法 + TreeMap result = (TreeMap)Whitebox.invokeMethod(consistentHashRing, + "buildConsistentHashRing", Arrays.asList(new Server("url"))); + Assert.assertEquals(10, result.entrySet().size()); + } + + @Test + public void testLocate() { + Server result = ConsistentHashRing.getInstance(Arrays.asList(new Server("url"))) + .locate(Integer.valueOf(0)); + Assert.assertNotNull(result); + } +}