目录
1.遇到线上相关问题怎么排查?
2.高并发系统的限流如何实现?
3.高并发秒杀系统的设计?
补充问题:
秒杀并发情况下库存为负数问题
4.负载均衡如何设计?
服务端负载均衡
客户端负载均衡
负载均衡算法
静态负载均衡算法
动态负载均衡算法
5.假如双十一等一些促销有高并发访问量要来访问我们的数据,怎么样做到可靠的服务?
6.一个黑名单集合,数据量很大,快速查询一个值是否在集合里,怎么设计?
背景知识:
1.布隆过滤器基本介绍、特点及使用场景
2.布隆过滤器原理
3.简单实现一个布隆过滤
4.Guava实现布隆过滤及源码分析
补充问题:
一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。
7.常见的设计模式及应用场景。
参考书籍、文献和资料
备注:针对基本问题做一些基本的总结,不是详细解答!
以个人看法来看:
首先,必须了解当前的问题是什么?有什么现象?具体的业务场景是什么?先将遇到的问题进行明确!
然后,按当前的现象结合实际情况和业务日志来定位问题:网络原因?服务环境问题(CPU/IO/内存等)?数据库问题?Redis等中间件问题?JVM异常问题?业务场景本身存在的问题?
然后,按照具体问题来分析:
常见的限流算法有计数器、漏桶和令牌桶算法。
漏桶算法在分布式环境中消息中间件或者Redis都是可选的方案。
令牌桶算法发放令牌的频率增加可以提升整体数据处理的速度,通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。
相关见博客:https://blog.csdn.net/xiaofeng10330111/article/details/86772740
架构设计上要提前分析,梳理主流程和可能遇到的种种问题,优化设计,同时做到“4 要 1 不要”原则,也就是:数据要尽量少、请求数要尽量少、路径要尽量短、依赖要尽量少,以及不要有单点。
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。
update products set quantity = quantity-1 WHERE id=3;
select quantity from products WHERE id=3 for update;
quantity = select quantity from products WHERE id=3;
update products set quantity = ($quantity-1) WHERE id=3 and queantity = $quantity;
负载均衡建立在现有网络结构上,提供了一种廉价、有效、透明的方法扩展服务器的带宽、增加吞吐量、加强网络数据处理能力,以及提高网络的灵活性。以各种负载均衡算法为基础的分发策略决定了负载均衡的效果,根据服务器地址列表所存放的位置可以分为两大类,一类是服务器负载均衡,另一类是客服端负载均衡。
客户端发送请求到负载均衡器LB,负载均衡器负责将接收到的各个请求转发到运行中的某台服务节点上,然后接收到请求的微服务做响应处理,常见的有Apache、Nginx、HAProxy。
其实现机制比较忙简单,只需要在客服端与各个微服务实例之间架设集中式的负载均衡器即可,负载均衡器动态获取各个微服务运行时的信息,决定负载均衡的目标服务,若负载均衡器检测到某个服务已经不可用的时候就会自动移除该服务。
注意,负载均衡器运行在一台独立的服务器上并充当代理的作用,同时,需要注意的是当服务请求越来越大的时候,负载均衡器就会成为系统的瓶颈,同时若负载均衡器自身发生失败时,整体服务的调用都将发生失败。
客户端负载均衡机制的主要优势就是不会出现集中式负载均均衡所产生的瓶颈问题,因为每个客户端都有自己的负载均衡器,负载均衡器失败也不会造成严重的后果,但是运行时的信息在多个负载均衡器之间进行服务配置信息的传递会在一定程度上加重网络流量负载。
实现上,需要在客服端程序里面自己设定一个调度算法,在向服务器发起请求的时候,先执行调度算法计算出目标服务器地址。
客户端负载均衡比较适合于客户端具有成熟的调度库函数、算法以及API的工具和框架。
大致可以分为两大类,即静态负载均衡算法和动态负载均衡算法。
主要指的是各种随机算法和轮询算法。
根据服务器的实时性能分配连接是常见的动态策略。所有涉及权重的静态算法都可以转变为动态算法。常见的有以下几种:
相关见博客:https://blog.csdn.net/xiaofeng10330111/article/details/85682513
架构设计上要提前分析,梳理主流程和可能遇到的种种问题,优化设计,同时做到“4 要 1 不要”原则,也就是:数据要尽量少、请求数要尽量少、路径要尽量短、依赖要尽量少,以及不要有单点。
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。
采用布隆过滤器,使用一个byte数组保存黑名单集合,使用布隆过滤器原理来判断快速查询一个值是否在集合里。
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
假设遇到这样一个问题:一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。
哈希算法得出的Integer哈希值最大为:Integer.MAX_VALUE=2147483647
,意思就是任何一个URL的哈希都会在0~2147483647之间。那么可以定义一个2147483647长度的byte数组,用来存储集合所有可能的值。为了存储这个byte数组,系统只需要:2147483647/8/1024/1024=256M
。比如:某个URL(X)的哈希是2,那么落到这个byte数组在第二位上就是1,这个byte数组将是:000….00000010,重复的,将这20亿个数全部哈希并落到byte数组中。
判断逻辑:如果byte数组上的第二位是1,那么这个URL(X)可能存在。为什么是可能?因为有可能其它URL因哈希碰撞哈希出来的也是2,这就是误判。但是如果这个byte数组上的第二位是0,那么这个URL(X)就一定不存在集合中。
多次哈希:为了减少因哈希碰撞导致的误判概率,可以对这个URL(X)用不同的哈希算法进行N次哈希,得出N个哈希值,落到这个byte数组上,如果这N个位置没有都为1,那么这个URL(X)就一定不存在集合中。
算法特点
使用场景
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k
以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。
注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。
可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。
整个的写入、查询的流程就是这样,汇总起来就是:对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1 。当有数据查询时也是同样的方式定位到数组中。一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中。
所以布隆过滤有以下几个特点:
第一点应该都能理解,重点解释下 2、3 点。
为什么返回存在的数据却是可能存在呢,这其实也和 HashMap
类似。
在有限的数组长度中存放大量的数据,即便是再完美的 Hash 算法也会有冲突,所以有可能两个完全不同的 A、B
两个数据最后定位到的位置是一模一样的。这时拿 B 进行查询时那自然就是误报了。
删除数据也是同理,当我把 B 的数据删除时,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。
基于以上的 Hash
冲突的前提,所以 Bloom Filter
有一定的误报率,这个误报率和 Hash
算法的次数 H,以及数组长度 L 都是有关的。
基本思路:
hash
运算,同时把对应的位置置为 1。hash
运算,取到对应的值,一旦值为 0 ,则认为数据不存在。注意:提高数组长度以及 hash
计算次数可以降低误报率,但相应的 CPU、内存
的消耗就会提高;这就需要根据业务需要自行权衡。
public class BloomFilters {
/**
* 数组长度
*/
private int arraySize;
/**
* 数组
*/
private int[] array;
public BloomFilters(int arraySize) {
this.arraySize = arraySize;
array = new int[arraySize];
}
/**
* 写入数据
* @param key
*/
public void add(String key) {
int first = hashcode_1(key);
int second = hashcode_2(key);
int third = hashcode_3(key);
array[first % arraySize] = 1;
array[second % arraySize] = 1;
array[third % arraySize] = 1;
}
/**
* 判断数据是否存在
* @param key
* @return
*/
public boolean check(String key) {
int first = hashcode_1(key);
int second = hashcode_2(key);
int third = hashcode_3(key);
if (array[first % arraySize] == 0 || array[second % arraySize] == 0
array[third % arraySize] == 0 ) {
return false;
}
return true;
}
/**
* hash 算法1
* @param key
* @return
*/
private int hashcode_1(String key) {
int hash = 0;
int i;
for (i = 0; i < key.length(); ++i) {
hash = 33 * hash + key.charAt(i);
}
return Math.abs(hash);
}
/**
* hash 算法2
* @param data
* @return
*/
private int hashcode_2(String data) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < data.length(); i++) {
hash = (hash ^ data.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return Math.abs(hash);
}
/**
* hash 算法3
* @param key
* @return
*/
private int hashcode_3(String key) {
int hash, i;
for (hash = 0, i = 0; i < key.length(); ++i) {
hash += key.charAt(i);
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return Math.abs(hash);
}
}
为使性能效果和内存利用率做到最好,建议使用Guava BloomFilter实现。
@Test
public void guavaTest() {
long star = System.currentTimeMillis();
BloomFilter filter = BloomFilter.create(
Funnels.integerFunnel(),
10000000,
0.01);
for (int i = 0; i < 10000000; i++) {
filter.put(i);
}
Assert.assertFalse(filter.mightContain(96998));
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - star));
}
源码分析如下:
static BloomFilter create(
Funnel super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
checkNotNull(funnel);
checkArgument(expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
checkNotNull(strategy);
if (expectedInsertions == 0) {
expectedInsertions = 1;
}
long numBits = optimalNumOfBits(expectedInsertions, fpp);
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
try {
return new BloomFilter(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
}
}
从代码可以看出,需要4个参数,分别是
从上面代码可知,BloomFilter创建过程中先检查参数的合法性
使用n和p来计算bitmap的大小m(optimalNumOfBits(expectedInsertions, fpp))
通过n和m计算hash函数的个数k(optimalNumOfHashFunctions(expectedInsertions, numBits))
这俩方法的具体实现如下:
static int optimalNumOfHashFunctions(long n, long m) {
// (m / n) * log(2), but avoid truncation due to division!
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
除此之外,BloomFilter除了提供创建和几个核心的功能外,还支持写入Stream或从Stream中重新生成BloomFilter,方便数据的共享和传输。
最关键的两个函数如下:
put函数和mightContain函数
MURMUR128_MITZ_64() {
@Override
public boolean put(
T object, Funnel super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
boolean bitsChanged = false;
long combinedHash = hash1;
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
combinedHash += hash2;
}
return bitsChanged;
}
@Override
public boolean mightContain(
T object, Funnel super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
long combinedHash = hash1;
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
return false;
}
combinedHash += hash2;
}
return true;
}
抽象来看,put是写,mightContain是读,两个方法的代码有一点相似,都是先利用murmur3 hash对输入的funnel计算得到128位的字节数组,然后高低分别取8个字节(64位)创建2个long型整数hash1,hash2作为哈希值。循环体内采用了2个函数模拟其他函数的思想,即上文提到的gi(x) = h1(x) + ih2(x) ,这相当于每次累加hash2,然后通过基于bitSize取模的方式在bit数组中索引。
在put方法中,先是将索引位置上的二进制置为1,然后用bitsChanged记录插入结果,如果返回true表明没有重复插入成功,而mightContain方法则是将索引位置上的数值取出,并判断是否为0,只要其中出现一个0,那么立即判断为不存在。
再说一下底层bit数组的实现,主要代码如下:
static final class BitArray {
final long[] data;
long bitCount;
BitArray(long bits) {
this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
}
// Used by serialization
BitArray(long[] data) {
checkArgument(data.length > 0, "data length is zero!");
this.data = data;
long bitCount = 0;
for (long value : data) {
bitCount += Long.bitCount(value);
}
this.bitCount = bitCount;
}
/** Returns true if the bit changed value. */
boolean set(long index) {
if (!get(index)) {
data[(int) (index >>> 6)] |= (1L << index);
bitCount++;
return true;
}
return false;
}
boolean get(long index) {
return (data[(int) (index >>> 6)] & (1L << index)) != 0;
}
/** Number of bits */
long bitSize() {
return (long) data.length * Long.SIZE;
}
...
}
Guava没有使用java.util.BitSet,而是封装了一个long型的数组,另外还有一个long型整数,用来统计数组中已经占用(置为1)的数量,在第一个构造函数中,它把传入的long型整数按长度64分段(例如129分为3段),段数作为数组的长度,你可以想象成由若干个64位数组拼接成一个超长的数组,它的长度就是64乘以段数,即bitSize,在第二个构造函数中利用Long.bitCount方法来统计对应二进制编码中的1个数,这个方法在JDK1.5中就有了,其算法设计得非常精妙,有精力的同学可以自行研究。
另外两个重要的方法是set和get,在get方法中,参考put和mightContain方法,传入的参数index是经过bitSize取模的,因此一定能落在这个超长数组的范围之内,为了获取index对应索引位置上的值,首先将其无符号右移6位,并且强制转换成int型,这相当于除以64向下取整的操作,也就是换算成段数,得到该段上的数值之后,又将1左移index位,最后进行按位与的操作,如果结果等于0,那么返回false,从而在mightContain中判断为不存在。在set方法中,首先调用了get方法判断是否已经存在,如果不存在,则用同样的逻辑取出data数组中对应索引位置的数值,然后按位或并赋值回去。
到这里,对Guava中布隆过滤器的实现就基本讨论完了,简单总结一下:
答案如上!
这个后期出几篇关于这个的相关博客,具体会在这边做补充。
1.https://www.cnblogs.com/crossoverJie/p/10018231.html
2.https://cloud.tencent.com/developer/article/1533083
3.https://www.jianshu.com/p/88c6ac4b38c8
4.https://blog.csdn.net/xindoo/article/details/103183445
5.https://segmentfault.com/a/1190000012620152