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.结果分析
4.优缺点
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.概述:对当前访问的ip地址做一个hash值,相同的key被路由到同一台机器去。场景常见于分布式集群环境下,用户登录时的请求路由和会话保持。
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.概述:WeightRoundRobin,轮询只是机械的旋转,加权轮询弥补了所有机器一视同仁的缺点。在轮询的基础上,初始化时,机器携带一个比重
2.实现:维护一个链表,每个机器根据权重不同,占据的个数不同。轮询时权重大的,个数多,自然取到的次数变大。举个例子:a,b,c 三台机器,权重分别为4,2,1,排位后会是a,a,a,a,b,b,c,每次请求时,从列表中依次取节点,下次请求再取下一个。到末尾时,再从头开始
但是这样有一个问题:机器分布不够均匀,扎堆出现了…
解决:为解决机器平滑出现的问题,nginx的源码中使用了一种平滑的加权轮询的算法,规则如下:
每个节点两个权重,weight和currentWeight,weight永远不变是配置时的值,current不停变化
变化规律如下:选择前所有current+=weight,选current最大的响应,响应后让它的current-=total
次数 | 响应前 | 被选中 | 响应后 |
---|---|---|---|
1 | 4,2,1 | a | -3,2,1 |
2 | 1,4,2 | b | 1,-3,2 |
3 | 5,-1,3 | a | -2,-1,3 |
4 | 2,1,4 | c | 2,1,-3 |
5 | 6,3,-2 | a | -1,3,-2 |
6 | 3,5,-1 | b | 3,-2,-1 |
7 | 7,0,0 | a | 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();
}
}
}
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.结果分析
1.nginx upstream
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;
}
2.springcloud ribbon IRule
#设置负载均衡策略 eureka-application-service为调用的服务的名称
eureka-application-service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
RoundRobinRule:轮询
RandomRule:随机
AvailabilityFilteringRule:先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务轮询
WeightedResponseTimeRule:根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大。刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够,会切换到该策略
RetryRule:先按照RoundRobinRule的策略,如果获取服务失败则在指定时间内重试,获取可用的服务
BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
ZoneAvoidanceRule:默认规则,综合判断server所在区域的性能和server的可用性
3.dubbo负载均衡
@Service(loadbalance = "roundrobin",weight = 100)
1.概述:严格来讲这不算是一种加密,而应该叫做信息摘要算法。该算法使用散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。通过数据打乱混合,重新创建一个叫做 散列值
2.常见算法:MD5、SHA(128、256)系列
3.应用:
4.实现
添加commons坐标
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
<version>1.15version>
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));
}
}
1.概述:加密与解密用的都是同一个秘钥,性能比非对称加密高很多。
2.常见算法:常见的对称加密算法有 DES、3DES、AES
DES算法在POS、ATM、磁卡及智能卡(IC卡)、加油站、高速公路收费站等领域被广泛应用,以此来实现关键数据的保密,如信用卡持卡人的PIN的加密传输,IC卡与POS间的双向认证、金融交易数据包的MAC校验等
3DES是DES加密算法的一种模式,是DES的一个更安全的变形。从DES向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));
}
}
1.概述:非对称即加密与解密不是同一把钥匙,而是分成公钥和私钥。私钥在个人手里,公钥公开。这一对钥匙一个用于加密,另一个用于解密。使用其中一个加密后,则原始明文只能用对应的另一个密钥解密,即使最初用于加密的密钥也不能用作解密。正是因为这种特性,所以称为非对称加密
2.常见算法:RSA、ElGamal、背包算法、Rabin(RSA的特例)、迪菲-赫尔曼密钥交换协议中的公钥加密算法、椭圆曲线加密算法(英语:Elliptic Curve Cryptography, ECC)。使用最广泛的是RSA算法
数字签名则是用于验证报文是否为服务器发出的,用于防伪和认证。过程如下:
签发:
服务器外发布公钥,私钥保密
服务器对消息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));
}
}
负载均衡策略中,我们提到过源地址hash算法,让某些请求固定的落在对应的服务器上。这样可以解决会话信息保留的问题。同时,标准的hash,如果机器节点数发生变更。那么请求会被重新hash,打破了原始的设计初衷,怎么解决呢?一致性hash上场。
单调性(Monotonicity):单调性是指如果已经有一些请求通过哈希分派到了相应的服务器进行处理,又有新的服务器加入到系统中时候,应保证原有的请求可以被映射到原有的或者新的服务器中去,而不会被映射到原来的其它服务器上去。
分散性(Spread):分布式环境中,客户端请求时可能只知道其中一部分服务器,那么两个客户端看到不同的部分,并且认为自己看到的都是完整的hash环,那么问题来了,相同的key可能被路由到不同服务器上去。以上图为例,加入client1看到的是1,4;client2看到的是2,3;那么2-4之间的key会被俩客户端重复映射到3,4上去。分散性反应的是这种问题的严重程度。
平衡性(Balance):平衡性是指客户端hash后的请求应该能够分散到不同的服务器上去。一致性hash可以做到尽量分散,但是不能保证每个服务器处理的请求的数量完全相同。这种偏差称为hash倾斜。如果节点的分布算法设计不合理,那么平衡性就会收到很大的影响。
优化:增加虚拟节点可以优化hash算法,使得切段和分布更细化。即实际有m台机器,但是扩充n倍,在环上放置m*n个,那么均分后,key的段会分布更细化
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));
}
}
}
4台机器加入hash环
模拟请求,根据hash值,准确调度到下游节点
添加节点5,key取150
再次发起请求
1.场景:敏感词、文字过滤是一个网站必不可少的功能,高效的过滤算法是非常有必要的。针对过滤首先想到的可能是这样:
String[] s = "广告,广告词,中奖".split(",");
String text = "讨厌的广告词";
boolean flag = false;
for (String s1 : s) {
if (text.contains(s1)){
flag = true;
break;
}
}
System.out.println(flag);
System.out.println(text.matches(".*(广告|广告词|中奖).*"));
2.概述
DFA即Deterministic Finite Automaton,也就是确定有穷自动机,它是是通过event和当前的state得到下一个state,即event+state=nextstate。
对照到以上案例,查找和停止查找是动作,找没找到是状态,每一步的查找和结果决定下一步要不要继续。DFA算法在敏感词上应用的关键是构建敏感词库,如果我们把以上案例翻译成json表达如下
{
"isEnd": 0,
"广": {
"isEnd": 0,
"告": {
"isEnd": 1,
"词": {
"isEnd": 1
}
}
},
"中": {
"isEnd": 0,
"奖": {
"isEnd": 1
}
}
}
查找过程如下:首先把text按字拆分,逐个字查找词库的key,先从“讨”开始,没有就下一个字“厌”,直到“广”,找到就判断isEnd,如果为1,说明匹配成功包含敏感词,如果为0,那就继续匹配“告”,直到isEnd=1为止。
匹配策略上,有两种。最小和最大匹配。最小则匹配【广告】,最大则需要匹配到底【广告词】
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);
}
}
1.背景:topk是一个典型的业务场景,除了最优商品,包括推荐排名、积分排名所有涉及到排名前k的地方都是该算法的应用场合
2.方案;
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.结果分析