一般的なアプリケーションレベルのアルゴリズム
1. 負荷分散アルゴリズム
1. ポーリング(RR)
1. 概要: ポーリングとは、順番に列を作ることを意味します。前のスケジューリング アルゴリズムで使用されたタイム スライス ローテーションは、一般的なポーリングです。ただし、以前の実装は配列と添字ポーリングを使用して実装されました。ここでは、サーバー リストのリクエスト ポーリング アルゴリズムを実装するために二重リンク リストを手動で作成してみます。
2. コードの実装
package com.andy.load;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 15:58
*/
public class RR {
class Server {
Server prev;
Server next;
String name;
public Server(String name) {
this.name = name;
}
}
// 当前服务节点
Server current;
// 初始化轮询类,多个服务器ip用逗号隔开
public RR(String serverName) {
System.out.println("init server list : " + serverName);
String[] names = serverName.split(",");
for (int i = 0; i < names.length; i++) {
Server server = new Server(names[i]);
if (current == null) {
// 如果当前服务器为空,说明是第一台机器,current就指向新创建的server
this.current = server;
// 同时,server的前后均指向自己。
current.prev = current;
current.next = current;
} else {
// 否则说明已经有机器了,按新加处理。
addServer(names[i]);
}
}
}
// 添加机器
void addServer(String serverName) {
System.out.println("add server : " + serverName);
Server server = new Server(serverName);
Server next = this.current.next;
// 在当前节点后插入新节点
this.current.next = server;
server.prev = this.current;
// 修改下一节点的prev指针
server.next = next;
next.prev = server;
}
// 将当前服务器移除,同时修改前后节点的指针,让其直接关联
// 移除的current会被回收器回收掉
void remove() {
System.out.println("remove current = " + current.name);
this.current.prev.next = this.current.next;
this.current.next.prev = this.current.prev;
this.current = current.next;
}
// 请求。由当前节点处理即可
// 注意:处理完成后,current指针后移
void request() {
System.out.println(this.current.name);
this.current = current.next;
}
public static void main(String[] args) throws InterruptedException {
// 初始化两台机器
RR rr = new RR("192.168.0.1,192.168.0.2");
// 启动一个额外线程,模拟不停的请求
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
rr.request();
}
}
}).start();
// 3s后,3号机器加入清单
Thread.currentThread().sleep(3000);
rr.addServer("192.168.0.3");
// 3s后,当前服务节点被移除
Thread.currentThread().sleep(3000);
rr.remove();
}
}
3. 結果の分析
- 初期化後は、1 と 2 のみがポーリングされます。
- 3 回の結合の後、1、2、および 3 がポーリングされます。
- 2 を削除すると、1 と 3 の投票だけが残ります
4. メリットとデメリット
- 実装はシンプルで、マシンリストは自由に追加および削除でき、時間計算量は o(1) です。
- ノードに偏ったカスタマイズができず、ノードの処理能力の強弱が差別化できません。
2.ランダム
1. 概要: サービス可能なリストからランダムに 1 つを選択して応答します。ランダム アクセスのシナリオでは、配列を使用してランダム添字読み取りをより効率的に実装するのが適しています。
2. 実現する
package com.andy.load;
import java.util.ArrayList;
import java.util.Random;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 16:11
*/
public class Rand {
ArrayList<String> ips;
public Rand(String nodeNames) {
System.out.println("init list : " + nodeNames);
String[] nodes = nodeNames.split(",");
// 初始化服务器列表,长度取机器数
ips = new ArrayList<>(nodes.length);
for (String node : nodes) {
ips.add(node);
}
}
// 请求
void request() {
// 下标,随机数,注意因子
int i = new Random().nextInt(ips.size());
System.out.println(ips.get(i));
}
// 添加节点,注意,添加节点会造成内部数组扩容
// 可以根据实际情况初始化时预留一定空间
void addnode(String nodeName) {
System.out.println("add node : " + nodeName);
ips.add(nodeName);
}
// 移除
void remove(String nodeName) {
System.out.println("remove node : " + nodeName);
ips.remove(nodeName);
}
public static void main(String[] args) throws InterruptedException {
Rand rd = new Rand("192.168.0.1,192.168.0.2");
// 启动一个额外线程,模拟不停的请求
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
rd.request();
}
}
}).start();
// 3s后,3号机器加入清单
Thread.currentThread().sleep(3000);
rd.addnode("192.168.0.3");
// 3s后,当前服务节点被移除
Thread.currentThread().sleep(3000);
rd.remove("192.168.0.2");
}
}
3. 結果の分析
- 1、2 に初期化されると、2 つは順番にポーリングされず、ランダムに表示されます。
- 3サービスノードリストに参加する
- 2 を削除すると、1 と 3 だけが残りますが、どちらも依然としてランダムで無秩序です。
3. 送信元アドレスのハッシュ
1. 概要: 現在アクセスしている IP アドレスのハッシュ値を作成すると、同じキーが同じマシンにルーティングされます。このシナリオは分散クラスター環境で一般的で、ユーザーのログイン時にルーティングとセッションの維持を要求します。
2. 実現する
package com.andy.load;
import java.util.ArrayList;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 16:18
* 源地址哈希
*/
public class Hash {
ArrayList<String> ips;
public Hash(String nodeNames) {
System.out.println("init list : " + nodeNames);
String[] nodes = nodeNames.split(",");
// 初始化服务器列表,长度取机器数
ips = new ArrayList<>(nodes.length);
for (String node : nodes) {
ips.add(node);
}
}
// 添加节点,注意,添加节点会造成内部Hash重排,思考为什么呢???
// 这是个问题!在一致性hash中会进入详细探讨
void addnode(String nodeName) {
System.out.println("add node : " + nodeName);
ips.add(nodeName);
}
// 移除
void remove(String nodeName) {
System.out.println("remove node : " + nodeName);
ips.remove(nodeName);
}
// 映射到key的算法,这里取余数做下标
private int hash(String ip) {
int last = Integer.valueOf(ip.substring(ip.lastIndexOf(".") + 1, ip.length()));
return last % ips.size();
}
// 请求
// 注意,这里和来访ip是有关系的,采用一个参数,表示当前的来访ip
void request(String ip) {
// 下标
int i = hash(ip);
System.out.println(ip + "-->" + ips.get(i));
}
public static void main(String[] args) {
Hash hash = new Hash("192.168.0.1,192.168.0.2");
for (int i = 1; i < 10; i++) {
// 模拟请求的来源ip
String ip = "192.168.1." + i;
hash.request(ip);
}
hash.addnode("192.168.0.3");
for (int i = 1; i < 10; i++) {
// 模拟请求的来源ip
String ip = "192.168.1." + i;
hash.request(ip);
}
}
}
3. 結果の分析
- 初期化後は 1 と 2 のみで、添え字は最後の ip の残りであり、複数回実行しても応答するマシンは変化せず、セッションの永続性が実現されます。
- 3 が追加されると、再ハッシュされ、マシンの分布が変わります。
- 2 が削除された後、2 にハッシュされた元のリクエストは 3 レスポンスにリダイレクトされます。
4. 加重ポーリング
1. 概要: WeightRoundRobin、ポーリングは単なる機械的な回転であり、加重ポーリングはすべてのマシンが平等に扱われるという欠点を補います。ポーリングに基づいて、初期化時にマシンは比重を運びます
2. 実装: リンクされたリストを維持し、各マシンはその異なる重みに従って異なる番号を占有します。ポーリングの場合、重みが大きく数値が大きいと当然取得回数も多くなります。例: 3 台のマシン a、b、c があり、重みはそれぞれ 4、2、1 です。ランク付けすると、それらは a、a、a、a、b、b、c になります。要求されるたびに、ノードを取得し、次のリクエストで次のノードを取得します。最後まで行ったらまた最初から始めてください
-
しかし問題があります。マシンは均等に分散されておらず、クラスタ化されています...
-
解決策: マシン スムージングの問題を解決するために、nginx のソース コードでは次のようなスムーズな重み付きポーリング アルゴリズムを使用します。
-
各ノードには、weight と currentWeight という 2 つの重みがあります。Weight は変更されず、構成中の値であり、電流は変化し続けます。
-
変更ルールは次のとおりです。前にすべての電流 += 重みを選択し、最大の電流を持つ応答を選択し、応答後に電流 -= 合計にします。
頻度 | 応答する前に | 選択済み | 回答後 |
---|---|---|---|
1 | 4,2,1 | ある | -3,2,1 |
2 | 1,4,2 | b | 1,-3,2 |
3 | 5、-1、3 | ある | -2、-1、3 |
4 | 2,1,4 | c | 2,1,-3 |
5 | 6,3,-2 | ある | -1、3、-2 |
6 | 3,5,-1 | b | 3、-2、-1 |
7 | 7,0,0 | ある | 0,0,0 |
統計: a=4、b=2、c=1、分布は滑らかでバランスが取れています。
package com.andy.load;
import java.util.ArrayList;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 16:29
*/
public class WRR {
class Node {
int weight, currentWeight;
String name;
public Node(String name, int weight) {
this.name = name;
this.weight = weight;
this.currentWeight = 0;
}
@Override
public String toString() {
return String.valueOf(currentWeight);
}
}
// 所有节点的列表
ArrayList<Node> list;
// 总权重
int total;
// 初始化节点列表,格式:a#4,b#2,c#1
public WRR(String nodes) {
String[] ns = nodes.split(",");
list = new ArrayList<>(ns.length);
for (String n : ns) {
String[] n1 = n.split("#");
int weight = Integer.valueOf(n1[1]);
list.add(new Node(n1[0], weight));
total += weight;
}
}
// 获取当前节点
Node getCurrent() {
// 执行前,current加权重
for (Node node : list) {
node.currentWeight += node.weight;
}
// 遍历,取权重最高的返回
Node current = list.get(0);
int i = 0;
for (Node node : list) {
if (node.currentWeight > i) {
i = node.currentWeight;
current = node;
}
}
return current;
}
// 响应
void request() {
// 获取当前节点
Node node = this.getCurrent();
// 第一列,执行前的current
System.out.print(list.toString() + "---");
// 第二列,选中的节点开始响应
System.out.print(node.name + "---");
// 响应后,current减掉total
node.currentWeight -= total;
// 第三列,执行后的current
System.out.println(list);
}
public static void main(String[] args) {
WRR wrr = new WRR("a#4,b#2,c#1");
// 7次执行请求,看结果
for (int i = 0; i < 7; i++) {
wrr.request();
}
}
}
5. 重み付きランダム
1. 概要: WeightRandom、マシンはランダムに選別されますが、重み付けされた値のセットが作成され、重みに応じて選択の確率が異なります。この概念では、ランダム性は重みが等しい特殊なケースと考えることができます。
2. 実装: 設計思想は同じで、重みの大きさに応じて異なる数のノードが生成され、ノードがキューに入れられた後、ランダムに取得されます。ここでのデータ構造には主にランダムな読み取りが含まれるため、配列が推奨されます。ランダムと同じで、配列のランダムなスクリーニングです。違いは、ランダムはマシンごとに 1 つだけであり、重み付けを行うと複数になることです。
package com.andy.load;
import java.util.ArrayList;
import java.util.Random;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 16:32
*/
public class WR {
// 所有节点的列表
ArrayList<String> list;
// 初始化节点列表
public WR(String nodes) {
String[] ns = nodes.split(",");
list = new ArrayList<>();
for (String n : ns) {
String[] n1 = n.split("#");
int weight = Integer.valueOf(n1[1]);
for (int i = 0; i < weight; i++) {
list.add(n1[0]);
}
}
}
void request() {
// 下标,随机数,注意因子
int i = new Random().nextInt(list.size());
System.out.println(list.get(i));
}
public static void main(String[] args) {
WR wr = new WR("a#2,b#1");
for (int i = 0; i < 9; i++) {
wr.request();
}
}
}
3. 結果の分析
- 9 回実行すると、a と b が交互に表示され、a=6、b=3、2:1 の比率が満たされます。
- 知らせ!ランダムなのでランダム性があり、実行されるたびに厳密に比例しない可能性があります。サンプルが無限大になる傾向がある場合、比率はほぼ正確です
6.応用事例
1.nginxアップストリーム
upstream frontend {
#源地址hash
ip_hash;
server 192.168.0.1:8081;
server 192.168.0.2:8082 weight=1 down;
server 192.168.0.3:8083 weight=2;
server 192.168.0.4:8084 weight=3 backup;
server 192.168.0.5:8085 weight=4 max_fails=3 fail_timeout=30s;
}
- ip_hash: 送信元アドレスのハッシュ アルゴリズム
- down: 現在のサーバーが一時的に負荷に参加していないことを示します
- Weight: 重み付けアルゴリズム。デフォルトは 1 です。重みが大きいほど、負荷の重量も大きくなります。
- バックアップ: バックアップ マシン、他のすべての非バックアップ マシンがダウンしているかビジー状態の場合のみ、リクエスト + バックアップ マシン
- max_fails: 失敗の最大数。デフォルト値は 1、ここでは 3、つまり最大 3 回の試行です。
- failed_timeout: タイムアウトは 30 秒で、デフォルト値は 10 秒です。
- 知らせ!Weight および Backup は ip_hash キーワードと一緒に使用できません。
2.springcloudリボンIRule
#设置负载均衡策略 eureka-application-service为调用的服务的名称
eureka-application-service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
-
ラウンドロビンルール: ポーリング
-
ランダムルール: ランダム
-
AvailabilityFilteringRule: まず、複数のアクセス障害によりサーキット ブレーカー トリップ状態にあるサービス、および同時接続数がしきい値を超えているサービスをフィルターで除外し、次に残りのサービスをポーリングします。
-
WeightedResponseTimeRule: 平均応答時間に基づいてすべてのサービスの重みを計算します。応答時間が速いほど、サービスの重みは大きくなります。起動時に統計情報が不足している場合は RoundRobinRule 戦略が使用され、統計情報が十分な場合はこの戦略に切り替わります。
-
RetryRule: まず RoundRobinRule の戦略に従い、サービスの取得に失敗した場合は、指定された時間内に再試行して利用可能なサービスを取得します。
-
BestAvailableRule: まず、複数のアクセス障害によりサーキット ブレーカー トリップ状態にあるサービスをフィルタリングして除外し、次に同時実行量が最小のサービスを選択します。
-
ZoneAvoidanceRule: サーバーが配置されているゾーンのパフォーマンスとサーバーの可用性を総合的に判断するデフォルトのルール
3.ダボロードバランシング
@Service(loadbalance = "roundrobin",weight = 100)
- RandomLoadBalance: ランダム、このメソッドは dubbo のデフォルトの負荷分散戦略です
- RoundRobinLoadBalance: ポーリング
- LeastActiveLoadBalance: アクティブ時間の最小数 dubbo フレームワークは、サービスが呼び出された回数を計算するためにフィルターをカスタマイズしました。
- ConsistentHashLoadBalance: 一貫したハッシュ
2. 暗号化アルゴリズムの適用
1.ハッシュ化
1. 概要: 厳密には暗号化ではありませんが、情報ダイジェストアルゴリズムと呼ぶべきものです。このアルゴリズムは、ハッシュ関数を使用してメッセージまたはデータをダイジェストに圧縮し、データ量を減らし、データの形式を固定します。データを破壊、混合することにより、ハッシュ値と呼ばれる新しい値が生成されます
2. 共通アルゴリズム:MD5、SHA(128、256) シリーズ
3. アプリケーション:
- パスワードの保存やファイルの指紋認証によく使用されます。
- Webサイト利用者登録後、MD5暗号化後のパスワード値がDBに保存されます。再度ログインすると、ユーザーが入力したパスワードは同じ方法で暗号化され、データベース内の暗号文と比較されます。このように、MD5 の不可逆性により、データベースがクラックされたり開発者に見えたりしたとしても、パスワードが何であるかはわかりません。
- 2 つ目はファイル検証シナリオです。たとえば、特定の Web サイトからダウンロードされたファイル (特にシステム イメージ ISO などの大きなファイル) には、公式 Web サイトが署名 (おそらく MD5 または SHA) を配置し、ユーザーがファイルを取得すると、ローカルで実行できます。ハッシュ アルゴリズムと公式 Web サイトの署名 ファイルの整合性を比較して、ファイルが改ざんされているかどうかを判断します
4.
コモンズ座標の実装と追加
<!-- 加密算法包 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
コード:
package com.andy.encrypt;
import org.apache.commons.codec.digest.DigestUtils;
import java.math.BigInteger;
import java.security.MessageDigest;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 17:03
* 散列
*/
public class Hash {
/**
* jdk的security实现md5
* 也可以借助commons-codec包
*/
public static String md5(String src) {
byte[] pwd = null;
try {
pwd = MessageDigest.getInstance("md5").digest(src.getBytes("utf-8"));
} catch (Exception e) {
e.printStackTrace();
}
String code = new BigInteger(1, pwd).toString(16);
for (int i = 0; i < 32 - code.length(); i++) {
code = "0" + code;
}
return code;
}
public static String commonsMd5(String src) {
return DigestUtils.md5Hex(src);
}
/**
* jdk实现sha算法
* 也可以借助commons-codec包
*/
public static String sha(String src) throws Exception {
MessageDigest sha = MessageDigest.getInstance("sha");
byte[] shaByte = sha.digest(src.getBytes("utf-8"));
StringBuffer code = new StringBuffer();
for (int i = 0; i < shaByte.length; i++) {
int val = ((int) shaByte[i]) & 0xff;
if (val < 16) {
code.append("0");
}
code.append(Integer.toHexString(val));
}
return code.toString();
}
public static String commonsSha(String src) throws Exception {
return DigestUtils.sha1Hex(src);
}
public static void main(String[] args) throws Exception {
String name = "Andy测试算法";
System.out.println(name);
System.out.println(md5(name));
System.out.println(commonsMd5(name));
System.out.println(sha(name));
System.out.println(commonsSha(name));
}
}
4. 結果の分析
- jdk と commons の両方が同じハッシュ値を生成します
- 複数回実行しても固定値が生成されます。
- commons-codec には、sha256、sha512 など、利用可能なメソッドも多数あります。
2. 対称性
1. 概要: 暗号化と復号化に同じ秘密鍵が使用されるため、非対称暗号化に比べてパフォーマンスが大幅に向上します。
2. 一般的なアルゴリズム: 一般的な対称暗号化アルゴリズムには、DES、3DES、AES などがあります。
-
DES アルゴリズムは、POS、ATM、磁気カードおよびスマート カード (IC カード)、ガソリン スタンド、高速道路料金所などの分野で広く使用されており、クレジット カード所有者の PIN、IC の暗号化された送信などの主要データの機密性を実現します。カード POS との双方向認証、金融取引データ パケットの MAC 検証など。
-
3DES は DES 暗号化アルゴリズムのモードであり、DES のより安全な変形です。DES から AES への移行アルゴリズム
-
AESは高速かつより高いセキュリティレベルを備えた次世代の暗号化アルゴリズム規格です。
3.用途:高効率が要求されるリアルタイムデータの暗号化通信によく使われます。
4. 実装:
package com.andy.encrypt;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 17:27
*/
public class AES {
public static void main(String[] args) throws Exception {
// 生成KEY
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
// key转换
Key key = new SecretKeySpec(keyGenerator.generateKey().getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
String src = "Andy测试AES";
System.out.println("明文:" + src);
// 加密
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] result = cipher.doFinal(src.getBytes());
System.out.println("加密:" + Base64.encodeBase64String(result));
// 解密
cipher.init(Cipher.DECRYPT_MODE, key);
result = cipher.doFinal(result);
System.out.println("解密:" + new String(result));
}
}
5. 実行結果の分析: 暗号化は成功し、復号化後の平文は一貫しています。
3.非対称
1. 概要: 非対称とは、暗号化と復号化が同じ鍵ではなく、公開鍵と秘密鍵に分割されることを意味します。秘密鍵は個人が所有しており、公開鍵は公開されています。キーの 1 つは暗号化に使用され、もう 1 つは復号化に使用されます。これらのいずれかを使用して暗号化した後、元の平文は対応する他のキーでのみ復号化でき、元々暗号化に使用されたキーであっても復号化には使用できません。非対称暗号化と呼ばれるのはまさにこの特性のためです。
2. 一般的なアルゴリズム: RSA、ElGamal、ナップザック アルゴリズム、Rabin (RSA の特殊なケース)、Diffie-Hellman 鍵交換プロトコルの公開鍵暗号化アルゴリズム、楕円楕円曲線暗号(ECC)。最も広く使用されているのは RSA アルゴリズムです
3.申請
-
最も一般的なのは、https とデジタル署名の 2 点です。
-
厳密に言えば、すべての https リクエストが非対称を使用するわけではありません。パフォーマンスの考慮事項に基づいて、https は最初に非対称キーを使用してキーを合意し、その後、このキーを対称暗号化とデータ送信に使用します。
デジタル署名は、メッセージがサーバーから送信されたかどうかを検証するために使用され、偽造防止と認証に使用されます。プロセスは次のとおりです。
発行:
公開鍵はサーバーの外部に公開され、秘密鍵は機密に保たれます。
サーバーはメッセージ M のダイジェスト (MD5 やその他の公開アルゴリズムなど) を計算し、ダイジェスト D を取得します。サーバーは
秘密鍵を使用して、 D に署名し、署名 S を取得し、
M と S を一緒にクライアントに送信します。
検証:
クライアントは、同じダイジェスト アルゴリズムを使用して M のダイジェストを計算し、ダイジェスト D を取得します。
サーバーの公開鍵を使用して S を復号化し、ダイジェスト D' を取得します
。D と D' が同じであれば、M が証明されます。確かにサーバーによって送信されました。
4. コード:
package com.andy.encrypt;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 17:32
*/
public class RSAUtil {
static String privKey;
static String publicKey;
public static void main(String[] args) throws Exception {
// 生成公钥和私钥
genKeyPair();
// 加密字符串
String message = "Andy测试RES";
System.out.println("明文:" + message);
System.out.println("随机公钥为:" + publicKey);
System.out.println("随机私钥为:" + privKey);
String messageEn = encrypt(message, publicKey);
System.out.println("公钥加密:" + messageEn);
String messageDe = decrypt(messageEn, privKey);
System.out.println("私钥解密:" + messageDe);
}
/**
* 随机生成密钥对
*/
public static void genKeyPair() throws NoSuchAlgorithmException {
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,密钥大小为96-1024位
keyPairGen.initialize(1024, new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
privKey = new String(Base64.encodeBase64((keyPair.getPrivate().getEncoded())));
publicKey = new String(Base64.encodeBase64(keyPair.getPublic().getEncoded()));
}
/**
* RSA公钥加密
*/
public static String encrypt(String str, String publicKey) throws Exception {
// base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
// RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
String outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));
return outStr;
}
/**
* RSA私钥解密
*/
public static String decrypt(String str, String privateKey) throws Exception {
// 64位解码加密后的字符串
byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
}
}
5. 結果の分析
- 暗号化と復号化により完全な復元を実現
- 別のキーで復号化する必要があります。公開キーで暗号化してから公開キーで復号化すると失敗します。
3. 整合性のあるハッシュとその応用
1.背景
負荷分散戦略の中で、ソース アドレス ハッシュ アルゴリズムについて説明しました。これにより、特定のリクエストが対応するサーバーに到達することが可能になります。これにより、セッション情報の保持の問題を解決できます。同時に、マシンノードの数が変化した場合は、標準ハッシュが使用されます。その後、リクエストは再びハッシュ化され、元の設計意図が崩れてしまいます。一貫したハッシュが機能します。
2. 原則
- 4 台のマシンを例にとると、コンシステント ハッシュのアルゴリズムは次のとおりです。
- まず、各サーバーのハッシュ値を見つけて、0 ~ 232 の範囲でサークル上に設定します。
- 次に、同じ方法を使用してデータを保存するキーのハッシュ値を見つけ、それを円にマッピングします。
- データがマッピングされている場所から時計回りに検索し、最初に見つかったサーバーにデータを保存します
- それでも最大値が見つからない場合は、最初の値を取得します。これが比喩的にリングと呼ばれる理由です。
3. 特徴
-
単調性: 単調性とは、一部のリクエストが処理のために対応するサーバーにハッシュされ、新しいサーバーがシステムに追加された場合、元のリクエストが元のサーバーにマッピングされるか、マッピングされずに新しいサーバーに移動できることを保証する必要があることを意味します。他の元のサーバーにマッピングされます。
-
分散 (スプレッド): 分散環境では、クライアントはリクエストを行うときにサーバーの一部しか知らない可能性があります。その場合、2 つのクライアントは異なる部分を認識し、見ているものが完全なハッシュ リングであると考えます。すると問題が発生します。同じキーが異なるサーバーにルーティングされる場合があります。上の図を例にとると、client1 が追加されると 1,4 が認識され、client2 は 2,3 が認識され、2 ~ 4 の間のキーは 2 つのクライアントによって繰り返し 3,4 にマッピングされます。分散は問題の深刻さを反映します。
-
バランス: バランスとは、クライアントからのハッシュされたリクエストを異なるサーバーに分散できる必要があることを意味します。一貫したハッシュは可能な限り分散させることができますが、各サーバーがまったく同じ数のリクエストを処理することは保証できません。この偏差はハッシュ スキューと呼ばれます。ノード分散アルゴリズムが適切に設計されていない場合、バランスに大きな影響を与えます。
最適化: 仮想ノードを追加すると、ハッシュ アルゴリズムが最適化され、セグメンテーションと分散がより詳細になります。つまり、実際には m 台のマシンがありますが、それらが n 回拡張され、m*n がリング上に配置されると、イコライズ後のキー セグメントはより詳細になります。
4. 実装
package com.andy.hash;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 17:41
* Hash一致性算法
*/
public class Hash {
// 服务器列表
private static String[] servers = {
"192.168.0.1",
"192.168.0.2", "192.168.0.3", "192.168.0.4"};
// key表示服务器的hash值,value表示服务器
private static SortedMap<Integer, String> serverMap = new TreeMap<Integer, String>();
static {
for (int i = 0; i < servers.length; i++) {
int hash = getHash(servers[i]);
// 理论上,hash环的最大值为2^32
// 这里为做实例,将ip末尾作为上限也就是254
// 那么服务器是0-4,乘以60后可以均匀分布到 0-254 的环上去
// 实际的请求ip到来时,在环上查找即可
hash *= 60;
System.out.println("add " + servers[i] + ", hash=" + hash);
serverMap.put(hash, servers[i]);
}
}
// 查找节点
private static String getServer(String key) {
int hash = getHash(key);
// 得到大于该Hash值的所有server
SortedMap<Integer, String> subMap = serverMap.tailMap(hash);
if (subMap.isEmpty()) {
// 如果没有比该key的hash值大的,则从第一个node开始
Integer i = serverMap.firstKey();
// 返回对应的服务器
return serverMap.get(i);
} else {
// 第一个Key就是顺时针过去离node最近的那个结点
Integer i = subMap.firstKey();
// 返回对应的服务器
return subMap.get(i);
}
}
// 运算hash值
// 该函数可以自由定义,只要做到取值离散即可
// 这里取ip地址的最后一节
private static int getHash(String str) {
String last = str.substring(str.lastIndexOf(".") + 1, str.length());
return Integer.valueOf(last);
}
public static void main(String[] args) {
// 模拟5个随机ip请求
for (int i = 1; i < 8; i++) {
String ip = "192.168.1." + i * 30;
System.out.println(ip + " ---> " + getServer(ip));
}
// 将5号服务器加到2-3之间,取中间位置,150
System.out.println("add 192.168.0.5,hash=150");
serverMap.put(150, "192.168.0.5");
// 再次发起5个请求
for (int i = 1; i < 8; i++) {
String ip = "192.168.1." + i * 30;
System.out.println(ip + " ---> " + getServer(ip));
}
}
}
5.検証
-
4台のマシンがハッシュリングに参加
-
リクエストをシミュレートし、ハッシュ値に基づいてダウンストリーム ノードへのリクエストを正確にスケジュールします。
-
ノード 5 を追加、キーは 150
-
もう一度リクエストしてください
4. 典型的なビジネス シナリオのアプリケーション
1. Web サイトのセンシティブワードフィルタリング
1. シナリオ: 機密性の高い単語とテキスト フィルタリングは Web サイトの重要な機能であり、効率的なフィルタリング アルゴリズムが非常に必要です。フィルタリングについて最初に思い浮かぶのは次のようなことかもしれません。
- 解決策 1: Java で String contains を使用して、機密性の高い単語を 1 つずつスキャンします。
String[] s = "广告,广告词,中奖".split(",");
String text = "讨厌的广告词";
boolean flag = false;
for (String s1 : s) {
if (text.contains(s1)){
flag = true;
break;
}
}
System.out.println(flag);
- オプション 2、正規表現:
System.out.println(text.matches(".*(广告|广告词|中奖).*"));
2. 概要
DFA は Deterministic Finite Automaton の略で、イベントと現在の状態、つまりイベント + 状態 = 次の状態を通じて次の状態を取得する決定性有限オートマトンです。
上記の場合を比較すると、検索と検索の停止がアクション、検索が見つかるかどうかがステータスとなり、各ステップの検索と結果によって次のステップに進むかどうかが決まります。DFA アルゴリズムを機密単語に適用するための鍵は、機密語彙ライブラリを構築することです。上記のケースを JSON に変換すると、次の式になります。
{
"isEnd": 0,
"广": {
"isEnd": 0,
"告": {
"isEnd": 1,
"词": {
"isEnd": 1
}
}
},
"中": {
"isEnd": 0,
"奖": {
"isEnd": 1
}
}
}
検索プロセスは次のとおりです: まず、テキストを単語ごとに分割し、単語ごとに語彙のキーを検索します。「夰」から始めて、そうでない場合は、次の単語「褋」、「広」まで進みます。見つかった場合は isEnd と判断します。1 の場合は、成功した一致に機密単語が含まれていることを示します。0 の場合は、isEnd=1 になるまで「レポート」との一致を続けます。
マッチング戦略は 2 つあります。最小値と最大値の一致。最小のものは [広告] と一致する必要があり、最大のものは [広告の単語] と最後まで一致する必要があります。
3.java実装:
package com.andy.app;
import com.alibaba.fastjson2.JSON;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 17:49
* 敏感词处理DFA算法
*/
public class SensitiveWordUtil {
// 短匹配规则,如:敏感词库["广告","广告词"],语句:"我是广告词",匹配结果:我是[广告]
public static final int SHORT_MATCH = 1;
// 长匹配规则,如:敏感词库["广告","广告词"],语句:"我是广告词",匹配结果:我是[广告词]
public static final int LONG_MATCH = 2;
/**
* 敏感词库
*/
public static HashMap sensitiveWordMap;
/**
* 初始化敏感词库
* words:敏感词,多个用英文逗号分隔
*/
private static void initSensitiveWordMap(String words) {
String[] w = words.split(",");
sensitiveWordMap = new HashMap(w.length);
Map nowMap;
for (String key : w) {
nowMap = sensitiveWordMap;
for (int i = 0; i < key.length(); i++) {
// 转换成char型
char keyChar = key.charAt(i);
// 库中获取关键字
Map wordMap = (Map) nowMap.get(keyChar);
// 如果不存在新建一个,并加入词库
if (wordMap == null) {
wordMap = new HashMap();
wordMap.put("isEnd", "0");
nowMap.put(keyChar, wordMap);
}
nowMap = wordMap;
if (i == key.length() - 1) {
// 最后一个
nowMap.put("isEnd", "1");
}
}
}
}
/**
* 判断文字是否包含敏感字符
*
* @return 若包含返回true,否则返回false
*/
public static boolean contains(String txt, int matchType) {
for (int i = 0; i < txt.length(); i++) {
int matchFlag = checkSensitiveWord(txt, i, matchType); // 判断是否包含敏感字符
if (matchFlag > 0) {
// 大于0存在,返回true
return true;
}
}
return false;
}
/**
* 沿着文本字符挨个往后检索文字中的敏感词
*/
public static Set<String> getSensitiveWord(String txt, int matchType) {
Set<String> sensitiveWordList = new HashSet<>();
for (int i = 0; i < txt.length(); i++) {
// 判断是否包含敏感字符
int length = checkSensitiveWord(txt, i, matchType);
if (length > 0) {
// 存在,加入list中
sensitiveWordList.add(txt.substring(i, i + length));
// 指针沿着文本往后移动敏感词的长度
// 也就是一旦找到敏感词,加到列表后,越过这个词的字符,继续往下搜索
// 但是必须减1,因为for循环会自增,如果不减会造成下次循环跳格而忽略字符
// 这会造成严重误差
i = i + length - 1;
}
// 如果找不到,i就老老实实一个字一个字的往后移动,作为begin进行下一轮
}
return sensitiveWordList;
}
/**
* 从第beginIndex个字符的位置,往后查找敏感词
* 如果找到,返回敏感词字符的长度,不存在返回0
* 这个长度用于找到后提取敏感词和后移指针,是个性能关注点
*/
private static int checkSensitiveWord(String txt, int beginIndex, int matchType) {
// 敏感词结束标识位:用于敏感词只有1位的情况
boolean flag = false;
// 匹配到的敏感字的个数,也就是敏感词长度
int length = 0;
char word;
// 从根Map开始查找
Map nowMap = sensitiveWordMap;
for (int i = beginIndex; i < txt.length(); i++) {
// 被判断语句的第i个字符开始
word = txt.charAt(i);
// 获取指定key,并且将敏感库指针指向下级map
nowMap = (Map) nowMap.get(word);
if (nowMap != null) {
// 存在,则判断是否为最后一个
// 找到相应key,匹配长度+1
length++;
// 如果为最后一个匹配规则,结束循环,返回匹配标识数
if ("1".equals(nowMap.get("isEnd"))) {
// 结束标志位为true
flag = true;
// 短匹配,直接返回,长匹配还需继续查找
if (SHORT_MATCH == matchType) {
break;
}
}
} else {
// 敏感库不存在,直接中断
break;
}
}
if (length < 2 || !flag) {
// 长度必须大于等于1才算是词,字的话就不必这么折腾了
length = 0;
}
return length;
}
public static void main(String[] args) {
// 初始化敏感词库
SensitiveWordUtil.initSensitiveWordMap("广告,广告词,中奖");
System.out.println("敏感词库结构:" + JSON.toJSONString(sensitiveWordMap));
String string = "关于中奖广告的广告词筛选";
System.out.println("被检测文本:" + string);
System.out.println("待检测字数:" + string.length());
// 是否含有关键字
boolean result = SensitiveWordUtil.contains(string, SensitiveWordUtil.LONG_MATCH);
System.out.println("长匹配:" + result);
result = SensitiveWordUtil.contains(string, SensitiveWordUtil.SHORT_MATCH);
System.out.println("短匹配:" + result);
// 获取语句中的敏感词
Set<String> set = SensitiveWordUtil.getSensitiveWord(string, SensitiveWordUtil.LONG_MATCH);
System.out.println("长匹配到:" + set);
set = SensitiveWordUtil.getSensitiveWord(string, SensitiveWordUtil.SHORT_MATCH);
System.out.println("短匹配到:" + set);
}
}
4. 結果の分析
- 初期化後の機密単語の構造は期待どおりです
- 検出とロングショートマッチングには結果があります
- 一致する機密用語のリストは正しいです
2. 最高の製品トップ
1. 背景:topk は典型的なビジネスシナリオであり、最優秀商品に加え、おすすめランキングやポイントランキングなど、上位 k に関わるすべての箇所がこのアルゴリズムの適用シナリオとなります。
2. 計画を立てる。
- オプション 1
- グローバルソートでは、コレクション全体をソートした後、最大の k 値を取り出すことが望ましい結果になります。
- この解決策は最悪です。必要なのは上位 k 個の要素だけです。他の nk 要素の順序は気にしません。しかし、操作中に無駄な並べ替え操作を実行する必要があります。
- オプション II
- ローカルな並べ替えでは、グローバルな状況は必要ないため、最初の k だけを取得し、後続のものに注意を払う必要はありません。
- バブル ソートは、この操作に適したソート アルゴリズムです。例として上向きのバブリングの最大値を考えると、k 個のバブルが実行されれば、上位 k 個の名前が決定されます。しかし、この解決策は依然として最適なアプローチではありません。必要なのは上位 k 人の名前だけなので、これらの k については、誰が年上で誰が年下であるかを気にする必要はありません。
- 3番目の解決策
- 最小ヒープ。ソートする必要がないため、ソートしません。
- まず、最初の k 個の要素を最小ヒープに形成し、次の nk 個の要素をヒープの先頭と順番に比較し、小さい場合は破棄し、大きい場合はヒープの先頭を破棄します。置き換えられ、ヒープが調整されます。すべての n が完了するまで。Min-heap は、topk の古典的なソリューションです
3. コード:
package com.andy.app;
import java.util.Arrays;
/**
* @author Andy
* @version 0.0.1
* @since 2023-09-15 18:00
*/
public class Topk {
// 堆元素下沉,形成最小堆,序号从i开始
static void down(int[] nodes, int i) {
// 顶点序号遍历,只要到1半即可,时间复杂度为O(log2n)
while (i << 1 < nodes.length) {
// 左子,为何左移1位?回顾一下二叉树序号
int left = i << 1;
// 右子,左+1即可
int right = left + 1;
// 标记,指向 本节点,左、右子节点里最小的,一开始取i自己
int flag = i;
// 判断左子是否小于本节点
if (nodes[left] < nodes[i]) {
flag = left;
}
// 判断右子
if (right < nodes.length && nodes[flag] > nodes[right]) {
flag = right;
}
// 两者中最小的与本节点不相等,则交换
if (flag != i) {
int temp = nodes[i];
nodes[i] = nodes[flag];
nodes[flag] = temp;
i = flag;
} else {
// 否则相等,堆排序完成,退出循环即可
break;
}
}
}
public static void main(String[] args) {
// 原始数据
int[] src = {
3, 6, 2, 7, 4, 8, 1, 9, 2, 5};
// 要取几个
int k = 5;
// 堆,为啥是k+1?请注意,最小堆的0是无用的,序号从1开始
int[] nodes = new int[k + 1];
// 取前k个数,注意这里只是个二叉树,还不满足最小堆的要求
for (int i = 0; i < k; i++) {
nodes[i + 1] = src[i];
}
System.out.println("before:" + Arrays.toString(nodes));
// 从最底的子树开始,堆顶下沉
// 这里才真正的形成最小堆
for (int i = k >> 1; i >= 1; i--) {
down(nodes, i);
}
System.out.println("create:" + Arrays.toString(nodes));
// 把余下的n-k个数,放到堆顶,依次下沉,topk堆算法的开始
for (int i = src.length - k; i < src.length; i++) {
if (nodes[1] < src[i]) {
nodes[1] = src[i];
down(nodes, 1);
}
}
System.out.println("topk:" + Arrays.toString(nodes));
}
}
4. 結果の分析
- 最終的に、k 値が正常に取得され、要件を満たしました。
- 並べ替えの問題は関係ありません