diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000..55754d07
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "compact": false
+}
diff --git a/.gitattributes b/.gitattributes
index 07962a1f..eaae227f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -22,6 +22,7 @@
*.less text
*.sql text
*.properties text
+*.md text
# unix style
*.sh text eol=lf
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..04010943
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,36 @@
+name: CI
+
+# 在master分支发生push事件时触发。
+on:
+ push:
+ branches:
+ - master
+
+env: # 设置环境变量
+ TZ: Asia/Shanghai # 时区(设置时区可使页面中的`最近更新时间`使用时区时间)
+
+jobs:
+ build: # 自定义名称
+ runs-on: ubuntu-latest # 运行在虚拟机环境ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [16.x]
+
+ steps:
+ # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions
+ - name: Checkout
+ uses: actions/checkout@master
+
+ # 指定 nodejs 版本
+ - name: Use Nodejs ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ # 部署
+ - name: Deploy
+ env: # 设置环境变量
+ GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
+ GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
+ run: npm install && npm run deploy
diff --git a/.gitignore b/.gitignore
index 83948575..7d98dac9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,7 +29,6 @@ hs_err_pid*
# maven plugin temp files
.flattened-pom.xml
-package-lock.json
# ------------------------------- javascript -------------------------------
@@ -37,10 +36,12 @@ package-lock.json
node_modules
# temp folders
-.temp
+build
dist
_book
_jsdoc
+.temp
+.deploy*/
# temp files
*.log
@@ -48,7 +49,11 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
bundle*.js
+.DS_Store
+Thumbs.db
+db.json
book.pdf
+package-lock.json
# ------------------------------- intellij -------------------------------
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 7f7498fb..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-# 持续集成 CI
-# @see https://docs.travis-ci.com/user/tutorial/
-
-language: node_js
-
-sudo: required
-
-node_js: stable
-
-branches:
- only:
- - master
-
-before_install:
- - export TZ=Asia/Shanghai
-
-script: bash ./scripts/deploy.sh
-
-notifications:
- email:
- recipients:
- - forbreak@163.com
- on_success: change
- on_failure: always
diff --git a/README.md b/README.md
index 8da6685f..7d3dbdd3 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,27 @@
children = client.getChildren().forPath("/zkid");
+ if (CollectionUtil.isNotEmpty(children)) {
+ for (String child : children) {
+ client.delete().forPath("/zkid/" + child);
+ }
+ }
+ client.delete().forPath("/zkid");
+
+ // 关闭客户端
+ client.close();
+ }
+
+}
diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java
new file mode 100644
index 00000000..69d5d543
--- /dev/null
+++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java
@@ -0,0 +1,46 @@
+package io.github.dunwu.distributed.id;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.atomic.AtomicValue;
+import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+
+/**
+ * ZK 分布式 ID
+ *
+ * 基于原子计数器生成 ID
+ *
+ * @author Zhang Peng
+ * @date 2024-12-20
+ */
+@Slf4j
+public class ZookeeperDistributedId2 {
+
+ public static void main(String[] args) throws Exception {
+
+ // 获取客户端
+ RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
+ CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
+ DistributedAtomicLong atomicLong = new DistributedAtomicLong(client, "/zkid", retryPolicy);
+
+ // 开启会话
+ client.start();
+
+ // 基于原子计数器生成 ID
+ AtomicValue id1 = atomicLong.increment();
+ log.info("id: {}", id1.postValue());
+
+ AtomicValue id2 = atomicLong.increment();
+ log.info("id: {}", id2.postValue());
+
+ // 清理节点
+ client.delete().forPath("/zkid");
+
+ // 关闭客户端
+ client.close();
+ }
+
+}
diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua
new file mode 100644
index 00000000..e0c9ad00
--- /dev/null
+++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua
@@ -0,0 +1,21 @@
+-- 缓存 Key
+local key = KEYS[1]
+-- 访问请求数
+local permits = tonumber(ARGV[1])
+-- 过期时间
+local seconds = tonumber(ARGV[2])
+-- 限流阈值
+local limit = tonumber(ARGV[3])
+
+-- 获取统计值
+local count = tonumber(redis.call('GET', key) or "0")
+
+if count + permits > limit then
+ -- 请求拒绝
+ return -1
+else
+ -- 请求通过
+ redis.call('INCRBY', key, permits)
+ redis.call('EXPIRE', key, seconds)
+ return count + permits
+end
\ No newline at end of file
diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua
new file mode 100644
index 00000000..541d70c9
--- /dev/null
+++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua
@@ -0,0 +1,39 @@
+local tokenKey = KEYS[1]
+local timeKey = KEYS[2]
+
+-- 申请令牌数
+local permits = tonumber(ARGV[1])
+-- QPS
+local qps = tonumber(ARGV[2])
+-- 桶的容量
+local capacity = tonumber(ARGV[3])
+-- 当前时间(单位:毫秒)
+local nowMillis = tonumber(ARGV[4])
+-- 填满令牌桶所需要的时间
+local fillTime = capacity / qps
+local ttl = math.min(capacity, math.floor(fillTime * 2))
+
+local currentTokenNum = tonumber(redis.call("GET", tokenKey))
+if currentTokenNum == nil then
+ currentTokenNum = capacity
+end
+
+local endTimeMillis = tonumber(redis.call("GET", timeKey))
+if endTimeMillis == nil then
+ endTimeMillis = 0
+end
+
+local gap = nowMillis - endTimeMillis
+local newTokenNum = math.max(0, gap * qps / 1000)
+local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum)
+
+if currentTokenNum < permits then
+ -- 请求拒绝
+ return -1
+else
+ -- 请求通过
+ local finalTokenNum = currentTokenNum - permits
+ redis.call("SETEX", tokenKey, ttl, finalTokenNum)
+ redis.call("SETEX", timeKey, ttl, nowMillis)
+ return finalTokenNum
+end
diff --git a/codes/java-distributed/java-load-balance/pom.xml b/codes/java-distributed/java-load-balance/pom.xml
index 6ba9e97c..b4e73f0f 100644
--- a/codes/java-distributed/java-load-balance/pom.xml
+++ b/codes/java-distributed/java-load-balance/pom.xml
@@ -1,20 +1,12 @@
-
-
+
+
4.0.0
-
- io.github.dunwu
- dunwu-parent
- 0.5.1
-
-
- io.github.dunwu.javatech
- java-distributed-load-balance
+ io.github.dunwu.distributed
+ java-load-balance
1.0.0
jar
- ${project.artifactId}
UTF-8
@@ -25,13 +17,19 @@
- io.github.dunwu
- dunwu-tool-core
- 0.5.7
+ cn.hutool
+ hutool-all
+ 5.4.1
+
+
+ org.projectlombok
+ lombok
+ 1.18.12
junit
junit
+ 4.13
test
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java
new file mode 100644
index 00000000..ebb317a8
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java
@@ -0,0 +1,37 @@
+package io.github.dunwu.distributed;
+
+import cn.hutool.core.collection.CollectionUtil;
+
+import java.util.List;
+
+/**
+ * @author Zhang Peng
+ * @since 2021-01-18
+ */
+public abstract class BaseLoadBalance implements LoadBalance {
+
+ @Override
+ public N select(List nodes, String ip) {
+ // nodes 列表为空,返回 null
+ if (CollectionUtil.isEmpty(nodes)) {
+ return null;
+ }
+
+ // 如果 nodes 列表中仅有一个 node,直接返回即可
+ if (nodes.size() == 1) {
+ return nodes.get(0);
+ }
+
+ return doSelect(nodes, ip);
+ }
+
+ /**
+ * 负载均衡算法抽象方法,各个算法需要自行实现
+ *
+ * @param nodes 节点列表
+ * @param ip 请求方 IP
+ * @return 被选中的节点
+ */
+ protected abstract N doSelect(List nodes, String ip);
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java
new file mode 100644
index 00000000..c67558a2
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java
@@ -0,0 +1,129 @@
+package io.github.dunwu.distributed;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class ConsistentHashLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ private final ConcurrentMap> selectors = new ConcurrentHashMap<>();
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected N doSelect(List nodes, String ip) {
+ // 分片数,这里设为节点数的 4 倍
+ Integer replicaNum = nodes.size() * 4;
+ // 获取 nodes 原始的 hashcode
+ int identityHashCode = System.identityHashCode(nodes);
+
+ // 如果 nodes 是一个新的 List 对象,意味着节点数量发生了变化
+ // 此时 selector.identityHashCode != identityHashCode 条件成立
+ ConsistentHashSelector selector = (ConsistentHashSelector) selectors.get(ip);
+ if (selector == null || selector.identityHashCode != identityHashCode) {
+ // 创建新的 ConsistentHashSelector
+ selectors.put(ip, new ConsistentHashSelector<>(nodes, identityHashCode, replicaNum));
+ selector = (ConsistentHashSelector) selectors.get(ip);
+ }
+ // 调用 ConsistentHashSelector 的 select 方法选择 Node
+ return selector.select(ip);
+ }
+
+ /**
+ * 一致性哈希选择器
+ */
+ private static final class ConsistentHashSelector {
+
+ /**
+ * 存储虚拟节点
+ */
+ private final TreeMap virtualNodes;
+
+ private final int identityHashCode;
+
+ /**
+ * 构造器
+ *
+ * @param nodes 节点列表
+ * @param identityHashCode hashcode
+ * @param replicaNum 分片数
+ */
+ ConsistentHashSelector(List nodes, int identityHashCode, Integer replicaNum) {
+ this.virtualNodes = new TreeMap<>();
+ this.identityHashCode = identityHashCode;
+ // 获取虚拟节点数,默认为 100
+ if (replicaNum == null) {
+ replicaNum = 100;
+ }
+ for (N node : nodes) {
+ for (int i = 0; i < replicaNum / 4; i++) {
+ // 对 url 进行 md5 运算,得到一个长度为16的字节数组
+ byte[] digest = md5(node.getUrl());
+ // 对 digest 部分字节进行 4 次 hash 运算,得到四个不同的 long 型正整数
+ for (int j = 0; j < 4; j++) {
+ // h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算
+ // h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算
+ // h = 2, h = 3 时过程同上
+ long m = hash(digest, j);
+ // 将 hash 到 node 的映射关系存储到 virtualNodes 中,
+ // virtualNodes 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构
+ virtualNodes.put(m, node);
+ }
+ }
+ }
+ }
+
+ public N select(String key) {
+ // 对参数 key 进行 md5 运算
+ byte[] digest = md5(key);
+ // 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法,
+ // 寻找合适的 Node
+ return selectForKey(hash(digest, 0));
+ }
+
+ private N selectForKey(long hash) {
+ // 查找第一个大于或等于当前 hash 的节点
+ Map.Entry entry = virtualNodes.ceilingEntry(hash);
+ // 如果 hash 大于 Node 在哈希环上最大的位置,此时 entry = null,
+ // 需要将 TreeMap 的头节点赋值给 entry
+ if (entry == null) {
+ entry = virtualNodes.firstEntry();
+ }
+ // 返回 Node
+ return entry.getValue();
+ }
+
+ }
+
+ /**
+ * 计算 hash 值
+ */
+ public static long hash(byte[] digest, int number) {
+ return (((long) (digest[3 + number * 4] & 0xFF) << 24)
+ | ((long) (digest[2 + number * 4] & 0xFF) << 16)
+ | ((long) (digest[1 + number * 4] & 0xFF) << 8)
+ | (digest[number * 4] & 0xFF))
+ & 0xFFFFFFFFL;
+ }
+
+ /**
+ * 计算 MD5 值
+ */
+ public static byte[] md5(String value) {
+ MessageDigest md5;
+ try {
+ md5 = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ md5.reset();
+ byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+ md5.update(bytes);
+ return md5.digest();
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java
new file mode 100644
index 00000000..3d71cbb7
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java
@@ -0,0 +1,29 @@
+package io.github.dunwu.distributed;
+
+import cn.hutool.core.util.HashUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.util.List;
+
+/**
+ * @author peng.zhang
+ * @date 2021/1/19
+ */
+public class IpHashLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ @Override
+ protected N doSelect(List nodes, String ip) {
+ if (StrUtil.isBlank(ip)) {
+ ip = "127.0.0.1";
+ }
+
+ int length = nodes.size();
+ int index = hash(ip) % length;
+ return nodes.get(index);
+ }
+
+ public int hash(String text) {
+ return HashUtil.fnvHash(text);
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java
new file mode 100644
index 00000000..23a7f03c
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java
@@ -0,0 +1,83 @@
+package io.github.dunwu.distributed;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * @author peng.zhang
+ * @date 2021/1/18
+ */
+public class LeastActiveLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ private final Random random = new Random();
+
+ @Override
+ protected N doSelect(List nodes, String ip) {
+ int length = nodes.size();
+ // 最小的活跃数
+ int leastActive = -1;
+ // 具有相同“最小活跃数”的服务者提供者(以下用 Node 代称)数量
+ int leastCount = 0;
+ // leastIndexs 用于记录具有相同“最小活跃数”的 Node 在 nodes 列表中的下标信息
+ int[] leastIndexs = new int[length];
+ int totalWeight = 0;
+ // 第一个最小活跃数的 Node 权重值,用于与其他具有相同最小活跃数的 Node 的权重进行对比,
+ // 以检测是否“所有具有相同最小活跃数的 Node 的权重”均相等
+ int firstWeight = 0;
+ boolean sameWeight = true;
+
+ // 遍历 nodes 列表
+ for (int i = 0; i < length; i++) {
+ N node = nodes.get(i);
+ // 发现更小的活跃数,重新开始
+ if (leastActive == -1 || node.getActive() < leastActive) {
+ // 使用当前活跃数更新最小活跃数 leastActive
+ leastActive = node.getActive();
+ // 更新 leastCount 为 1
+ leastCount = 1;
+ // 记录当前下标值到 leastIndexs 中
+ leastIndexs[0] = i;
+ totalWeight = node.getWeight();
+ firstWeight = node.getWeight();
+ sameWeight = true;
+
+ // 当前 Node 的活跃数 node.getActive() 与最小活跃数 leastActive 相同
+ } else if (node.getActive() == leastActive) {
+ // 在 leastIndexs 中记录下当前 Node 在 nodes 集合中的下标
+ leastIndexs[leastCount++] = i;
+ // 累加权重
+ totalWeight += node.getWeight();
+ // 检测当前 Node 的权重与 firstWeight 是否相等,
+ // 不相等则将 sameWeight 置为 false
+ if (sameWeight && i > 0
+ && node.getWeight() != firstWeight) {
+ sameWeight = false;
+ }
+ }
+ }
+
+ // 当只有一个 Node 具有最小活跃数,此时直接返回该 Node 即可
+ if (leastCount == 1) {
+ return nodes.get(leastIndexs[0]);
+ }
+
+ // 有多个 Node 具有相同的最小活跃数,但它们之间的权重不同
+ if (!sameWeight && totalWeight > 0) {
+ // 随机生成一个 [0, totalWeight) 之间的数字
+ int offsetWeight = random.nextInt(totalWeight);
+ // 循环让随机数减去具有最小活跃数的 Node 的权重值,
+ // 当 offset 小于等于0时,返回相应的 Node
+ for (int i = 0; i < leastCount; i++) {
+ int leastIndex = leastIndexs[i];
+ // 获取权重值,并让随机数减去权重值
+ offsetWeight -= nodes.get(leastIndex).getWeight();
+ if (offsetWeight <= 0) {
+ return nodes.get(leastIndex);
+ }
+ }
+ }
+ // 如果权重相同或权重为0时,随机返回一个 Node
+ return nodes.get(leastIndexs[random.nextInt(leastCount)]);
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java
new file mode 100644
index 00000000..f2d0c561
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java
@@ -0,0 +1,15 @@
+package io.github.dunwu.distributed;
+
+import java.util.List;
+
+/**
+ * 负载均衡策略接口
+ *
+ * @author Zhang Peng
+ * @since 2020-01-21
+ */
+public interface LoadBalance {
+
+ N select(List nodes, String ip);
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java
new file mode 100644
index 00000000..57dc61f1
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java
@@ -0,0 +1,146 @@
+package io.github.dunwu.distributed;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.TreeMap;
+
+/**
+ * 负载均衡算法测试例
+ *
+ * @author peng.zhang
+ * @date 2021/1/19
+ */
+public class LoadBalanceDemo {
+
+ private static final Random random = new Random();
+
+ public static String randomIpv4() {
+ int[][] range = { { 607649792, 608174079 }, // 36.56.0.0-36.63.255.255
+ { 1038614528, 1039007743 }, // 61.232.0.0-61.237.255.255
+ { 1783627776, 1784676351 }, // 106.80.0.0-106.95.255.255
+ { 2035023872, 2035154943 }, // 121.76.0.0-121.77.255.255
+ { 2078801920, 2079064063 }, // 123.232.0.0-123.235.255.255
+ { -1950089216, -1948778497 }, // 139.196.0.0-139.215.255.255
+ { -1425539072, -1425014785 }, // 171.8.0.0-171.15.255.255
+ { -1236271104, -1235419137 }, // 182.80.0.0-182.92.255.255
+ { -770113536, -768606209 }, // 210.25.0.0-210.47.255.255
+ { -569376768, -564133889 }, // 222.16.0.0-222.95.255.255
+ };
+
+ Random rdint = new Random();
+ int index = rdint.nextInt(10);
+ String ip = num2ip(range[index][0]
+ + new Random().nextInt(range[index][1] - range[index][0]));
+ return ip;
+ }
+
+ private static String num2ip(final int ip) {
+ int[] b = new int[4];
+ String result = "";
+ b[0] = (ip >> 24) & 0xff;
+ b[1] = ((ip >> 16) & 0xff);
+ b[2] = ((ip >> 8) & 0xff);
+ b[3] = (ip & 0xff);
+ result = Integer.toString(b[0]) + "." + Integer.toString(b[1]) + "."
+ + Integer.toString(b[2]) + "." + Integer.toString(b[3]);
+ return result;
+ }
+
+ /**
+ * 生成 num 个随机 IP 地址
+ */
+ private static List initRandomIpList(int num) {
+ List list = new ArrayList<>();
+ for (int i = 1; i <= num; i++) {
+ list.add(randomIpv4());
+ }
+ return list;
+ }
+
+ /**
+ * 生成 num 个样本节点
+ *
+ * @param num 节点数
+ * @param sameWeight 各节点权重是否相同
+ * @param sameActive 各节点活跃数是否相同
+ */
+ private static List initNodeList(Integer num, boolean sameWeight, boolean sameActive) {
+
+ List nodes = new ArrayList<>();
+ for (int i = 1; i <= num; i++) {
+ Node node = new Node("192.168.0." + i);
+ if (!sameWeight) {
+ node.setWeight(random.nextInt(10));
+ }
+ if (!sameActive) {
+ node.setActive(random.nextInt(10));
+ }
+ nodes.add(node);
+ }
+ return nodes;
+ }
+
+ /**
+ * 统计负载均衡命中次数,样本数为 10000 次访问
+ */
+ private static Map loadBalance10000(LoadBalance algorithm, List nodes,
+ List ipList) {
+ Map staticMap = new TreeMap<>();
+
+ int ipLength = ipList.size();
+ for (int i = 0; i < 10000; i++) {
+ String ip = ipList.get(random.nextInt(ipLength));
+ Node node = algorithm.select(nodes, ip);
+ // 打印每一次负载均衡的选择结果
+ // System.out.println(StrUtil.format("ip = {}, node url = {}", ip, node.getUrl()));
+ if (staticMap.containsKey(node)) {
+ Long value = staticMap.get(node);
+ staticMap.put(node, ++value);
+ } else {
+ staticMap.put(node, 1L);
+ }
+ }
+
+ System.out.println("======================= 统计数据 =======================");
+ staticMap.forEach((key, value) -> {
+ System.out.printf("key = %s, value = %s\n", key, value);
+ });
+ System.out.printf("方差:%s, ", StatisticsUtil.variance(staticMap.values().toArray(new Long[0])));
+ System.out.printf("标准差:%s\n", StatisticsUtil.standardDeviation(staticMap.values().toArray(new Long[] {})));
+ return staticMap;
+ }
+
+ public static void main(String[] args) {
+ // 构造 100 个候选服务器节点
+ List nodes = initNodeList(100, false, false);
+ // 构造 100 个随机IP
+ List ipList = initRandomIpList(100);
+
+ // ============================================================================
+ // 基于以上构造数据,对每种算法都 负载均衡选择 10000 次,然后统计方差、标准差,查看负载均衡效果。
+
+ System.out.println("======================= 随机负载均衡 =======================");
+ loadBalance10000(new RandomLoadBalance<>(), nodes, ipList);
+
+ System.out.println("======================= 加权随机负载均衡 =======================");
+ loadBalance10000(new WeightRandomLoadBalance<>(), nodes, ipList);
+
+ System.out.println("======================= 轮询负载均衡 =======================");
+ loadBalance10000(new RoundRobinLoadBalance<>(), nodes, ipList);
+
+ System.out.println("======================= 加权轮询负载均衡 =======================");
+ loadBalance10000(new WeightRoundRobinLoadBalance<>(), nodes, ipList);
+
+ System.out.println("======================= 源地址哈希负载均衡 =======================");
+ loadBalance10000(new IpHashLoadBalance<>(), nodes, ipList);
+
+ System.out.println("======================= 最小活跃数负载均衡 =======================");
+ loadBalance10000(new LeastActiveLoadBalance<>(), nodes, ipList);
+
+ System.out.println("======================= 一致性哈希负载均衡 =======================");
+ loadBalance10000(new ConsistentHashLoadBalance<>(), nodes, ipList);
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java
similarity index 69%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java
index c6957e5d..2fc9c712 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech;
+package io.github.dunwu.distributed;
import java.util.Objects;
@@ -11,19 +11,22 @@
public class Node implements Comparable {
public static final Integer DEFAULT_WEIGHT = 1;
+ public static final Integer DEFAULT_ACTIVE = 0;
protected String url;
protected Integer weight;
+ protected Integer active;
+
public Node(String url) {
- this.url = url;
- this.weight = DEFAULT_WEIGHT;
+ this(url, DEFAULT_WEIGHT, DEFAULT_ACTIVE);
}
- public Node(String url, Integer weight) {
+ public Node(String url, Integer weight, Integer active) {
this.url = url;
this.weight = weight;
+ this.active = active;
}
@Override
@@ -53,6 +56,7 @@ public String toString() {
return "Node{" +
"url='" + url + '\'' +
", weight=" + weight +
+ ", active=" + active +
'}';
}
@@ -60,18 +64,24 @@ public String getUrl() {
return url;
}
- public Node setUrl(String url) {
+ public void setUrl(String url) {
this.url = url;
- return this;
}
public Integer getWeight() {
return weight;
}
- public Node setWeight(Integer weight) {
+ public void setWeight(Integer weight) {
this.weight = weight;
- return this;
+ }
+
+ public Integer getActive() {
+ return active;
+ }
+
+ public void setActive(Integer active) {
+ this.active = active;
}
}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java
new file mode 100644
index 00000000..5b775dd2
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java
@@ -0,0 +1,23 @@
+package io.github.dunwu.distributed;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * (加权)随机负载均衡策略
+ *
+ * @author Zhang Peng
+ * @see Zhang Peng
+ * @since 2020-01-20
+ */
+public class RandomLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ private final Random random = new Random();
+
+ @Override
+ protected N doSelect(List nodes, String ip) {
+ int index = random.nextInt(nodes.size());
+ return nodes.get(index);
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java
new file mode 100644
index 00000000..c0858152
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java
@@ -0,0 +1,26 @@
+package io.github.dunwu.distributed;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * (加权)轮询负载均衡策略
+ *
+ * @author Zhang Peng
+ * @since 2020-01-20
+ */
+public class RoundRobinLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ private final AtomicInteger position = new AtomicInteger(0);
+
+ @Override
+ protected N doSelect(List nodes, String ip) {
+ int length = nodes.size();
+ // 如果位置值已经等于节点数,重置为 0
+ position.compareAndSet(length, 0);
+ N node = nodes.get(position.get());
+ position.getAndIncrement();
+ return node;
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/test/java/io/github/dunwu/javatech/StatisticsUtil.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java
similarity index 59%
rename from codes/java-distributed/java-load-balance/src/test/java/io/github/dunwu/javatech/StatisticsUtil.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java
index be6fafe9..cbb66d13 100644
--- a/codes/java-distributed/java-load-balance/src/test/java/io/github/dunwu/javatech/StatisticsUtil.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java
@@ -1,8 +1,13 @@
-package io.github.dunwu.javatech;
+package io.github.dunwu.distributed;
public class StatisticsUtil {
- // 方差 s^2=[(x1-x)^2 +...(xn-x)^2]/n
+ private StatisticsUtil() {}
+
+ /**
+ * 方差计算
+ * 公式:s^2 = [(x1-x)^2 +...(xn-x)^2]/n
+ */
public static double variance(Long[] array) {
int m = array.length;
double sum = 0;
@@ -17,18 +22,13 @@ public static double variance(Long[] array) {
return value / m;
}
- // 标准差σ=sqrt(s^2)
+ /**
+ * 标准差
+ * 公式 result = sqrt(s^2),即 sqrt(variance(array))
+ */
public static double standardDeviation(Long[] array) {
int m = array.length;
- double sum = 0;
- for (Long item : array) {// 求和
- sum += item;
- }
- double avg = sum / m;// 求平均值
- double value = 0;
- for (Long item : array) {// 求方差
- value += (item - avg) * (item - avg);
- }
+ double value = variance(array);
return Math.sqrt(value / m);
}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java
new file mode 100644
index 00000000..c7135667
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java
@@ -0,0 +1,42 @@
+package io.github.dunwu.distributed;
+
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author peng.zhang
+ * @date 2021/1/19
+ */
+public class WeightRandomLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ private final Random random = ThreadLocalRandom.current();
+
+ @Override
+ protected N doSelect(List nodes, String ip) {
+
+ int length = nodes.size();
+ AtomicInteger totalWeight = new AtomicInteger(0);
+ for (N node : nodes) {
+ Integer weight = node.getWeight();
+ totalWeight.getAndAdd(weight);
+ }
+
+ if (totalWeight.get() > 0) {
+ int offset = random.nextInt(totalWeight.get());
+ for (N node : nodes) {
+ // 让随机值 offset 减去权重值
+ offset -= node.getWeight();
+ if (offset < 0) {
+ // 返回相应的 Node
+ return node;
+ }
+ }
+ }
+
+ // 直接随机返回一个
+ return nodes.get(random.nextInt(length));
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java
new file mode 100644
index 00000000..3f71a573
--- /dev/null
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java
@@ -0,0 +1,160 @@
+package io.github.dunwu.distributed;
+
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author peng.zhang
+ * @date 2021/1/19
+ */
+public class WeightRoundRobinLoadBalance extends BaseLoadBalance implements LoadBalance {
+
+ /**
+ * 60秒
+ */
+ private static final int RECYCLE_PERIOD = 60000;
+
+ /**
+ * Node hashcode 到 WeightedRoundRobin 的映射关系
+ */
+ private ConcurrentMap weightMap = new ConcurrentHashMap<>();
+
+ /**
+ * 原子更新锁
+ */
+ private AtomicBoolean updateLock = new AtomicBoolean();
+
+ @Override
+ protected N doSelect(List nodes, String ip) {
+
+ int totalWeight = 0;
+ long maxCurrent = Long.MIN_VALUE;
+
+ // 获取当前时间
+ long now = System.currentTimeMillis();
+ N selectedNode = null;
+ WeightedRoundRobin selectedWRR = null;
+
+ // 下面这个循环主要做了这样几件事情:
+ // 1. 遍历 Node 列表,检测当前 Node 是否有相应的 WeightedRoundRobin,没有则创建
+ // 2. 检测 Node 权重是否发生了变化,若变化了,则更新 WeightedRoundRobin 的 weight 字段
+ // 3. 让 current 字段加上自身权重,等价于 current += weight
+ // 4. 设置 lastUpdate 字段,即 lastUpdate = now
+ // 5. 寻找具有最大 current 的 Node,以及 Node 对应的 WeightedRoundRobin,
+ // 暂存起来,留作后用
+ // 6. 计算权重总和
+ for (N node : nodes) {
+ int hashCode = node.hashCode();
+ WeightedRoundRobin weightedRoundRobin = weightMap.get(hashCode);
+ int weight = node.getWeight();
+ if (weight < 0) {
+ weight = 0;
+ }
+
+ // 检测当前 Node 是否有对应的 WeightedRoundRobin,没有则创建
+ if (weightedRoundRobin == null) {
+ weightedRoundRobin = new WeightedRoundRobin();
+ // 设置 Node 权重
+ weightedRoundRobin.setWeight(weight);
+ // 存储 url 唯一标识 identifyString 到 weightedRoundRobin 的映射关系
+ weightMap.putIfAbsent(hashCode, weightedRoundRobin);
+ weightedRoundRobin = weightMap.get(hashCode);
+ }
+ // Node 权重不等于 WeightedRoundRobin 中保存的权重,说明权重变化了,此时进行更新
+ if (weight != weightedRoundRobin.getWeight()) {
+ weightedRoundRobin.setWeight(weight);
+ }
+
+ // 让 current 加上自身权重,等价于 current += weight
+ long current = weightedRoundRobin.increaseCurrent();
+ // 设置 lastUpdate,表示近期更新过
+ weightedRoundRobin.setLastUpdate(now);
+ // 找出最大的 current
+ if (current > maxCurrent) {
+ maxCurrent = current;
+ // 将具有最大 current 权重的 Node 赋值给 selectedNode
+ selectedNode = node;
+ // 将 Node 对应的 weightedRoundRobin 赋值给 selectedWRR,留作后用
+ selectedWRR = weightedRoundRobin;
+ }
+
+ // 计算权重总和
+ totalWeight += weight;
+ }
+
+ // 对 weightMap 进行检查,过滤掉长时间未被更新的节点。
+ // 该节点可能挂了,nodes 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。
+ // 若未更新时长超过阈值后,就会被移除掉,默认阈值为60秒。
+ if (!updateLock.get() && nodes.size() != weightMap.size()) {
+ if (updateLock.compareAndSet(false, true)) {
+ try {
+ // 遍历修改,即移除过期记录
+ weightMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
+ } finally {
+ updateLock.set(false);
+ }
+ }
+ }
+
+ if (selectedNode != null) {
+ // 让 current 减去权重总和,等价于 current -= totalWeight
+ selectedWRR.decreaseCurrent(totalWeight);
+ // 返回具有最大 current 的 Node
+ return selectedNode;
+ }
+
+ // should not happen here
+ return nodes.get(0);
+ }
+
+ protected static class WeightedRoundRobin {
+
+ // 服务提供者权重
+ private int weight;
+ // 当前权重
+ private AtomicLong current = new AtomicLong(0);
+ // 最后一次更新时间
+ private long lastUpdate;
+
+ public long increaseCurrent() {
+ // current = current + weight;
+ return current.addAndGet(weight);
+ }
+
+ public long decreaseCurrent(int total) {
+ // current = current - total;
+ return current.addAndGet(-1 * total);
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public void setWeight(int weight) {
+ this.weight = weight;
+ // 初始情况下,current = 0
+ current.set(0);
+ }
+
+ public AtomicLong getCurrent() {
+ return current;
+ }
+
+ public void setCurrent(AtomicLong current) {
+ this.current = current;
+ }
+
+ public long getLastUpdate() {
+ return lastUpdate;
+ }
+
+ public void setLastUpdate(long lastUpdate) {
+ this.lastUpdate = lastUpdate;
+ }
+
+ }
+
+}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java
similarity index 76%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java
index 8633bb5a..4d8b7a26 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java
@@ -4,4 +4,4 @@
* @author Zhang Peng
* @since 2020-01-22
*/
-package io.github.dunwu.javatech;
+package io.github.dunwu.distributed;
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java
similarity index 98%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java
index 8622bb51..5c732a5b 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech.support;
+package io.github.dunwu.distributed.support;
import java.nio.charset.StandardCharsets;
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java
similarity index 92%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java
index 807f3c33..fc7cea13 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech.support;
+package io.github.dunwu.distributed.support;
public class FnvHashStrategy implements HashStrategy {
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java
similarity index 59%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java
index 2ad6deef..f574c86e 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech.support;
+package io.github.dunwu.distributed.support;
public interface HashStrategy {
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java
similarity index 77%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java
index f4a8389c..6541e2a4 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech.support;
+package io.github.dunwu.distributed.support;
public class JdkHashCodeStrategy implements HashStrategy {
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java
similarity index 96%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java
index 8e98ef1f..ef479aaf 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech.support;
+package io.github.dunwu.distributed.support;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java
similarity index 96%
rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java
rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java
index a100f3d3..82ee1620 100644
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java
+++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java
@@ -1,4 +1,4 @@
-package io.github.dunwu.javatech.support;
+package io.github.dunwu.distributed.support;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java
deleted file mode 100644
index 53b4a998..00000000
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package io.github.dunwu.javatech;
-
-import io.github.dunwu.javatech.support.HashStrategy;
-import io.github.dunwu.javatech.support.MurmurHashStrategy;
-
-import java.util.*;
-
-public class ConsistentHashLoadBalance implements LoadBalance {
-
- private HashStrategy hashStrategy = new MurmurHashStrategy();
-
- private final static int VIRTUAL_NODE_SIZE = 1000;
-
- private final static String VIRTUAL_NODE_SUFFIX = "&&";
-
- private Set nodes = new LinkedHashSet<>();
-
- private TreeMap hashRing = new TreeMap<>();
-
- @Override
- public void buildInList(final Collection collection) {
- this.nodes = new LinkedHashSet<>(collection);
- this.hashRing = buildConsistentHashRing(this.nodes);
- }
-
- @Override
- public void addNode(V node) {
- this.nodes.add(node);
- this.hashRing = buildConsistentHashRing(this.nodes);
- }
-
- @Override
- public void removeNode(V node) {
- this.nodes.removeIf(v -> v.equals(node));
- this.hashRing = buildConsistentHashRing(this.nodes);
- }
-
- @Override
- public V next() {
- return next(UUID.randomUUID().toString());
- }
-
- public V next(String key) {
- int hashCode = hashStrategy.hashCode(key);
- // 向右找到第一个 key
- Map.Entry entry = hashRing.ceilingEntry(hashCode);
- if (entry == null) {
- // 想象成一个环,超过尾部则取第一个 key
- entry = hashRing.firstEntry();
- }
- return entry.getValue();
- }
-
- private TreeMap buildConsistentHashRing(Set nodes) {
- TreeMap hashRing = new TreeMap<>();
- for (V node : nodes) {
- for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {
- // 新增虚拟节点的方式如果有影响,也可以抽象出一个由物理节点扩展虚拟节点的类
- hashRing.put(hashStrategy.hashCode(node + VIRTUAL_NODE_SUFFIX + i), node);
- }
- }
- return hashRing;
- }
-
-}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java
deleted file mode 100644
index 06dda46e..00000000
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.github.dunwu.javatech;
-
-import java.util.Collection;
-
-/**
- * 负载均衡策略接口
- *
- * @author Zhang Peng
- * @since 2020-01-21
- */
-public interface LoadBalance {
-
- void buildInList(Collection collection);
-
- void addNode(V node);
-
- void removeNode(V node);
-
- V next();
-
-}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java
deleted file mode 100644
index ba835a61..00000000
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package io.github.dunwu.javatech;
-
-import cn.hutool.core.collection.CollectionUtil;
-
-import java.util.*;
-import java.util.concurrent.ThreadLocalRandom;
-
-/**
- * (加权)随机负载均衡策略
- *
- * @author Zhang Peng
- * @see Zhang Peng
- * @since 2020-01-20
- */
-public class RandomLoadBalance implements LoadBalance {
-
- private boolean weightMode;
-
- private final Random random = ThreadLocalRandom.current();
-
- private Set nodes = Collections.emptyNavigableSet();
-
- public RandomLoadBalance() {
- this.weightMode = false;
- }
-
- public RandomLoadBalance(boolean weightMode) {
- this.weightMode = weightMode;
- }
-
- @Override
- public void buildInList(final Collection collection) {
- this.nodes = new LinkedHashSet<>(collection);
- }
-
- @Override
- public void addNode(V node) {
- this.nodes.add(node);
- }
-
- @Override
- public void removeNode(V node) {
- this.nodes.remove(node);
- }
-
- @Override
- public V next() {
- if (weightMode) {
- return getNextInWeightMode();
- } else {
- return getNextInNormalMode();
- }
- }
-
- private V getNextInWeightMode() {
- if (CollectionUtil.isEmpty(nodes)) {
- return null;
- }
-
- List list = new ArrayList<>();
- for (V node : nodes) {
- for (int i = 0; i < node.getWeight(); i++) {
- list.add(node);
- }
- }
-
- int totalWeight = nodes.stream().mapToInt(Node::getWeight).sum();
- int number = random.nextInt(totalWeight);
- return list.get(number);
- }
-
- private V getNextInNormalMode() {
- if (CollectionUtil.isEmpty(nodes)) {
- return null;
- }
-
- int number = random.nextInt(nodes.size());
- Iterator iterator = nodes.iterator();
- while (number-- > 0) {
- iterator.next();
- }
- return iterator.next();
- }
-
-}
diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java
deleted file mode 100644
index 6bcabaa5..00000000
--- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package io.github.dunwu.javatech;
-
-import cn.hutool.core.collection.CollectionUtil;
-
-import java.util.*;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * (加权)轮询负载均衡策略
- *
- * @author Zhang Peng
- * @since 2020-01-20
- */
-public class RoundRobinLoadBalance implements LoadBalance {
-
- private boolean weightMode;
-
- private AtomicInteger offset = new AtomicInteger(0);
-
- private Set nodes = Collections.emptyNavigableSet();
-
- public RoundRobinLoadBalance() {
- this.weightMode = false;
- }
-
- public RoundRobinLoadBalance(boolean weightMode) {
- this.weightMode = weightMode;
- }
-
- @Override
- public void buildInList(final Collection collection) {
- this.offset = new AtomicInteger(0);
- this.nodes = new LinkedHashSet<>(collection);
- }
-
- @Override
- public void addNode(V node) {
- this.nodes.add(node);
- }
-
- @Override
- public void removeNode(V node) {
- this.nodes.remove(node);
- }
-
- @Override
- public V next() {
- if (weightMode) {
- return getNextInWeightMode();
- } else {
- return getNextInNormalMode();
- }
- }
-
- private V getNextInWeightMode() {
- if (CollectionUtil.isEmpty(nodes)) {
- return null;
- }
-
- int totalWeight = nodes.stream().mapToInt(Node::getWeight).sum();
- int number = offset.getAndIncrement() % totalWeight;
-
- for (V node : nodes) {
- if (node.getWeight() > number) {
- return node;
- }
- number -= node.getWeight();
- }
- return null;
- }
-
- private V getNextInNormalMode() {
- if (CollectionUtil.isEmpty(this.nodes)) {
- return null;
- }
-
- int size = this.nodes.size();
- offset.compareAndSet(size, 0);
- int number = offset.getAndIncrement();
- Iterator iterator = nodes.iterator();
- while (number-- > 0) {
- iterator.next();
- }
- return iterator.next();
- }
-
-}
diff --git a/codes/java-distributed/java-load-balance/src/test/java/io/github/dunwu/javatech/LoadBalanceTests.java b/codes/java-distributed/java-load-balance/src/test/java/io/github/dunwu/javatech/LoadBalanceTests.java
deleted file mode 100644
index 412c74a8..00000000
--- a/codes/java-distributed/java-load-balance/src/test/java/io/github/dunwu/javatech/LoadBalanceTests.java
+++ /dev/null
@@ -1,164 +0,0 @@
-package io.github.dunwu.javatech;
-
-import org.junit.Test;
-
-import java.util.*;
-
-/**
- * 负载均衡测试
- *
- * @author Zhang Peng
- * @since 2020-01-20
- */
-public class LoadBalanceTests {
-
- /**
- * 生成 100 个样本节点,权重值为 10 以内的随机数
- */
- private List initNodes() {
- Random random = new Random();
- List nodes = new ArrayList<>();
- for (int i = 1; i <= 100; i++) {
- Node node = new Node("192.168.0." + i, random.nextInt(10));
- nodes.add(node);
- }
- return nodes;
- }
-
- /**
- * 统计负载均衡命中次数,样本数为 10000 次访问
- */
- private Map staticLoadBalance(LoadBalance algorithm) {
- Map staticMap = new TreeMap<>();
-
- for (int i = 0; i < 10000; i++) {
- Node node = algorithm.next();
- // System.out.printf(">>>> url = %s\n", node.url);
- if (staticMap.containsKey(node)) {
- Long value = staticMap.get(node);
- staticMap.put(node, ++value);
- } else {
- staticMap.put(node, 1L);
- }
- }
-
- System.out.println("======================= 统计数据 =======================");
- staticMap.forEach((key, value) -> {
- System.out.printf("key = %s, value = %s\n", key, value);
- });
- System.out.printf("方差:%s, ", StatisticsUtil.variance(staticMap.values().toArray(new Long[0])));
- System.out.printf("标准差:%s\n", StatisticsUtil.standardDeviation(staticMap.values().toArray(new Long[] {})));
- return staticMap;
- }
-
- @Test
- public void randomLoadBalanceDistribution() {
- List nodes = initNodes();
-
- LoadBalance loadBalance = new RandomLoadBalance<>();
- loadBalance.buildInList(nodes);
- System.out.println("======================= 随机负载均衡 =======================");
- staticLoadBalance(loadBalance);
-
- LoadBalance loadBalance2 = new RandomLoadBalance<>(true);
- loadBalance2.buildInList(nodes);
- System.out.println("======================= 加权随机负载均衡 =======================");
- staticLoadBalance(loadBalance2);
- }
-
- @Test
- public void randomLoadBalanceUpdateNodes() {
- List oldNodes = initNodes();
- List newNodes = oldNodes.subList(0, 80);
-
- LoadBalance oldLoadBalance = new RandomLoadBalance<>();
- oldLoadBalance.buildInList(oldNodes);
- LoadBalance newLoadBalance = new RandomLoadBalance<>();
- newLoadBalance.buildInList(newNodes);
-
- double count = 0.0d;
- int size = newNodes.size();
- for (int i = 0; i < newNodes.size(); i++) {
- Node oldNode = oldLoadBalance.next();
- Node newNode = newLoadBalance.next();
- if (oldNode.equals(newNode)) count++;
- }
- System.out.println(count / size);
- }
-
- @Test
- public void roundRobinLoadBalanceDistribution() {
- List nodes = initNodes();
-
- LoadBalance loadBalance = new RoundRobinLoadBalance<>();
- loadBalance.buildInList(nodes);
- System.out.println("======================= 轮询负载均衡 =======================");
- staticLoadBalance(loadBalance);
-
- LoadBalance loadBalance2 = new RoundRobinLoadBalance<>(true);
- loadBalance2.buildInList(nodes);
- System.out.println("======================= 加权轮询负载均衡 =======================");
- staticLoadBalance(loadBalance2);
- }
-
- @Test
- public void roundRobinLoadBalanceUpdateNodes() {
- List oldNodes = initNodes();
- List newNodes = oldNodes.subList(0, 80);
-
- LoadBalance oldLoadBalance = new RoundRobinLoadBalance<>();
- oldLoadBalance.buildInList(oldNodes);
- LoadBalance newLoadBalance = new RoundRobinLoadBalance<>();
- newLoadBalance.buildInList(newNodes);
-
- double count = 0.0d;
- int size = newNodes.size();
- for (int i = 0; i < newNodes.size(); i++) {
- Node oldNode = oldLoadBalance.next();
- Node newNode = newLoadBalance.next();
- if (oldNode.equals(newNode)) count++;
- }
- System.out.println(count / size);
- }
-
- @Test
- public void consistentHashLoadBalanceDistribution() {
- LoadBalance loadBalance = new ConsistentHashLoadBalance<>();
- loadBalance.buildInList(initNodes());
- System.out.println("======================= 一致性 Hash 负载均衡 =======================");
- staticLoadBalance(loadBalance);
- }
-
- /**
- * 测试节点新增删除后的变化程度
- */
- @Test
- public void testNodeAddAndRemove() {
- // 构造 10000 随机请求
- List keys = new ArrayList<>();
- for (int i = 0; i < 10000; i++) {
- keys.add(UUID.randomUUID().toString());
- }
-
- List nodes = new ArrayList<>();
- for (int i = 1; i <= 100; i++) {
- Node node = new Node("192.168.0." + i);
- nodes.add(node);
- }
-
- List newNodes = nodes.subList(0, 80);
- ConsistentHashLoadBalance oldLoadBalance = new ConsistentHashLoadBalance<>();
- oldLoadBalance.buildInList(nodes);
- ConsistentHashLoadBalance newLoadBalance = new ConsistentHashLoadBalance<>();
- newLoadBalance.buildInList(newNodes);
-
- int count = 0;
- for (String key : keys) {
- Node oldNode = oldLoadBalance.next(key);
- Node newNode = newLoadBalance.next(key);
- if (oldNode.equals(newNode)) count++;
- }
- System.out.println(count / 10000D);
- }
-
-}
diff --git a/codes/java-distributed/java-rate-limit/pom.xml b/codes/java-distributed/java-rate-limit/pom.xml
new file mode 100644
index 00000000..312d7f21
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ io.github.dunwu.distributed
+ java-rate-limit
+ 1.0.0
+ jar
+
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
+
+
+
+
+ redis.clients
+ jedis
+ 5.1.0
+
+
+ cn.hutool
+ hutool-all
+ 5.8.25
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.3
+ true
+
+
+
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java
new file mode 100644
index 00000000..0af8d142
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java
@@ -0,0 +1,59 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 固定时间窗口限流算法
+ *
+ * @author Zhang Peng
+ * @date 2024-01-18
+ */
+public class FixedWindowRateLimiter implements RateLimiter {
+
+ /**
+ * 允许的最大请求数
+ */
+ private final long maxPermits;
+
+ /**
+ * 窗口期时长
+ */
+ private final long periodMillis;
+
+ /**
+ * 窗口期截止时间
+ */
+ private long lastPeriodMillis;
+
+ /**
+ * 请求计数
+ */
+ private AtomicLong count = new AtomicLong(0);
+
+ public FixedWindowRateLimiter(long qps) {
+ this(qps, 1000, TimeUnit.MILLISECONDS);
+ }
+
+ public FixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit) {
+ this.maxPermits = maxPermits;
+ this.periodMillis = timeUnit.toMillis(period);
+ this.lastPeriodMillis = System.currentTimeMillis() + this.periodMillis;
+ }
+
+ @Override
+ public synchronized boolean tryAcquire(int permits) {
+ long now = System.currentTimeMillis();
+ if (lastPeriodMillis <= now) {
+ this.lastPeriodMillis = now + this.periodMillis;
+ count = new AtomicLong(0);
+ }
+ if (count.get() + permits <= maxPermits) {
+ count.addAndGet(permits);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java
new file mode 100644
index 00000000..0d99a227
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java
@@ -0,0 +1,64 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 漏桶限流算法
+ *
+ * @author Zhang Peng
+ * @date 2024-01-18
+ */
+public class LeakyBucketRateLimiter implements RateLimiter {
+
+ /**
+ * QPS
+ */
+ private final int qps;
+
+ /**
+ * 桶的容量
+ */
+ private final long capacity;
+
+ /**
+ * 计算的起始时间
+ */
+ private long beginTimeMillis;
+
+ /**
+ * 桶中当前的水量
+ */
+ private final AtomicLong waterNum = new AtomicLong(0);
+
+ public LeakyBucketRateLimiter(int qps, int capacity) {
+ this.qps = qps;
+ this.capacity = capacity;
+ }
+
+ @Override
+ public synchronized boolean tryAcquire(int permits) {
+
+ // 如果桶中没有水,直接放行
+ if (waterNum.get() == 0) {
+ beginTimeMillis = System.currentTimeMillis();
+ waterNum.addAndGet(permits);
+ return true;
+ }
+
+ // 计算水量
+ long leakedWaterNum = ((System.currentTimeMillis() - beginTimeMillis) / 1000) * qps;
+ long currentWaterNum = waterNum.get() - leakedWaterNum;
+ waterNum.set(Math.max(0, currentWaterNum));
+
+ // 重置时间
+ beginTimeMillis = System.currentTimeMillis();
+
+ if (waterNum.get() + permits < capacity) {
+ waterNum.addAndGet(permits);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java
new file mode 100644
index 00000000..4fbc9646
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java
@@ -0,0 +1,13 @@
+package io.github.dunwu.distributed.ratelimit;
+
+/**
+ * 限流器
+ *
+ * @author Zhang Peng
+ * @date 2024-01-18
+ */
+public interface RateLimiter {
+
+ boolean tryAcquire(int permits);
+
+}
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java
new file mode 100644
index 00000000..e4a50641
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java
@@ -0,0 +1,95 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import cn.hutool.core.thread.ThreadUtil;
+import cn.hutool.core.util.RandomUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 限流器示例
+ *
+ * @author Zhang Peng
+ * @date 2024-01-18
+ */
+@Slf4j
+public class RateLimiterDemo {
+
+ public static void main(String[] args) {
+
+ // ============================================================================
+
+ int qps = 20;
+
+ System.out.println("======================= 固定时间窗口限流算法 =======================");
+ FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(qps);
+ testRateLimit(fixedWindowRateLimiter, qps);
+
+ System.out.println("======================= 滑动时间窗口限流算法 =======================");
+ SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(qps, 10);
+ testRateLimit(slidingWindowRateLimiter, qps);
+
+ System.out.println("======================= 漏桶限流算法 =======================");
+ LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(qps, 100);
+ testRateLimit(leakyBucketRateLimiter, qps);
+
+ System.out.println("======================= 令牌桶限流算法 =======================");
+ TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter(qps, 100);
+ testRateLimit(tokenBucketRateLimiter, qps);
+ }
+
+ private static void testRateLimit(RateLimiter rateLimiter, int qps) {
+
+ AtomicInteger okNum = new AtomicInteger(0);
+ AtomicInteger limitNum = new AtomicInteger(0);
+ ExecutorService executorService = ThreadUtil.newFixedExecutor(10, "限流测试", true);
+ long beginTime = System.currentTimeMillis();
+
+ int threadNum = 4;
+ final CountDownLatch latch = new CountDownLatch(threadNum);
+ for (int i = 0; i < threadNum; i++) {
+ executorService.submit(() -> {
+ try {
+ batchRequest(rateLimiter, okNum, limitNum, 1000);
+ } catch (Exception e) {
+ log.error("发生异常!", e);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ try {
+ latch.await(10, TimeUnit.SECONDS);
+ long endTime = System.currentTimeMillis();
+ long gap = endTime - beginTime;
+ log.info("限流 QPS: {} -> 实际结果:耗时 {} ms,{} 次请求成功,{} 次请求被限流,实际 QPS: {}",
+ qps, gap, okNum.get(), limitNum.get(), okNum.get() * 1000 / gap);
+ if (okNum.get() == qps) {
+ log.info("限流符合预期");
+ }
+ } catch (Exception e) {
+ log.error("发生异常!", e);
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ private static void batchRequest(RateLimiter rateLimiter, AtomicInteger okNum, AtomicInteger limitNum, int num)
+ throws InterruptedException {
+ for (int j = 0; j < num; j++) {
+ if (rateLimiter.tryAcquire(1)) {
+ log.info("请求成功");
+ okNum.getAndIncrement();
+ } else {
+ log.info("请求限流");
+ limitNum.getAndIncrement();
+ }
+ TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(0, 10));
+ }
+ }
+
+}
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java
new file mode 100644
index 00000000..ec5d77d9
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java
@@ -0,0 +1,100 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 基于 Redis + Lua 实现的固定时间窗口限流算法
+ *
+ * @author Zhang Peng
+ * @date 2024-01-23
+ */
+public class RedisFixedWindowRateLimiter implements RateLimiter {
+
+ private static final String REDIS_HOST = "localhost";
+
+ private static final int REDIS_PORT = 6379;
+
+ private static final Jedis JEDIS;
+
+ public static final String SCRIPT;
+
+ static {
+ // Jedis 有多种构造方法,这里选用最简单的一种情况
+ JEDIS = new Jedis(REDIS_HOST, REDIS_PORT);
+
+ // 触发 ping 命令
+ try {
+ JEDIS.ping();
+ System.out.println("jedis 连接成功");
+ } catch (JedisConnectionException e) {
+ e.printStackTrace();
+ }
+
+ SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/fixed_window_rate_limit.lua"),
+ StandardCharsets.UTF_8);
+ }
+
+ private final long maxPermits;
+ private final long periodSeconds;
+ private final String key;
+
+ public RedisFixedWindowRateLimiter(long qps, String key) {
+ this(qps * 60, 60, TimeUnit.SECONDS, key);
+ }
+
+ public RedisFixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, String key) {
+ this.maxPermits = maxPermits;
+ this.periodSeconds = timeUnit.toSeconds(period);
+ this.key = key;
+ }
+
+ @Override
+ public boolean tryAcquire(int permits) {
+ List keys = Collections.singletonList(key);
+ List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(periodSeconds),
+ String.valueOf(maxPermits));
+ Object eval = JEDIS.eval(SCRIPT, keys, args);
+ long value = (long) eval;
+ return value != -1;
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+
+ int qps = 20;
+ RateLimiter jedisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter(qps, "rate:limit:20240122210000");
+
+ // 模拟在一分钟内,不断收到请求,限流是否有效
+ int seconds = 60;
+ long okNum = 0L;
+ long total = 0L;
+ long beginTime = System.currentTimeMillis();
+ int num = RandomUtil.randomInt(qps, 100);
+ for (int second = 0; second < seconds; second++) {
+ for (int i = 0; i < num; i++) {
+ total++;
+ if (jedisFixedWindowRateLimiter.tryAcquire(1)) {
+ okNum++;
+ System.out.println("请求成功");
+ } else {
+ System.out.println("请求限流");
+ }
+ }
+ TimeUnit.SECONDS.sleep(1);
+ }
+ long endTime = System.currentTimeMillis();
+ long time = (endTime - beginTime) / 1000;
+ System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time));
+ }
+
+}
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java
new file mode 100644
index 00000000..9dd219df
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java
@@ -0,0 +1,104 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 基于 Redis + Lua 实现的令牌桶限流算法
+ *
+ * @author Zhang Peng
+ * @date 2024-01-23
+ */
+public class RedisTokenBucketRateLimiter implements RateLimiter {
+
+ private static final String REDIS_HOST = "localhost";
+
+ private static final int REDIS_PORT = 6379;
+
+ private static final Jedis JEDIS;
+
+ public static final String SCRIPT;
+
+ static {
+ // Jedis 有多种构造方法,这里选用最简单的一种情况
+ JEDIS = new Jedis(REDIS_HOST, REDIS_PORT);
+
+ // 触发 ping 命令
+ try {
+ JEDIS.ping();
+ System.out.println("jedis 连接成功");
+ } catch (JedisConnectionException e) {
+ e.printStackTrace();
+ }
+
+ SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/token_bucket_rate_limit.lua"),
+ StandardCharsets.UTF_8);
+ }
+
+ private final long qps;
+ private final long capacity;
+ private final String tokenKey;
+ private final String timeKey;
+
+ public RedisTokenBucketRateLimiter(long qps, long capacity, String tokenKey, String timeKey) {
+ this.qps = qps;
+ this.capacity = capacity;
+ this.tokenKey = tokenKey;
+ this.timeKey = timeKey;
+ }
+
+ @Override
+ public boolean tryAcquire(int permits) {
+ long now = System.currentTimeMillis();
+ List keys = CollectionUtil.newLinkedList(tokenKey, timeKey);
+ List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(qps),
+ String.valueOf(capacity), String.valueOf(now));
+ Object eval = JEDIS.eval(SCRIPT, keys, args);
+ long value = (long) eval;
+ return value != -1;
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+
+ int qps = 20;
+ int bucket = 100;
+ RedisTokenBucketRateLimiter redisTokenBucketRateLimiter =
+ new RedisTokenBucketRateLimiter(qps, bucket, "token:rate:limit", "token:rate:limit:time");
+
+ // 先将令牌桶预热令牌申请完,后续才能真实反映限流 QPS
+ redisTokenBucketRateLimiter.tryAcquire(bucket);
+ TimeUnit.SECONDS.sleep(1);
+
+ // 模拟在一分钟内,不断收到请求,限流是否有效
+ int seconds = 60;
+ long okNum = 0L;
+ long total = 0L;
+ long beginTime = System.currentTimeMillis();
+ for (int second = 0; second < seconds; second++) {
+ int num = RandomUtil.randomInt(qps, 100);
+ for (int i = 0; i < num; i++) {
+ total++;
+ if (redisTokenBucketRateLimiter.tryAcquire(1)) {
+ okNum++;
+ System.out.println("请求成功");
+ } else {
+ System.out.println("请求限流");
+ }
+ }
+ TimeUnit.SECONDS.sleep(1);
+ }
+ long endTime = System.currentTimeMillis();
+ long time = (endTime - beginTime) / 1000;
+ System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time));
+ }
+
+}
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java
new file mode 100644
index 00000000..a93613a2
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java
@@ -0,0 +1,87 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 滑动时间窗口限流算法
+ *
+ * @author Zhang Peng
+ * @date 2024-01-18
+ */
+public class SlidingWindowRateLimiter implements RateLimiter {
+
+ /**
+ * 允许的最大请求数
+ */
+ private final long maxPermits;
+
+ /**
+ * 窗口期时长
+ */
+ private final long periodMillis;
+
+ /**
+ * 分片窗口期时长
+ */
+ private final long shardPeriodMillis;
+
+ /**
+ * 窗口期截止时间
+ */
+ private long lastPeriodMillis;
+
+ /**
+ * 分片窗口数
+ */
+ private final int shardNum;
+
+ /**
+ * 请求总计数
+ */
+ private final AtomicLong totalCount = new AtomicLong(0);
+
+ /**
+ * 分片窗口计数列表
+ */
+ private final List countList = new LinkedList<>();
+
+ public SlidingWindowRateLimiter(long qps, int shardNum) {
+ this(qps, 1000, TimeUnit.MILLISECONDS, shardNum);
+ }
+
+ public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) {
+ this.maxPermits = maxPermits;
+ this.periodMillis = timeUnit.toMillis(period);
+ this.lastPeriodMillis = System.currentTimeMillis();
+ this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum;
+ this.shardNum = shardNum;
+ for (int i = 0; i < shardNum; i++) {
+ countList.add(new AtomicLong(0));
+ }
+ }
+
+ @Override
+ public synchronized boolean tryAcquire(int permits) {
+ long now = System.currentTimeMillis();
+ if (now > lastPeriodMillis) {
+ for (int shardId = 0; shardId < shardNum; shardId++) {
+ long shardCount = countList.get(shardId).get();
+ totalCount.addAndGet(-shardCount);
+ countList.set(shardId, new AtomicLong(0));
+ lastPeriodMillis += shardPeriodMillis;
+ }
+ }
+ int shardId = (int) (now % periodMillis / shardPeriodMillis);
+ if (totalCount.get() + permits <= maxPermits) {
+ countList.get(shardId).addAndGet(permits);
+ totalCount.addAndGet(permits);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java
new file mode 100644
index 00000000..e03e4c7d
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java
@@ -0,0 +1,59 @@
+package io.github.dunwu.distributed.ratelimit;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 令牌桶限流算法
+ *
+ * @author Zhang Peng
+ * @date 2024-01-18
+ */
+public class TokenBucketRateLimiter implements RateLimiter {
+
+ /**
+ * QPS
+ */
+ private final long qps;
+
+ /**
+ * 桶的容量
+ */
+ private final long capacity;
+
+ /**
+ * 上一次令牌发放时间
+ */
+ private long endTimeMillis;
+
+ /**
+ * 桶中当前的令牌数量
+ */
+ private final AtomicLong tokenNum = new AtomicLong(0);
+
+ public TokenBucketRateLimiter(long qps, long capacity) {
+ this.qps = qps;
+ this.capacity = capacity;
+ this.endTimeMillis = System.currentTimeMillis();
+ }
+
+ @Override
+ public synchronized boolean tryAcquire(int permits) {
+
+ long now = System.currentTimeMillis();
+ long gap = now - endTimeMillis;
+
+ // 计算令牌数
+ long newTokenNum = (gap * qps / 1000);
+ long currentTokenNum = tokenNum.get() + newTokenNum;
+ tokenNum.set(Math.min(capacity, currentTokenNum));
+
+ if (tokenNum.get() < permits) {
+ return false;
+ } else {
+ tokenNum.addAndGet(-permits);
+ endTimeMillis = now;
+ return true;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua
new file mode 100644
index 00000000..e0c9ad00
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua
@@ -0,0 +1,21 @@
+-- 缓存 Key
+local key = KEYS[1]
+-- 访问请求数
+local permits = tonumber(ARGV[1])
+-- 过期时间
+local seconds = tonumber(ARGV[2])
+-- 限流阈值
+local limit = tonumber(ARGV[3])
+
+-- 获取统计值
+local count = tonumber(redis.call('GET', key) or "0")
+
+if count + permits > limit then
+ -- 请求拒绝
+ return -1
+else
+ -- 请求通过
+ redis.call('INCRBY', key, permits)
+ redis.call('EXPIRE', key, seconds)
+ return count + permits
+end
\ No newline at end of file
diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua
new file mode 100644
index 00000000..541d70c9
--- /dev/null
+++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua
@@ -0,0 +1,39 @@
+local tokenKey = KEYS[1]
+local timeKey = KEYS[2]
+
+-- 申请令牌数
+local permits = tonumber(ARGV[1])
+-- QPS
+local qps = tonumber(ARGV[2])
+-- 桶的容量
+local capacity = tonumber(ARGV[3])
+-- 当前时间(单位:毫秒)
+local nowMillis = tonumber(ARGV[4])
+-- 填满令牌桶所需要的时间
+local fillTime = capacity / qps
+local ttl = math.min(capacity, math.floor(fillTime * 2))
+
+local currentTokenNum = tonumber(redis.call("GET", tokenKey))
+if currentTokenNum == nil then
+ currentTokenNum = capacity
+end
+
+local endTimeMillis = tonumber(redis.call("GET", timeKey))
+if endTimeMillis == nil then
+ endTimeMillis = 0
+end
+
+local gap = nowMillis - endTimeMillis
+local newTokenNum = math.max(0, gap * qps / 1000)
+local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum)
+
+if currentTokenNum < permits then
+ -- 请求拒绝
+ return -1
+else
+ -- 请求通过
+ local finalTokenNum = currentTokenNum - permits
+ redis.call("SETEX", tokenKey, ttl, finalTokenNum)
+ redis.call("SETEX", timeKey, ttl, nowMillis)
+ return finalTokenNum
+end
diff --git a/codes/java-distributed/java-task/pom.xml b/codes/java-distributed/java-task/pom.xml
new file mode 100644
index 00000000..8d7cc462
--- /dev/null
+++ b/codes/java-distributed/java-task/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+
+ io.github.dunwu.distributed
+ java-distributed
+ 1.0.0
+
+
+ io.github.dunwu.distributed
+ java-task
+ 1.0.0
+ jar
+
+
+ UTF-8
+ 1.8
+ ${java.version}
+ ${java.version}
+
+
+
+
+ cn.hutool
+ hutool-all
+
+
+ org.projectlombok
+ lombok
+
+
+ ch.qos.logback
+ logback-classic
+ 1.2.3
+ true
+
+
+
diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java
new file mode 100644
index 00000000..d510eed1
--- /dev/null
+++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java
@@ -0,0 +1,52 @@
+package io.github.dunwu.local.task;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Date;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.DelayQueue;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class DelayQueueExample {
+
+ public static void main(String[] args) throws InterruptedException {
+ BlockingQueue delayQueue = new DelayQueue<>();
+ long now = System.currentTimeMillis();
+ delayQueue.put(new SampleTask(now + 1000));
+ delayQueue.put(new SampleTask(now + 2000));
+ delayQueue.put(new SampleTask(now + 3000));
+ for (int i = 0; i < 3; i++) {
+ log.info("task 执行时间:{}", DateUtil.format(new Date(delayQueue.take().getTime()), "yyyy-MM-dd HH:mm:ss"));
+ }
+ }
+
+ static class SampleTask implements Delayed {
+
+ long time;
+
+ public SampleTask(long time) {
+ this.time = time;
+ }
+
+ public long getTime() {
+ return time;
+ }
+
+ @Override
+ public int compareTo(Delayed o) {
+ return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+ }
+
+ }
+
+}
+
+
diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java
new file mode 100644
index 00000000..78e8f5bd
--- /dev/null
+++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java
@@ -0,0 +1,37 @@
+package io.github.dunwu.local.task;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Date;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class ScheduledExecutorServiceExample {
+
+ public static void main(String[] args) {
+ // 创建一个 ScheduledExecutorService 对象,它将使用一个线程池来执行任务
+ ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+
+ // 创建一个 Runnable 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次
+ Runnable task = () -> {
+ log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
+ };
+
+ // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次
+ executor.scheduleAtFixedRate(task, 2, 1, TimeUnit.SECONDS);
+
+ // 主线程等待 10 秒后结束
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // 关闭 executor,这将停止所有正在执行的任务,并拒绝新任务的提交
+ executor.shutdown();
+ }
+
+}
diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java
new file mode 100644
index 00000000..ce1d7756
--- /dev/null
+++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java
@@ -0,0 +1,39 @@
+package io.github.dunwu.local.task;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Date;
+import java.util.Timer;
+import java.util.TimerTask;
+
+@Slf4j
+public class TimerExample {
+
+ public static void main(String[] args) {
+ // 创建一个 Timer 对象
+ Timer timer = new Timer();
+
+ // 创建一个 TimerTask 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
+ }
+ };
+
+ // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次
+ timer.schedule(task, 2000, 1000);
+
+ // 主线程等待 10 秒后结束
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // 取消定时器和所有已安排的任务
+ timer.cancel();
+ }
+
+}
diff --git a/codes/java-distributed/pom.xml b/codes/java-distributed/pom.xml
index 35fd5860..aa88d15d 100644
--- a/codes/java-distributed/pom.xml
+++ b/codes/java-distributed/pom.xml
@@ -3,15 +3,52 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- io.github.dunwu.javatech
+ io.github.dunwu.distributed
java-distributed
1.0.0
pom
- JAVA-DISTRIBUTED
- JAVA-DISTRIBUTED 示例源码
java-load-balance
+ java-rate-limit
+ java-distributed-id
+ java-task
+
+
+
+ org.apache.zookeeper
+ zookeeper
+ 3.9.2
+
+
+ org.apache.curator
+ curator-recipes
+ 4.3.0
+
+
+ redis.clients
+ jedis
+ 5.1.0
+
+
+ cn.hutool
+ hutool-all
+ 5.8.34
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ ch.qos.logback
+ logback-classic
+ 1.4.12
+ true
+
+
+
+
diff --git a/codes/javatech/javatech-cache/pom.xml b/codes/javatech/javatech-cache/pom.xml
new file mode 100644
index 00000000..4503c60c
--- /dev/null
+++ b/codes/javatech/javatech-cache/pom.xml
@@ -0,0 +1,76 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.1.RELEASE
+
+
+ io.github.dunwu.javatech
+ javatech-cache
+ 1.0.0
+ jar
+ JAVATECH-缓存示例
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ net.sf.ehcache
+ ehcache
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
+
+ net.spy
+ spymemcached
+ 2.12.2
+
+
+ com.google.guava
+ guava
+ 29.0-jre
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ org.projectlombok
+ lombok
+
+
+ mysql
+ mysql-connector-java
+
+
+ com.h2database
+ h2
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java
new file mode 100644
index 00000000..4a5220d3
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java
@@ -0,0 +1,87 @@
+package io.github.dunwu.javatech;
+
+import io.github.dunwu.javatech.data.User;
+import io.github.dunwu.javatech.data.UserDao;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.sql.DataSource;
+
+/**
+ * @author Zhang Peng
+ * @since 2019-10-14
+ */
+@EnableCaching
+@SpringBootApplication
+public class SpringBootDataCacheApplication implements CommandLineRunner {
+
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ private final UserDao userDao;
+
+ public SpringBootDataCacheApplication(UserDao userDao) {
+ this.userDao = userDao;
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDataCacheApplication.class, args);
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+
+ if (userDao != null) {
+ printDataSourceInfo(userDao.getJdbcTemplate());
+ log.info("连接数据源成功!");
+ } else {
+ log.error("连接数据源失败!");
+ return;
+ }
+
+ for (int i = 1; i <= 3; i++) {
+ User user = userDao.queryByName("张三");
+ log.info("第 {} 次查询 name = {}", i, user.toString());
+ }
+
+ for (int i = 1; i <= 3; i++) {
+ User user = userDao.queryByName("李四");
+ log.info("第 {} 次查询 name = {}", i, user.toString());
+ }
+
+ User result = userDao.queryByName("张三");
+ result.setAddress("深圳");
+ userDao.update(result);
+
+ for (int i = 1; i <= 3; i++) {
+ User user = userDao.queryByName("张三");
+ log.info("第 {} 次查询 name = {}", i, user.toString());
+ }
+ }
+
+ public void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException {
+
+ DataSource dataSource = jdbcTemplate.getDataSource();
+
+ Connection connection;
+ if (dataSource != null) {
+ connection = dataSource.getConnection();
+ } else {
+ log.error("获取 DataSource 失败");
+ return;
+ }
+
+ if (connection != null) {
+ log.info("DB URL: {}", connection.getMetaData().getURL());
+ } else {
+ log.error("获取 Connection 失败");
+ }
+ }
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java
new file mode 100644
index 00000000..484259a6
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java
@@ -0,0 +1,23 @@
+package io.github.dunwu.javatech.cache;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Zhang Peng
+ * @since 2020-07-09
+ */
+public class CaffeineDemo {
+
+ public static void main(String[] args) {
+ Cache cache = Caffeine.newBuilder()
+ .expireAfterWrite(1, TimeUnit.SECONDS)
+ .expireAfterAccess(1, TimeUnit.SECONDS)
+ .maximumSize(10)
+ .build();
+ cache.put("hello", "hello");
+ }
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java
new file mode 100644
index 00000000..1cdb585e
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java
@@ -0,0 +1,55 @@
+package io.github.dunwu.javatech.cache;
+
+import com.google.common.cache.*;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Zhang Peng
+ * @since 2020-07-09
+ */
+public class GuavaCacheDemo {
+
+ public static void main(String[] args) {
+ CacheLoader loader = new CacheLoader() {
+ @Override
+ public String load(String key) throws Exception {
+ Thread.sleep(1000);
+ if ("key".equals(key)) {
+ return null;
+ }
+ System.out.println(key + " is loaded from a cacheLoader!");
+ return key + "'s value";
+ }
+ };
+
+ RemovalListener removalListener = new RemovalListener() {
+ @Override
+ public void onRemoval(RemovalNotification removal) {
+ System.out.println("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");
+ }
+ };
+
+ LoadingCache testCache = CacheBuilder.newBuilder()
+ .maximumSize(7)
+ .expireAfterWrite(10, TimeUnit.MINUTES)
+ .removalListener(removalListener)
+ .build(loader);
+
+ for (int i = 0; i < 10; i++) {
+ String key = "key" + i;
+ String value = "value" + i;
+ testCache.put(key, value);
+ System.out.println("[" + key + ":" + value + "] is put into cache!");
+ }
+
+ System.out.println(testCache.getIfPresent("key6"));
+
+ try {
+ System.out.println(testCache.get("key"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java
new file mode 100644
index 00000000..3d38eae3
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java
@@ -0,0 +1,60 @@
+package io.github.dunwu.javatech.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 通过继承 LinkedHashMap 来实现一个简单的 LRUHashMap
+ *
+ * 核心思想就是:LRU (最近最少使用)算法
+ *
+ * @author Zhang Peng
+ * @since 2020-01-18
+ */
+class LRUCache extends LinkedHashMap {
+
+ private final int max;
+ private Object lock;
+
+ public LRUCache(int max) {
+ //无需扩容
+ super((int) (max * 1.4f), 0.75f, true);
+ this.max = max;
+ this.lock = new Object();
+ }
+
+ /**
+ * 重写LinkedHashMap的removeEldestEntry方法即可 在Put的时候判断,如果为true,就会删除最老的
+ *
+ * @param eldest
+ * @return
+ */
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > max;
+ }
+
+ public Object getValue(Object key) {
+ synchronized (lock) {
+ return get(key);
+ }
+ }
+
+ public void putValue(Object key, Object value) {
+ synchronized (lock) {
+ put(key, value);
+ }
+ }
+
+ public boolean removeValue(Object key) {
+ synchronized (lock) {
+ return remove(key) != null;
+ }
+ }
+
+ public boolean removeAll() {
+ clear();
+ return true;
+ }
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java
new file mode 100644
index 00000000..9399b12d
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java
@@ -0,0 +1,294 @@
+package io.github.dunwu.javatech.cache;
+
+import net.spy.memcached.CASResponse;
+import net.spy.memcached.CASValue;
+import net.spy.memcached.MemcachedClient;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.Future;
+
+/**
+ * Memcached 客户端连接示例
+ *
+ * @author Zhang Peng
+ * @since 2020-07-10
+ */
+public class MemcachedDemo {
+
+ public static final String URL = "127.0.0.1";
+ public static final int PORT = 11211;
+
+ public static void main(String[] args) {
+ add();
+ remove();
+ append();
+ prepend();
+ cas();
+ get();
+ delete();
+ incrAndDecr();
+ }
+
+ public static void add() {
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数据
+ Future fo = mcc.set("MyKey", 900, "Free Education");
+
+ // 打印状态
+ System.out.println("set status:" + fo.get());
+
+ // 输出
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 添加
+ fo = mcc.add("MyKey", 900, "memcached");
+
+ // 打印状态
+ System.out.println("add status:" + fo.get());
+
+ // 添加新key
+ fo = mcc.add("codingground", 900, "All Free Compilers");
+
+ // 打印状态
+ System.out.println("add status:" + fo.get());
+
+ // 输出
+ System.out.println("codingground value in cache - " + mcc.get("codingground"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void remove() {
+
+ try {
+ //连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加第一个 key=》value 对
+ Future fo = mcc.set("MyKey", 900, "Free Education");
+
+ // 输出执行 add 方法后的状态
+ System.out.println("add status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 添加新的 key
+ fo = mcc.replace("MyKey", 900, "Largest Tutorials' Library");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("replace status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void append() {
+
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数据
+ Future fo = mcc.set("MyKey", 900, "Free Education");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("set status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 对存在的key进行数据添加操作
+ fo = mcc.append(900, "MyKey", " for All");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("append status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("codingground"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void prepend() {
+
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数据
+ Future fo = mcc.set("MyKey", 900, "Education for All");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("set status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 对存在的key进行数据添加操作
+ fo = mcc.prepend(900, "MyKey", "Free ");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("prepend status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("codingground"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void cas() {
+
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数据
+ Future fo = mcc.set("MyKey", 900, "Free Education");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("set status:" + fo.get());
+
+ // 使用 get 方法获取数据
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 通过 gets 方法获取 CAS token(令牌)
+ CASValue casValue = mcc.gets("MyKey");
+
+ // 输出 CAS token(令牌) 值
+ System.out.println("CAS token - " + casValue);
+
+ // 尝试使用cas方法来更新数据
+ CASResponse casresp = mcc.cas("MyKey", casValue.getCas(), 900, "Largest Tutorials-Library");
+
+ // 输出 CAS 响应信息
+ System.out.println("CAS Response - " + casresp);
+
+ // 输出值
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void get() {
+
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数据
+ Future fo = mcc.set("MyKey", 900, "Free Education");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("set status:" + fo.get());
+
+ // 使用 get 方法获取数据
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void delete() {
+
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数据
+ Future fo = mcc.set("MyKey", 900, "World's largest online tutorials library");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("set status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("MyKey"));
+
+ // 对存在的key进行数据添加操作
+ fo = mcc.delete("MyKey");
+
+ // 输出执行 delete 方法后的状态
+ System.out.println("delete status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("MyKey value in cache - " + mcc.get("codingground"));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+ public static void incrAndDecr() {
+
+ try {
+
+ // 连接本地的 Memcached 服务
+ MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT));
+ System.out.println("Connection to server sucessful.");
+
+ // 添加数字值
+ Future fo = mcc.set("number", 900, "1000");
+
+ // 输出执行 set 方法后的状态
+ System.out.println("set status:" + fo.get());
+
+ // 获取键对应的值
+ System.out.println("value in cache - " + mcc.get("number"));
+
+ // 自增并输出
+ System.out.println("value in cache after increment - " + mcc.incr("number", 111));
+
+ // 自减并输出
+ System.out.println("value in cache after decrement - " + mcc.decr("number", 112));
+
+ // 关闭连接
+ mcc.shutdown();
+ } catch (Exception ex) {
+ System.out.println(ex.getMessage());
+ }
+ }
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java
new file mode 100644
index 00000000..758e3c9a
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java
@@ -0,0 +1,38 @@
+package io.github.dunwu.javatech.data;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serializable;
+
+@Data
+@ToString
+public class User implements Serializable {
+
+ private static final long serialVersionUID = 4142994984277644695L;
+
+ private Long id;
+
+ private String name;
+
+ private Integer age;
+
+ private String address;
+
+ private String email;
+
+ public User() {}
+
+ public User(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public User(String name, Integer age, String address, String email) {
+ this.name = name;
+ this.age = age;
+ this.address = address;
+ this.email = email;
+ }
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java
new file mode 100644
index 00000000..370147cd
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java
@@ -0,0 +1,33 @@
+package io.github.dunwu.javatech.data;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.List;
+
+public interface UserDao {
+
+ void batchInsert(List users);
+
+ Integer count();
+
+ @CacheEvict(value = "dunwu:users", key = "#name")
+ int deleteByName(String name);
+
+ void insert(User user);
+
+ List list();
+
+ @Cacheable(value = "dunwu:users", key = "#name")
+ User queryByName(String name);
+
+ void recreateTable();
+
+ @CachePut(value = "dunwu:users", key = "#user.name")
+ User update(User user);
+
+ JdbcTemplate getJdbcTemplate();
+
+}
diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java
new file mode 100644
index 00000000..a69ec012
--- /dev/null
+++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java
@@ -0,0 +1,105 @@
+package io.github.dunwu.javatech.data;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class UserDaoImpl implements UserDao {
+
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public UserDaoImpl(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void batchInsert(List users) {
+ String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)";
+
+ List