面试官初体验
前言
近期作为后台开发面试官整理的一些简单面试题(仅供参考仅供参考仅供参考
基础题
Golang
Golang中函数调用是传值还是传引用
golang中所有函数参数传递都是传值,
slice
、map
和chan
看上去像传引用只是因为他们内部有指针或本身就是指针而已;slice
结构体里有一个指向底层数组array的指针,所以slice
在作为函数参数传递进去的时候,虽然和map
以及chan
一样可以修改其中的值,但是内部slice
若使用append之类的方法修改了大小,则这部分长度信息的变化不会反馈到外层slice
中,甚至会因为底层数组扩容(cap
扩充)导致内外slice
指向了不同的底层数组;而map
和chan
因为本质上就是指针,故所有函数内的变动都会反馈到外面,除非在函数内部改变了这些指针指向的内存(这也是map
和chan
的copy
的实现方法)Golang中
make
和new
的区别?make
只能用来分配及初始化类型为slice
、map
、chan
的数据,new
可以分配任意类型的数据new
分配返回的是指针,即类型*Type
。make
返回引用,即 Typenew
分配的空间被清零;make
分配空间后,会进行初始化
以下程序输出为?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3}
mymap := make(map[int]*int)
for index, value := range slice {
mymap[index] = &value
}
for key, value := range mymap {
fmt.Printf("map[%v]: %v\n", key, *value)
}
}实际输出为:
1
2
3
4map[3]=3
map[0]=3
map[1]=3
map[2]=3因为
for range
创建了迭代对象每个元素的副本,而不是直接返回每个元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会导致错误,在迭代时,返回的变量是同一个迭代过程中根据切片依次赋值的变量,所以最终map
中存储的地址都是同一个变量的地址,而其值即为最后一次迭代中赋的值以下程序输出为?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import (
"fmt"
"sync"
)
func main() {
var nums = []int{1, 2, 3}
var wg sync.WaitGroup
for i := range nums {
wg.Add(1)
go func() {
fmt.Print(nums[i])
wg.Done()
}()
}
wg.Wait()
}实际输出为:
1
333
每轮循环启动一个协程,而协程启动与循环变量递增不是在同一个协程,协程启动的速度远小于循环执行的速度,所以即使是第一个协程刚起启动时,循环变量可能已经递增完毕。由于所有的协程共享循环变量i,而且这个i会在最后一个使用它的协程结束后被销毁,所以最后输出结果时
i
是循环变量的末值即2
,输出的都是nums[2]
要想正确地打印
nums
内的元素,我们可以这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main
import (
"fmt"
"sync"
)
func main() {
var nums = []int{1, 2, 3}
var wg sync.WaitGroup
for i := range nums {
wg.Add(1)
go func(i int) {
fmt.Print(nums[i])
wg.Done()
}(i)
}
wg.Wait()
}以下程序的输出是?
1
2
3var b uint64 = 10
atomic.AddUint64(&b,-3)
fmt.Println(b)实际上这样写会使 Go 语言的编译器报错:
constant -3 overflows uint64
因为这样的运算溢出了
要正确的做这样的运算我们需要:
1
2
3
4var b uint64 = 10
var c int64 = -3
atomic.AddUint64(&b, uint64(c))
fmt.Println(b)go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?
阅读以下程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package main
import (
"fmt"
)
func main() {
fmt.Println("a return:", a())
fmt.Println("b return:", b())
}
func a() int {
var i int
defer func() {
i++
fmt.Println("defer:", i)
}()
defer func() {
i++
fmt.Println("defer:", i)
}()
return i
}
func b() (i int) {
defer func() {
i++
fmt.Println("defer:", i)
}()
defer func() {
i++
fmt.Println("defer:", i)
}()
return
}输出为?
1
2
3
4
5
6
7
8
9a defer: ?
a defer: ?
a return: ?
b defer: ?
b defer: ?
b return: ?
c defer: ?
c defer: ?
c return: ?实际输出为:
1
2
3
4
5
6
7
8
9a defer: 1
a defer: 2
a return: 0
b defer: 1
b defer: 2
b return: 2
c defer: 1
c defer: 2
c return: 2- 多个defer的执行顺序为“后进先出”,栈的形式
- defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出
返回值不同的原因:
a()int
函数的返回值没有被提前声明,其值来自于其他变量的赋值,而defer中修改的也是其他变量,而非返回值本身,因此函数退出时返回值并没有被改变b()(i int)
函数的返回值被提前声明,也就意味着defer中是可以调用到真实返回值的,因此defer在return赋值返回值 i 之后,再一次地修改了 i 的值,最终函数退出后的返回值才会是defer修改过的值c()*int
的返回值虽然没有被提前声明,但是由于c()*int
的返回值是指针变量,那么在return将变量 i 的地址赋给返回值后,defer再次修改了 i 在内存中的实际值,因此函数退出时返回值虽然依旧是原来的指针地址,但是其指向的内存实际值已经被成功修改了Golang 中的
interface
是什么?go里面的interface分为两种:不带接口的eface和带接口的的iface
eface只包含两个成员变量,分别是 指向interface类型信息结构体的指针和指向interface所包含的值的空间的指针
iface包含两部分,itab结构和data结构。data结构与eface中的data结构一样,指向的是interface的值。itab结构中包含了interfacetype指针指向了该interface的静态类型,也就是该interface的类型,比如上文提到的TestInterface这个interface,以及这个interface所包含的方法集。itab的_type字段指向了该iface的动态类型类型,也就是我们把一个实现了该interface所有方法的interface赋值给当前interface的类型。fun字段指向了这个动态类型的函数集,这个函数集是大于静态类型的interface的方法集的。因为它可能包含自己一些另外的方法集。这些函数的集合以字母数序表做排序。至于为何这是一个动态数组,系统位数确定的情况下,函数指针大小是 固定的,往后照着排就可以了
反射是什么?Golang中的反射是基于什么实现的?
在编译时不知道类型的情况下,通过反射机制可以获取对象的类型、值、方法甚至动态改变对象的成员,这就是反射机制
Golang Reflection 三大法则:
- Reflection goes from interface value to reflection object
- Reflection goes from reflection object to interface value
- To modify a reflection object, the value must be settable
总而言之,Golang中的反射是基于
interface
实现的,因为任意类型都实现了interface{}
类型,所以可以把任意类型的变量转换成interface{}
类型,所以反射能从interface{}
的数据结构中反射出对象,也就是利用reflect.ValueOf和reflect.TypeOf从interface反射出对象的具体信息以下程序会输出什么?
1
2
3
4
5
6
7
8
9
10package main
import "reflect"
func main() {
var x int = 8
v := reflect.ValueOf(x)
v.SetInt(24)
println(x)
}会
Panic
:1
panic: reflect: reflect.Value.SetInt using unaddressable value
对应 Reflection 三大法则的第三条,要修改一个反射对象,它的值必须是能修改的;简单点说,就是通过反射,必须反射出它的本身,而不是它的一份拷贝
传入
reflect.ValueOf()
的x只是x的一个拷贝,对v的修改反应不到x本身上,所以会panic那怎么反射出它的本身,和我们函数传参的时候很像:地址;将上文的
v := reflect.ValueOf(x)
改为v := reflect.ValueOf(&x).Elem()
即可,如果想要操作原变量,反射变量Value
必须要 hold 住原变量的地址才行:1
2
3
4
5
6
7
8
9
10package main
import "reflect"
func main() {
var x int = 8
v := reflect.ValueOf(&x).Elem()
v.SetInt(24)
println(x)
}已关闭的channel再次读取会出现什么现象?
closed channel是可以被消费者继续读取的,在读完了有意义的数据之后,将读到一堆空值
如何判断一个
channel
已关闭?1
2
3
4
5
6
7
8
9func IsClosed(ch <-chan interface{}) bool {
select {
case <-ch:
return true
default:
}
return false
}函数中ch是一个只读的channel,因为<-ch左侧没有接收者,所以如果channel没有关闭,
case <-ch
就会被阻塞,又因为select中写了default语句,所以select会直接执行default的空分支,最终return false;如果channel关闭了,case <-ch
就不会被阻塞,直接执行该分支return truemap 循环是有序的还是无序的?
无序的
Java
ArrayList和LinkedList的区别
数据的更新和查找
ArrayList的所有数据是在同一个地址上,而LinkedList的每个数据都拥有自己的地址.所以在对数据进行查找的时候,由于LinkedList的每个数据地址不一样,get数据的时候ArrayList的速度会优于LinkedList,而更新数据的时候,虽然都是通过循环循环到指定节点修改数据,但LinkedList的查询速度已经是慢的,而且对于LinkedList而言,更新数据时不像ArrayList只需要找到对应下标更新就好,LinkedList需要修改指针,速率不言而喻
数据的增加和删除
对于数据的增加元素,ArrayList是通过移动该元素之后的元素位置,其后元素位置全部+1,所以耗时较长,而LinkedList只需要将该元素前的后续指针指向该元素并将该元素的后续指针指向之后的元素即可。与增加相同,删除元素时ArrayList需要将被删除元素之后的元素位置-1,而LinkedList只需要将之后的元素前置指针指向前一元素,前一元素的指针指向后一元素即可。当然,事实上,若是单一元素的增删,尤其是在List末端增删一个元素,二者效率不相上下
HashMap 和 HashTable 区别
最大的区别:
HashMap 不是线程安全的
HashMap 是 map 接口的实现类,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap 允许 null key 和 null value,而 HashTable 不允许
HashTable 是线程安全 Collection
HashMap 是 HashTable 的轻量级实现,他们都完成了Map 接口,主要区别在于 HashMap 允许 null key 和 null value,由于非线程安全,效率上可能高于 Hashtable
其它区别如下:
- HashMap允许将 null 作为一个 entry 的 key 或者 value,而 Hashtable 不允许。
- HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey。因为 contains 方法容易让人引起误解。
- HashTable 继承自 Dictionary 类,而 HashMap 是 Java1.2 引进的 Map interface 的一个实现。
- HashTable 的方法是 Synchronize 的,而 HashMap 不是,在多个线程访问 Hashtable 时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供外同步。
- Hashtable 和 HashMap 采用的 hash/rehash 算法都大概一样,所以性能不会有很大的差异。
List接口、Set接口和Map接口的区别
Collection表示一组对象,这些对象也称为collection的元素;一些 collection允许有重复的元素,而另一些则不允许;一些collection是有序的,而另一些则是无序的;JDK中不提供此接口的任何直接实 现,它提供更具体的子接口(如 Set 和 List)实现;Map没有继承Collection接口,Map提供key到value的映射;一个Map中不能包含相同key,每个key只能映射一个value;Map接口提供3种集合的视图,Map的内容可以被当做一组key集合,一组value集合,或者一组key-value映射
Java线程间的通信方式
wait()方法
wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。
当前的线程必须拥有当前对象的monitor,也即lock,就是锁。
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
一个小比较:
当线程调用了wait()方法时,它会释放掉对象的锁。
另一个会导致线程暂停的方法:Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。
notify()方法
notify()方法会唤醒一个等待当前对象的锁的线程。
如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait方法中的一个)。
被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁。
被唤醒的线程将和其他线程以通常的方式进行竞争,来获得对象的锁。也就是说,被唤醒的线程并没有什么优先权,也没有什么劣势,对象的下一个线程还是需要通过一般性的竞争。
notify()方法应该是被拥有对象的锁的线程所调用。
(This method should only be called by a thread that is the owner of this object’s monitor)
换句话说,和wait()方法一样,notify方法调用必须放在synchronized方法或synchronized块中。
wait()和notify()方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中。
一个线程变为一个对象的锁的拥有者是通过下列三种方法:
- 执行这个对象的synchronized实例方法。
- 执行这个对象的synchronized语句块。这个语句块锁的是这个对象。
- 对于Class类的对象,执行那个类的synchronized、static方法。
判断对象是垃圾 ?
有两种经典的判断方法:
引用计数法,思路很简单,但是如果出现循环引用,即:A 引用 B,B 又引用 A,这种情况下就不好办了,所以 JVM 中使用了另一种称为“可达性分析”的判断方法
可达性分析法
如果 A 引用 B,B 又引用 A,这 2 个对象是否能被 GC 回收?
关键不是在于 A、B 之间是否有引用,而是 A、B 是否可以一直向上追溯到 GC Roots。如果与 GC Roots 没有关联,则会被回收,否则将继续存活。
上图是一个用“可达性分析”标记垃圾对象的示例图,灰色的对象表示不可达对象,将等待回收。
常用的 GC 算法
mark-sweep 标记清除法
如上图,黑色区域表示待清理的垃圾对象,标记出来后直接清空。该方法简单快速,但是缺点也很明显,会产生很多内存碎片
mark-copy 标记复制法
思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50%的内存。
mark-compact 标记-整理(也称标记-压缩)法
避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。
equals()和 ==的区别
JAVA当中所有的类都是继承于Object这个超类的,在Object类中定义了一个equals的方法,equals的源码是这样写的:
1
2
3
4
5public boolean equals(Object obj) {
//this - s1
//obj - s2
return (this == obj);
}可以看到,这个方法的初始默认行为是比较对象的内存地址值,一般来说,意义不大。所以,在一些类库当中这个方法被重写了,如String、Integer、Date。在这些类当中equals有其自身的实现(一般都是用来比较对象的成员变量值是否相同),而不再是比较类在堆内存中的存放地址了。
所以说,对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是内存中的存放位置的地址值,跟双等号(==)的结果相同;如果被复写,按照复写的要求来。我们对上面的两段内容做个总结吧:
== 的作用:
基本类型:比较的就是值是否相同
引用类型:比较的就是地址值是否相同
equals 的作用:
引用类型:默认情况下,比较的是地址值。
注:不过,我们可以根据情况自己重写该方法。一般重写都是自动生成,比较对象的成员变量值是否相同
Redis
redis事务满足关系性数据库事务的特性吗
Redis
事务仅具有一致性与隔离性,不保证原子性和持久性zset的实现原理(或者说数据结构)
redis中pipeline的作用
pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回,主要作用是减少与
redis server
的IO次数,减少网络IO消耗LRU 和 LFU 是?
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也很高”,反过来说“如果数据最近这段时间一直都没有访问,那么将来被访问的概率也会很低”,两种理解是一样的;常用于页面置换算法,为虚拟页式存储管理服务。
分布式锁的基本实现以及坑(展开到进阶实现)
基于 Redis 的
NX EX
参数利用
Redis set key
的 NX 参数来保证在这个 key 不存在的情况下写入成功,并且再加上 EX 参数设置过期时间进阶:
A进程释放了B进程的锁
如果进程 A 获取了锁设置了超时时间,但是由于执行周期较长导致到了超时时间之后锁就自动释放了。这时进程 B 获取了该锁,然而这时进程 A 执行完了,释放了该锁;这样就会出现进程 A 将进程 B 的锁释放了
所以最好的方式是在每次解锁时都需要判断锁是否是自己的(生成uuid放到value中)锁过期了,业务还没执行完
设置看门狗
,在业务执行时自动延长锁的过期时间(举例子:假如加锁的时间是30秒,过10秒检查一次,一旦加锁的业务没有执行完,就会进行一次续期,把锁的过期时间再次重置成30秒)redis 主从复制导致锁丢失
在分布式redis中,主节点没来的及把刚刚set进来这条数据给从节点,就挂了,这就造成了redis异步复制造成的锁丢失
使用RedLock
:红锁算法认为,只要n/2+1个节点加锁成功,那么就认为获取了锁, 解锁时将所有实例解锁。 流程为:- 顺序向五个节点请求加锁
- 根据一定的超时时间来推断是不是跳过该节点
- 三个节点加锁成功并且花费时间小于锁的有效期
- 认定加锁成功
Redis的内存碎片是什么?Redis的内存碎片清理机制是什么?
产生内存碎片的主要原因:
redis
自己实现的内存分配器:在redis
中新建key-value
值时,redis
需要向操作系统申请内存,一般的进程在不需要使用申请的内存后,会直接释放掉、归还内存;但redis
不一样,redis
在使用完内存后并不会直接归还内存,而是放在redis
自己实现的内存分配器中管理,这样就不需要每次都向操作系统申请内存了,实现了高性能(但这样其它应用可就不高兴了,自私的Redis)value
的更新:redis
的每个key-value
对初始化的内存大小是最适合的,当这个value
改变的并且原来内存大小不适用的时候,就需要重新分配内存了,重新分配之后,就会有一部分内存redis
无法正常回收,一直占用着
如何清理内存碎片?
Redis版本4.0以下
重启
redis
,自动归还所有内存,简单粗暴Redis版本4.0以上
可以开启自动内存碎片清理:
1
2
3TEXT
127.0.0.1:6379[6]> config set activedefrag yes
OK自动内存清理的一些相关配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16TEXT
# Enabled active defragmentation
# 碎片整理总开关
# activedefrag yes
# Minimum amount of fragmentation waste to start active defrag
# 内存碎片达到多少的时候开启整理
active-defrag-ignore-bytes 100mb
# Minimum percentage of fragmentation to start active defrag
# 碎片率达到百分之多少开启整理
active-defrag-threshold-lower 10
# Maximum percentage of fragmentation at which we use maximum effort
# 碎片率小余多少百分比开启整理
active-defrag-threshold-upper 100当然,在面对一些复杂的场景时我们希望能根据自己设计的策略来进行内存碎片清理,
redis
也提供了手动内存碎片清理的命令:1
2
3TEXT
127.0.0.1:6379> memory purge
OK
数据库
如何用Mysql实现分布式锁
创建一张锁表,然后通过操作该表中的数据来实现加锁和解锁。当要锁住某个方法或资源时,就向该表插入一条记录,表中设置方法名为唯一键,这样多个请求同时提交数据库时,只有一个操作可以成功,判定操作成功的线程获得该方法的锁,可以执行方法内容;想要释放锁的时候就删除这条记录,其他线程就可以继续往数据库中插入数据获取锁
什么是触发器?触发器的使用场景有哪些?
触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程。触发器是指一段代码,当触发某个事件时,自动执行这些代码
使用场景
可以通过数据库中的相关表实现级联更改。
实时监控某张表中的某个字段的更改而需要做出相应的处理。
例如可以生成某些业务的编号。
注意不要滥用,否则会造成数据库及应用程序的维护困难。
大家需要牢记以上基础知识点,重点是理解数据类型CHAR和VARCHAR的差异,表存储引擎InnoDB和MyISAM的区别MySQL中都有哪些触发器?
在MySQL数据库中有如下六种触发器:
Before Insert
After Insert
Before Update
After Update
Before Delete
After DeleteSQL 约束有哪几种?
NOT NULL: 用于控制字段的内容一定不能为空(NULL)。
UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。
PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。
FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
CHECK: 用于控制字段的值范围六种关联查询
交叉连接(CROSS JOIN)
内连接(INNER JOIN)
外连接(LEFT JOIN/RIGHT JOIN)
联合查询(UNION与UNION ALL)
全连接(FULL JOIN)
交叉连接(CROSS JOIN)
SELECT * FROM A,B(,C)或者SELECT * FROM A CROSS JOIN B (CROSS JOIN C)#没有任何关联条件,结果是笛卡尔积,结果集会很大,没有意义,很少使用内连接(INNER JOIN)SELECT * FROM A,B WHERE A.id=B.id或者SELECT * FROM A INNER JOIN B ON A.id=B.id多表中同时符合某种条件的数据记录的集合,INNER JOIN可以缩写为JOIN内连接分为三类
等值连接:ON A.id=B.id
不等值连接:ON A.id > B.id
自连接:SELECT * FROM A T1 INNER JOIN A T2 ON T1.id=T2.pid
外连接(LEFT JOIN/RIGHT JOIN)左外连接:LEFT OUTER JOIN, 以左表为主,先查询出左表,按照ON后的关联条件匹配右表,没有匹配到的用NULL填充,可以简写成LEFT JOIN
右外连接:RIGHT OUTER JOIN, 以右表为主,先查询出右表,按照ON后的关联条件匹配左表,没有匹配到的用NULL填充,可以简写成RIGHT JOINvarchar与char的区别
char的特点
char表示定长字符串,长度是固定的;
如果插入数据的长度小于char的固定长度时,则用空格填充;
因为长度固定,所以存取速度要比varchar快很多,甚至能快50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;
对于char来说,最多能存放的字符个数为255,和编码无关
varchar的特点
varchar表示可变长字符串,长度是可变的;
插入的数据是多长,就按照多长来存储;
varchar在存取方面与char相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;
对于varchar来说,最多能存放的字符个数为65532
索引有哪几种类型?
主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引
全文索引: 是目前搜索引擎使用的一种关键技术。
可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引
网络
讲讲Nginx负载均衡
- 轮询、轮询是默认的,每一个请求按顺序逐一分配到不同的后端服务器,如果后端服务器down掉了,则能自动剔除
- ip_hash、个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。
- weight、weight是设置权重,用于后端服务器性能不均的情况,访问比率约等于权重之比
- fair(第三方)、这是比上面两个更加智能的负载均衡算法。此种算法可以依据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx本身是不支持fair的,如果需要使用这种调度算法,必须下载Nginx的upstream_fair模块。
- url_hash(第三方)此方法按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装Nginx 的hash软件包。
正向代理,反向代理是什么?
正向代理,也就是传说中的代理, 简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器,这个代理服务器呢,他能访问那个我不能访问的网站,于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我。从网站的角度,只在代理服务器来取内容的时候有一次记录,有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站。
反向代理: 结论就是,反向代理正好相反,对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。
Cookie和Session的区别
cookie和session都是用来跟踪浏览器用户身份的会话方式
区别:
- cookie数据保存在客户端,session数据保存在服务器端。简单的说,当你登录一个网站的时候, 如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候会发送当前会话的sessionid,服务器根据当前sessionid判断相应的用户数据标志,以确定用户是否登录或具有某种权限。由于数据是存储在服务器上面,所以你不能伪造。
- sessionid是服务器和客户端链接时候随机分配的. 如果浏览器使用的是cookie,那么所有的数据都保存在浏览器端,比如你登录以后,服务器设置了cookie用户名,那么当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器,这些变量有一定的特殊标记。服务器会解释为cookie变量,所以只要不关闭浏览器,那么cookie变量一直是有效的,所以能够保证长时间不掉线。如果你能够截获某个用户的 cookie变量,然后伪造一个数据包发送过去,那么服务器还是认为你是合法的。所以,使用 cookie被攻击的可能性比较大。
如果设置了的有效时间,那么它会将 cookie保存在客户端的硬盘上,下次再访问该网站的时候,浏览器先检查有没有 cookie,如果有的话,就读取该 cookie,然后发送给服务器。如果你在机器上面保存了某个论坛 cookie,有效期是一年,如果有人入侵你的机器,将你的 cookie拷走,然后放在他的浏览器的目录下面,那么他登录该网站的时候就是用你的的身份登录的。所以 cookie是可以伪造的。当然,伪造的时候需要主意,直接copy cookie文件到 cookie目录,浏览器是不认的,他有一个index.dat文件,存储了 cookie文件的建立时间,以及是否有修改,所以你必须先要有该网站的 cookie文件,并且要从保证时间上骗过浏览器
两个都可以用来存私密的东西,同样也都有有效期的说法,区别在于session是放在服务器上的,过期与否取决于服务期的设定,cookie是存在客户端的,过去与否可以在cookie生成的时候设置进去。
(1)cookie数据存放在客户的浏览器上,session数据放在服务器上
(2)cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
(3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
(4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。csrf攻击是什么?常见的防御手段有什么?
跨站点请求伪造
简单来说就是通过伪装成受信任用户的请求来利用受信任的网站
在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行
防御手段:
- 验证HTTP Referer字段
- 在请求地址中添加token并验证
- 在HTTP头中自定义属性并验证
请说明一下http和https的区别
https协议要申请证书到ca,需要一定经济成本;2) http是明文传输,https是加密的安全传输;3) 连接的端口不一样,http是80,https是443;4)http连接很简单,没有状态;https是ssl加密的传输,身份认证的网络协议,相对http传输比较安全
请讲一下浏览器从接收到一个URL,到最后展示出页面,经历了哪些过程
1.DNS解析
2.TCP连接
3.发送HTTP请求
4.服务器处理请求并返回HTTP报文
5.浏览器解析渲染页面
锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中
java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
读的多,冲突几率小,乐观锁。
写的多,冲突几率大,悲观锁。
Docker
docker多层构建镜像的意义
每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,但是,能够将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义
最大的使用场景是将编译环境和运行环境分离
讲讲docker网络模式(bridge, host)
Docker 默认提供了 5 种网络驱动模式
- bridge: 默认的网络驱动模式。如果不指定驱动程序,bridge 便会作为默认的网络驱动模式。当应用程序运行在需要通信的独立容器 (standalone containers) 中时,通常会选择 bridge 模式
- host:移除容器和 Docker 宿主机之间的网络隔离,并直接使用主机的网络。host 模式仅适用于 Docker 17.06+
- overlay:overlay 网络将多个 Docker 守护进程连接在一起,并使集群服务能够相互通信。您还可以使用 overlay 网络来实现 swarm 集群和独立容器之间的通信,或者不同 Docker 守护进程上的两个独立容器之间的通信。该策略实现了在这些容器之间进行操作系统级别路由的需求
- macvlan:Macvlan 网络允许为容器分配 MAC 地址,使其显示为网络上的物理设备。 Docker 守护进程通过其 MAC 地址将流量路由到容器。对于希望直连到物理网络的传统应用程序而言,使用 macvlan 模式一般是最佳选择,而不应该通过 Docker 宿主机的网络进行路由
- none:对于此容器,禁用所有联网。通常与自定义网络驱动程序一起使用。none 模式不适用于集群服务
如何在一个自定义ip上运行docker容器?
1
2docker network create --subnet=172.18.0.0/16 mynetwork // 创建自定义网络,指定子网段
docker run -itd --name networkTest --net mynetwork --ip 172.18.0.2 centos:latest /bin/bash // 使用该网络创建容器,并在该网段上指定ipdockerFile中最常见的指令是什么?
- COPY 复制文件
- ADD 更高级的复制文件
- CMD 容器启动命令
- ENTRYPOINT 入口点
- ENV 设置环境变量
- ARG 构建参数
- VOLUME 定义匿名卷
- EXPOSE 暴露端口
ADD和COPY的区别:
ADD可以完成COPY的所有工作,ADD还可以很方便的解压文件
Docker的常用命今?
如何临时退出一个正在交互的容器的终端,而不终止它?
什么是docker-compose?
如何退出容器时候自动删除?
Kubenetes
- 简述Kubernetes和Docker的关系?
- Kubernetes中的相关基础概念(deployment,service,pod,job等)
- 简述Kubernetes中Pod的健康检查方式
现场算法题
子集(78)
Tag
: 位运算 / 回溯
题目要求
给你一个整数数组 nums
,数组中的元素互不相同 ,返回该数组所有可能的子集(幂集)
解集不能包含重复的子集,你可以按任意顺序返回解集
时间要求
15 min
输入输出示例
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]示例 2:
输入:nums = [0]
输出:[[],[0]]
提示
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
参考代码
1 | func subsets(nums []int) [][]int { |
TOP K(215)
Tag
: 分治 / 堆
题目要求
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素
时间要求
20 min
输入输出示例
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
提示
- 1 <= k <= nums.length <= 104
- -104 <= nums[i] <= 104
参考代码
1 | func findKthLargest(nums []int, k int) int { |
口答算法题
判断一个链表是否有环,如何找到这个环的起点
给定一个单链表,只给出头指针h:
1、如何判断是否存在环?
2、如何知道环的长度?
1、对于问题1,使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。
2、对于问题2,记录下问题1的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s。
设计一种数据结构
满足:push、pop、getLast、getmax
在单链表中如何用最快的方法找到中间元素?
快慢指针
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
1 | 输入: [2,2,1] |
示例 2:
1 | 输入: [4,1,2,1,2] |
题目解析
根据题目描述,由于加上了时间复杂度必须是 O(n) ,并且空间复杂度为 O(1) 的条件,因此不能用排序方法,也不能使用 map 数据结构。
程序员小吴想了一下午没想出来,答案是使用 位操作Bit Operation 来解此题。
将所有元素做异或运算,即a[1] ⊕ a[2] ⊕ a[3] ⊕ …⊕ a[n],所得的结果就是那个只出现一次的数字,时间复杂度为O(n)。
异或
异或运算A ⊕ B的真值表如下:
AB⊕FFFFTTTFTTTF
动画演示
有一个 n 个元素的数组,除了两个数只出现一次外,其余元素都出现两次,让你找出这两个只出现一次的数分别是几,要求时间复杂度为 O(n) 且再开辟的内存空间固定(与 n 无关)。
示例 :
输入: [1,2,2,1,3,4]
输出: [3,4]
题目再解析
根据前面找一个不同数的思路算法,在这里把所有元素都异或,那么得到的结果就是那两个只出现一次的元素异或的结果。
然后,因为这两个只出现一次的元素一定是不相同的,所以这两个元素的二进制形式肯定至少有某一位是不同的,即一个为 0 ,另一个为 1 ,现在需要找到这一位。
根据异或的性质 任何一个数字异或它自己都等于 0
,得到这个数字二进制形式中任意一个为 1 的位都是我们要找的那一位。
再然后,以这一位是 1 还是 0 为标准,将数组的 n 个元素分成两部分。
- 将这一位为 0 的所有元素做异或,得出的数就是只出现一次的数中的一个
- 将这一位为 1 的所有元素做异或,得出的数就是只出现一次的数中的另一个。
这样就解出题目。忽略寻找不同位的过程,总共遍历数组两次,时间复杂度为O(n)。
动画再演示
两个水杯的问题
题目描述
有一种玻璃杯从一栋100层的大楼扔下,该种玻璃杯超过某一层楼会摔碎。 现在给你两个杯子,问确定最低摔碎的楼层需要摔多少次?
题目分析
这道题的假设是:最低摔碎的楼层可能是每一层楼,且概率相同。我们需要找一种方法,使得定位到[1-100]之间的任意一个数都是快速的。
解题思路
最简单的方法是用一个杯子从第一层开始,不断一层层的往上试。但是这样的时间复杂度是O(n)。直觉也告诉我们想放大步子扔。
因为我们有两个杯子,可以考虑成一个杯子Cup1
不断扔直到破碎,它用来确定最低摔碎的楼层在什么范围,
另一个杯子Cup2
再此基础上一层层的扔。用来准确确定最低摔碎的楼层是多少。如果凭空想象,我们可能会想到二分法,每次隔5个楼层扔,10个楼层扔…
可是我们马上也应该会想到这么分的不妥之处在于:
确定最低摔碎的楼层所需次数是不均匀分布的。
我们再来看:每次扔的楼层间隔会带来什么影响?
确定最低摔碎的楼层:
总次数 = Cup1扔的次数 + Cup2扔的次数
楼层间隔越大,Cup2需要扔的次数越多。
相同楼层间隔下:最低摔碎的楼层越高,Cup1需要扔的次数越多,Cup2需要扔的次数可认为相同。
我们的目的其实是需要尽可能保证:不管最低摔碎的楼层是第一层还是第99层,扔的总次数都尽可能一致且减少。
如果小伙伴有看我上篇文章中LSMT分层步隆过滤器的实现,有没有受到启发?
这里我们可以使Cup1需要扔的楼层间隔递减,这样可改善高楼层所需Cup1/Cup2扔的次数。
假设第一次扔的楼层间隔为X,此后依次递减1层,直到楼层间隔为2.则: x+(x-1)+(x-2)+…+2 >=100
求解出答案为14。
如何得到一个数据流中的中位数?
数据是从一个数据流中读出来的,数据的数目随着时间的变化而增加。如果用一个数据容器来保存从流中读出来的数据,当有新的数据流中读出来时,这些数据就插入到数据容器中。
如果从数据流中读出奇数个数值,那么中位数就是所有值排序之后位于中间的数值。如果数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
数组是最简单的容器。如果数组没有排序,可以用 Partition 函数找出数组中的中位数。在没有排序的数组中插入一个数字和找出中位数的时间复杂度是 O(1)和 O(n)。
我们还可以往数组里插入新数据时让数组保持排序,这是由于可能要移动 O(n)个数,因此需要 O(n)时间才能完成插入操作。在已经排好序的数组中找出中位数是一个简单的操作,只需要 O(1)时间即可完成。
排序的链表时另外一个选择。我们需要 O(n)时间才能在链表中找到合适的位置插入新的数据。如果定义两个指针指向链表的中间结点(如果链表的结点数目是奇数,那么这两个指针指向同一个结点),那么可以在 O(1)时间得出中位数。此时时间效率与及基于排序的数组的时间效率一样。
思路:
如果能够保证数据容器左边的数据都小于右边的数据,这样即使左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。如何快速从一个容器中找出最大数?用最大堆实现这个数据容器,因为位于堆顶的就是最大的数据。同样,也可以快速从最小堆中找出最小数。 因此可以用如下思路来解决这个问题:用一个最大堆实现左边的数据容器,用最小堆实现右边的数据容器。往堆中插入一个数据的时间效率是O(logn).由于只需O(1)时间就可以得到位于堆顶的数据,因此得到中位数的时间效率是O(1).
接下来考虑用最大堆和最小堆实现的一些细节。首先要保证数据平均分配到两个堆中,因此两个堆中数据的数目之差不能超过1(为了实现平均分配,可以在数据的总数目是偶数时把新数据插入到最小堆中,否则插入到最大堆中)。还要保证最大堆中里的所有数据都要小于最小堆中的数据。当数据的总数目是偶数时,按照前面分配的规则会把新的数据插入到最小堆中。如果此时新的数据比最大堆中的一些数据要小,怎么办呢?
可以先把新的数据插入到最大堆中,接着把最大堆中的最大的数字拿出来插入到最小堆中。由于最终插入到最小堆的数字是原最大堆中最大的数字,这样就保证了最小堆中的所有数字都大于最大堆中的数字。
当需要把一个数据插入到最大堆中,但这个数据小于最小堆里的一些数据时,这个情形和前面类似。
编程题
协程池
原生实现一个协程池package
,使用者应能使用该协程池package
并发完成自己输入的一系列自定义任务(要求解耦,即协程池本身和自定义任务完全无关)
示例:
输入任务列表(以下仅为伪代码,具体任务的数据结构请自行设计)
[(“fibonacci”, 6), (“padovan”, 11), (“js_error_rate”, {“pv”: 30627846, “js_error_num”: 78456})]
使用者应能基于自定义的handler
完成并发任务(上述任务分别是斐波那契数列的第n个数
、巴都万数列的第n个数
、计算js_error率
),得到以下输出:
8, 12, 0.002562