Tencent后台开发Java岗二面:Java中高级核心知识全面解析

这里很简单,内部仅维护了一个 `byte` 类型的 `data` 数组,实际上 `byte` 仍然占有一个字节之多,可以优化成 `bit`来代替,这里也仅仅是用于方便模拟。另外我也创建了三个不同的`hash`函数,其实也就是借鉴`HashMap`哈希抖动的办法,分别使用自身的 `hash `和右移不同位数相异或的结果。并且提供了基础的 `add` 和 `contains` 方法。

下面我们来简单测试一下这个布隆过滤器的效果如何:
```java
public static void main(String[] args) { 
   Random random = new Random(); 
   // 假设我们的数据有 1 百万 
   int size = 1_000_000; 
   // 用一个数据结构保存一下所有实际存在的值 
   LinkedList<Integer> existentNumbers = new LinkedList<>(); 
   BloomFilter bloomFilter = new BloomFilter(size); 
   
   for (int i = 0; i < size; i++) { 
   	int randomKey = random.nextInt(); 
   	existentNumbers.add(randomKey); 
   	bloomFilter.add(randomKey); 
   }
   
   // 验证已存在的数是否都存在 
   AtomicInteger count = new AtomicInteger(); 
   AtomicInteger finalCount = count; 
   existentNumbers.forEach(number -> { 
   	if (bloomFilter.contains(number)) {
   		finalCount.incrementAndGet(); 
   	} 
   }); 
   System.out.printf("实际的数据量: %d, 判断存在的数据量: %d \n", size, count.get()); 
   
   // 验证10个不存在的数 
   count = new AtomicInteger(); 
   while (count.get() < 10) { 
   	int key = random.nextInt(); 
   	if (existentNumbers.contains(key)) { 
   		continue; 
   	} else { 
   		// 这里一定是不存在的数 
   		System.out.println(bloomFilter.contains(key)); 
   		count.incrementAndGet(); 
   	} 
   } 
}

输出如下:

实际的数据量: 1000000, 判断存在的数据量: 1000000 
false 
true 
false 
true 
true 
true 
false 
false 
true 
false

这就是前面说到的,当布隆过滤器说某个值 存在时,这个值 可能不存在,当它说某个值不存在时,那就 肯定不存在,并且还有一定的误判率…

2)手动实现参考

当然上面的版本特别 low,不过主体思想是不差的,这里也给出一个好一些的版本用作自己实现测试的参考:

import java.util.BitSet; 

public class MyBloomFilter { 

	/**
		* 位数组的大小 
	*/ 
	private static final int DEFAULT_SIZE = 2 << 24; 
	/**
		* 通过这个数组可以创建 6 个不同的哈希函数 
		*/ 
	private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; 

	/**
		* 位数组。数组中的元素只能是 0 或者 1
		*/ 
	private BitSet bits = new BitSet(DEFAULT_SIZE); 
	
	/**
		* 存放包含 hash 函数的类的数组 
		*/ 
	private SimpleHash[] func = new SimpleHash[SEEDS.length]; 
	
	/**
		* 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 
		*/ 
	public MyBloomFilter() { 
		// 初始化多个不同的 Hash 函数 
		for (int i = 0; i < SEEDS.length; i++) { 
			func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); 
		} 
	}
	
	/**
		* 添加元素到位数组 
		*/ 
	public void add(Object value) { 
		for (SimpleHash f : func) { 
			bits.set(f.hash(value), true); 
		} 
	}
	
	/**
		* 判断指定元素是否存在于位数组 
		*/ 
	public boolean contains(Object value) { 
		boolean ret = true; 
		for (SimpleHash f : func) { 
			ret = ret && bits.get(f.hash(value)); 
		}
		return ret; 
	}
	
	/**
		* 静态内部类。用于 hash 操作! 
		*/ 
	public static class SimpleHash { 
		private int cap; 
		private int seed; 
		public SimpleHash(int cap, int seed) { 
			this.cap = cap; 
			this.seed = seed; 
		}
		
		/**
			* 计算 hash 值 
			*/ 
		public int hash(Object value) { 
			int h; 
			return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = 
value.hashCode()) ^ (h >>> 16)));
		} 
	} 
}

3)使用 Google 开源的 Guava 中自带的布隆过滤器

自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。

首先我们需要在项目中引入 Guava 的依赖:

<dependency> 
	<groupId>com.google.guava</groupId> 
	<artifactId>guava</artifactId> 
	<version>28.0-jre</version> 
</dependency>

实际使用如下:
我们创建了一个最多存放 最多 1500 个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)

// 创建布隆过滤器对象 
BloomFilter<Integer> filter = BloomFilter.create( 
	Funnels.integerFunnel(), 
	1500, 
	0.01); 
// 判断指定元素是否存在 
System.out.println(filter.mightContain(1)); 
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器 
filter.put(1); 
filter.put(2); 
System.out.println(filter.mightContain(1)); 
System.out.println(filter.mightContain(2));

在我们的示例中,当 mightContain()方法返回 true 时,我们可以 99% 确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100% 确定该元素不存在于过滤器中。

Guava 提供的布隆过滤器的实现还是很不错的 (想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 (另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了。

二、GeoHash查找附近的人

像微信 “附近的人”,美团 “附近的餐厅”,支付宝共享单车 “附近的车” 是怎么设计实现的呢?

1.使用数据库实现查找附近的人

我们都知道,地球上的任何一个位置都可以使用二维的 经纬度 来表示,经度范围 [-180, 180],纬度范围 [-90, 90],纬度正负以赤道为界,北正南负,经度正负以本初子午线 (英国格林尼治天文台) 为界,东正西负。比如说,北京人民英雄纪念碑的经纬度坐标就是 (39.904610, 116.397724),都是正数,因为中国位于东北半球。

所以,当我们使用数据库存储了所有人的 经纬度 信息之后,我们就可以基于当前的坐标节点,来划分出一个矩形的范围,来得知附近的人,如下图:
image
所以,我们很容易写出下列的伪 SQL 语句:

SELECT id FROM positions WHERE x0 - r < x < x0 + r AND y0 - r < y < y0 + r

如果我们还想进一步地知道与每个坐标元素的距离并排序的话,就需要一定的计算。

当两个坐标元素的距离不是很远的时候,我们就可以简单利用 勾股定理 就能够得出他们之间的 距离。不过需要注意的是,地球不是一个标准的球体,经纬度的密度不一样 的,所以我们使用勾股定理计算平方之后再求和时,需要按照一定的系数 加权 再进行求和。当然,如果不准求精确的话,加权也不必了。

参考下方 参考资料 2 我们能够差不多能写出如下优化之后的 SQL 语句来:(仅供参考)

SELECT
	* 
FROM
	users_location 
WHERE
	latitude > '.$lat.' - 1 
	AND latitude < '.$lat.' + 1 
	AND longitude > '.$lon.' - 1 
	AND longitude < '.$lon.' + 1 
ORDER BY 
	ACOS(SIN( ( '.$lat.' * 3.1415 ) / 180 ) * SIN( ( latitude * 3.1415 ) / 180 ) + COS( ( '.$lat.' * 3.1415 ) / 180 ) * 
	COS( ( latitude * 3.1415 ) / 180 ) * COS( ( '.$lon.' * 3.1415 ) / 180 - ( longitude * 3.1415 ) / 180 ) ) * 6380 ASC LIMIT 10 ';

为了满足高性能的矩形区域算法,数据表也需要把经纬度坐标加上 双向复合索引 (x, y),这样可以满足最大优化查询性能。

2.GeoHash 算法简述

这是业界比较通用的,用于 地理位置距离排序 的一个算法,Redis 也采用了这样的算法。GeoHash算法将 二维的经纬度 数据映射到 一维 的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算 「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。

它的核心思想就是把整个地球看成是一个 二维的平面,然后把这个平面不断地等分成一个一个小的方格,每一个 坐标元素都位于其中的 唯一一个方格 中,等分之后的 方格越小,那么坐标也就越精确,类似下图:
QQ:2046136117免费获取资料
经过划分的地球,我们需要对其进行编码:
QQ:2046136117免费获取资料
经过这样顺序的编码之后,如果你仔细观察一会儿,你就会发现一些规律:

  • 横着的所有编码中,第 2 位和第 4 位都是一样的,例如第一排第一个 0101 和第二个 0111 ,他们的第 2 位和第 4 位都是 1
  • 竖着的所有编码中,第 1 位和第 3 位是递增的,例如第一排第一个 0101 ,如果单独把第 1 位和第 3 位拎出来的话,那就是 00 ,同理看第一排第二个0111,同样的方法第 1 位和第 3 位拎出来是 01 ,刚好是 00 递增一个;

通过这样的规律我们就把每一个小方块儿进行了一定顺序的编码,这样做的 好处 是显而易见的:每一个元素坐标既能够被 唯一标识 在这张被编码的地图上,也不至于 暴露特别的具体的位置,因为区域是共享的,我可以告诉你我就在公园附近,但是在具体的哪个地方你就无从得知了。

总之,我们通过上面的思想,能够把任意坐标变成一串二进制的编码了,类似于11010010110001000100这样 (注意经度和维度是交替出现的哦…),通过这个整数我们就可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程序就越小。对于 “附近的人” 这个功能来说,损失的一点经度可以忽略不计。

最后就是一个 Base32 (0~9, a~z, 去掉 a/i/l/o 四个字母) 的编码操作,让它变成一个字符串,例如上面那一串儿就变成了 wx4g0ec1

在 Redis 中,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset的 value 是元素的 keyscoreGeoHash52 位整数值。zset 的score虽然是浮点数,但是对于 52 位的整数值来说,它可以无损存储。

3.在Redis中使用Geo

下方内容引自 参考资料 1 - 《Redis 深度历险》

在使用 Redis 进行 Geo 查询 时,我们要时刻想到它的内部结构实际上只是一个 zset(skiplist)。通过zset 的 score 排序就可以得到坐标附近的其他元素 (实际情况要复杂一些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标了。

Redis 提供的 Geo 指令只有 6 个,很容易就可以掌握。

1)增加

geoadd指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组。

127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin 
(integer) 1 
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader 
(integer) 1 
127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan 
(integer) 1 
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 
xiaomi 
(integer) 2

不过很奇怪… Redis 没有直接提供 Geo 的删除指令,但是我们可以通过 zset 相关的指令来操作 Geo 数据,所以元素删除可以使用 zrem 指令即可。

2)距离

geodist指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。

127.0.0.1:6379> geodist company juejin ireader km 
"10.5501" 
127.0.0.1:6379> geodist company juejin meituan km 
"1.3878" 
127.0.0.1:6379> geodist company juejin jd km 
"24.2739" 
127.0.0.1:6379> geodist company juejin xiaomi km 
"12.9606" 
127.0.0.1:6379> geodist company juejin juejin km 
"0.0000"

我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 m 、 km 、 ml 、 ft ,分别代表米、千米、英里和尺。

3)获取元素位置

geopos指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。

127.0.0.1:6379> geopos company juejin 
1) 1) "116.48104995489120483" 
	2) "39.99679348858259686" 
127.0.0.1:6379> geopos company ireader 
1) 1) "116.5142020583152771" 
	2) "39.90540918662494363" 
127.0.0.1:6379> geopos company juejin ireader 
1) 1) "116.48104995489120483" 
	2) "39.99679348858259686" 
2) 1) "116.5142020583152771" 
	2) "39.90540918662494363"

我们观察到获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 Geohash 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于 「附近的人」 这种功能来说,这点误差根本不是事。

4)获取元素的 hash 值

geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32编码。 你可以使用这个编码值去 http://geohash.org/${hash} 中进行直接定位,它是 Geohash 的标准编码值。

127.0.0.1:6379> geohash company ireader 
1) "wx4g52e1ce0" 
127.0.0.1:6379> geohash company juejin 
1) "wx4gd94yjn0"

让我们打开地址 http://geohash.org/wx4g52e1ce0 ,观察地图指向的位置是否正确:
QQ:2046136117免费获取资料
很好,就是这个位置,非常准确。

5)附近的公司

georadiusbymember指令是最为关键的指令,它可以用来查询指定元素附近的其它元素,它的参数非常复杂。

# 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身 
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc 
1) "ireader" 
2) "juejin" 
3) "meituan" 
# 范围 20 公里以内最多 3 个元素按距离倒排
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc 
1) "jd" 
2) "meituan" 
3) "juejin" 
# 三个可选参数 withcoord withdist withhash 用来携带附加参数 
# withdist 很有用,它可以用来显示距离 
127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc 
1) 1) "ireader" 
	2) "0.0000" 
	3) (integer) 4069886008361398 
	4) 1) "116.5142020583152771" 
	2) "39.90540918662494363" 
2) 1) "juejin" 
	2) "10.5501" 
	3) (integer) 4069887154388167 
	4) 1) "116.48104995489120483" 
	2) "39.99679348858259686" 
3) 1) "meituan" 
	2) "11.5748" 
	3) (integer) 4069887179083478 
	4) 1) "116.48903220891952515" 
	2) "40.00766997707732031"

除了georadiusbymember指令根据元素查询附近的元素,Redis 还提供了根据坐标值来查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近的餐馆」等。它的参数和georadiusbymember基本一致,除了将目标元素改成经纬度坐标值:

127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc 
1) 1) "ireader" 
	2) "0.0000" 
2) 1) "juejin" 
	2) "10.5501" 
3) 1) "meituan" 
	2) "11.5748"

6)注意事项

在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 RedisGeo数据结构,它们将 全部放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。

所以,这里建议 Geo 的数据使用 单独的 Redis 实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。

三、持久化

1.持久化简介

Redis 的数据 全部存储内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证Redis的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态保存到磁盘中。

1)持久化发生了什么 | 从内存到磁盘

我们来稍微考虑一下 Redis 作为一个 “内存数据库” 要做的关于持久化的事情。通常来说,从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情:
QQ:2046136117免费获取资料
详细版 的文字描述大概就是下面这样:

  1. 客户端向数据库 发送写命令 (数据在客户端的内存中)
  2. 数据库 接收 到客户端的 写请求 (数据在服务器的内存中)
  3. 数据库 调用系统 API 将数据写入磁盘 (数据在内核缓冲区中)
  4. 操作系统将 写缓冲区 传输到 磁盘控控制器 (数据在磁盘缓存中)
  5. 操作系统的磁盘控制器将数据 写入实际的物理媒介 中 (数据在磁盘中)

注意: 上面的过程其实是 极度精简 的,在实际的操作系统中,缓存缓冲区会比这多得多…

2)如何尽可能保证持久化的安全

如果我们故障仅仅涉及到 软件层面 (该进程被管理员终止或程序崩溃) 并且没有接触到内核,那么在 上 述步骤 3 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。

如果我们考虑 停电/ 火灾更具灾难性 的事情,那么只有在完成了第 5 步之后,才是安全的。

所以我们可以总结得出数据安全最重要的阶段是:步骤三、四、五,即:

  • 数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少?
  • 内核多久从缓冲区取数据刷新到磁盘控制器?
  • 磁盘控制器多久把数据写入物理媒介一次?
  • 注意: 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断丢失,所以只能 尽可能地保证 数据的安全,这对于所有数据库来说都是一样的。

我们从 第三步 开始。Linux 系统提供了清晰、易用的用于操作文件的 POSIX file API20 多年过去,仍然还有很多人对于这一套 API 的设计津津乐道,我想其中一个原因就是因为你光从API 的命名就能够很清晰地知道这一套 API 的用途:

Kafka进阶篇知识点

image

Kafka高级篇知识点

image

44个Kafka知识点(基础+进阶+高级)解析如下

image

安全,这对于所有数据库来说都是一样的。

我们从 第三步 开始。Linux 系统提供了清晰、易用的用于操作文件的 POSIX file API20 多年过去,仍然还有很多人对于这一套 API 的设计津津乐道,我想其中一个原因就是因为你光从API 的命名就能够很清晰地知道这一套 API 的用途:

Kafka进阶篇知识点

[外链图片转存中…(img-pUIAh7VV-1628140079975)]

Kafka高级篇知识点

[外链图片转存中…(img-QfdNF2Nh-1628140079976)]

44个Kafka知识点(基础+进阶+高级)解析如下

[外链图片转存中…(img-uM6KTOuv-1628140079977)]

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**,有需求的朋友可以戳这里免费下载

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值