知识图谱
2021-04-26 10:23:54 0 举报
AI智能生成
知识图谱
作者其他创作
大纲/内容
项目实战
秒杀抢购系统
过程
步骤
1.redis分布式锁,多个实例共享库存或者销量
2.判断销量是否小于库存,如果小于则继续秒杀,如果大于则返回秒杀结束
3.增加销量
4.创建订单
5.支付成功,更新订单状态为已支付,如果一定时间没有支付,则需要将销量减一
6.
缺点
百万qps,数据库扛不住,mysql单机几百qps
吞吐量 200 qps
分布式锁
特性
不能产生死锁
理解
1.如果业务a加锁后,宕机了,不能解锁了,产生死锁
解决办法
ex设置过期时间,单位是秒,px设置过期时间,单位是毫秒
如果没有获取到锁,尝试多次获取锁,每次获取的时候,要usleep(10000)一下
互斥性,任一时刻只能有一个客户端获取到锁
加锁和解锁必须是同一个客户端,不能误解锁
理解
子主题
解决办法
加锁的时候,记录key=>value值,删除锁的时候,判断redis里的value值是否等于之前记录的value值
特点
高并发、瞬时
库存少
流程短
预热
购物流程
选择商品(商品列表、商品详情页)
加入购物车
确认订单
提交订单
支付
压测工具
jmeter
压测参数
nginx并发数
redis并发数
数据库最大连接数
linux系统句柄数
小度会员
业务模块
会员服务
接口数量
31个
技术指标
qps
平均
200
峰值
400
平均耗时
71.3ms
最大耗时
30,203.6ms
可用性
99.96
订单模块
支付模块
接口数量
子主题
技术指标
qps
平均
业务数据
截止20200308,有效会员数
344,939
历史会员总数
570,958
会员总订单数
357,920
会员每日新增订单
3000
会员每日收益
4万多
总收益
子主题
业务部署
分机房
上海、北京、南京、广州、北京
php-fpm.conf配置
pm=static
pm.max_children =128
pm.start_servers = 20
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 10000
10000个请求数的时候重启fpm,以解决内存泄漏问题
nginx.conf配置
fastcgi_connect_timeout 5;
fastcgi_send_timeout 10;
fastcgi_read_timeout 10;
gzip on;
worker_processes
理解
如果nginx处理的是cpu密集型(比较耗费cpu的)的操作,建议将此值设置为cpu个数或cpu的核数。
worker_connections
理解
每一个worker进程能并发处理(发起)的最大连接数(包含所有连接数
最大连接数
理解
nginx作为http服务器的时候:
max_clients = worker_processes * worker_connections/2
nginx作为反向代理服务器的时候:
max_clients = worker_processes * worker_connections/4
max_clients = worker_processes * worker_connections/2
nginx作为反向代理服务器的时候:
max_clients = worker_processes * worker_connections/4
原因
如果作为反向代理,因为浏览器默认会开启2个连接到server,而且Nginx还会使用fds(file descriptor)从同一个连接池建立连接到upstream后端。则最大连接数
技术难点
支付项目设计
原子性操作
1.开启事务,校验会员订单是否存在,状态判断,创建订单、更新会员订单状态为待支付
2.开启事务,更新交易表状态为支付成功,新增交易历史记录表数据
3.发放权益时,开启事务,如果有不成功则通过抛出异常,回滚
数据结构设计
订单表
读写分离、分库分表
每张表数据量大约40万,总共400万
支付成功状态 38万,总共380万
会员订单表
总共877万数据
支付成功订单35万
会员表
权益表
商品表
交易表
服务稳定性保证
1.生产环境分机房qps、耗时、可用性监控报警
2.提高单机性能
措施
cdn动静页面分离
nginx负载均衡
优化nginx和php-fpm配置
nginx优化
1.优化work_proccesss和work_connections
work_proccess设置成cpu的核数
work_connections受linux系统进程的最大打开文件数限制,65535
2.绑定nginx进程到不同的cpu上,防止nginx进程使用cpu资源不均
做法
比如是两核cpu worker_cpu_affinity 01 10
比如是四核cpu worker_cpu_affinity 0001 0010 0100 1000
3.开启Gzip压缩,减少传输带宽
4.浏览器本地缓存静态数据,减少带宽并提高性能
expires 365d;
5. 优化linux内核参数(最大文件数量),limits.conf
6.keepalive_timeout 设置,一般15s
7. 开启文件高效传输模式 sendfile on,相当于零拷贝技术
php-fpm优化
模式
static
理解
对于内存比较大的服务器,比如8G以上,指定静态的max_children实际上更为妥当,因为这样不需要进行额外的进程数目控制,提高效率
进程数
内存/30M
dynamic
增加缓存
优化数据库查询
对于依赖方接口可用性低的情况,重试多次后,需要熔断降级或者部分数据降级提高可用性
3.利用集群实现高可用
措施
redis集群
mysql集群
读写分离
子主题
4.压测预案
压测工具ab/jemter
子主题
5.服务灾备
削峰
限流
熔断
理解
业务方为了整个服务的稳定性,不再继续调用目标服务,直接返回,如果依赖服务恢复了,再恢复调用
降级
批量发放权益一致性解决方案
1.根据会员类型,获取该类型下会员权益类实例,调用分阶段函数,precall, precommit完成事务提交
自然月计算
判断流程
1.根据会员类型,计算会员天数,一个月以30天为准
2.首次开通,开始时间为支付时间,过期时间调用计算函数计算
3.非首次开通,如果支付时间大于会员过期时间,过期时间调用计算函数计算
4.非首次开通,如果支付时间小于过期时间,属于续费,开始时间不变,过期时间另算
计算方式
小于30天,直接strtotime(开始时间+ 20 day )
大于30天,求得月数,求几个月的最后一天
如果当前日期大于,月末最后一天,则取diff,然后加到当前日期
如果当前日期小于等于当前日期,则加上月数
解决方案
自我提升
支付项目设计能力
并发情况,一致性处理
电商项目风险评估
分布式主键id生成
缓存
redis
底层数据结构
list
压缩列表
应用场景
- 节约内存,列表键只包含少量列表项,且列表项是小整数值、比较短的字符串
理解
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,相比双端链表,物理上是一块连续的内存区域
节点
结构
previous_entry_length: 字段代表前一个节点(entry)的长度,有了这个值,就可以通过当前节点的起始地址进行指针偏移运算得到前一个节点的起始地址,从而直接访问前一个节点。
如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。
如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制值254),而之后的四个字节则用于保存前一节点的长度。
encoding:节点的encoding保存的是节点的content的内容类型以及长度,encoding类型一共有两种,一种字节数组一种是整数,encoding区域长度为1字节、2字节或者5字节长
content区域用于保存节点的内容,节点内容类型和长度由encoding决定
结构
zlbytes:存储一个无符号整数,固定四个字节长度,用于存储压缩列表所占用的字节,当重新分配内存的时候使用,不需要遍历整个列表来计算内存大小。
zltail:存储一个无符号整数,固定四个字节长度,代表指向列表尾部的偏移量,偏移量是指压缩列表的起始位置到指定列表节点的起始位置的距离。
zllen:压缩列表包含的节点个数,固定两个字节长度,源码中指出当节点个数大于65535个数的时候,该值将无效,此时需要遍历列表来计算列表节点的个数。
entryX:列表节点区域,长度不定,由列表节点紧挨着组成。
zlend:一字节长度固定值为255,用于表示列表结束。
使用时机
在决定是否应用压缩列表作为当前数据结构类型的底层编码的时候都会依赖一个开关和一个阈值,开关用来决定我们是否要启用压缩列表编码,阈值总的来说通常指当前结构存储的key数量有没有达到一个数值(条件),或者是value值长度有没有达到一定的长度(条件)
双端列表
节点
结构体
typedef struct listNode {
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value;//节点的值
} listNode;
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value;//节点的值
} listNode;
特性
1、 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
2、 无环:表头节点的prev指针和表尾节点的next指针都指向null,对链表的访问以null为终点。
3、 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
4、 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,所以获取节点数量的复杂度为O(1)。
5、 多态:链表节点使用void *指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点设置类型特定函数,所以链表可以用于保存各种不同类型的值。
6、 什么情况下使用压缩列表
1、所有字符串元素的长度都小于64个字节
2、元素数量小于512个
hash
底层数据结构
压缩列表
字典
hash表
Redis的字典底层使用哈希表实现,每个字典通常有两个哈希表,一个平时使用,另一个用于rehash时使用,使用链地址法解决哈希冲突
hash冲突
采用链地址法
理解
是一个String类型的key与value的映射表,特别适合用于存储对象
子主题
hash的扩容和缩容
什么情况下使用压缩列表
1、 键和值的长度都小于64个字节
2、键值对个数小于512个
set
整数集合(intset)
字典(hashtable)
什么情况下使用整数集合
1、保存的所有元素都是整数值
2、 元素的数量不超过512个
sort set
底层数据结构
压缩列表
跳跃表
理解
Redis的跳跃表由redis.h\zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点数量,以及指向表头界定啊和表尾节点的指针等等。
相关链接
https://www.jianshu.com/p/c2841d65df4c
https://www.cnblogs.com/qixinbo/p/9682721.html
结构
zskiplist
1 header:指向跳跃表的表头节点。
2 tail:指向跳跃表的表尾节点。
3 level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
4 length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。
zskiplistNode
1 每个层都带有三个属性:前进指针、向下指针和跨度
2 后退指针:节点中用BW(backward)字样标记节点的后退指针,他指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
3 分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排量。
4 成员对象(obj):各个节点中o1、o2和o3是节点所保存的成员对象。
特质
(1) 由很多层结构组成
(2) 每一层都是一个有序的链表
(3) 最底层(Level 1)的链表包含所有元素
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
字典
典型应用
延迟队列
举例
1.Redis的有序集合(zset)也可以用于实现延时队列,消息作为value,时间作为score
2.zrangebyscore 按分数区间(时间戳)取出消息,zrem移除有序集合中的一个或多个成员
3.将 zrangebyscore 和 zrem 一同挪到服务器端进行原子化操作,这样多个进程之间争抢任务时就不会出现这种浪费了
为什么有序集合要同时使用字典和跳跃表
1、 单独使用字典时,o(1)的查找复杂度会被保留,但是在执行zrange或者zrank时,程序需要对所有保存的字典进行排序,时间复杂度为nlogn,同时需要o(n)的内存空间来保存排序好的数组
2、单独使用跳跃表时,跳跃表执行范围型的操作会被保留,但是根据成员查找分值的这一操作时间复杂度由o(1)上升为o(logn)
3、同时使用字典和跳跃表时,重复展示了各个元素的成员和分值,但是由于底层指针共享,不会出现重复的成员和分值
编码的转换
什么情况下使用压缩列表
1、 所有元素的长度小于64字节
2、有序集合保存的元素数量小于128个
string
底层数据结构
简单动态字符串SDS
1、len 保存了SDS保存字符串的长度
2、buf[] 数组用来保存字符串的每个元素
3、free j记录了 buf 数组中未使用的字节数量
1、len 保存了SDS保存字符串的长度
2、buf[] 数组用来保存字符串的每个元素
3、free j记录了 buf 数组中未使用的字节数量
常用操作命令
exists key
检查给定的key是否存在
expire key seconds
为key设置过期时间
expire key timestamp
键在秒级时间戳timestamp后过期
pexpire key milliseconds
key在milliseconds毫秒后过期
ttl key
查看键剩余过期时间
randomkey
从当前数据库中随机返回一个key
incr key
key 自增1 范围是signed long的最大值和最小值
decr key
key 自减1
特点
1.二进制安全
理解
因为有了对字符串长度定义len, 所以在处理字符串时候不会以零值字节(\0)为字符串结尾标志
二进制安全就是输入任何字节都能正确处理, 即使包含零值字节,包括jpg图片或者序列化对象
key存储极限
512M
2.获取字符串长度复杂度为o(1)
理解
由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。
3.杜绝缓冲区溢出
理解
我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。
4.减少修改字符串的内存重新分配次数
理解
C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露
5.可存储整型、浮点型、二进制等数据结构
6. string扩容机制
7. 被用作AOF缓存区和客户端输入缓存区
分布式锁
实现方式
redis
memached
子主题
bitmap
子主题
pipline
geo
类型检查与命令多态
1、命令多态
del、expire、type、rename等是所有类型的键都可以使用,是键类型的多态
2、编码多态
llen是会检查底层对象是使用压缩列表还是双端列表,然后选择不同的API获取长度
内存回收
采用引用计数的方法去实现
对象共享
redis会对字符串键中包含整数的对象,进行对象共享,范围是0~9999,其它类型比如hash、压缩列表等对象,由于检查其值是否相同,消耗的cpu性能比较大,所以不会做对象共享
对象
1、数据结构
type struct redisobject {
unsigned type //类型
unsigned encoding //编码
*ptr //指向底层数据结构的指针
}
unsigned type //类型
unsigned encoding //编码
*ptr //指向底层数据结构的指针
}
2、有哪些对象
redis共有字符串、列表、hash、集合、有序集合五种类型的对象,每种对象至少有两种及以上的编码方式,不同编码可以在不同的场景优化编码的使用效率
特性
存取速度快
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
单进程单线程
理解
1.单线程避免了线程切换和竞态产生的消耗
2.单线程可以简化数据结构和算法实现
举例
不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
使用多路I/O复用模型,非阻塞IO
理解
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作
特点
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)
数据持久化
注意点
1、save命令会用主线程创建RDB文件,会阻塞进程,bgsave命令不会阻塞进程,fork出子进程来创建RDB文件
2、服务器启动时,先判断是否开启AOF持久化功能,如果开启,载入AOF文件,否则才载入RDB文件
3、
RDB
定义
二进制压缩文件,主要存储着键值对
触发条件
根据配置规则进行自动快照
save 900 1
理解
表示900秒之内数据集存在1次修改时,自动触发bgsave
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
用户执行SAVE或BGSAVE命令
执行FLUSHALL命令
执行复制(replication)时
过程
(1) Redis使用fork函数复制一份当前进程的副本,fork的过程父进程会阻塞;
(2) 父进程(这里是当前进程)继续接收并处理客户端发来的命令,而子进程(副本)开始将内存中的数据写入硬盘中临时文件;
(3) 当子进程写完所有数据后,就用临时文件覆盖掉就掉RDB文件,至此,完成一次快照操作。
(2) 父进程(这里是当前进程)继续接收并处理客户端发来的命令,而子进程(副本)开始将内存中的数据写入硬盘中临时文件;
(3) 当子进程写完所有数据后,就用临时文件覆盖掉就掉RDB文件,至此,完成一次快照操作。
保证RDB文件完整性
Redis进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的
风险
优点
RDB是一个紧凑压缩的二进制文件,代表Redis在某一个时间点上的数据快照。非常适合用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快,适合做冷备
缺点
RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高
在fork函数执行的时候,操作系统会使用写时复制(copy-on-write)策略,即fork函数发生的那一刻,父子进程共享同一内存数据,当父进程要更改其中某片数据时,操作系统会将该片数据复制一份,以保证子进程不受影响,所以新的RDB格式文件保存的是执行fork那一刻的内存数据,一旦redis异常退出,最后一次快照后更改就丢失了,总结一句就是容易丢失数据
文件结构
redis db_version databases EOF check_sum
databases部分可以保存任意多个非空数据库,每个非空数据库由 selectDB db_number key_value_pairs 组成
key value pairs 保存了数据库中所有键值对数据,如果有过期时间,过期时间和键值对保存在一起 EXPIRETMIE_MS ms type key value
AOF
定义
每执行一条会更改Redis中数据的命令,Redis就会将该命令写入硬盘中的AOF格式的文件
实现
1.命令追加
当AOF功能打开时,服务器会执行完写命令,会以协议格式将被执行的写命令追加到服务器的aof_buf缓存去
2.文件写入
3.文件同步
缺点
对于同一份文件AOF文件比RDB数据快照要大,每次重启redis都会运行一遍,运行时间长
解决方式
可以通过BGREWRITEAOF 命令通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件,使AOF文件的体积变得尽可能地小
数据恢复比较慢,不适合做冷备。
优点
不会丢失数据
数据淘汰策略
内存淘汰策略
voltile-lru 从已设置过期时间的数据集中挑选最久没有使用的数据淘汰
voltile-lru 从所有数据集中挑选最久没有使用的数据淘汰
volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-random 从数据集中任意选择数据淘汰
volatile-lfu 从设置了过期时间中淘汰使用频率最少的键
allkeys-lfu 从所有键中淘汰使用频率最少的键
volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错
过期淘汰策略
理解
删除过期时间的key值
方式
1.定期删除
理解
每隔一定时间,随机抽取一定数量设置过期时间key,检查其是否过期,过期则删除
缺陷
定期删除可能会导致很多过期key到了时间并没有被删除掉
2.惰性删除
理解
在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西
缺陷
如果定期删除漏掉了很多过期key,然后也没及时去查,也就没走惰性删除,此时会走内存淘汰机制
配置项
通过配置redis.conf中的maxmemory这个值来开启内存淘汰功能,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略来淘汰内存(key),从而换取一定的内存
主从同步
全量同步
步骤
1. 从服务器连接主服务器,发送SYNC命令;
2.主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3.主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4.主节点 bgsave 并保存 RDB 到本地,发送RDB文件到从节点
5.从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
6.主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
7.从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
示意图
增量同步
步骤
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令
异常情况
当从节点出现网络中断,超过了 repl-timeout 时间,主节点就会中断复制连接
主节点会将请求的数据写入到“复制积压缓冲区”,默认 1MB
当从节点恢复,重新连接上主节点,从节点会将 offset 和主节点 id 发送到主节点
主节点校验后,如果偏移量的数后的数据在缓冲区中,就发送 cuntinue 响应 —— 表示可以进行部分复制
主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态
高可用集群
主从复制
定义同上
优点
同一个Master可以同步多个Slaves。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点
不具备自动容错和恢复功能,主节点故障,集群无法进行写操作
Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
哨兵
定义
一个一主多从的Redis系统中,可以使用多个哨兵进行监控任务以保证系统足够稳健。此时,不仅哨兵会同时监控主数据库和从数据库,哨兵之间也会相互监控。在这里,建议大家哨兵至少部署3个,并且使用奇数个哨兵
任务
监控
定义
不断地检查你的Master和Slave是否运作正常
原理
每秒一次的频率与之前创建了命令连接的实例发送PING,包括主服务器、从服务器和sentinel实例,以此来判断当前实例的状态。down-after-milliseconds时间内PING连接无效,则将该实例视为主观下线。之后该sentinel会向其他监控同一主服务器的sentinel实例询问是否也将该服务器视为主观下线状态,当超过某quorum后将其视为客观下线状态
当一个主服务器被某sentinel视为客观下线状态后,该sentinel会与其他sentinel协商选出领头sentinel进行故障转移工作。每个发现主服务器进入客观下线的sentinel都可以要求其他sentinel选自己为领头sentinel,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元,每个纪元中只会选择一个领头sentinel。如果所有超过一半的sentinel选举某sentinel领头sentinel。之后该sentinel进行故障转移操作。
提醒
定义
当被监控的某个 Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知
自动故障迁移
定义
当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master; 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master
原理
首先是从主服务器的从服务器中选出一个从服务器作为新的主服务器。选点的依据依次是:网络连接正常->5秒内回复过INFO命令->10*down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小的。选出之后通过slaveif no ont将该从服务器升为新主服务器
故障转移成功后会通过发布订阅连接广播新的配置信息,其他sentinel收到后依据配置纪元更大来更新主服务器信息。
缺点
1.主从服务器的数据要经常进行主从复制,这样造成性能下降
2.当主服务器宕机后,从服务器切换成主服务器的那段时间,服务是不能用的
redis cluster
定义
redis cluster采用虚拟槽分区,所有的键根据哈希函数(CRC16[key]&16383)映射到0-16383槽内,共16384个槽位,每个节点维护部分槽及槽所映射的键值数据
hash函数
Hash()=CRC16[key]&16383 按位与
高可用
一个集群里面有M1、M2、M3三个节点,其中节点 M1包含 0 到 5500号哈希槽,节点M2包含5501 到 11000 号哈希槽,节点M3包含11001 到 16384号哈希槽。如果M2宕掉了,就会导致5501 到 11000 号哈希槽不可用,从而使整个集群不可用
一个集群里面有M1-S1、M2-S2、M3-S3六个主从节点,其中节点 M1包含 0 到 5500号哈希槽,节点M2包含5501 到 11000 号哈希槽,节点M3包含11001 到 16384号哈希槽。如果是M2宕掉,集群便会选举S2为新节点继续服务,整个集群还会正常运行。当M2、S2都宕掉了,这时候集群就不可用了
投票机制
每一个节点都存有这个集群所有主节点以及从节点的信息,它们之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的备用节点。如果某个节点和所有从节点全部挂掉,我们集群就进入faill状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入发力了状态。这就是我们的redis的投票机制
优点
解耦数据与节点关系,节点自身维护槽映射关系
Jedis的Redis Sharding(分片)
定义
1、采用一致性哈希算法,将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小
2、为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。
3、ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。
一致性hash
原理
普通hash算法是对服务器数量取模,一致性hash是用服务器ip对2^ 32次方取模,得到一个环形的空间
将缓存的数据同样对2^32次方取模,顺时针落到最近的服务器上
优点
服务器节点增减,不会影响整个缓存,只会影响个别节点
利用代理中间件实现大规模Redis集群
定义
中间件的作用是将我们需要存入redis中的数据的key通过一套算法计算得出一个值。然后根据这个值找到对应的redis节点,将这些数据存在这个redis的节点中。
常见中间件
Twemproxy
codis
nginx
事务
步骤
开始事务
MULTI
命令入队
事务队列
属性
要执行的命令(cmd)
命令的参数(argv)
参数的个数(argc)
执行事务,返回结果
EXEC
取消事务
DISCARD
带watch的事务
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)
特性
有一致性
有隔离性
定义
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的
有原子性
原因
如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动作
当事务失败时,Redis 也不会进行任何的重试或者回滚动作
持久性
原因
在单纯的内存模式下,事务肯定是不持久的
在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的
在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的
分布式锁
应用场景
比如下订单和减库存,需要使用锁来锁住
解决方案
主要利用SETNX 命令和 GETSET命令,解决高并发
SETNX
用法
setnx key value
GETSET
用法
GETSET key newvalue
死锁
解决方案
可能出现的问题
1.正常情况下,锁定后,业务执行完毕,解锁,使用del(key)
2.方案1容易出现死锁,获取锁后,需要设置锁超时时间 expire(key, 30)
3.当我们设置了SETNX后,如果redis马上挂掉了,那我们再次 EXPIRE 指令是不是就会无法生效,出现了资源锁死的情况,这时候就要采用方案3来解决,setnx指令本身是不支持传入超时时间的,Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令
4.超时后使用del 导致误删其他线程的锁
解决方案
问题4
以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
队列
延时队列
应用场景
订单支付失败,每隔一段时间提醒用户
用户并发量的情况,可以延时2分钟给用户发短信
实现
Redis的有序集合(zset)也可以用于实现延时队列,消息作为value,时间作为score
项目实战
session共享
微博feed流
点赞、收藏、评论数
可能认识的人推荐
数据库
存储引擎
innodb
特性
1.支持事务
2.支持行级锁
3.支持外键约束
4.聚簇索引
6.统计行数,扫描全表
7.删除表时,逐个记录删除
myisam
特性
1.支持表级锁
2.支持全文索引
3.统计行数,不需要扫描全表
4.非聚簇索引
SQL执行步骤
查询
1.建立连接,连接线程池,身份校验
2.查询缓存
3.没有命中缓存,分析器做词法分析,语法检查
4.语法没有问题,优化器对sql语句结构和索引进行优化,确定执行方案
5.返回查询引擎执行结果
更新
1.引擎从内存查询缓存,有就返回该数据,没有从磁盘查找,查到后写入内存,再返回
2.执行器拿到引擎的数据后,更改数据,调用引擎的API接口写入内存这行数据
3.InnoDB 引擎写入内存的同时,再把这行数据写入redo log, 标记状态为 prepare,并且告诉执行器已经更新数据完成,可以随时提交事务
4.执行器把此操作写入bin log,并且把bin log写入磁盘
5.最后执行器调用引擎的提交事务接口,引擎把 redo log 的状态改 commit ,至此整个更新操作完成
SQL执行顺序
示例SQL
select passport from table1 left join table2 on table1.id=table2.id where table1.create_time > '20190101' group by table1.passport having count(table1.id) > 1 order by table1.create_time limit 100
执行顺序
开始->FROM子句->WHERE子句->JOIN->GROUP BY子句->HAVING子句->ORDER BY子句->SELECT子句->LIMIT子句->最终结果
索引
类型
唯一索引
特性
索引列的值必须唯一,但允许有空值
主键索引
特性
索引列的值必须唯一,不允许有空值
联合索引
意义
多个索引合并为一个索引,降低对索引的维护成本
生效原则
从前往后依次使用生效,如果中间某个索引没有使用,那么断点前面的索引部分起作用,断点后面的索引没有起作用;
示例
联合索引(a,b,c)
where a=3 and b=45 and c=5 .... 这种三个索引顺序使用中间没有断点,全部发挥作用;
where b=45 and a=3 and c=5 .... 这个跟第一个一样,全部发挥作用,abc只要用上了就行,跟写的顺序无关
where a=3 and c=5... 这种情况下b就是断点,a发挥了效果,c没有效果
where b=3 and c=4... 这种情况下a就是断点,在a后面的索引都没有发挥作用,这种写法联合索引没有发挥任何效果;
where a=3 and b>7 and c=3 这种情况 b是范围查询,也算断点,a发挥作用,b发挥作用,c没有作用
where a=3 order by b 这种情况a用到了索引,b在结果排序中也用到了索引的效果,前面说了,a下面任意一段的b是排好序的
where a=3 order by c 这种情况a用到了索引,b是断点,c没有用到索引
覆盖索引
理解
是select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。
举例
然而,叶节点中除了有user_name表主键ID的值以外,user_name字段的值也在里面,因此不需要通过主键ID值的查找数据行的真实所在,直接取得叶节点中user_name的值返回即可
链接
https://baijiahao.baidu.com/s?id=1645514817836645220&wfr=spider&for=pc
全文索引
适用范围
仅可用于 MyISAM 表,针对较大的数据,生成全文索引很耗时好空间。
普通索引
实现原理
数据结构
B+tree
特点
非叶子节点存储索引,叶子节点存储数据和指向一下个叶子节点的指针
意义
每个节点保存信息多,每层节点数多,层数变小,磁盘I/O减少,查询时间变短
为什么快
理解
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度
B/B- tree
特点
平衡多路查找树
理解
每个节点有多个分支,查找数,说明节点从左、根、右符合从小到大排序
B树叶子节点和非叶子节点都存储索引和行记录,这样的话,同样的空间,B+树能存储更多的索引。这就意味着同样的数据,B+树比B树更”矮胖“,减少IO次数。
B树范围查询只能通过中序遍历查询来定位最小和最大值,而B+树通过链表就能实现,查询更方便。
hash
特点
等值查询,时间复杂度是O(1)
不能用作索引
group by、order by、范围查询时,时间复杂度是O(n),大于B+ tree时间复杂度O(log(n))
索引类型
聚簇索引
理解
数据和索引在一个文件里(innodb)
非聚簇索引
理解
数据和索引不在一个文件里(mysiam)
事务
四大特性
原子性
理解
要不全部完成,要么都不完成
一致性
理解
保证操作前和操作后数据或者数据结构的一致性
隔离性
理解
并发执行时,互不影响
隔离级别
读未提交
理解
事务a读到了事务b更新数据,但没有提交的记录
产生的问题
脏读
理解
事务b回滚,导致事务a读到脏数据
不可重复读
理解
事务b提交后,读到的结果不一致与提交之前不一致
幻读
理解
同一个sql查询,执行两次返回的结果多记录或者少记录的情况
读已提交
理解
事务a只能读事务b已经提交的记录
产生的问题
不可重复读
幻读
可重复读(默认)
理解
不管事务b是否提交,事务a读到的数据都不会改变
产生的问题
幻读
串行化
理解
事务a更新或者新增数据时,对表进行写锁,其它事务无法读取,需要等事务a提交后(释放锁),才能读取
优点
避免幻读
缺点
并发性降低
持久性
理解
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
底层实现
多版本控制 mvcc(Multi Version Concurrency Control)
理解
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
实现原理
隐式字段
DB_TRX_ID
理解
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR
理解
7byte,回滚指针,指向这条记录的上一个版本(存储于undo log里)
DB_ROW_ID
理解
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
undo log
insert undo log
理解
代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log
理解
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
Read View(读视图)
理解
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
隔离级别
MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE会对所有读取到的行都加锁。
主从同步
方式
异步,存在主从延迟
过程
1.从库会生成两个线程,一个I/O线程,一个SQL线程,主库使用一个I/O线程
2.主库会生成一个log dump线程,用来给从库I/O线程传binlog
3.从库的I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中
4.从库的SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行;
锁
按使用方式
乐观锁
悲观锁
sql实现
select * for update
前提
必须关闭MySQL的自动提交
注意点
子主题
按锁级别
共享锁
排它锁
按锁粒度
行级锁
表级锁
页级锁
SQL优化
1.定位语句
指令
show status
用途
定位各种sql的执行频率
用法
show status like 'com_%'
显示所有统计参数的值
show status like 'slow_queries'
慢查询次数
show status like 'com_select'
执行select操作的次数
show status like 'connections';
连接到msyql的连接数
explain
用途
分析sql执行计划
用法
explain sql
结果分析
select_type
SIMPLE
简单的select查询,查询中不包含子查询或者UNION
PRIMARY
查询中若包含任何复杂的子部分,最外层查询则被标记为PRIMARY
SUBQUERY
在SELECT或WHERE列表中包含了子查询
DERIVED
在FROM列表中包含的子查询被标记为DERIVED(衍生),MySQL会递归执行这些子查询,把结果放在临时表中
UNION
若第二个SELECT出现在UNION之后,则被标记为UNION:若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
UNION RESUL
从UNION表获取结果的SELECT
type
system
表只有一行记录(等于系统表),这是const类型的特列,平时不会出现,这个也可以忽略不计
const
表示通过索引一次就找到了,const用于比较primary key 或者unique索引。因为只匹配一行数据,所以很快。如将主键置于where列表中,MySQL就能将该查询转换为一个常量。
eq_ref
唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描
ref
非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫描的混合体。
range
只检索给定范围的行,使用一个索引来选择行,key列显示使用了哪个索引,一般就是在你的where语句中出现between、< 、>、in等的查询,这种范围扫描索引比全表扫描要好,因为它只需要开始于索引的某一点,而结束于另一点,不用扫描全部索引
index
Index与All区别为index类型只遍历索引树。这通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然all和Index都是读全表,但index是从索引中读取的,而all是从硬盘读取的)
all
将遍历全表以找到匹配的行
possible_key
理解
查询时可能用到的索引
key
理解
实际使用的索引
key_len
理解
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度
rows
理解
扫描的行数
show profile
用途
分析sql的执行过程,每个过程的耗时
优化措施
1.优化索引
聚合索引优化
2.分库分表
3.优化sql语句
1.优化分页查询
记录前一页最后一条记录的id,下次查询用 select * from table where id> last_page_id limit n
语法
窗口函数
rank
用法
select id, name, rank() over(order by score desc) as r from students;
注意点
子主题
dense rank
row_number
视图
理解
查询的中间结果表
用法
创建视图other
create view other as select a.name as username, b.name as goodsname from user as a, goods as b, ug as c where a.id=c.userid and c.goodsid=b.id;
使用视图
select * from other;
好处
1. 提高了重用性,就像一个函数
2. 对数据库重构,却不影响程序的运行
存储过程和函数
存储过程
理解
先经过编译并存储在数据库中的一段SQL语句的集合
特点
存储过程的参数可以是IN、OUT、INOUT类型,不需要有返回值
函数
理解
存储过程的参数可以是IN、OUT、INOUT类型
特点
函数的参数只能是IN,函数必须有返回值
消息队列
网络
七层OSI
应用层
协议
FTP
默认端口
21
SSH
默认端口
22
Telnet
默认端口
23
SMTP
默认端口
25
Domain
默认端口
53
redis
默认端口
6379
DNS
HTTP
默认端口
80
组成
请求报文
请求行
组成
GET www.baidu.com HTTP/1.1
请求头
组成
User-Agent
发送请求的应用程序名称
host
可以是域名或者IP:端口号
Connection
keepalive
close
Accept-Encoding
通知服务端可以发送的数据压缩格式
Accept-Language
通知服务端可以发送的语言
Accept-Charset
通知服务端可以发送的编码格式
请求体
响应报文
响应行
组成
协议版本 + 空格 + 状态码 + 空格 + 状态码描述 + 回车符 + 换行符
响应头
组成
server
子主题
content_Type
content_Length
content_charset
content_encoding
content_lauage
方法
GET
HEAD
理解
在有限的带宽和速度下,请求资源的头部信息,并且这些头部与HTTP GET方法请求时返回一致,该请求方法使用场景是在下载一个大文件前先获取其大小再决定是否要下载,以此可以节约带宽资源
OPTIONS
理解
用于获取目的资源所支持的通信选项
PUT
理解
用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
PUT和POST区别
相同点
都可以创建资源
不同点
1. PUT方法是幂等的,连续多次调用效果相同,而POST方法是非幂等的
2. PUT的URI指向单一具体资源,而POST可以指向资源集合,例如
POST
理解
发送数据给服务器
DELETE
理解
删除指定的资源
PATCH
理解
用于对资源进行部分修改
PATCH与PUT的区别
相同点
都可以更新资源
不同点
PATCH对局部资源进行更新
CONNECT
理解
http1.1中预留能够将连接改为管道方式的代理服务器
TRACE
理解
回显服务器的请求,主要用于测试或者诊断
版本比较
1.1相比1.0
1.默认持久性连接 connection:keep_alive
2.增加管道机制,请求可以同时发出,但是响应必须按照请求发出的顺序依次返回
3.带宽优化,在请求消息中引入了range头域,它允许只请求资源的某个部分
4.分块传输,服务器产生部分数据,那么就发送部分数据,可以节省很多等待的时间
2.0相比1.1
1.应用层和传输层之间增加二进制分帧层
2.多路复用,每个连接可以承载任意数量的数据流,每个数据流由消息的形式发送,每个消息由一个或者多个帧组成,这些帧可以乱序发送,然后再根据每个帧头部的流标识符重组
3. 首部压缩 ,使用HPACK算法对header的数据压缩,数据体积小了,在网络上的传输就会更快
4.服务器推送,服务器可以对客户端一个请求发送多个响应,即服务器可以额外的向客户端推送资源
状态码
1XX
理解
表示请求已接收,继续处理
100
客户必须继续发送请求
101
客户要求服务器根据请求转换http协议版本
2XX
理解
表示请求已被成功接收、理解和接受
200
服务器成功处理请求
201
已创建,一般用于post/put,服务器已创建新资源
3XX
理解
重定向
301
理解
而301重定向是永久的重定向,搜索引擎在抓取新的内容的同时也将旧的网址替换为了重定向之后的网址。
应用
比如,我们访问 http://www.baidu.com 会跳转到 https://www.baidu.com,发送请求之后,就会返回301状态码,然后返回一个location,提示新的地址,浏览器就会拿着这个新的地址去访问。
302
理解
302重定向只是暂时的重定向,搜索引擎会抓取新的内容而保留旧的地址,因为服务器返回302,所以,搜索搜索引擎认为新的网址是暂时的。
应用
比如未登陆的用户访问用户中心重定向到登录页面。
访问404页面会重新定向到首页。
访问404页面会重新定向到首页。
4XX
理解
客户端错误,请求有语法错误或请求无法实现
400
服务器不理解请求的语法
401
请求要求身份验证
403
服务器拒绝请求
5XX
理解
服务器错误
500
代码错误
501
服务器不具备完成请求的功能,例如:无法识别请求方法
503
服务器由于超载或者停机维护
502
理解
作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
排查PHP-fpm
1. max_children
2. request_terminate_timeout、max_execution_time
3. 数据库
4. 网关服务是否启动如php-fpm
2. request_terminate_timeout、max_execution_time
3. 数据库
4. 网关服务是否启动如php-fpm
504
理解
作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。
排查
主要查看nginx.conf关于网关如fastcgi的配置
fastcgi_connect_timeout、fastcgi_send_timeout、fastcgi_read_timeout,如果fastcgi缓冲区太小会导致fastcgi进程被挂起从而演变为504错误
MySQL数据库服务
默认端口
3306
rsync
873
表示层
定义
提供格式化的表示和转换数据服务
数据的压缩和解压缩, 加密和解密
会话层
由来
现在我们已经保证给正确的计算机,发送正确的封装过后的信息了。但是用户级别的体验好不好?难道我每次都要调用TCP去打包,然后调用IP协议去找路由,自己去发?当然不行,所以我们要建立一个自动收发包,自动寻址的功能
定义
建立会话,SESSION认证、断点续传
协议
JPEG、MPEG、ASII
传输层
协议
TCP
特性
提供的是面向连接,可靠的字节流服务。即客户和服务器交换数据前,必须现在双方之间建立一个TCP连接,之后才能传输数据。并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端
可靠连接
三次握手
第一次握手:客户端发送syn包(syn=1,seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=1,seq=y,ACK=1),即SYN+ACK包,此时服务器进入SYN_RECV状态
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1, ACK=1, seq=x+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
四次挥手
第一次挥手
客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手
服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
第三次挥手
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手
客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
常见问题
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
【问题3】为什么不能用两次握手进行连接?
答:第三次握手是为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手。如果只有两次握手,那么到这里,连接就建立了,但是此时客户端并没有任何数据要发送,而服务端还在傻傻的等候佳音,造成很大的资源浪费。所以需要第三次握手,只有客户端再次回应一下,就可以避免这种情况。
链接 https://blog.csdn.net/qq_38950316/article/details/81087809
如何保证可靠传输
校验和
理解
发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,一个CRC校验,TCP将丢弃这个报文段和不确认收到此报文段
丢弃重复数据
确认应答+序列号
理解
发送方对每一个包进行编号,接收方对数据包进行排序,去掉重复序列号的数据。
超时重传
理解
当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
超时时常
超时以500ms(0.5秒)为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,累计到一定的重传次数,TCP就认为网络或者对端出现异常,强制关闭连接。
流量控制
理解
TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议,简单来说就是接收方处理不过来的时候,就把窗口缩小,并把窗口值告诉发送端。
拥塞控制
理解
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。
四个算法控制
慢开始
拥塞避免
快重传
快恢复
长连接与短连接
区别及应用场景
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,
这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都
不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果
用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都
不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果
用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网
站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成
千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频
繁操作情况下需用短连好。
站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成
千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频
繁操作情况下需用短连好。
UDP
特性
是一个简单的面向数据报的运输层协议。它不提供可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地。由于UDP在传输数据报前不用再客户和服务器之间建立一个连接,且没有超时重发等机制,所以传输速度很快。
应用
DNS
视频、音频等对实时性要求比较高的多媒体通信
区别
定义
提供端到端的可靠报文传递和错误恢复(段Segment)
网络层
由来
传输层只是解决了打包的问题。但是如果我有多台计算机,怎么找到我要发的那台?或者,A要给F发信息,中间要经过B,C,D,E,但是中间还有好多节点如K.J.Z.Y。我怎么选择最佳路径?这就是路由要做的事
定义
即路由器,交换机那些具有寻址功能的设备所实现的功能
arp协议
也称地址解析协议,是根据IP地址获取物理地址的一个TCP/IP协议。它可以解决同一个局域网内主机或路由器的IP地址和MAC地址的映射问题。
数据链路层
由来
我还须要保证传输大量文件时的准确性。于是,我要对发出去的数据进行封装。就像发快递一样,一个个地发
定义
将比特组装成帧和点到点的传递(帧Frame)
物理层
由来
主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特
https
加密算法
对称加密
定义
加密和解密同一个密钥
非对称加密
定义
共钥加密、私钥解密或者私钥加密 共钥解密
加密通信过程
步骤
1.浏览器获取服务器发来的共钥
获取方式
1.网站生成密钥对,将私钥保存,私钥和网站域名给CA
2.CA用私钥对指纹和指纹算法加密得到数字签名
3.CA发送给服务器指纹、指纹算法、数字签名和明文
4.浏览器请求服务器获取数字证书,浏览器解密获取到共钥
2.浏览器生成随机串A作为通信密钥
3.浏览器用共钥加密随机串A,发送给服务器
4.服务器用私钥解密出随机串A得到通信密钥
5.服务端和客户端用随机串A以及对称加密算法进行通信
SSL
过程
客户端发送随机数1,支持的加密方法(如RSA公钥加密)
服务端发送随机数2,和服务器公钥,并确认加密方法
客户端发送用服务器公钥加密的随机数3
服务器用私钥解密这个随机数3,用加密方法计算生成对称加密的密钥给客户端
接下来的报文都用双方协定好的加密方法和密钥,进行加密
URL请求响应过程
DNS域名解析
过程
1.操作系统会先检查自己本地的hosts文件是否有这个网址映射关系
2.查找本地DNS解析器缓存
3.首先会找TCP/ip参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器
4.13台根DNS
TCP连接
三次握手
http或者https请求
服务器接收数据,返回结果
关闭TCP连接
四次挥手
因此,TIME_WAIT状态是出现在主动发起连接关闭的一点,和是谁发起的连接无关,可以是client端,也可以是server端。
而从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)
而从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)
socket通信
步骤
客户端创建socket
理解
socket()函数的作用就是生成一个用于通信的套接字文件描述符
客户端请求连接服务端(三次握手)
理解
connect()函数则用于向某个已监听的套接字发起连接请求,也就是发起TCP的三次握手过程。从这里可以看出,连接请求方(如客户端)才会使用connect()函数,当然,在发起connect()之前,连接发起方也需要生成一个sockfd,且使用的很可能是绑定了随机端口的套接字。既然connect()函数是向某个套接字发起连接的,自然在使用connect()函数时需要带上连接的目的地,即目标地址和目标端口,这正是服务端的监听套接字上绑定的地址和端口。同时,它还要带上自己的地址和端口,对于服务端来说,这就是连接请求的源地址和源端口。于是,TCP连接的两端的套接字都已经成了五元组的完整格式。
服务端创建socket
理解
同样生成一个用于通信的套接字文件描述符
绑定ip和端口到socket
理解
使用bind()函数将这个套接字绑定到要监听的地址和端口组合"addr:port"上。绑定了端口的套接字可以作为listen()函数的监听对象。
监听端口和ip
理解
listen()函数就是监听已经通过bind()绑定了addr+port的套接字的。监听之后,套接字就从CLOSE状态转变为LISTEN状态,于是这个套接字就可以对外提供TCP连接的窗口(socket buffer)了。
接受客户端的请求
理解
accept
TCP协议连接双方均维护了socket缓冲区
send()函数
TCP协议是面向字节流的传输协议,要通过TCP连接发送出去的数据都先拷贝到send buffer,可能是从用户空间进程的app buffer拷入的,也可能是从内核的kernel buffer拷入的,拷入的过程是通过send()函数完成的。
最终数据是通过网卡流出去的,所以send buffer中的数据需要拷贝到网卡中。由于一端是内存,一端是网卡设备,可以直接使用DMA(Direct Memory Access,直接存储器访问,外部设备不通过CPU而直接与系统内存交换数据的接口)的方式进行拷贝,无需CPU的参与。也就是说,send buffer中的数据通过DMA的方式拷贝到网卡中并通过网络传输给TCP连接的另一端:接收端。
recv()函数
当通过TCP连接接收数据时,数据肯定是先通过网卡流入的,然后同样通过DMA的方式拷贝到recv buffer中,再通过recv()函数将数据从recv buffer拷入到用户空间进程的app buffer中。
子主题
网络安全
sql注入
起因
url中增加特殊的字符,然后向数据库发起请求,拼凑出sql语句,达到攻击的目的
解决办法
1.查询的时候用预处理的方式
2.addslashes()和mysql_real_escape_string()等转义函数进行转义
xss(跨站脚本)
起因
往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的特殊目的
解决办法
htmlspecialchars() 将字符串中的标签转换为html实体
csrf(跨站请求伪造)
起因
利用用户身份伪造非用户本意的请求操作
解决办法
1.验证码
2. cookie中放token,服务器验证token
ddos
syn洪泛攻击
粘包
TCP会粘包
原因
TCP是面向流的传输,接受端应用程序从缓存区取数据就只能得到整体数据而不知道怎么拆分
解决办法
定义包结构,一般可以定义为包头+包体,包头包含了文件的大小字段,包体就是发送文件的二进制数据,接收端先收到包头,解析文件大小字段,等到读取到指定大小的包后,数据就已经是完整的了
UDP不会粘包
原因
UDP是面向消息的传输,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址、端口等信息),这样对于接收端来说就容易进行区分了。
go语言
语法
基础知识
defer执行顺序
类似栈的先入后出原则(FILO)
new和make的区别
new 的作用是初始化一个指向类型的指针(*T),传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针
make 的作用是为 slice,map 或 chan 初始化并返回引用(T)
Printf()、Sprintf()、Fprintf()函数的区别用法是什么?
Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。
Printf() 是和标准输出文件(stdout)关联的,Fprintf 则没有这个限制.
Fprintf(), 是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*。主要用于文件操作。Fprintf()是格式化输出到一个stream,通常是到文件。
数组和切片的区别
数组
1 数组固定长度
2 数组长度是数组类型的一部分
3.数组需要指定大小,不指定也会根据初始化的自动推算出大小,不可改变
4.数组是值传递
切片
1.切片可变长度
2.不需要指定大小
3.切片是地址传递(引用传递)
go语言同步锁
1.当一个go程获得Mutex,其他的只能等,除非这个go程释放这个Mutex
2.RWMutex在读锁占用的情况下,会阻止写,但不会阻止读
3.RwMutex在写锁占用的情况下,会阻止写,也阻止读
select
select 的功能和 select, poll, epoll 相似, 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作
CGO是C语言和Go语言之间的桥梁
函数
接口
子主题
并发
goroutine调度原理
M
系统线程OS Thread,由操作系统管理
P
Processor,上下文处理器,它的主要用途就是用来连接执行的goroutine和内核线程的,所以它也维护了一个未被执行的goroutine队列,里面存储了所有需要它来执行的goroutine。
G
代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度
调度原理
首先创建一个G对象,G对象保存到P本地队列或者是全局队列。P此时去唤醒一个M。P继续执行它的执行序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。
M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。
M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。
反射
内存管理
内存泄露
理解
内存泄露,是从操作系统的角度上来阐述的,形象的比喻就是“操作系统可提供给所有进程的存储空间(虚拟内存空间)正在被某个进程榨干”,导致的原因就是程序在运行的时候,会不断地动态开辟的存储空间,这些存储空间在在运行结束之后后并没有被及时释放掉。
gc(内存管理)
理解
垃圾回收器(Garbage Collector)尝试回收不再被程序所需要的对象所占用的内存
1.3版本以前
go runtime在一定条件下(内存超过阈值或定期如2min),暂停所有任务的执行,进行mark&sweep操作,操作完成后启动所有任务的执行。在内存使用较多的场景下,go程序在进行垃圾回收时会发生非常明显的卡顿现象(Stop The World)。在对响应速度要求较高的后台服务进程中,这种延迟简直是不能忍受的!
1.3版本
go runtime分离了mark和sweep操作,和以前一样,也是先暂停所有任务执行并启动mark,mark完成后马上就重新启动被暂停的任务了,而是让sweep任务和普通协程任务一样并行的和其他任务一起执行。如果运行在多核处理器上,go会试图将gc任务放到单独的核心上运行而尽量不影响业务代码的执行。go team自己的说法是减少了50%-70%的暂停时间。
常见的垃圾回收方法
引用计数(reference counting)
理解
对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。
标记-清除(mark and sweep)
理解
该方法分为两步,标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。
PHP语言
底层实现原理
zend引擎
核心数据结构
hashTable
应用
数组、全局变量、函数符号表等
结构
hash散列结构 key=>value
双向链表
作用
正向和反向查找
快速遍历
解决hash冲突
目的
快速删除,避免遍历
扩容方式
hashTable是自增长的数据结构,当hash表数目满了之后,会以两倍方式扩容
zval
应用
简单变量(int、string、bool),集合类型(array、recource、object),常量
结构体组成
type:变量类型,整型、字符串、数组等
refcount:引用计数
is_ref 代表是否有地址引用
value 变量值
Extensions
作用
extensions通过组件的方式提供各种基础服务,常见的内置函数(如array系列)、标准库都是通过extension来实现
Sapi
全称
server application programming interface
理解
通过一系列钩子函数,使得php可以和外围交互数据
包括
cgi
webserver和php直接交互的一种方式,fastcgi协议
cli
命令行调用模式
相关应用
foreach
通过遍历hashtable的双向链表完成
全局变量和局部变量
执行流程
将PHP代码转换为语言片段(Tokens)
将Tokens转换成简单而有意义的表达式
词法、语法解析,源程序被翻译成opcodes
zend虚拟机顺序执行指令,完成操作
垃圾回收机制
如果发现一个zval容器中的refcount在增加,说明不是垃圾
如果发现一个zval容器中的refcount在减少,如果减到了0,直接当做垃圾回收
如果发现一个zval容器中的refcount在减少,并没有减到0,PHP会把该值放到缓冲区,当做有可能是垃圾的怀疑对象。
当缓冲区达到了临界值,PHP会自动调用一个方法去遍历每一个值,如果发现是垃圾就清理
设计模式
单例模式
class Singleton
{
//创建静态私有的变量保存该类对象
static private $instance;
//防止使用new直接创建对象
private function __construct(){}
//防止使用clone克隆对象
private function __clone(){}
static public function getInstance()
{
//判断$instance是否是Singleton的对象,不是则创建
if (!self::$instance instanceof self) {
self::$instance = new self();
}
return self::$instance;
}
{
//创建静态私有的变量保存该类对象
static private $instance;
//防止使用new直接创建对象
private function __construct(){}
//防止使用clone克隆对象
private function __clone(){}
static public function getInstance()
{
//判断$instance是否是Singleton的对象,不是则创建
if (!self::$instance instanceof self) {
self::$instance = new self();
}
return self::$instance;
}
工厂模式
理解
解决问题
使用工厂模式,可以避免当改变某个类的名字或者方法之后,在调用这个类的所有的代码中都修改它的名字或者参数。
注册模式
理解
解决全局共享和交换对象。已经创建好的对象,挂在到某个全局可以使用的数组上,在需要使用的时候,直接从该数组上获取即可。将对象注册到全局的树上。任何地方直接去访问。
解决问题
适配器模式
理解
PHP中的数据库操作有MySQL,MySQLi,PDO三种,可以用适配器模式统一成一致,使不同的数据库操作,统一成一样的API。类似的场景还有cache适配器,可以将memcache,redis,file,apc等不同的缓存函数,统一成一致。
首先定义一个接口(有几个方法,以及相应的参数)。然后,有几种不同的情况,就写几个类实现该接口。将完成相似功能的函数,统一成一致的方法。
PHP中的数据库操作有MySQL,MySQLi,PDO三种,可以用适配器模式统一成一致,使不同的数据库操作,统一成一样的API。类似的场景还有cache适配器,可以将memcache,redis,file,apc等不同的缓存函数,统一成一致。
首先定义一个接口(有几个方法,以及相应的参数)。然后,有几种不同的情况,就写几个类实现该接口。将完成相似功能的函数,统一成一致的方法。
解决问题
将各种截然不同的函数接口封装成统一的API。
策略模式
理解
将一组特定的行为和算法封装成类,以适应某些特定的上下文环境。
解决问题
假如有一个电商网站系统,针对男性女性用户要各自跳转到不同的商品类目,并且所有的广告位展示不同的广告。在传统的代码中,都是在系统中加入各种if else的判断,硬编码的方式。如果有一天增加了一种用户,就需要改写代码。使用策略模式,如果新增加一种用户类型,只需要增加一种策略就可以。其他所有的地方只需要使用不同的策略就可以。
首先声明策略的接口文件,约定了策略的包含的行为。然后,定义各个具体的策略实现类。
首先声明策略的接口文件,约定了策略的包含的行为。然后,定义各个具体的策略实现类。
观察者模式
理解
观察者模式(Observer),当一个对象状态发生变化时,依赖它的对象全部会收到通知,并自动更新。
解决问题
观察者模式实现了低耦合,非侵入式的通知与更新机制。
7与5的版本区别
区别
1、性能提升:PHP7比PHP5.0性能提升了两倍。
2、以前的许多致命错误,现在改成抛出异常。
3、PHP 7.0比PHP5.0移除了一些老的不在支持的SAPI(服务器端应用编程端口)和扩展。
4、PHP 7.0比PHP5.0新增了空接合操作符。
5、PHP 7.0比PHP5.0新增加了结合比较运算符。
6、PHP 7.0比PHP5.0新增加了函数的返回类型声明。
7、PHP 7.0比PHP5.0新增加了标量类型声明。
8、PHP 7.0比PHP5.0新增加匿名类。
9、错误处理和64位支持
如果您了解错误和异常之间的区别,那么您就会知道在PHP 5中处理致命错误非常不容易。PHP7简化了流程,因为它已用可以轻松处理的异常替换了几个主要错误。这是通过引入新的引擎异常对象实现的。
您可能已经知道,PHP 5不支持64位整数或大文件,但PHP 7中的情况已发生变化。PHP7具有64位支持,因此您也可以使用本机64位整数作为大文件,因此,您可以在64位系统体系结构上完美运行应用程序。
10、声明返回类型
在PHP 5中,程序员无法定义函数或方法的返回类型。在现实生活中,这是一个巨大的缺点,因为程序员无法防止意外的返回类型并在其他情况下生成异常。
幸运的是,PHP 7允许程序员根据期望的返回值声明函数的返回类型。这肯定会使代码健壮和准确。有四种不同的返回类型可用-bool,int,string和float。
2、以前的许多致命错误,现在改成抛出异常。
3、PHP 7.0比PHP5.0移除了一些老的不在支持的SAPI(服务器端应用编程端口)和扩展。
4、PHP 7.0比PHP5.0新增了空接合操作符。
5、PHP 7.0比PHP5.0新增加了结合比较运算符。
6、PHP 7.0比PHP5.0新增加了函数的返回类型声明。
7、PHP 7.0比PHP5.0新增加了标量类型声明。
8、PHP 7.0比PHP5.0新增加匿名类。
9、错误处理和64位支持
如果您了解错误和异常之间的区别,那么您就会知道在PHP 5中处理致命错误非常不容易。PHP7简化了流程,因为它已用可以轻松处理的异常替换了几个主要错误。这是通过引入新的引擎异常对象实现的。
您可能已经知道,PHP 5不支持64位整数或大文件,但PHP 7中的情况已发生变化。PHP7具有64位支持,因此您也可以使用本机64位整数作为大文件,因此,您可以在64位系统体系结构上完美运行应用程序。
10、声明返回类型
在PHP 5中,程序员无法定义函数或方法的返回类型。在现实生活中,这是一个巨大的缺点,因为程序员无法防止意外的返回类型并在其他情况下生成异常。
幸运的是,PHP 7允许程序员根据期望的返回值声明函数的返回类型。这肯定会使代码健壮和准确。有四种不同的返回类型可用-bool,int,string和float。
PHP7为什么比PHP5性能提高
1、存储变量的结构体变小,尽量使结构体里成员共用内存空间,减少引用,这样内存占用降低,变量的操作速度得到提升。
2、字符串结构体的改变,字符串信息和数据本身原来是分成两个独立内存块存放,php7尽量将它们存入同一块内存,提升了cpu缓存命中率。
3、数组结构的改变,数组元素和hash映射表在php5中会存入多个内存块,php7尽量将它们分配在同一块内存里,降低了内存占用、提升了cpu缓存命中率。
4、改进了函数的调用机制,通过对参数传递环节的优化,减少一些指令操作,提高了执行效率。
2、字符串结构体的改变,字符串信息和数据本身原来是分成两个独立内存块存放,php7尽量将它们存入同一块内存,提升了cpu缓存命中率。
3、数组结构的改变,数组元素和hash映射表在php5中会存入多个内存块,php7尽量将它们分配在同一块内存里,降低了内存占用、提升了cpu缓存命中率。
4、改进了函数的调用机制,通过对参数传递环节的优化,减少一些指令操作,提高了执行效率。
PHP-FPM 的运行方式
static
理解
表示在 `php-fpm` 运行时直接 `fork` 出 `pm.max_chindren` 个子进程,
进程数
M / (m * 1.2)
dynamic
理解
表示,运行时 `fork` 出 `start_servers` 个进程,随着负载的情况,动态的调整,最多不超过 `max_children` 个进程。
进程数
在 N + 20% 和 M / m 之间。
参数说明:
N 是 CPU 内核数量。
M 是 PHP 能利用的内存数量。
m 是每个 PHP 进程平均使用的内存数量。
参数说明:
N 是 CPU 内核数量。
M 是 PHP 能利用的内存数量。
m 是每个 PHP 进程平均使用的内存数量。
底层语法实现
数组的实现方式
数据结构
hashtable
理解
字符串的键先会被传递给一个 hash 函数(hashing function,中文也翻译为散列函数),然后这个函数会返回一个整数,而这个整数就是“通常”的数组的索引,通过这个数字索引可以访问到 字符串的键对应的数据
关键词
键(key)
理解
用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等。
槽(slot/bucket)
哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。
哈希函数(hash function)
理解
将key映射(map)到数据应该存放的slot所在位置的函数。
哈希冲突
常用方法
开放定址法
理解
当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者
碰到一个开放的地址(即该地址单元为空)为止
碰到一个开放的地址(即该地址单元为空)为止
举例
{12,67,56,16,25,37,22,29,15,47,48,34}
散列函数f(key) = key mod 12 发现f(37) = 1,此时就与25所在的位置冲突
f(37) = (f(37)+1) mod 12 = 2。于是将37存入下标为2的位置
链地址法
理解
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向
链表连接起来
链表连接起来
举例
如: 键值对k2, v2与键值对k1, v1通过计算后的索引值都为2,这时及产生冲突,但是可以通道next指针将k2, k1所在的节点连接起来,这样就解决了哈希的冲突问题
再哈希法
理解
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数
计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间
计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间
建立公共溢出区
理解
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
变量的实现方式
数据结构
zval
zend_uchar type 变量类型
value 变量值
zend_uint refcount__gc 引用计数内存中使用次数,为0删除该变量
zend_uchar is_ref__gc 区分是否是引用变量,是引用为1,否则为0
对象在内存中的分配
数据段
理解
通常是指用来存放程序中已初始化且不为0的全局变量如:静态变量和常量
栈内存
理解
空间小,读取快,一般存储长度类型不变的数据类型,比如int、float、bool局部变量
堆内存
理解
存储长度不固定,比较大的数据类型,比如数组、类、字符串、new出来的对象
代码段
理解
函数或者方法
lnmp工作原理
nginx服务器fork出n个子进程(worker),php-fpm管理器fork出n个子进程。
当有用户请求,nginx的一个worker接收请求,并将请求抛到socket中。
php-fpm空闲的子进程监听到socket中有请求,接收并处理请求。
当有用户请求,nginx的一个worker接收请求,并将请求抛到socket中。
php-fpm空闲的子进程监听到socket中有请求,接收并处理请求。
php-fpm 是多进程的 fast-cgi 管理服务
nginx 主进程是单线程的, master 进程会 fork 出多个 worker 进程,每个 worker 进程也同样是单线程模式,worker 进程采用的是 epoll 模型,异步非阻塞/事件驱动/IO多路复用,所以 nginx 比较适用于处理高并发请求的场景,就像现在的 nodejs,nodejs 是单进程且单线程模式,master 进程只有一个线程,负责请求调度和轮询回调事件的处理工作。
nginx 主进程是单线程的, master 进程会 fork 出多个 worker 进程,每个 worker 进程也同样是单线程模式,worker 进程采用的是 epoll 模型,异步非阻塞/事件驱动/IO多路复用,所以 nginx 比较适用于处理高并发请求的场景,就像现在的 nodejs,nodejs 是单进程且单线程模式,master 进程只有一个线程,负责请求调度和轮询回调事件的处理工作。
nginx 的 worker 服务进程会把请求转发给 php-fpm 服务,它们是通过 unix socket 或 tcp socket 进行通信的,worker 把 request_1 发送给 php-fpm 后并不会同步阻塞等待 php-fpm 返回数据,而是将此次请求放入事件队列,接着去处理新的请求 request_2,同时会监测有没有回调事件触发了,这里的回调事件便是 request_1 的请求处理好了并已经返回了数据,worker 只需要把数据返回给客户端即可。
php-fpm 相当于一个进程池,里面维系着一定数量的php服务进程,等待 nginx 的 worker 发送任务,处理完成后返回给 nginx 的 worker。
php-fpm 相当于一个进程池,里面维系着一定数量的php服务进程,等待 nginx 的 worker 发送任务,处理完成后返回给 nginx 的 worker。
Nginx 是非阻塞IO & IO复用模型,通过操作系统提供的类似 epoll 的功能,可以在一个线程里处理多个客户端的请求。
Nginx 的进程就是线程,即每个进程里只有一个线程,但这一个线程可以服务多个客户端。
PHP-FPM 是阻塞的单线程模型,pm.max_children 指定的是最大的进程数量,pm.max_requests 指定的是每个进程处理多少个请求后重启(因为 PHP 偶尔会有内存泄漏,所以需要重启).
PHP-FPM 的每个进程也只有一个线程,但是一个进程同时只能服务一个客户端。
大多数的 Linux 程序都倾向于使用进程而不是线程,因为 Linux 下相对来说创建进程的开销比较小,而 Linux 的线程功能又不是很强大。
Nginx 的进程就是线程,即每个进程里只有一个线程,但这一个线程可以服务多个客户端。
PHP-FPM 是阻塞的单线程模型,pm.max_children 指定的是最大的进程数量,pm.max_requests 指定的是每个进程处理多少个请求后重启(因为 PHP 偶尔会有内存泄漏,所以需要重启).
PHP-FPM 的每个进程也只有一个线程,但是一个进程同时只能服务一个客户端。
大多数的 Linux 程序都倾向于使用进程而不是线程,因为 Linux 下相对来说创建进程的开销比较小,而 Linux 的线程功能又不是很强大。
内存管理
内存泄漏
理解
内存泄漏指的是在程序运行过程中申请了内存,但是在使用完成后没有及时释放的现象, 对于普通运行时间较短的程序来说可能问题不会那么明显,但是对于长时间运行的程序, 比如Web服务器,后台进程等就比较明显了,随着系统运行占用的内存会持续上升, 可能会因为占用内存过高而崩溃,或被系统杀掉
PHP如何保持mysql和redis的长链接
PHP的MySQL连接资源是怎么被hold住的呢,这需要查看PHP的mysql_pconnect的函数代码,我看了下,大概的做法就是mysql_pconnect根据当前Apache进程号,生成hash key,找hash表内有无对应的连接资源,没有则推入hash表,有则直接使用
php 执行模式
cli
运行过程
1.PHP初始化执行,启动zend引擎,加载已经注册的扩展模块
2.读取脚本文件,zend引擎对脚本进行词法分析、语法分析、生成语法生成树
3.zend引擎编译语法树,生成opcode中间代码
4. zend引擎执行opcode,返回执行结果
php-fpm
opcache
理解
是PHP应对每次都需要重复编译的脚本文件而开发的组件,可以节省解析脚本文件的开销
原理
opcache是使用了共享内存的机制,将需要缓存的内容放入到共享内存中,供其它进程使用
注意
不要给opcahe设置过期时间,不要在流量高峰期发布代码
PHP常用函数
数组
数组的一些关于键名和值的基础操作函数
array_keys
array_values
array_flip
交换数组中键和值的位置,若重复前面的会被后面的覆盖
in_array
array_search
在数组中搜索某个值,在则返回它的键,不在则返回FALSE:array_search()
$bool = array_key_exists('a',$arr)
给定键是否存在数组中
$lowarr = array_change_key_case($arr,CASE_LOWER)
将数组中的键名改为全小写或大写
$arr_count = array_count_values($arr);
统计数组中所有的值出现的次数,返回一个数组,键是原数组的值,值是这个元素在原数组出现的次数
$key = array_key_first($arr)
得到数组的第一个或最后一个键名:array_key_first(array)、array_key_last(array)
$last = array_pop($array);
弹出数组的最后一个元素
$new_array = array_push($array,$value1,$value2,...);
$new_array = array_unshift($array,$value1,$value2,...);
$new_array = array_unshift($array,$value1,$value2,...);
将一个或多个单元压入数组的末尾或数组的开头,并返回新数组的个数
$reverse = array_reverse($arr)
$sum = array_sum($array);
$product = array_product($array);
$product = array_product($array);
对数组中所有值求和或求乘积
array_unique($array,SORT_STRING);
$bool = shuffle($arr);
打乱数组:shuffle(array)
array_rand(array,num=1)
从数组中随机取得一个或多个键名
current():返回数组中的当前元素(第一个指针指向的元素)。
数组的一些关于创建和分割的操作函数总结
$myarr = array_chunk($arr,2)
size:指明每个数组的元素个数
preserve_keys:指明是否保留原来的键名,默认为false。
preserve_keys:指明是否保留原来的键名,默认为false。
$arr_1 = ['A','B','C'];
$arr_2 = ['a','b','c'];
$arr_3 = array_combine($arr_1,$arr_2);
$arr_2 = ['a','b','c'];
$arr_3 = array_combine($arr_1,$arr_2);
创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值:array_combine(keys,values)
array_fill_keys
array_merge
键名相同时,若是字符键名则会被覆盖,数字键名则不会被覆盖,而是附加到后面
array_merge_recursive
如果数组具有相同的数组键名,后一个值将不会覆盖原来的值,而是附加到后面.
extract(array)
从数组中导出变量:extract(array),键名为变量名,值为变量的值
list($drink, , $power) = array('coffee', 'brown', 'caffeine');
把数组的值赋予变量:list(var1,var2,...)
range(0,8,2) ==> [0,2,4,6,8]
根据范围创建数组,包含指定的元素:range(start,end,step)
数组排序基本函数名为 sort,可以添加其他拓展:r表示逆向排序,k表示对键名进行排序,a表示保持索引关系,u表示用自定义的函数进行比较
2.sort()、rsort():对值进行升序和降序的排序
3.ksort()、krsort():对键名进行升序和降序的排序
4.asort()、arsort():保持索引关系的同时,对值进行升序和降序的排序
5.usort()、uksort()、uasort():使用自定义的排序函数,进行按值的升序排序、按键名的升序排序、保持索引关系的升序排序
6.natsort():使用自然排序算法对数组进行排序
7.natcasesort():使用自然排序算法对数组进行不区分大小写字母的排序
3.ksort()、krsort():对键名进行升序和降序的排序
4.asort()、arsort():保持索引关系的同时,对值进行升序和降序的排序
5.usort()、uksort()、uasort():使用自定义的排序函数,进行按值的升序排序、按键名的升序排序、保持索引关系的升序排序
6.natsort():使用自然排序算法对数组进行排序
7.natcasesort():使用自然排序算法对数组进行不区分大小写字母的排序
数组运算
array_walk($arr,'myFunction'):对数组中的每个成员应用自定义函数
array_diff
对比array1和其他数组,返回在array1中但不在其他数组中的值。返回一个数组,但是键名不保留
array_diff_key
使用键名比较计算数组的差集
array_intersect($array1, $array2, ...)
计算数组的交集;返回一个数组,该数组包含了所有在 array1 中也同时出现在所有其它参数数组中的值。注意键名保留不变。
array_map()
说明
返回数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。
举例
<?php
function cube($n)
{
return($n * $n * $n);
}
$a = array(1, 2, 3, 4, 5);
$b = array_map("cube", $a);
print_r($b);
function cube($n)
{
return($n * $n * $n);
}
$a = array(1, 2, 3, 4, 5);
$b = array_map("cube", $a);
print_r($b);
array_walk()
说明
使用用户自定义函数对数组中的每个元素做回调处理,不会受到 array 内部数组指针的影响。array_walk() 会遍历整个数组而不管指针的位置。
举例
$fruits = array("d" => "lemon", "a" => "orange", "b" => "banana", "c" => "apple");
function test_alter(&$item1, $key, $prefix) {
$item1 = "$prefix: $item1";
}
function test_print($item2, $key) {
echo "$key. $item2<br />\n";
}
echo "Before ...:\n";
array_walk($fruits, 'test_print');
array_walk($fruits, 'test_alter', 'fruit');
echo "... and after:\n";
array_walk($fruits, 'test_print');
function test_alter(&$item1, $key, $prefix) {
$item1 = "$prefix: $item1";
}
function test_print($item2, $key) {
echo "$key. $item2<br />\n";
}
echo "Before ...:\n";
array_walk($fruits, 'test_print');
array_walk($fruits, 'test_alter', 'fruit');
echo "... and after:\n";
array_walk($fruits, 'test_print');
字符串
取整
floor
理解
舍去法取整
ceil
理解
进一法取整
round
理解
对浮点数进行四舍五入
intval
理解
对变数转成整数型态
常用函数
字符串查找
strpos($str,'a’);
字符串a 在$str 第一次出现的位置 索引0开始 没有出现返回false 区分大小写
stripos($str,'a’);
但是不区分大小写
strrpos($str,'a’);
字符串a 在$str 最后一次出现的位置 索引0开始 没有出现返回false 区分大小写
strripos($str,'a’);
但是不区分大小写
截取字符串
submit($str,int start[,int length])
从$str中strat位置开始提取[length长度的字符串]。
substr($str,0,3);
截取字符串 $str 的第一个字符 截取长度3 长度不填默认截取到最后 参数为负数则倒数
strstr($str,'a');
截取字符串 $str 中的第一个字符'a'后的字符串 如 sabc -> abc
strrchr($str,'a');
截取字符串 $str 中最后一一个字符'a'后的字符串
替换字符串
str_replace('a','b',$str);
b替换$str 中的a 区分大小写
str_ireplace('a','b',$str);
替换 不区分大小写
strtr("Hilla Warld","ia","eo");
把字符串中的字符 "ia" 替换成 "eo":
substr_replace($Str,$rep,$start[,length])
$str原始字符串,$rep替换后的新字符串,$start起始位置,$length替换的长度,该项可选
字符长度
strlen($str);//返回字符串长度 mb_strlen($str) 可以返回中文字符长度;
分割成数组
str_split($str,len)
把$str按len长度进行分割返回数组
explode('-',$str);
指定分隔符分割字符串 返回数组
implode('-',$str)
数组拼接字符串 与explode()相反
字符大小写转换
strtolower($str)
字符串转换为小写
strtoupper($str)
字符串转换为大写
ucfirst($str)
将函数的第一个字符转换为大写
ucwords($str)
将每个单词的首字母转换为大写
返回指定的字符或ascii
chr
返回相对应于 ascii 所指定的单个字符
ord
返回字符串 string 第一个字符的 ASCII 码值
java语言
面试题
JVM
1 JVM内存模型分为哪些区域及其作用
2 介绍Java类加载机制,什么是父亲委派机制?
3 什么是GC?Serial 与 Parallel GC 之间的不同之处
4 如何评估一个Object在内存中占用空间的大小?什么是指针压缩?
5 介绍一次jvm故障分析案例?常用的故障处理命令有哪些?
语言基础
1 Java中的基本数据类型?
2 int类型占几个字节?
3 Object有哪些常用方法?介绍具体作用
4 hashcode()和equals()方法之间的关联?
5 对象在内存中都包含哪些部分及其作用?
6 重载和重写的区别
7 说出JDK中5个常用包名,并展开介绍其中一个包含哪些常用类
8 String、StringBuffer和StringBuilder的区别
9 String的常用方法
10 接口和抽象类的区别
11 Throw 和 throws 的区别
12 介绍三个你所知道的Java注解及其作用
13 Java的反射机制?了解哪些应用场景
14 Java中如何实现序列化?
15 serialVersionUID的作用
16 transient关键字的作用
17 Java NIO的核心组件及其作用?
集合
1 Java有哪些集合类型?主要的接口和实现类
2 ArrayList和LinkedList的区别
3 HashMap底层结构和插入过程,扩容过程
4 线程安全的Map解决方案,concurrentHashMap和HashTable的区别
多线程
1 Java有哪些实现线程的方式
2 线程的生命周期
3 sleep和wait的区别 是否释放锁
4 线程池工作原理,ThreadPoolExecutor主要参数的作用
5 Synchronized和ReentrantLock的区别,公平锁还是非公平锁
6 乐观锁和悲观锁,在Java中的具体应用
7 CountDownLatch/CyclicBarrier/Semaphore的作用和使用场景
Spring框架
1 IoC是指什么?如何实现的?
2 列举几种依赖注入的方式
3 Spring框架有哪些核心组件?
4 bean的生命周期
5 介绍SpringMVC的一次http请求的执行过程
6 SpringMVC常用的注解有哪些
7 SpringBoot有哪些特点?自动配置是如何实现的?
8 什么是SPI机制?你了解的还有哪些地方使用了SPI?
Dubbo框架
子主题
算法
查找
二分查找
条件
顺序数组
时间复杂度
logn
空间复杂度
o(1)
代码
function check($arr,$res)
{
$right = count($arr);
$left = 0;
//匹配循环
while ($left <= $right) {
//得到中间位置
$middle = floor(($right + $left) / 2);
//匹配数据
if ($arr[$middle] == $res) {
return $middle+1;
}
//没有找到
if ($arr[$middle] < $res) {
$left = $middle + 1;
} else {
$right = $middle - 1;
}
}
return false;
}
{
$right = count($arr);
$left = 0;
//匹配循环
while ($left <= $right) {
//得到中间位置
$middle = floor(($right + $left) / 2);
//匹配数据
if ($arr[$middle] == $res) {
return $middle+1;
}
//没有找到
if ($arr[$middle] < $res) {
$left = $middle + 1;
} else {
$right = $middle - 1;
}
}
return false;
}
顺序查找
树表查找
二叉树查询
特点
树的左分支的值小于右分支的值,通过中序遍历可得有序的数列。
bitmap
理解
在所有具有性能优化的数据结构中,大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美。但是数据量大了,内存就不够了。
应用
1)可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下。
2)去重数据而达到压缩数据
大量数据进行排序,查找和去重上可以使用这个策略来降低内存的使用
2)去重数据而达到压缩数据
大量数据进行排序,查找和去重上可以使用这个策略来降低内存的使用
布隆过滤器
位运算
子主题
排序
冒泡排序
理解
从左到右,两个元素相比较,假定小的在前,大的在后,如果左边大于右边,则互换值,循环n-1轮
时间复杂度
平均o(n2)
最好 o(n)
一轮比较后,顺序排好
空间复杂度
o(1)
代码
for ($i = 0; $i < count($a) ; $i++) {
// 第二层为从$i+1的地方循环到数组最后
for ($j = $i+1; $j < count($a); $j++) {
// 比较数组中两个相邻值的大小
if ($a[$i] > $a[$j]) {
$tem = $a[$i]; // 这里临时变量,存贮$i的值
$a[$i] = $a[$j]; // 第一次更换位置
$a[$j] = $tem; // 完成位置互换
}
}
}
// 第二层为从$i+1的地方循环到数组最后
for ($j = $i+1; $j < count($a); $j++) {
// 比较数组中两个相邻值的大小
if ($a[$i] > $a[$j]) {
$tem = $a[$i]; // 这里临时变量,存贮$i的值
$a[$i] = $a[$j]; // 第一次更换位置
$a[$j] = $tem; // 完成位置互换
}
}
}
快速排序
理解
取个中间值(一般是数组第一个元素),如果是数组的元素个数<=1,则没必要比较直接返回,如果大于1,则从左到右依次与中间值比较,小于中间值,放左边数组,反之,放右边数组,左右数组作为参数,递归调用该函数,最后合并左数组、中间值、右数组
时间复杂度
平均nlogn
最坏 n2
每次划分只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序
最好 nlogn
计算规则
空间复杂度
nlogn
代码
// 判断是否需要运行,因下面已拿出一个中间值,这里<=1
if (count($a) <= 1) {
return $a;
}
$middle = $a[0]; // 中间值
$left = array(); // 接收小于中间值
$right = array();// 接收大于中间值
// 循环比较
for ($i=1; $i < count($a); $i++) {
if ($middle < $a[$i]) {
// 大于中间值
$right[] = $a[$i];
} else {
// 小于中间值
$left[] = $a[$i];
}
}
// 递归排序划分好的2边
$left = quick_sort($left);
$right = quick_sort($right);
// 合并排序后的数据,别忘了合并中间值
return array_merge($left, array($middle), $right);
if (count($a) <= 1) {
return $a;
}
$middle = $a[0]; // 中间值
$left = array(); // 接收小于中间值
$right = array();// 接收大于中间值
// 循环比较
for ($i=1; $i < count($a); $i++) {
if ($middle < $a[$i]) {
// 大于中间值
$right[] = $a[$i];
} else {
// 小于中间值
$left[] = $a[$i];
}
}
// 递归排序划分好的2边
$left = quick_sort($left);
$right = quick_sort($right);
// 合并排序后的数据,别忘了合并中间值
return array_merge($left, array($middle), $right);
归并排序
原理
利用递归,先拆分、后合并、再排序。
步骤
均分数列为两个子数列
递归重复上一步骤,直到子数列只有一个元素
父数列合并两个子数列并排序,递归返回数列
复杂度
时间复杂度O(n log n)
代码
// 归并排序主程序
function mergeSort($arr) {
$len = count($arr);
if ($len <= 1) {
return $arr;
} // 递归结束条件, 到达这步的时候, 数组就只剩下一个元素了, 也就是分离了数组
$mid = intval($len / 2); // 取数组中间
$left = array_slice($arr, 0, $mid); // 拆分数组0-mid这部分给左边left
$right = array_slice($arr, $mid); // 拆分数组mid-末尾这部分给右边right
$left = mergeSort($left); // 左边拆分完后开始递归合并往上走
$right = mergeSort($right); // 右边拆分完毕开始递归往上走
$arr = merge($left, $right); // 合并两个数组,继续递归
return $arr;
}
// merge函数将指定的两个有序数组(arrA, arr)合并并且排序
function merge($arrA, $arrB) {
$arrC = array();
while (count($arrA) && count($arrB)) {
// 这里不断的判断哪个值小, 就将小的值给到arrC, 但是到最后肯定要剩下几个值,
// 不是剩下arrA里面的就是剩下arrB里面的而且这几个有序的值, 肯定比arrC里面所有的值都大所以使用
$arrC[] = $arrA[0] < $arrB[0] ? array_shift($arrA) : array_shift($arrB);
}
return array_merge($arrC, $arrA, $arrB);
}
function mergeSort($arr) {
$len = count($arr);
if ($len <= 1) {
return $arr;
} // 递归结束条件, 到达这步的时候, 数组就只剩下一个元素了, 也就是分离了数组
$mid = intval($len / 2); // 取数组中间
$left = array_slice($arr, 0, $mid); // 拆分数组0-mid这部分给左边left
$right = array_slice($arr, $mid); // 拆分数组mid-末尾这部分给右边right
$left = mergeSort($left); // 左边拆分完后开始递归合并往上走
$right = mergeSort($right); // 右边拆分完毕开始递归往上走
$arr = merge($left, $right); // 合并两个数组,继续递归
return $arr;
}
// merge函数将指定的两个有序数组(arrA, arr)合并并且排序
function merge($arrA, $arrB) {
$arrC = array();
while (count($arrA) && count($arrB)) {
// 这里不断的判断哪个值小, 就将小的值给到arrC, 但是到最后肯定要剩下几个值,
// 不是剩下arrA里面的就是剩下arrB里面的而且这几个有序的值, 肯定比arrC里面所有的值都大所以使用
$arrC[] = $arrA[0] < $arrB[0] ? array_shift($arrA) : array_shift($arrB);
}
return array_merge($arrC, $arrA, $arrB);
}
链表归并排序
class Solution {
public:
ListNode *sortList(ListNode *head){
if (!head || !head->next) return head; //空链表和只有一个元素的链表不需要排序
ListNode *p = head, *q = head->next;
//找到链表的中间位置
while (q&&q->next){ //快慢指针,注意必须前两步存在
p = p->next;
q = q->next->next;
}
ListNode *left = sortList(p->next); //右链表
p->next = NULL; //将其断开,为两个链表
ListNode *right = sortList(head);
return merge(left, right);
}
ListNode *merge(ListNode *left, ListNode *right)
{
ListNode dummy(0); //申请一个假节点
ListNode *p = &dummy;
while (left&&right){
if (left->val < right->val){
p->next = left;
left = left->next;
}
else{
p->next = right;
right = right->next;
}
p = p->next;
}
if (left)p->next = left;
if (right)p->next = right;
return dummy.next;
}
};
public:
ListNode *sortList(ListNode *head){
if (!head || !head->next) return head; //空链表和只有一个元素的链表不需要排序
ListNode *p = head, *q = head->next;
//找到链表的中间位置
while (q&&q->next){ //快慢指针,注意必须前两步存在
p = p->next;
q = q->next->next;
}
ListNode *left = sortList(p->next); //右链表
p->next = NULL; //将其断开,为两个链表
ListNode *right = sortList(head);
return merge(left, right);
}
ListNode *merge(ListNode *left, ListNode *right)
{
ListNode dummy(0); //申请一个假节点
ListNode *p = &dummy;
while (left&&right){
if (left->val < right->val){
p->next = left;
left = left->next;
}
else{
p->next = right;
right = right->next;
}
p = p->next;
}
if (left)p->next = left;
if (right)p->next = right;
return dummy.next;
}
};
直接插入排序
思路
第一层循环:遍历待比较的所有数组元素
第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。如果:selected > ordered,那么将二者交换。
第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。如果:selected > ordered,那么将二者交换。
应用
如果有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法
代码
//插入排序
function insert_sort($arr) {
//获取数组单元个数
$count = count($arr);
//外层循环用于从未排序区域中取出待排序元素 for ($i=1; $i < $count; $i++) {
//获取当前需要插入已排序区域的元素值
$temp = $arr[$i];
//内层循环用于从已排序区域寻找待排序元素的插入位置
for ($j=$i-1; $j >= 0; $j--) {
//如果$arr[$i]比已排序区域的$arr[$j]小,就后移$arr[$j]
if ($temp < $arr[$j]) {
$arr[$j+1] = $arr[$j];
$arr[$j] = $temp;
} }
}
return $arr; }
function insert_sort($arr) {
//获取数组单元个数
$count = count($arr);
//外层循环用于从未排序区域中取出待排序元素 for ($i=1; $i < $count; $i++) {
//获取当前需要插入已排序区域的元素值
$temp = $arr[$i];
//内层循环用于从已排序区域寻找待排序元素的插入位置
for ($j=$i-1; $j >= 0; $j--) {
//如果$arr[$i]比已排序区域的$arr[$j]小,就后移$arr[$j]
if ($temp < $arr[$j]) {
$arr[$j+1] = $arr[$j];
$arr[$j] = $temp;
} }
}
return $arr; }
堆排序
理解
堆分为两种:最大堆和最小堆,在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
时间复杂度
O(nlogn)
基本思路
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
代码
因为是数组,下标从0开始,所以,下标为n根结点的左子结点为2n+1,右子结点为2n+2;
$arr=array(49,38,65,97,76,13,27,50);
$arrSize=count($arr); //将第一次排序抽出来,因为最后一次排序不需要再交换值了。
buildHeap($arr,$arrSize);
for($i=$arrSize-1;$i>0;$i--){
swap($arr,$i,0);
$arrSize--;
buildHeap($arr,$arrSize);
}
//用数组建立最小堆
function buildHeap(&$arr,$arrSize){
//计算出最开始的下标$index,如图,为数字"97"所在位置,比较每一个子树的父结点和子结点,将最小值存入父结点中 //从$index处对一个树进行循环比较,形成最小堆
for($index=intval($arrSize/2)-1; $index>=0; $index--){
//如果有左节点,将其下标存进最小值$min
if($index*2+1<$arrSize){
$min=$arr[$index*2+1] > $arr[$index*2+2] ? $index*2+2 : $index*2+1;
}
//将子结点中较小的和父结点比较,若子结点较小,与父结点交换位置,同时更新较小
if($arr[$min]<$arr[$index]){
swap($arr,$min,$index);
}
}
}
//此函数用来交换下数组$arr中下标为$one和$another的数据
function swap(&$arr,$one,$another){
$tmp=$arr[$one];
$arr[$one]=$arr[$another];
$arr[$another]=$tmp;
}
$arrSize=count($arr); //将第一次排序抽出来,因为最后一次排序不需要再交换值了。
buildHeap($arr,$arrSize);
for($i=$arrSize-1;$i>0;$i--){
swap($arr,$i,0);
$arrSize--;
buildHeap($arr,$arrSize);
}
//用数组建立最小堆
function buildHeap(&$arr,$arrSize){
//计算出最开始的下标$index,如图,为数字"97"所在位置,比较每一个子树的父结点和子结点,将最小值存入父结点中 //从$index处对一个树进行循环比较,形成最小堆
for($index=intval($arrSize/2)-1; $index>=0; $index--){
//如果有左节点,将其下标存进最小值$min
if($index*2+1<$arrSize){
$min=$arr[$index*2+1] > $arr[$index*2+2] ? $index*2+2 : $index*2+1;
}
//将子结点中较小的和父结点比较,若子结点较小,与父结点交换位置,同时更新较小
if($arr[$min]<$arr[$index]){
swap($arr,$min,$index);
}
}
}
//此函数用来交换下数组$arr中下标为$one和$another的数据
function swap(&$arr,$one,$another){
$tmp=$arr[$one];
$arr[$one]=$arr[$another];
$arr[$another]=$tmp;
}
子主题
选择排序
理解
1.选出最小数与第一个数交换
2.从除去第一个数,剩余的数中选出最小数与第二个数交换
3.以此类推
时间复杂度
平均、最坏、最好 都是n2
空间复杂度
n1
代码
function order($arr){
//定义中间变量
$temp = 0;
$count = count($arr);
for($i=0; $i<$count-1; $i++){
//定义最小位置
$minIndex = $i;
for($j= $i+1; $j<$count; $j++){
if($arr[$j] < $arr[$minIndex]){
$minIndex = $j;
}
}
if($i != $minIndex){
$temp = $arr[$i];
$arr[$i] = $arr[$minIndex];
$arr[$minIndex] = $temp;
}
}
return $arr;
}
//定义中间变量
$temp = 0;
$count = count($arr);
for($i=0; $i<$count-1; $i++){
//定义最小位置
$minIndex = $i;
for($j= $i+1; $j<$count; $j++){
if($arr[$j] < $arr[$minIndex]){
$minIndex = $j;
}
}
if($i != $minIndex){
$temp = $arr[$i];
$arr[$i] = $arr[$minIndex];
$arr[$minIndex] = $temp;
}
}
return $arr;
}
递归和循环
斐波那契数列
理解
从n=3开始,第一个数+第二个数=第三个数
思路
long long Fibonacci(unsigned int n)
{
return n < 2 ? n : Fibonacci(n - 1) + Fibonacci(n - 2);
}
{
return n < 2 ? n : Fibonacci(n - 1) + Fibonacci(n - 2);
}
时间复杂度 o(n2)
空间复杂度 o(n)
代码
class Solution {
/**
* @param Integer $N
* @return Integer
*/
function fib($N) {
if ($N < 2) return $N;
return $this->fib($N - 1) + $this->fib($N - 2);
}
}
/**
* @param Integer $N
* @return Integer
*/
function fib($N) {
if ($N < 2) return $N;
return $this->fib($N - 1) + $this->fib($N - 2);
}
}
应用题
1.有n级台阶,每次走1级或2级,问有多少种走法,可以走完.
青蛙跳台阶
题解
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法
代码
function jumpFloor($number)
{
$prePreNum = 1;
$preNum = 2;
$temp = 3;
// write code here
if($number == 1){
return 1;
}
if($number == 2){
return 2;
}
for($i=3;$i<=$number;$i++){
$temp = $prePreNum + $preNum;
$prePreNum = $preNum;
$preNum = $temp;
}
return $temp;
}
{
$prePreNum = 1;
$preNum = 2;
$temp = 3;
// write code here
if($number == 1){
return 1;
}
if($number == 2){
return 2;
}
for($i=3;$i<=$number;$i++){
$temp = $prePreNum + $preNum;
$prePreNum = $preNum;
$preNum = $temp;
}
return $temp;
}
变态跳台阶
题解
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
代码
function jumpFloorII($number)
{
// write code here
if($number == 1) return 1;
return pow(2,($number - 1));
}
{
// write code here
if($number == 1) return 1;
return pow(2,($number - 1));
}
pow是x的y次方函数
最大子序和
题解
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
代码
function maxSubArray($nums) {
for($i=1;$i<count($nums);$i++)
$nums[$i] = max($nums[$i],$nums[$i]+$nums[$i-1]);
return max($nums);
}
for($i=1;$i<count($nums);$i++)
$nums[$i] = max($nums[$i],$nums[$i]+$nums[$i-1]);
return max($nums);
}
最长公共前缀
题目
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
如果不存在公共前缀,返回空字符串 ""。
思路
先把第一个字符串认为是公共前缀,然后从第二个遍历每个字符串,与前一个字符串的每个字母进行比较,如果遇到不同的,则记录不同的位置,然后获取公共前缀,接着用这个公共前缀和后面的字符串再进行比较,最终把公共前缀获取到
代码
class Solution {
/**
* @param String[] $strs
* @return String
*/
function longestCommonPrefix($strs) {
if (empty($strs)) {
return "";
}
$ans = $strs[0];
$count = count($strs);
for ($i = 1; $i < $count; $i++) {
$j = 0;
for (; $j < strlen($ans) && $j < strlen($strs[$i]); $j++) {
if (substr($ans, $j, 1) != substr($strs[$i], $j, 1)) {
break;
}
}
$ans = substr($ans, 0, $j);
if ($ans == "") {
return "";
}
}
return $ans;
}
}
/**
* @param String[] $strs
* @return String
*/
function longestCommonPrefix($strs) {
if (empty($strs)) {
return "";
}
$ans = $strs[0];
$count = count($strs);
for ($i = 1; $i < $count; $i++) {
$j = 0;
for (; $j < strlen($ans) && $j < strlen($strs[$i]); $j++) {
if (substr($ans, $j, 1) != substr($strs[$i], $j, 1)) {
break;
}
}
$ans = substr($ans, 0, $j);
if ($ans == "") {
return "";
}
}
return $ans;
}
}
分治法
理解
分治法其实就是将一个不好解决的大问题,分成性质一样规模小且容易解决的子问题分别解决之后再合并起来得到大问题的答案,难点在于合并的时候如何合并以及子问题的设置。
快速排序
归并排序
二分查找
数组
删除排序数组中的重复项
理解
双指针,快慢指针
代码
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int i = 0;
for (int j = 1; j < nums.length; j++) {
if (nums[j] != nums[i]) {
i++;
nums[i] = nums[j];
}
}
return i + 1;
}
if (nums.length == 0) return 0;
int i = 0;
for (int j = 1; j < nums.length; j++) {
if (nums[j] != nums[i]) {
i++;
nums[i] = nums[j];
}
}
return i + 1;
}
两个数组的交集
理解
给定两个数组,编写一个函数来计算它们的交集。
思路
把一个数组存到hash中,遍历另一个数组,检查是否在哈希中,在的话就是目标元素,同时为了唯一性删除哈希中已存在元素。
代码
class Solution {
/**
* @param Integer[] $nums1
* @param Integer[] $nums2
* @return Integer[]
*/
function intersection($nums1, $nums2) {
$map = [];
$res = [];
for ($i = 0;$i < count($nums1); $i++) {
$map[$nums1[$i]] = true;
}
for ($i = 0; $i < count($nums2); $i++) {
if ($map[$nums2[$i]]) {
$res[] = $nums2[$i];
unset($map[$nums2[$i]]);
}
}
return $res;
}
}
/**
* @param Integer[] $nums1
* @param Integer[] $nums2
* @return Integer[]
*/
function intersection($nums1, $nums2) {
$map = [];
$res = [];
for ($i = 0;$i < count($nums1); $i++) {
$map[$nums1[$i]] = true;
}
for ($i = 0; $i < count($nums2); $i++) {
if ($map[$nums2[$i]]) {
$res[] = $nums2[$i];
unset($map[$nums2[$i]]);
}
}
return $res;
}
}
合并两个有序数组
理解
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
思路
两个指针从后向前:
两个指针分别指向每个数组的尾部:
这样不用额外的空间。
两个指针分别指向每个数组的尾部:
这样不用额外的空间。
代码
function merge2(&$nums1, $m, $nums2, $n) {
//两个指针的下标
$p1 = $m - 1;
$p2 = $n - 1;
$p = $m + $n - 1;//指向 nums1合并后末尾的位置
while(($p1 >=0) && ($p2 >=0)) {
$nums1[$p--] = ($nums1[$p1] < $nums2[$p2]) ? $nums2[$p2--] : $nums1[$p1--];//谁大,把谁放在末尾
}
//当然也有可能nums1都很大,已经循环结束,但是nums2还没合并过去
while ($p1 < 0 && $p2 >= 0) {
$nums1[$p--] = $nums2[$p2 --];
}
$nums1 = array_splice($nums1, 0, $m+$n);//截取后面多余的
}
//两个指针的下标
$p1 = $m - 1;
$p2 = $n - 1;
$p = $m + $n - 1;//指向 nums1合并后末尾的位置
while(($p1 >=0) && ($p2 >=0)) {
$nums1[$p--] = ($nums1[$p1] < $nums2[$p2]) ? $nums2[$p2--] : $nums1[$p1--];//谁大,把谁放在末尾
}
//当然也有可能nums1都很大,已经循环结束,但是nums2还没合并过去
while ($p1 < 0 && $p2 >= 0) {
$nums1[$p--] = $nums2[$p2 --];
}
$nums1 = array_splice($nums1, 0, $m+$n);//截取后面多余的
}
数组中的第K个最大元素
题目
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
思路
1.先排序,那么第k个元素自然就是第k大的元素了
代码
class Solution {
/**
* @param Integer[] $nums
* @param Integer $k
* @return Integer
*/
private $nums = [];
function findKthLargest($nums, $k) {
$count = count($nums);
$this->nums = $nums;
$this->quicksort($this->nums, 0, $count-1);
return $this->nums[$count-$k];
}
function quicksort(&$arr, $begin, $end){
if($begin >= $end){
return;
}
$l = $begin;
$r = $end;
$flag = $arr[$begin];
while($l < $r){
while($l < $r && $flag < $arr[$r] ){
$r--;
}
if($l < $r){
$this->swap($arr, $l, $r);
$l ++;
}
while($l < $r && $flag > $arr[$l] ){
$l++;
}
if($l < $r){
$this->swap($arr, $l, $r);
$r --;
}
}
$arr[$l] = $flag;
$this->quicksort($arr,$begin,$l-1);
$this->quicksort($arr,$l+1,$end);
}
function swap(&$arr, $l, $r){
$temp = $arr[$l];
$arr[$l] = $arr[$r];
$arr[$r] = $temp;
}
}
/**
* @param Integer[] $nums
* @param Integer $k
* @return Integer
*/
private $nums = [];
function findKthLargest($nums, $k) {
$count = count($nums);
$this->nums = $nums;
$this->quicksort($this->nums, 0, $count-1);
return $this->nums[$count-$k];
}
function quicksort(&$arr, $begin, $end){
if($begin >= $end){
return;
}
$l = $begin;
$r = $end;
$flag = $arr[$begin];
while($l < $r){
while($l < $r && $flag < $arr[$r] ){
$r--;
}
if($l < $r){
$this->swap($arr, $l, $r);
$l ++;
}
while($l < $r && $flag > $arr[$l] ){
$l++;
}
if($l < $r){
$this->swap($arr, $l, $r);
$r --;
}
}
$arr[$l] = $flag;
$this->quicksort($arr,$begin,$l-1);
$this->quicksort($arr,$l+1,$end);
}
function swap(&$arr, $l, $r){
$temp = $arr[$l];
$arr[$l] = $arr[$r];
$arr[$r] = $temp;
}
}
二维数组查找
题目
- 在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
代码
function findNumberIn2DArray($matrix, $target) {
//从左下角开始往上找
$row = count($matrix);
$col = count($matrix[0]);
$x = $row -1 ;
$y = 0;
while($x >=0 && $y < $col){
if($matrix[$x][$y] == $target ){
return true;
}elseif($matrix[$x][$y] > $target ){
$x --;
}else{
$y ++;
}
}
return false;
}
//从左下角开始往上找
$row = count($matrix);
$col = count($matrix[0]);
$x = $row -1 ;
$y = 0;
while($x >=0 && $y < $col){
if($matrix[$x][$y] == $target ){
return true;
}elseif($matrix[$x][$y] > $target ){
$x --;
}else{
$y ++;
}
}
return false;
}
旋转数组
题目
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
举例
输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
代码
function rotate(&$nums, $k){
$len = count($nums);
$k %= $len;
$this->reverse($nums, 0, $len-1);
$this->reverse($nums, 0, $k-1);
$this->reverse($nums, $k, $len-1);
}
function reverse(&$nums, $start, $end){
while($start < $end){
$tmp = $nums[$start];
$nums[$start++] = $nums[$end];
$nums[$end--] = $tmp;
}
}
$len = count($nums);
$k %= $len;
$this->reverse($nums, 0, $len-1);
$this->reverse($nums, 0, $k-1);
$this->reverse($nums, $k, $len-1);
}
function reverse(&$nums, $start, $end){
while($start < $end){
$tmp = $nums[$start];
$nums[$start++] = $nums[$end];
$nums[$end--] = $tmp;
}
}
杨辉三角
题目
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
代码
function generate($numRows) {
$res = [];
for($i = 0; $i< $numRows; $i++){
for($j = 0; $j<= $i; $j++){
if($j==$i || $j == 0){
$res[$i][$j] = 1;
}else{
$res[$i][$j] = $res[$i-1][$j-1]+ $res[$i-1][$j];
}
}
}
return $res;
}
$res = [];
for($i = 0; $i< $numRows; $i++){
for($j = 0; $j<= $i; $j++){
if($j==$i || $j == 0){
$res[$i][$j] = 1;
}else{
$res[$i][$j] = $res[$i-1][$j-1]+ $res[$i-1][$j];
}
}
}
return $res;
}
按类型
双指针
移动零
题目
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
代码
class Solution {
/**
* @param Integer[] $nums
* @return NULL
*/
function moveZeroes(&$nums) {
//快慢指针
$j = 0;
foreach($nums as $i => $num){
if($num != 0){
$nums[$j++] = $num;
}
}
for($i = $j; $i< count($nums); $i++){
$nums[$i] = 0;
}
}
}
/**
* @param Integer[] $nums
* @return NULL
*/
function moveZeroes(&$nums) {
//快慢指针
$j = 0;
foreach($nums as $i => $num){
if($num != 0){
$nums[$j++] = $num;
}
}
for($i = $j; $i< count($nums); $i++){
$nums[$i] = 0;
}
}
}
两数之和
解题思路
将数组的值作为key,下标作为value,放到数组中(hash表),循环判断总数-当前value的值是否是该数组的key
复杂度
时间复杂度和空间复杂度都是o(n)
代码
function twoSum($nums, $target) {
$count = count($nums);
for($i=0; $i< $count; $i++){
$ele = $target - $nums[$i];
$keys = array_keys($nums, $ele);
foreach($keys as $key) {
if ( $key && $key != $i) {
return [$i, $key];
}
}
}
return [0,0];
}
$count = count($nums);
for($i=0; $i< $count; $i++){
$ele = $target - $nums[$i];
$keys = array_keys($nums, $ele);
foreach($keys as $key) {
if ( $key && $key != $i) {
return [$i, $key];
}
}
}
return [0,0];
}
快乐数
题目
编写一个算法来判断一个数是不是“快乐数”。
一个“快乐数”定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
一个“快乐数”定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
代码
class Solution {
/**
* @param Integer $n
* @return Boolean
*/
function isHappy($n) {
//快慢指针
$slow = $n;
$fast = $n;
do{
$slow = $this->getSum($slow);
$fast = $this->getSum($fast);
$fast = $this->getSum($fast);
}while($slow != $fast);
if($fast == 1){
return true;
}else{
return false;
}
}
function getSum($n){
$sum = 0;
while($n != 0){
$sum += ($n%10)*($n%10);
$n = $n/10;
}
return $sum;
}
}
/**
* @param Integer $n
* @return Boolean
*/
function isHappy($n) {
//快慢指针
$slow = $n;
$fast = $n;
do{
$slow = $this->getSum($slow);
$fast = $this->getSum($fast);
$fast = $this->getSum($fast);
}while($slow != $fast);
if($fast == 1){
return true;
}else{
return false;
}
}
function getSum($n){
$sum = 0;
while($n != 0){
$sum += ($n%10)*($n%10);
$n = $n/10;
}
return $sum;
}
}
三数之和
思路
特判,对于数组长度 nn,如果数组为 nullnull 或者数组长度小于 33,返回 [][]。
对数组进行排序。
遍历排序后数组:
若 nums[i]>0nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 00,直接返回结果。
对于重复元素:跳过,避免出现重复解
令左指针 L=i+1L=i+1,右指针 R=n-1R=n−1,当 L<RL<R 时,执行循环:
当 nums[i]+nums[L]+nums[R]==0nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,RL,R 移到下一位置,寻找新的解
若和大于 00,说明 nums[R]nums[R] 太大,RR 左移
若和小于 00,说明 nums[L]nums[L] 太小,LL 右移
对数组进行排序。
遍历排序后数组:
若 nums[i]>0nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 00,直接返回结果。
对于重复元素:跳过,避免出现重复解
令左指针 L=i+1L=i+1,右指针 R=n-1R=n−1,当 L<RL<R 时,执行循环:
当 nums[i]+nums[L]+nums[R]==0nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,RL,R 移到下一位置,寻找新的解
若和大于 00,说明 nums[R]nums[R] 太大,RR 左移
若和小于 00,说明 nums[L]nums[L] 太小,LL 右移
题目
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
代码
function threeSum($nums) {
$res = [];
sort($nums);
$len = count($nums);
for ($i = 0; $i < $len; $i++) {
if ($nums[$i] > 0) continue;
if ($i > 0 && $nums[$i -1] == $nums[$i]) continue;
$front = $i + 1;
$rear = $len - 1;
while ($front < $rear) {
$sum = $nums[$i] + $nums[$front] + $nums[$rear];
if ($sum == 0) {
$res[] = [$nums[$i], $nums[$front], $nums[$rear]];
while ($front < $rear && $nums[$front] == $nums[$front + 1]) $front++;
while ($front < $rear && $nums[$rear - 1] == $nums[$rear]) $rear--;
$front++;
$rear--;
} elseif ($sum > 0) {
$rear--;
} else {
$front++;
}
}
}
return $res;
}
}
$res = [];
sort($nums);
$len = count($nums);
for ($i = 0; $i < $len; $i++) {
if ($nums[$i] > 0) continue;
if ($i > 0 && $nums[$i -1] == $nums[$i]) continue;
$front = $i + 1;
$rear = $len - 1;
while ($front < $rear) {
$sum = $nums[$i] + $nums[$front] + $nums[$rear];
if ($sum == 0) {
$res[] = [$nums[$i], $nums[$front], $nums[$rear]];
while ($front < $rear && $nums[$front] == $nums[$front + 1]) $front++;
while ($front < $rear && $nums[$rear - 1] == $nums[$rear]) $rear--;
$front++;
$rear--;
} elseif ($sum > 0) {
$rear--;
} else {
$front++;
}
}
}
return $res;
}
}
平方数之和
题目
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。
代码
function judgeSquareSum($c) {
//双指针法
if($c == 1 || $c == 2){
return true;
}
$left = 0;
$right = floor(sqrt($c));
while($left <= $right){
if($left* $left + $right*$right == $c){
return true;
}else if($left*$left + $right*$right > $c ){
$right --;
}else{
$left ++;
}
}
return false;
}
//双指针法
if($c == 1 || $c == 2){
return true;
}
$left = 0;
$right = floor(sqrt($c));
while($left <= $right){
if($left* $left + $right*$right == $c){
return true;
}else if($left*$left + $right*$right > $c ){
$right --;
}else{
$left ++;
}
}
return false;
}
链表
反转链表
思路
代码实现
class Solution
{
/**
* @param ListNode $head
* @return ListNode
*/
function reverseList($head)
{
// double pointer
// 我们可以申请两个指针,第一个指针叫 prev,最初是指向 null 的。
// 第二个指针 cur 指向 head,然后不断遍历 cur。
// 每次迭代到 cur,都将 cur 的 next 指向 pre,然后 pre 和 cur 前进一位。
// 都迭代完了 (cur 变成 null 了),pre 就是最后一个节点了。
$prev = null;
$cur = $head;
while ($cur) {
$next = $cur->next;
$cur->next = $prev;
$prev = $cur;
$cur = $next;
}
return $prev;
}
}
{
/**
* @param ListNode $head
* @return ListNode
*/
function reverseList($head)
{
// double pointer
// 我们可以申请两个指针,第一个指针叫 prev,最初是指向 null 的。
// 第二个指针 cur 指向 head,然后不断遍历 cur。
// 每次迭代到 cur,都将 cur 的 next 指向 pre,然后 pre 和 cur 前进一位。
// 都迭代完了 (cur 变成 null 了),pre 就是最后一个节点了。
$prev = null;
$cur = $head;
while ($cur) {
$next = $cur->next;
$cur->next = $prev;
$prev = $cur;
$cur = $next;
}
return $prev;
}
}
链表从尾到头打印数组
代码实现
function printListFromTailToHead($head)
{
$list = [];
while($head != null){
$list[] = $head->val;
$head = $head->next;
}
return array_reverse($list);
}
{
$list = [];
while($head != null){
$list[] = $head->val;
$head = $head->next;
}
return array_reverse($list);
}
找到两个单链表相交的起始节点
题解
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
解法
方法一: 暴力法
理解
对链表A中的每一个结点 a_ia i,遍历整个链表 B 并检查链表 B 中是否存在结点和 a_ia i
相同。
相同。
复杂度
时间复杂度 : o(n^2)。
空间复杂度 : O(1)。
空间复杂度 : O(1)。
方法二: 哈希表法
理解
遍历链表 A 并将每个结点的地址/引用存储在哈希表中。然后检查链表 B 中的每一个结点 b_ib i是否在哈希表中。若在,则 b_ib i为相交结点。
复杂度
时间复杂度O(m + n) 空间复杂度O(m) 或 O(n)
方法三:双指针法
理解
A的指针遍历完A 接着从headB开始遍历
B的指针遍历完B 接着从headA开始遍历
两个指针都最多会运行m + n次,当都为空时,表示不相交;当相等时,表示相交
B的指针遍历完B 接着从headA开始遍历
两个指针都最多会运行m + n次,当都为空时,表示不相交;当相等时,表示相交
复杂度
时间复杂度O(m + n) 空间复杂度O(1)
代码
ListNode a = headA;
ListNode b = headB;
//定义了两个节点a和b,只要a和b不等就继续遍历
while(a!=b) {
//这步很关键,请对照动态图配合理解,当a的下一个为空时,
//就a就从b链表头开始遍历
a = (a!=null)? a.next : headB;
//同理,b也是类似的
b = (b!=null)? b.next : headA;
}
return a;
}
ListNode b = headB;
//定义了两个节点a和b,只要a和b不等就继续遍历
while(a!=b) {
//这步很关键,请对照动态图配合理解,当a的下一个为空时,
//就a就从b链表头开始遍历
a = (a!=null)? a.next : headB;
//同理,b也是类似的
b = (b!=null)? b.next : headA;
}
return a;
}
判断是否是回文链表
解法
利用快慢指针
代码
function isPalindrome($head){
$fast = $head; //设定快指针;
$slow = $head; //设定慢指针;
//先利用快慢指针遍历链表,快指针到达终点最后一个元素时,使慢指针指向中点;
while($fast !== null && $fast->next !== null){
$fast = $fast->next->next; //走2步;
$slow = $slow->next; //走1步;
}
//此时slow已到达中点;反转后半部分
$reveredList = null; //定义空指针,用于存放反转链表,指向头元素;
$tmpCurrent = $slow->next; //定义当前需要反转的元素,反转后半部分;
while($tmpCurrent !== null){ //当当前的元素为空时退出;
$next = $tmpCurrent->next; //先将下一个要反转的元素赋值给next;
$tmpCurrent->next = $reveredList; //实现当前元素的next指针反转过来指向reveredList头元素;
$reveredList =$tmpCurrent; //reveredList向后移动一位指向最新反转的元素,准备下一次反转;
$tmpCurrent = $next; //把当前需要反转指针向后移动一位,指向下一位需要反转的元素;
}
//遍历链表比较
$h = $head->next; //准备好原始的链表
while($reveredList !== null){ //当已经反转好的链表最后元素为空
if ($reveredList->val != $h->val) {//比较元素的值
return false;
}
$h = $h->next; //向后移动一位
$reveredList = $reveredList->next; //准备比较下一位元素
}
return true;
}
$fast = $head; //设定快指针;
$slow = $head; //设定慢指针;
//先利用快慢指针遍历链表,快指针到达终点最后一个元素时,使慢指针指向中点;
while($fast !== null && $fast->next !== null){
$fast = $fast->next->next; //走2步;
$slow = $slow->next; //走1步;
}
//此时slow已到达中点;反转后半部分
$reveredList = null; //定义空指针,用于存放反转链表,指向头元素;
$tmpCurrent = $slow->next; //定义当前需要反转的元素,反转后半部分;
while($tmpCurrent !== null){ //当当前的元素为空时退出;
$next = $tmpCurrent->next; //先将下一个要反转的元素赋值给next;
$tmpCurrent->next = $reveredList; //实现当前元素的next指针反转过来指向reveredList头元素;
$reveredList =$tmpCurrent; //reveredList向后移动一位指向最新反转的元素,准备下一次反转;
$tmpCurrent = $next; //把当前需要反转指针向后移动一位,指向下一位需要反转的元素;
}
//遍历链表比较
$h = $head->next; //准备好原始的链表
while($reveredList !== null){ //当已经反转好的链表最后元素为空
if ($reveredList->val != $h->val) {//比较元素的值
return false;
}
$h = $h->next; //向后移动一位
$reveredList = $reveredList->next; //准备比较下一位元素
}
return true;
}
删除链表中的结点
理解
输入: head = [4,5,1,9], node = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
代码
func deleteNode(node *ListNode) {
node.Val = node.Next.Val
node.Next = node.Next.Next
}
node.Val = node.Next.Val
node.Next = node.Next.Next
}
合并两个有序链表
思路
何时终止:当 l1 为空或 l2 为空时结束
返回值:每一层调用都返回排序好的链表头
本级递归处理:如果 l1.val 值更小,则将 l1.next 与排序好的链表头相接,l2 同理
O(m+n),mm 分别为 l1、l2 的长度
返回值:每一层调用都返回排序好的链表头
本级递归处理:如果 l1.val 值更小,则将 l1.next 与排序好的链表头相接,l2 同理
O(m+n),mm 分别为 l1、l2 的长度
代码
// 递归实现
function mergeTwoLists($l1, $l2) {
if(empty($l1)) return $l2;
if(empty($l2)) return $l1;
if($l1->val<= $l2->val) {
$l1->next = $this->mergeTwoLists($l1->next,$l2);
return $l1;
}else{
$l2->next = $this->mergeTwoLists($l1,$l2->next);
return $l2;
}
}
function mergeTwoLists($l1, $l2) {
if(empty($l1)) return $l2;
if(empty($l2)) return $l1;
if($l1->val<= $l2->val) {
$l1->next = $this->mergeTwoLists($l1->next,$l2);
return $l1;
}else{
$l2->next = $this->mergeTwoLists($l1,$l2->next);
return $l2;
}
}
判断环形链表
题目
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
代码
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
如何一次遍历就找到链表中间位置节点
题目
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
如果有两个中间结点,则返回第二个中间结点。
代码
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
public:
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
删除排序链表中的重复元素
题目
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
思路
前后指针比较值,相同则删除
代码
function deleteDuplicates($head) {
$cur = $head;
while($cur != null && $cur->next != null){
if($cur->val == $cur->next->val){
$cur->next = $cur->next->next;
}else{
$cur = $cur->next;
}
}
return $head;
}
$cur = $head;
while($cur != null && $cur->next != null){
if($cur->val == $cur->next->val){
$cur->next = $cur->next->next;
}else{
$cur = $cur->next;
}
}
return $head;
}
单链表中倒数第 k 个节点
题目
子主题
代码
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast = head;
ListNode* low = head;
while (fast != NULL) {
fast = fast->next;
if (k == 0) {
low = low->next;
} else {
k--;
}
}
return low;
}
};
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* fast = head;
ListNode* low = head;
while (fast != NULL) {
fast = fast->next;
if (k == 0) {
low = low->next;
} else {
k--;
}
}
return low;
}
};
树
重建二叉树
题解
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
代码实现
function reConstructBinaryTree($pre, $vin)
{
// write code here
if($pre && $vin){
$treeRoot = new TreeNode($pre[0]);
$index = array_search($pre[0],$vin);
$treeRoot->left = reConstructBinaryTree(array_slice($pre,1,$index), array_slice($vin,0,$index));
$treeRoot->right = reConstructBinaryTree(array_slice($pre,$index + 1), array_slice($vin,$index + 1));
return $treeRoot;
}
}
{
// write code here
if($pre && $vin){
$treeRoot = new TreeNode($pre[0]);
$index = array_search($pre[0],$vin);
$treeRoot->left = reConstructBinaryTree(array_slice($pre,1,$index), array_slice($vin,0,$index));
$treeRoot->right = reConstructBinaryTree(array_slice($pre,$index + 1), array_slice($vin,$index + 1));
return $treeRoot;
}
}
栈
先入后出
函数
array_push
理解
向数组尾部插入 "blue" 和 "yellow"
举例
array_push($arr,$root)
array_pop
理解
删除数组中的最后一个元素,返回最后一个元素
队列
先入先出
函数
array_shift
理解
删除数组中的第一个元素,并返回第一个元素
二叉树深度
题解
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
代码
function TreeDepth($pRoot)
{
// write code here
if($pRoot == null)
return 0;
$l = TreeDepth($pRoot->left);
$r = TreeDepth($pRoot->right);
return $l > $r ? $l+1 : $r +1;
}
{
// write code here
if($pRoot == null)
return 0;
$l = TreeDepth($pRoot->left);
$r = TreeDepth($pRoot->right);
return $l > $r ? $l+1 : $r +1;
}
二叉树最大宽度
题目
子主题
二叉树路径总和
代表题目
子主题
二叉树的最近公共祖先
题目
理解
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
代码
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == NULL)
return NULL;
return bfs(root,p,q);
}
TreeNode* bfs(TreeNode* root, TreeNode* p, TreeNode* q) {
// 边界条件
if(root == NULL) return NULL;
if(root == p) return p;
if(root == q) return q;
TreeNode* l = bfs(root -> left, p, q); // 向左查
TreeNode* r = bfs(root -> right, p, q); // 向右查
// 当前子树的最近公共祖先判断
if(l != NULL && r != NULL) return root;
else if(l != NULL) return l;
else return r;
}
if(root == NULL)
return NULL;
return bfs(root,p,q);
}
TreeNode* bfs(TreeNode* root, TreeNode* p, TreeNode* q) {
// 边界条件
if(root == NULL) return NULL;
if(root == p) return p;
if(root == q) return q;
TreeNode* l = bfs(root -> left, p, q); // 向左查
TreeNode* r = bfs(root -> right, p, q); // 向右查
// 当前子树的最近公共祖先判断
if(l != NULL && r != NULL) return root;
else if(l != NULL) return l;
else return r;
}
二叉搜索树的最近公共祖先
题目
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
代码
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//如果是分布在两边,则为root,如果都在左边,遍历左边,如果都在右边,遍历右边
if(root->val > p->val && root->val > q->val){
root = lowestCommonAncestor(root->left, p, q);
}else if(root->val < p->val && root->val < q->val){
root = lowestCommonAncestor(root->right, p, q);
}
return root;
}
//如果是分布在两边,则为root,如果都在左边,遍历左边,如果都在右边,遍历右边
if(root->val > p->val && root->val > q->val){
root = lowestCommonAncestor(root->left, p, q);
}else if(root->val < p->val && root->val < q->val){
root = lowestCommonAncestor(root->right, p, q);
}
return root;
}
'二叉树的镜像
题目
请完成一个函数,输入一个二叉树,该函数输出它的镜像
代码
function mirrorTree($root) {
if($root == null){
return [];
}
$temp = new TreeNode(0);
$temp = $root->left;
$root->left = $root->right;
$root->right = $temp;
$this->mirrorTree($root->left);
$this->mirrorTree($root->right);
return $root;
}
}
if($root == null){
return [];
}
$temp = new TreeNode(0);
$temp = $root->left;
$root->left = $root->right;
$root->right = $temp;
$this->mirrorTree($root->left);
$this->mirrorTree($root->right);
return $root;
}
}
二叉树的直径
题目
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。
代码
class Solution {
/**
* @param TreeNode $root
* @return Integer
*/
function diameterOfBinaryTree($root) {
$max = 0;
if ($root == null) return $max;
$this->deep($root, $max);
return $max;
}
public function deep($node, &$max)
{
if ($node == null) return 0;
$left = $this->deep($node->left, $max);
$right = $this->deep($node->right, $max);
$max = max($max, $left + $right);
return max($left, $right) + 1;
}
}
/**
* @param TreeNode $root
* @return Integer
*/
function diameterOfBinaryTree($root) {
$max = 0;
if ($root == null) return $max;
$this->deep($root, $max);
return $max;
}
public function deep($node, &$max)
{
if ($node == null) return 0;
$left = $this->deep($node->left, $max);
$right = $this->deep($node->right, $max);
$max = max($max, $left + $right);
return max($left, $right) + 1;
}
}
二叉树路径和
合并二叉树
题目
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
代码
function mergeTrees($t1, $t2) {
if($t1 ==null){
return $t2;
}
if($t2 ==null){
return $t1;
}
$t1->val += $t2->val;
$t1->left = $this->mergeTrees($t1->left, $t2->left);
$t1->right = $this->mergeTrees($t1->right, $t2->right);
return $t1;
}
if($t1 ==null){
return $t2;
}
if($t2 ==null){
return $t1;
}
$t1->val += $t2->val;
$t1->left = $this->mergeTrees($t1->left, $t2->left);
$t1->right = $this->mergeTrees($t1->right, $t2->right);
return $t1;
}
反转二叉树
理解
子主题
对称二叉树
代码
class Solution {
/**
* @param TreeNode $root
* @return Boolean
*/
function isSymmetric($root) {
return $this->isMirror($root,$root);
}
function isMirror($t1,$t2){
if($t1==null&&$t2==null){
return true;
}
if($t1==null||$t2==null){
return false;
}
return ($t1->val==$t2->val)&&
$this->isMirror($t1->right,$t2->left)&&
$this->isMirror($t1->left,$t2->right);
}
}
/**
* @param TreeNode $root
* @return Boolean
*/
function isSymmetric($root) {
return $this->isMirror($root,$root);
}
function isMirror($t1,$t2){
if($t1==null&&$t2==null){
return true;
}
if($t1==null||$t2==null){
return false;
}
return ($t1->val==$t2->val)&&
$this->isMirror($t1->right,$t2->left)&&
$this->isMirror($t1->left,$t2->right);
}
}
相同的树
代码
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null && q==null) return true;
if(p==null || q==null) return false;
return (p.val==q.val) && isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
if(p==null && q==null) return true;
if(p==null || q==null) return false;
return (p.val==q.val) && isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
平衡二叉树
理解
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
题目
给定一个二叉树,判断它是否是高度平衡的二叉树。
代码
class Solution {
/**
* @param TreeNode $root
* @return Boolean
*/
function isBalanced($root) {
if($root == null){
return true;
}
$left = $this->gethight($root->left);
$right = $this->gethight($root->right);
if(abs($left-$right) > 1){
return false;
}
return $this->isBalanced($root->left) && $this->isBalanced($root->right);
}
function gethight($root){
if($root ==null){
return 0;
}
return max($this->gethight($root->left),$this->gethight($root->right))+1;
}
}
/**
* @param TreeNode $root
* @return Boolean
*/
function isBalanced($root) {
if($root == null){
return true;
}
$left = $this->gethight($root->left);
$right = $this->gethight($root->right);
if(abs($left-$right) > 1){
return false;
}
return $this->isBalanced($root->left) && $this->isBalanced($root->right);
}
function gethight($root){
if($root ==null){
return 0;
}
return max($this->gethight($root->left),$this->gethight($root->right))+1;
}
}
二叉搜索树
理解
也叫二叉排序树,简称为BST。它具有这样的性质:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。(仔细观察可以发现,二叉搜索树的中序遍历可以得到一个有序数组)。
题目
二叉搜索树中的搜索
思路
假设从节点root开始查找value,如果value == root->val,则找到结果;如果value < root->val,则移动到root->left进行查找;否则移动到root->right进行查找。如果查找完整棵树还没有发现,就返回空。
代码
function searchBST($root, $val) {
while($root != null && $root->val != $val){
$root = $root->val > $val ? $root->left : $root->right;
}
return $root;
}
while($root != null && $root->val != $val){
$root = $root->val > $val ? $root->left : $root->right;
}
return $root;
}
判断是否是二叉搜索树
思路:利用中序遍历,先将左子树押入栈,然后依次取出进行判断
代码
function isValidBST($root) {
if($root == null){
return true;
}
$inorder = PHP_INT_MIN;
$stack = [];
while(!empty($stack) || $root != null){
while($root != null){
array_push($stack, $root);
$root = $root->left;
}
$root = array_pop($stack);
if($root->val <= $inorder){
return false;
}
$inorder = $root->val;
$root = $root->right;
}
return true;
}
if($root == null){
return true;
}
$inorder = PHP_INT_MIN;
$stack = [];
while(!empty($stack) || $root != null){
while($root != null){
array_push($stack, $root);
$root = $root->left;
}
$root = array_pop($stack);
if($root->val <= $inorder){
return false;
}
$inorder = $root->val;
$root = $root->right;
}
return true;
}
二叉树最大路径和
题目
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
代码
class Solution {
/**
* @param TreeNode $root
* @return Integer
*/
public $max = 0;
function maxPathSum($root) {
if(empty($root)){
return 0;
}
$this->max = $root->val;
$this->maxPath($root);
return $this->max;
}
function maxPath($node){
if(empty($node)){
return 0;
}
$left = $this->maxPath($node->left);
$right = $this->maxPath($node->right);
$all = $node->val + $left + $right;
$half = $node->val + max(0, $left, $right);
$this->max = max($this->max, $all, $half);
return $half;
}
}
/**
* @param TreeNode $root
* @return Integer
*/
public $max = 0;
function maxPathSum($root) {
if(empty($root)){
return 0;
}
$this->max = $root->val;
$this->maxPath($root);
return $this->max;
}
function maxPath($node){
if(empty($node)){
return 0;
}
$left = $this->maxPath($node->left);
$right = $this->maxPath($node->right);
$all = $node->val + $left + $right;
$half = $node->val + max(0, $left, $right);
$this->max = max($this->max, $all, $half);
return $half;
}
}
二叉搜索树第k大的数
题目
给定一棵二叉搜索树,请找出其中第k大的节点。
代码
function kthLargest($root, $k) {
$stack = [];
$count = 1;
while(!empty($root) || !empty($stack)){
while($root != null){
array_push($stack, $root);
$root = $root->right;
}
$node = array_pop($stack);
if($count == $k){
return $node->val;
}
$count ++;
$root = $node->left;
}
return 0;
}
$stack = [];
$count = 1;
while(!empty($root) || !empty($stack)){
while($root != null){
array_push($stack, $root);
$root = $root->right;
}
$node = array_pop($stack);
if($count == $k){
return $node->val;
}
$count ++;
$root = $node->left;
}
return 0;
}
完全二叉树
理解
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
应用
堆排序
满二叉树
理解
高度为h,由2^h-1个节点构成的二叉树称为满二叉树。
深度搜索
理解
从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。利用数据结构“栈”,父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点。
题目
先序遍历
思路
压入根结点,出根结点,压入右子树,压入左子树
递归
代码
function preOrder($tree){
echo($tree->data);
if($tree->left!=null){
preOrder($tree->left);
}
if($tree->right!=null){
preOrder($tree->right);
}
}
echo($tree->data);
if($tree->left!=null){
preOrder($tree->left);
}
if($tree->right!=null){
preOrder($tree->right);
}
}
非递归
代码
function preorder($root){
9 $stack=array();
10 array_push($stack,$root);
11 while(!empty($stack)){
12 $center_node=array_pop($stack);
13 echo $center_node->value.' ';//先输出根节点
14 if($center_node->right!=null){
15 array_push($stack,$center_node->right);//压入右子树
16 }
17 if($center_node->left!=null){
18 array_push($stack,$center_node->left);//再压左子树
19 }
20 }
21 }
9 $stack=array();
10 array_push($stack,$root);
11 while(!empty($stack)){
12 $center_node=array_pop($stack);
13 echo $center_node->value.' ';//先输出根节点
14 if($center_node->right!=null){
15 array_push($stack,$center_node->right);//压入右子树
16 }
17 if($center_node->left!=null){
18 array_push($stack,$center_node->left);//再压左子树
19 }
20 }
21 }
中序遍历
非递归
思路
子主题
代码
function inorderTraversal($root) {
if($root == null){
return [];
}
$stack = [];
$res = [];
while($root != null || !empty($stack)){
while($root != null){
array_push($stack, $root);
$root = $root->left;
}
$root = array_pop($stack);
$res[] = $root->val;
$root = $root->right;
}
return $res;
}
if($root == null){
return [];
}
$stack = [];
$res = [];
while($root != null || !empty($stack)){
while($root != null){
array_push($stack, $root);
$root = $root->left;
}
$root = array_pop($stack);
$res[] = $root->val;
$root = $root->right;
}
return $res;
}
后序遍历
非递归
代码
function tailorder($root){
$stack = array();
$outstack = array();
array_push($stack, $root);
while($empty($stack)){
$center_node = array_pop($stack);
array_push($outstack, $center_node);
if($center_node->right != null)
array_push($stack, $center_node->right);
if($center_node->left != null)
array_push($stack, $center_node->left);
}
while($empty($outstack)){
$center_node = array_pop($outstack);
echo $center_node->value;
}
}
$stack = array();
$outstack = array();
array_push($stack, $root);
while($empty($stack)){
$center_node = array_pop($stack);
array_push($outstack, $center_node);
if($center_node->right != null)
array_push($stack, $center_node->right);
if($center_node->left != null)
array_push($stack, $center_node->left);
}
while($empty($outstack)){
$center_node = array_pop($outstack);
echo $center_node->value;
}
}
层次遍历
非递归
代码
function levelOrder($root) {
if($root == null){
return [];
}
$queue = [];
$res = [];
array_push($queue, $root);
while(!empty($queue)){
$tmp = [];
$num = count($queue);
while($num--){
$node = array_shift($queue);
$tmp[] = $node->val;
if($node->left != null)
array_push($queue, $node->left);
if($node->right != null)
array_push($queue, $node->right);
}
$res[] = $tmp;
}
return $res;
}
if($root == null){
return [];
}
$queue = [];
$res = [];
array_push($queue, $root);
while(!empty($queue)){
$tmp = [];
$num = count($queue);
while($num--){
$node = array_shift($queue);
$tmp[] = $node->val;
if($node->left != null)
array_push($queue, $node->left);
if($node->right != null)
array_push($queue, $node->right);
}
$res[] = $tmp;
}
return $res;
}
广度搜索
理解
从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。利用数据结构“栈”,父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点。
题目
层次遍历
class Solution {
/**
* @param TreeNode $root
* @return Integer[][]
*/
function levelOrder($root) {
if($root == null){
return [];
}
$queue = [];
$res = [];
array_push($queue, $root);
while(!empty($queue)){
$tmp = [];
$num = count($queue);
while($num--){
$node = array_shift($queue);
$tmp[] = $node->val;
if($node->left != null)
array_push($queue, $node->left);
if($node->right != null)
array_push($queue, $node->right);
}
$res[] = $tmp;
}
return $res;
}
}
/**
* @param TreeNode $root
* @return Integer[][]
*/
function levelOrder($root) {
if($root == null){
return [];
}
$queue = [];
$res = [];
array_push($queue, $root);
while(!empty($queue)){
$tmp = [];
$num = count($queue);
while($num--){
$node = array_shift($queue);
$tmp[] = $node->val;
if($node->left != null)
array_push($queue, $node->left);
if($node->right != null)
array_push($queue, $node->right);
}
$res[] = $tmp;
}
return $res;
}
}
将有序数组转换为二叉搜索树
代码
class Solution {
/**
* @param Integer[] $nums
* @return TreeNode
*/
public $nums = [];
function sortedArrayToBST($nums) {
$this->nums = $nums;
return $this->helper(0, count($nums)-1);
}
function helper($left, $right){
if($left > $right){
return null;
}
$mid = ($right + $left) / 2;
$mid = ceil($mid);
$node = new TreeNode($this->nums[$mid]);
$node->left = $this->helper($left, $mid-1);
$node->right = $this->helper($mid+1, $right);
return $node;
}
}
/**
* @param Integer[] $nums
* @return TreeNode
*/
public $nums = [];
function sortedArrayToBST($nums) {
$this->nums = $nums;
return $this->helper(0, count($nums)-1);
}
function helper($left, $right){
if($left > $right){
return null;
}
$mid = ($right + $left) / 2;
$mid = ceil($mid);
$node = new TreeNode($this->nums[$mid]);
$node->left = $this->helper($left, $mid-1);
$node->right = $this->helper($mid+1, $right);
return $node;
}
}
动态规划
理解
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
二叉树中的最大路径和
爬楼梯
function climbStairs1($n)
{
if ($n == 1) {
return 1;
}
if ($n == 2) {
return 2;
}
return climbStairs1($n - 1) + climbStairs1($n - 2);
}
{
if ($n == 1) {
return 1;
}
if ($n == 2) {
return 2;
}
return climbStairs1($n - 1) + climbStairs1($n - 2);
}
代码
function climbStairs($n) {
$prepre = 1;
$pre = 2;
$temp = 3;
if($n ==1 ){
return 1;
}
if($n ==2 ){
return 2;
}
for($i = 3 ; $i <= $n; $i ++){
$temp = $prepre + $pre;
$prepre = $pre;
$pre = $temp;
}
return $temp;
}
$prepre = 1;
$pre = 2;
$temp = 3;
if($n ==1 ){
return 1;
}
if($n ==2 ){
return 2;
}
for($i = 3 ; $i <= $n; $i ++){
$temp = $prepre + $pre;
$prepre = $pre;
$pre = $temp;
}
return $temp;
}
最大子序列和
题目
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和
代码
function maxSubArray($nums) {
for($i = 1; $i< count($nums); $i++){
$nums[$i] = max($nums[$i], $nums[$i] + $nums[$i-1]);
}
return max($nums);
}
for($i = 1; $i< count($nums); $i++){
$nums[$i] = max($nums[$i], $nums[$i] + $nums[$i-1]);
}
return max($nums);
}
买卖股票的最佳时机
题目
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
代码
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
// 0:用户手上不持股所能获得的最大利润,特指卖出股票以后的不持股,非指没有进行过任何交易的不持股
// 1:用户手上持股所能获得的最大利润
// 注意:因为题目限制只能交易一次,因此状态只可能从 1 到 0,不可能从 0 到 1
// 状态转移方程:
// dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
// dp[i][1] = max(dp[i - 1][1], -prices[i])
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
return dp[len - 1][0];
}
int len = prices.length;
if (len < 2) {
return 0;
}
// 0:用户手上不持股所能获得的最大利润,特指卖出股票以后的不持股,非指没有进行过任何交易的不持股
// 1:用户手上持股所能获得的最大利润
// 注意:因为题目限制只能交易一次,因此状态只可能从 1 到 0,不可能从 0 到 1
// 状态转移方程:
// dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
// dp[i][1] = max(dp[i - 1][1], -prices[i])
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
return dp[len - 1][0];
}
function maxProfit($prices) {
$max = 0;
$min = $prices[0];
for($i = 0 ; $i< count($prices); $i ++ ){
$min = min($min, $prices[$i]);
$max = max($max, ($prices[$i] - $min));
}
return $max;
}
$max = 0;
$min = $prices[0];
for($i = 0 ; $i< count($prices); $i ++ ){
$min = min($min, $prices[$i]);
$max = max($max, ($prices[$i] - $min));
}
return $max;
}
买卖股票的最佳时机 II
function maxProfit($prices) {
$count = count($prices);
$maxProfit = 0;
for($i = 1; $i < $count; $i ++ ){
$diff = $prices[$i] - $prices[$i-1];
if($diff > 0){
$maxProfit += $diff;
}
}
return $maxProfit;
}
$count = count($prices);
$maxProfit = 0;
for($i = 1; $i < $count; $i ++ ){
$diff = $prices[$i] - $prices[$i-1];
if($diff > 0){
$maxProfit += $diff;
}
}
return $maxProfit;
}
和为s的连续正数序列
贪心算法
理解
建立数学模型来描述问题;
把求解的问题分成若干个子问题;
对每一子问题求解,得到子问题的局部最优解;
把子问题的解局部最优解合成原来解问题的一个解。
把求解的问题分成若干个子问题;
对每一子问题求解,得到子问题的局部最优解;
把子问题的解局部最优解合成原来解问题的一个解。
分治法
理解
1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3 合并:将各个子问题的解合并为原问题的解。
2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3 合并:将各个子问题的解合并为原问题的解。
回溯法
理解
把问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解
动态规划
理解
能将大问题拆成几个小问题,且满足无后效性、最优子结构性质。
打家劫舍
深度优先搜索
理解
有点类似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念
特征
如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
广度优先搜索
步骤
(1)从队列头取出一个结点,检查它按照扩展规则是否能够扩展,如果能则产生一个新结点。
(2)检查新生成的结点,看它是否已在队列中存在,如果新结点已经在队列中出现过,就放弃这个结点,然后回到第(1)步。否则,如果新结点未曾在队列中出现过,则将它加入到队列尾。
(3)检查新结点是否目标结点。如果新结点是目标结点,则搜索成功,程序结束;若新结点不是目标结点,则回到第(1)步,再从队列头取出结点进行扩展。
(2)检查新生成的结点,看它是否已在队列中存在,如果新结点已经在队列中出现过,就放弃这个结点,然后回到第(1)步。否则,如果新结点未曾在队列中出现过,则将它加入到队列尾。
(3)检查新结点是否目标结点。如果新结点是目标结点,则搜索成功,程序结束;若新结点不是目标结点,则回到第(1)步,再从队列头取出结点进行扩展。
适用
BFS可用于解决2类问题:
从A出发是否存在到达B的路径;
从A出发到达B的最短路径(这个应该叫最少步骤合理);
从A出发是否存在到达B的路径;
从A出发到达B的最短路径(这个应该叫最少步骤合理);
栈
有效的括号
题目
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
代码
class Solution {
/**
* @param String $s
* @return Boolean
*/
function isValid($s) {
//构造映射map
$map = [')' => '(', ']' => '[', '}' => '{'];
$stack = [];
for($i = 0; $i < strlen($s); $i++){
if(isset($map[$s[$i]])) {
$key = array_pop($stack);
if($map[$s[$i]] != $key){
return false;
}
}else{
array_push($stack,$s[$i]);
}
}
return $stack ? false : true;
}
}
/**
* @param String $s
* @return Boolean
*/
function isValid($s) {
//构造映射map
$map = [')' => '(', ']' => '[', '}' => '{'];
$stack = [];
for($i = 0; $i < strlen($s); $i++){
if(isset($map[$s[$i]])) {
$key = array_pop($stack);
if($map[$s[$i]] != $key){
return false;
}
}else{
array_push($stack,$s[$i]);
}
}
return $stack ? false : true;
}
}
题解
子主题
LRU缓存机制
理解
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
代码
class LRUCache {
/**
* @param Integer $capacity
*/
function __construct($capacity) {
$map = [];
$list = [];
$this->capacity = $capacity;
}
/**
* @param Integer $key
* @return Integer
*/
function get($key) {
if(!array_key_exists( $key, $this->map)){
return -1;
}else{
unset($this->list[array_search($key, $this->list)]);
array_push($this->list, $key);
return $this->map[$key];
}
}
/**
* @param Integer $key
* @param Integer $value
* @return NULL
*/
function put($key, $value) {
if(array_key_exists($key, $this->map)){
unset($this->list[array_search($key, $this->list)]);
}else{
if(count($this->map) == $this->capacity){
$firstkey = array_shift($this->list);
unset($this->map[$firstkey]);
}
}
$this->map[$key] = $value;
array_push($this->list, $key);
}
}
/**
* @param Integer $capacity
*/
function __construct($capacity) {
$map = [];
$list = [];
$this->capacity = $capacity;
}
/**
* @param Integer $key
* @return Integer
*/
function get($key) {
if(!array_key_exists( $key, $this->map)){
return -1;
}else{
unset($this->list[array_search($key, $this->list)]);
array_push($this->list, $key);
return $this->map[$key];
}
}
/**
* @param Integer $key
* @param Integer $value
* @return NULL
*/
function put($key, $value) {
if(array_key_exists($key, $this->map)){
unset($this->list[array_search($key, $this->list)]);
}else{
if(count($this->map) == $this->capacity){
$firstkey = array_shift($this->list);
unset($this->map[$firstkey]);
}
}
$this->map[$key] = $value;
array_push($this->list, $key);
}
}
LFU算法
理解
LFU(Least Frequently Used ,最近最少使用算法)也是一种常见的缓存算法。
顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
最小栈
题目
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) -- 将元素 x 推入栈中。
pop() -- 删除栈顶的元素。
top() -- 获取栈顶元素。
getMin() -- 检索栈中的最小元素。
示例:
push(x) -- 将元素 x 推入栈中。
pop() -- 删除栈顶的元素。
top() -- 获取栈顶元素。
getMin() -- 检索栈中的最小元素。
示例:
代码
class MinStack {
/**
* initialize your data structure here.
*/
public $stack = [];
public $helper = [];
function __construct() {
}
/**
* @param Integer $x
* @return NULL
*/
function push($x) {
array_push($this->stack,$x);
if(count($this->helper) == 0 || $x <= end($this->helper)){
array_push($this->helper,$x);
}
}
/**
* @return NULL
*/
function pop() {
if(!empty($this->stack)){
$key = array_pop($this->stack);
}
if(isset($key) && count($this->helper) && $key == end($this->helper)){
array_pop($this->helper);
}
}
/**
* @return Integer
*/
function top() {
if(count($this->stack) == 0){
return null;
}
return end($this->stack);
}
/**
* @return Integer
*/
function getMin() {
if(count($this->helper) == 0){
return null;
}
return end($this->helper);
}
}
/**
* Your MinStack object will be instantiated and called as such:
* $obj = MinStack();
* $obj->push($x);
* $obj->pop();
* $ret_3 = $obj->top();
* $ret_4 = $obj->getMin();
*/
/**
* initialize your data structure here.
*/
public $stack = [];
public $helper = [];
function __construct() {
}
/**
* @param Integer $x
* @return NULL
*/
function push($x) {
array_push($this->stack,$x);
if(count($this->helper) == 0 || $x <= end($this->helper)){
array_push($this->helper,$x);
}
}
/**
* @return NULL
*/
function pop() {
if(!empty($this->stack)){
$key = array_pop($this->stack);
}
if(isset($key) && count($this->helper) && $key == end($this->helper)){
array_pop($this->helper);
}
}
/**
* @return Integer
*/
function top() {
if(count($this->stack) == 0){
return null;
}
return end($this->stack);
}
/**
* @return Integer
*/
function getMin() {
if(count($this->helper) == 0){
return null;
}
return end($this->helper);
}
}
/**
* Your MinStack object will be instantiated and called as such:
* $obj = MinStack();
* $obj->push($x);
* $obj->pop();
* $ret_3 = $obj->top();
* $ret_4 = $obj->getMin();
*/
字符串
验证回文串
理解
回文串是一个正读和反读都一样的字符串
解题思路
设置头,尾双指针,指针向中间遍历判断字符串。跳过非数字和字母字符,满足数字或字母的字符,统一转化为小写,判断两个字符值是否相等
代码
class Solution {
/**
* 双指针解法
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param String $s
* @return Boolean
*/
function isPalindrome($s)
{
$i = 0;
$j = strlen($s) - 1;
while($i < $j) {
// 非数字和字符串跳过
while ($i < $j && !ctype_alnum($s[$i])) $i++;
while ($i < $j && !ctype_alnum($s[$j])) $j--;
if (strtolower($s[$i]) != strtolower($s[$j])) return false;
$i++;
$j--;
}
return true;
}
}
/**
* 双指针解法
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param String $s
* @return Boolean
*/
function isPalindrome($s)
{
$i = 0;
$j = strlen($s) - 1;
while($i < $j) {
// 非数字和字符串跳过
while ($i < $j && !ctype_alnum($s[$i])) $i++;
while ($i < $j && !ctype_alnum($s[$j])) $j--;
if (strtolower($s[$i]) != strtolower($s[$j])) return false;
$i++;
$j--;
}
return true;
}
}
代码
function isPalindrome($x) {
if($x !== ''){
$x = (string)$x;
$x_length = strlen($x);
if($x_length == '1') return true;
for($i = 0; $i < $x_length/2; $i++){
if($x[$i] != $x[$x_length - $i - 1]) return false;
}
return true;
}
return false;
}
if($x !== ''){
$x = (string)$x;
$x_length = strlen($x);
if($x_length == '1') return true;
for($i = 0; $i < $x_length/2; $i++){
if($x[$i] != $x[$x_length - $i - 1]) return false;
}
return true;
}
return false;
}
ctype_alnum
判断是否是字母和数字或字母数字的组合
反转字符串
输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]
输出:["o","l","l","e","h"]
解题思路
双指针,首尾同时扫描并交换
代码
class Solution {
/**
* @param String[] $s
* @return NULL
*/
function reverseString(&$s) {
$front = 0;
$rear = count($s) - 1;
while ($front < $rear) {
$tmp = $s[$front];
$s[$front++] = $s[$rear];
$s[$rear--] = $tmp;
}
}
}
/**
* @param String[] $s
* @return NULL
*/
function reverseString(&$s) {
$front = 0;
$rear = count($s) - 1;
while ($front < $rear) {
$tmp = $s[$front];
$s[$front++] = $s[$rear];
$s[$rear--] = $tmp;
}
}
}
替换空格
题解
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
代码(用函数)
function replaceSpace($str)
{
return $r = str_replace(' ','%20',$str);
}
{
return $r = str_replace(' ','%20',$str);
}
字符串相加
题目
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。
注意:
num1 和num2 的长度都小于 5100.
num1 和num2 都只包含数字 0-9.
num1 和num2 都不包含任何前导零。
你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式。
注意:
num1 和num2 的长度都小于 5100.
num1 和num2 都只包含数字 0-9.
num1 和num2 都不包含任何前导零。
你不能使用任何內建 BigInteger 库, 也不能直接将输入的字符串转换为整数形式。
思路
双指针法
代码
function addStrings($num1, $num2) {
$p1 = strlen($num1) - 1;
$p2 = strlen($num2) - 1;
$carry = 0;
$res = '';
while ($p1 >= 0 OR $p2 >= 0) {
$n1 = $p1 >= 0 ? $num1[$p1] : 0;
$n2 = $p2 >= 0 ? $num2[$p2] : 0;
$sum = $n1 + $n2 + $carry;
$carry = intval($sum / 10);
$res = ($sum % 10) . $res;
$p1--;
$p2--;
}
if ($carry > 0) {
$res = $carry . $res;
}
return $res;
}
}
$p1 = strlen($num1) - 1;
$p2 = strlen($num2) - 1;
$carry = 0;
$res = '';
while ($p1 >= 0 OR $p2 >= 0) {
$n1 = $p1 >= 0 ? $num1[$p1] : 0;
$n2 = $p2 >= 0 ? $num2[$p2] : 0;
$sum = $n1 + $n2 + $carry;
$carry = intval($sum / 10);
$res = ($sum % 10) . $res;
$p1--;
$p2--;
}
if ($carry > 0) {
$res = $carry . $res;
}
return $res;
}
}
x 的平方根
题目
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
代码
class Solution {
/**
* @param Integer $x
* @return Integer
*/
function mySqrt($x) {
if($x <= 1){
return $x;
}
$left = 1;
$right = $x;
while($left < $right){
$mid = $left+ (int)(($right- $left)/2) +1 ;
$num = $mid*$mid;
if($num > $x){
$right = $mid-1;
}else{
$left = $mid;
}
}
return $right;
}
}
/**
* @param Integer $x
* @return Integer
*/
function mySqrt($x) {
if($x <= 1){
return $x;
}
$left = 1;
$right = $x;
while($left < $right){
$mid = $left+ (int)(($right- $left)/2) +1 ;
$num = $mid*$mid;
if($num > $x){
$right = $mid-1;
}else{
$left = $mid;
}
}
return $right;
}
}
实现 strStr() 函数
题目
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
思路
双指针法
代码
function strStr($haystack, $needle) {
$i=0;
$j=0;
while(isset($haystack[$i]) && isset($needle[$j])){
if($haystack[$i] == $needle[$j]){
$i++;
$j++;
}else{
$i = $i - $j + 1;
$j = 0;
}
}
if($j==strlen($needle)){
return $i-$j;
}
return -1;
}
$i=0;
$j=0;
while(isset($haystack[$i]) && isset($needle[$j])){
if($haystack[$i] == $needle[$j]){
$i++;
$j++;
}else{
$i = $i - $j + 1;
$j = 0;
}
}
if($j==strlen($needle)){
return $i-$j;
}
return -1;
}
最长公共前缀
题目
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
如果不存在公共前缀,返回空字符串 ""。
代码
class Solution {
/**
* @param String[] $strs
* @return String
*/
function longestCommonPrefix($strs) {
$count = count($strs);
$common = $strs[0];
if(empty($strs)){
return '';
}
for($i = 1 ; $i < $count; $i ++ ){
for($j = 0 ; $j< strlen($common) && $j< strlen($strs[$i]); $j ++){
if(substr($common, $j, 1) != substr($strs[$i], $j, 1)){
break;
}
}
$common = substr($common, 0, $j);
if($common == ''){
return '';
}
}
return $common;
}
}
/**
* @param String[] $strs
* @return String
*/
function longestCommonPrefix($strs) {
$count = count($strs);
$common = $strs[0];
if(empty($strs)){
return '';
}
for($i = 1 ; $i < $count; $i ++ ){
for($j = 0 ; $j< strlen($common) && $j< strlen($strs[$i]); $j ++){
if(substr($common, $j, 1) != substr($strs[$i], $j, 1)){
break;
}
}
$common = substr($common, 0, $j);
if($common == ''){
return '';
}
}
return $common;
}
}
平方数和
题目
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。
代码
子主题
位运算
运算符
&
两个位都为1时,结果才为1
|
两个位都为0时,结果才为0
^
异或
两个位相同为0,相异为1
微服务
理解
把一个大的系统拆分成多个独立的系统,系统之间可以通过接口访问数据,每个系统独立部署服务器,自己的数据库,日志,监控等,比如电商系统拆分成订单、支付、商品、上层业务等独立系统
开源框架
spring cloud
dubbo
grpc
thrift
服务通信
同步请求
定义
各系统之间由进程内的通信,变为进程之间的通信,最常用的通信方式就是网络调用
远程网络调用工具
RPC
理解
RPC是只远程过程调用,也就是说两台服务器A,B, 一个应用部署在A服务器上,另一个应用部署在B服务器上,A服务器上的应用想要调用B服务器上的应用提供的方法/函数,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语意和传递调用的参数。
异步请求
工具
kafka
定义
Kafka是分布式发布-订阅消息系统,它最初是由LinkedIn公司开发的,之后成为Apache项目的一部分,Kafka是一个分布式,可划分的,冗余备份的持久性的日志服务,它主要用于处理流式数据
发布订阅模式理解
在发布-订阅消息系统中,消息被持久化到一个topic中。与点对点消息系统不同的是,消费者可以订阅一个或多个topic,消费者可以消费该topic中所有的数据,同一条数据可以被多个消费者消费,数据被消费后不会立马删除。在发布-订阅消息系统中,消息的生产者称为发布者,消费者称为订阅者。
点对点模式理解
将有一个或多个消费者消费队列中的数据。但是一条消息只能被消费一次。当一个消费者消费了队列中的某条数据之后,该条数据则从消息队列中删除。
应用场景
日志收集系统和消息系统。
解耦
峰值处理能力
异步通信
顺序保证
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。
Kafka性能优秀原因
1.kafka中的message并不是保存在内存中的,而是保存在了磁盘上,唯一的区别是他运用了顺序写,而并非采用随机写,顺序写的速度在600MB/S,随机写的速度在100KB/S,这个性能的提升的效果明显的,顺序写的效率并不比内存写差,甚至合理运用效率更高
2.kafka通过sendfile命令,减少了数据拷贝,数据的拷贝基本全在内存中完成。(原先是将数据从硬盘读到内核区的pageCache,然后用户进程copy到用户区,用户区在吧数据写进socket中)现在是省掉了用户copy数据这一步,直接让内核区的数据写入socket中
3.批量生产,批量消费
3. 利用Page Cache
Partion 模型带来的高吞吐
Topic 下的 Partion 概念,可以横向扩展,部署到多台服务器上。通过增加 Partion 数量和对应 Consumer Group 中 Consumer 的数量,来提升系统的吞吐量。
Kafka Consumer Rebalance
组成
producer
broker
理解
broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。
consumer
理解
消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。
topic
理解
每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
Partition
理解
topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。partition中的数据是有序的,不同partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
构成
LogSegment
理解
每个分区又被划分为多个日志分段(LogSegment)组成,日志段是Kafka日志对象分片的最小单位;LogSegment算是一个逻辑概念,对应一个具体的日志文件(“.log”的数据文件)和两个索引文件(“.index”和“.timeindex”,分别表示偏移量索引文件和消息时间戳索引文件)组成;
命名规则
partition的名称规则为topic名称+有序序号
Consumer Group
理解
每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。
特点
各个consumer可以组成一个组,每个消息只能被组中的一个consumer消费,如果一个消息能被多个consumer消费,那么这些个consumer肯定不在同一个组中
Leader
理解
每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。
Follower
理解
Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
Offset
理解
每个partition中都由一系列有序的、不可变的消息组成,这些消息被顺序地追加到partition中。每个消息都有一个连续的序列号称之为offset—偏移量,用于在partition内唯一标识消息(并不表示消息在磁盘上的物理位置);
segment
理解
partition物理上由多个segment组成,每一个segment存储着多个message信息(默认是:1G),而每一个message是由一个key-value和一个时间戳组成。
组成
1.index索引文件
2.log数据文件
推送模式
push
理解
producer直接将消息推送到下游consumer,由broker决定消息推送的速率
坏处
broker推送速率如何大于consumer消费速率时,consumer恐怕要崩溃了
pull
理解
consumer可以根据自己的消费能力去决定拉取的速率
缺点
如果broker没有可供消费的消息,将导致consumer不断在循环中轮训,直到消息到达
解决办法
子主题
Rebalance
触发机制
1.消费组成员发生了变更,比如有新的消费者加入消费组或者有消费者宕机
2.消费者无法在指定的时间之内完成消息的消费
3.消费组订阅的Topic发生了变化
4.订阅的Topic的partition发生了变化
理解
是kafka协调者把partition分配给conumer-group下每个consumer实例的过程
在执行Reblance期间,Group内的所有consumer无法消费消息,因此频繁的Reblance会降低消费系统的TPS
过程
1. joining the group
理解
所有消费者会和协调者交互,请求协调者加入消费组,协调者会从所有消费者选择一个消费者作为leader consumer,选择的算法是随机选择
2. 同步分配partition
理解
leader consumer从协调者获取所有的消费者信息,并将消费组订阅的partition结果封装,需要注意的是leader consumer不会直接与组内其它消费者交互,leader consumer会将syncgroup发送给协调者,协调者再分配结果给各个consumer
coordinator(协调者)
理解
每个消费组都会有一个协调者,协调者负责管理组内的消费者和位移管理,并不负责组内的partition分配,消费者通过心跳告知协调者自己处于存活状态
作用
coordinator的作用是用来存储group的相关meta信息,并将对应partition的offset信息记录到kafka内置Topic中
选举
groupID
如何存储数据
1.kafka会把收到的message进行load balance,均匀的分布在这个topic下的不同partition上(hash(message)% broke数量),物理上存储,每个partition相对应一个物理目录,文件名是topicname_partition_序号
新增一个partition
partition里面的message不会重新进行分配,原来的partition里面的message不会变,新加的partition刚开始是空的,随后进入这个topic的message就会重新参与所有partition的load_balance
与传统MQ消息系统的三个关键区别
1.kafka持久化日志,这些日志可以被重复读取和无限期保留
子主题
持久化
Kafka提供两种策略去删除旧数据
1.基于时间
2.于partition文件大小
消费规则
理解
一个consumer对一个partition中的一条数据只需要消费一次,每一个consumer组维护一个下标文件,叫做offset,这个offset用于记录当前的consumer组消费数据的下标,每进行消费一条数据,当前的offset就会递增1(offset之前的数据,都表示已经消费过的数据)
相关面试题
什么情况下一个 broker 会从 isr中踢出去
leader会维护一个与其基本保持同步的Replica列表,该列表称为ISR(in-sync Replica),每个Partition都会有一个ISR,而且是由leader动态维护 ,如果一个follower比一个leader落后太多,或者超过一定时间未发起数据复制请求,则leader将其重ISR中移除 。
kafka producer 打数据,ack 为 0, 1, -1 的时候代表啥, 设置 -1 的时候,什么情况下,leader 会认为一条消息 commit了
1(默认) 数据发送到Kafka后,经过leader成功接收消息的的确认,就算是发送成功了。在这种情况下,如果leader宕机了,则会丢失数据。
0 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
-1 producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。当ISR中所有Replica都向Leader发送ACK时,leader才commit,这时候producer才能认为一个请求中的消息都commit了。
0 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
-1 producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。当ISR中所有Replica都向Leader发送ACK时,leader才commit,这时候producer才能认为一个请求中的消息都commit了。
kafka事务
kafka读取数据的查找message的步骤
索引文件中的元数据指向对应数据文件中message的物理偏移地址(也就是实际的偏移地址,因为会涉及到segment文件清理)。其中以.index索引文件中的元数据[3, 348]为例,在.log数据文件表示第3个消息,即在全局partition中offset为170410+3=170413,该消息的物理偏移地址为348
2.
如何保证消息消费的有序性
1.kafka在同一个patition中是追加写,offset读取,写和读都是顺序的
2.多个partition不能保证顺序读,解决办法是不同的key指定写到不同的partition,或者创建topic的时候,创建一个partition
3.多线程读取数据,解决办法
kafka丢包问题解决
1.对kafka进行限速,平滑流量
2.启用重试机制,重试间隔时间设置长一些
3.Kafka设置acks=all,即需要相应的所有处于ISR的分区都确认收到该消息后,才算发送成功。
kafka保证消息不被重复消费
原因
每个消息都有一个offset,kafka消费过消息后,需要提交offset,让消息队列知道自己已经消费过了,由于网络传输等故障,确认消息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其它消费者
解决办法
子主题
redis和kafka区别
1.kafka有确认机制,比redis可靠
2.kafka可以设置分组,redis不行
3.kafka可以通过offset来选择消费哪条,redis不行
4.kafka数据消费完还在,redis没有了
kafka如何知道往哪个topic写数据,消费者如何知道去哪个broke去读
Producer可以通过zookeeper获得topic的broker信息,从而得知需要往哪写数据。
Consumer也从zookeeper上获得该信息,从而得知要监听哪个partition。
Consumer也从zookeeper上获得该信息,从而得知要监听哪个partition。
服务注册与发现
理解
服务消费方如何知道提供方的端口和iP
工具
zookeeper
服务限流
限流算法
计数器
滑动窗口
漏桶
理解
一个固定容量的漏桶,会按照一定速率流出水滴,如果桶中无水,则不流出,可以以任意的速率注入水滴,如果流入的水滴超出了桶容量,则新添加的会被丢弃
适用场景
平滑流入的速率
令牌桶
理解
适用场景
可以支持一定程度的突发请求,有令牌就可以处理
熔断降级
熔断
理解
依赖的服务长时间响应失败或者超时,就要考虑不去调用下游了,熔断之后还要间歇性去访问下游,如果下游服务恢复了,关闭熔断策略
监控
cat监控组件
分布式事务
强一致性解决方案
两阶段提交 XA
定义
事务管理器负责协调多个数据库,每个数据库提交之前都通知事务管理器,是否ok,如果全部ok,则事务管理器通知每个数据库commit,如果有一个不ok,则所有事务回滚
缺点
锁定资源时间长,效率低,不适合高并发的场景
最终一致性解决方案
TCC
全称
try、confirm、cancel
定义
事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。
缺点
对应用的侵入性强
业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高
实现难度较大
要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
可靠消息最终一致性方案
MQ实现事务
Linux
常用命令
系统的关机、重启以及登出
shutdown -h now 关闭系统(1)
init 0 关闭系统(2)
telinit 0 关闭系统(3)
shutdown -h hours:minutes & 按预定时间关闭系统
shutdown -c 取消按预定时间关闭系统
shutdown -r now 重启(1)
reboot 重启(2)
logout 登出
文件与目录
cd
cd .. 返回上一级目录
cd ../..返回上两级目录
cd - 返回上次所在的目录
cd ~ 或者 cd 返回用户主目录
cd /返回用户根目录
ls
ls 查看目录中的文件
ls -F 查看目录中的文件
ls -l 显示文件和目录的详细资料
ls -a 显示隐藏文件
tree
显示目录的树形结构
mkdir
mkdir abc 默认权限drwxr-xr-x
mkdir -p test/test1 递归创建多个目录
mkdir -v hao 创建新目录都显示信息
mkdir -m 777 pc 创建权限为777的目录
rmdir
可以删除空文件或者空目录
rm
rm 删除文件,目录不会删除,文件可以恢复
rm -f 强制删除,不询问
rm -r 将参数中列出的全部目录和子目录均递归地删除
rm -i 询问是否删除
mv
mv test.log test1.txt 重命名
mv info/ logs 如果logs不存在,则重命名目录
mv log1.txt log2.txt log3.txt test3 将文件移动到目录
-i 询问是否覆盖已有同名文件
-f 强制覆盖已有同名文件
cp
cp file1 file2 当前目录下复制文件
cp file1 file2 dir 拷贝文件到dir目录下
cp -r directory_1 /home/pungki/office 拷贝目录
cp dir/* . 把目录下文件复制到当前目录
ln
ln -s file1 lnk1 创建一个指向文件或目录的软链接,类似于快捷方式,但是与主连接是同步修改的,可以跨分区和文件系统,可以创建目录和文件,如果原文件被删除,则软连接成为死连接
ln file1 lnk1 创建一个指向文件或目录的物理链接 ,创建一个,增加一个连接数,不能跨分区和文件系统,不能创建目录,只能创建文件,inode相同
touch
touch -r log.log log2012.log 更新log.log的时间和log2012.log时间戳相同
touch -t 201211142234.50 log.log 设定文件时间
iconv
iconv -l 查看所有编码
iconv -f gbk -t utf-8 cloud.cpp 将gbk转换成utf8
inode
介绍
Linux是索引式的文件系统,访问数据,都是要先访问索引,然后访问数据的,inode就是索引
硬盘存储分为2个区域,一个是数据区,另一个就是inode区
系统不使用文件名的,使用的是inode号,文件名是给人看的
根据一个文件名打开文件主要分为三步
1.找到文件名对应的inode号码
2.获取inode信息
3.根据inode信息,找到数据block,读取数据
文件inode包含的内容
文件的字节数
文件拥有者的User ID & Group ID
文件的读、写、执行权限
文件的时间戳,共有三个:ctime mtime atime
链接数,即有多少文件名指向这个inode
文件数据block的位置,真正的指针
**文件名,是不在文件inode中,而是在目录内容中
目录是一个特殊文件,包含2部分:1.所有的文件名 2.文件名对应的inode
相关命令
stat a.php 可以查看该文件inode的信息
分支主题
df -i 查看inode的使用情况
硬链接
所有的文件名,有相同的inode
编辑文件都受到影响
删除文件,只用到链接数减少为0时,才真正删除,但是,删除一个文件名,不影响另一个文件名的访问
创建目录时,生成两个链接,一个当前目录链接,一个是当前目录父目录链接,总之,任何一个目录的硬链接数都为2
ln 源文件 目标文件
软链接(符号链接)
Windows的快捷方式
AB两个文件的inode不相同,A的内容是B的路径
访问A的时候,系统会自动访问到B
B删除,访问A将报:文件不存在
ln -s 源文件 目标文件
文件搜索
find
-type b/d/c/p/l/f #查是块设备、目录、字符设备、管道、符号链接、普通文件
-mtime -n +n #按文件更改时间来查找文件,-n指n天以内,+n指n天以前
-atime -n +n #按文件访问时间来查找文件,-n指n天以内,+n指n天以前
-ctime -n +n #按文件创建时间来查找文件,-n指n天以内,+n指n天以前
find / -name file1 从 '/' 开始进入根文件系统搜索文件和目录
find / -user user1 搜索属于用户 'user1' 的文件和目录
find . -name "*.log" 根据关键字查找
find /usr/bin -type f -amin n 查找系统中最后N分钟访问的文件
find /usr/bin -type f -amin n 查找系统中最后N分钟访问的文件
find /usr/bin -type f -atime n 查找系统中最后n*24小时访问的文件
find /usr/bin -type f -cmin n 查找系统中最后N分钟被改变文件状态的文件
find /usr/bin -type f -ctime n 查找系统中最后n*24小时被改变文件状态的文件
find /usr/bin -type f -mmin n 查找系统中最后N分钟被改变文件数据的文件
find /usr/bin -type f -mtime n 查找系统中最后n*24小时被改变文件数据的文件
find /opt/soft/test/ -perm 777 按照目录或文件的权限来查找文件
find . -type d | sort 查找当前所有目录并排序
find /home -size +512k 查大于512k的文件
find /home -size -512k 查小于512k的文件
find /home -used -2 列出文件或目录被改动过之后,在2日内被存取过的文件或目录
find /tmp -name tmp.txt -exec cat {} \; 查找文件并执行cat命令
find / -group cat # 查找在系统中属于cat属组的文件
find / -nouser #查找在系统中属于作废用户的文件
find /home -name tmp.txt -maxdepth 4 列出/home内的tmp.txt 查时深度最多为3层
find ./ -mtime -1 -type f -exec ls -l {} \; 查询当天修改过的文件
find . -type d -print 从当前目录查找,仅查找目录,找到后,打印路径名
find / -name access_log 2 >/dev/null 无错误查找
find / -size 1500c (查找1,500字节大小的文件,c表示字节)
find . -name '*.html' -exec grep 'mailto:'{} 查找字符串
locate
定义
"locate"的速度比"find"快,因为它并不是真的查找文件,而是查数据库
新建的文件,我们立即用"locate"命令去查找,一般是找不到的, 因为数据库的更新不是实时的,数据库的更新时间由系统维护,一天一次
"locate"命令所搜索的后台数据库在"/var/lib/mlocate"这个目录下, 可能有些Linux系统位置不同,具体我们可以用"locate locate"查询。
并不是所有的目录下的文件都会用"locate"命令搜索到, "/etc/updatedb.conf"这个配置文件中,配置了一些"locate"命令的一些规则。
使用之前可以使用updatedb命令手动更新数据库。
命令
"locate -c" 查询指定文件的数目。(c为count的意思)
"locate -e" 只显示当前存在的文件条目。(e为existing的意思)
"locate -i" 查找时忽略大小写区别。(i为ignore的意思)
"locate -n" 最大显示条数" 至多显示"最大显示条数"条查询到的内容。
"locate -r" 使用正则运算式做寻找的条件。(r为regexp的意思)
which
定义
查找命令是否存在,以及命令的存放位置在哪儿。
whereis
定义
whereis命令只能用于搜索程序名,而且只搜索二进制文件(参数-b)、man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。
查看文件内容
cat file1 从第一个字节开始正向查看文件的内容
tac file1 从最后一行开始反向查看一个文件的内容
more file1 根据窗口大小,一页一页的现实文件内容
less file1 和more类似,但其优点可以往前翻页,而且进行可以搜索字符
head 只显示头几行
tail 只显示最后几行
nl nl的功能和cat -n一样,同样是从第一行输出全部内容,并且把行号显示出来
文件权限
chmod
读取--用数字4表示
写入--用数字2表示
执行--用数字1表示
有只读权限的用户不能用cd进入该目录:还必须有执行权限才能进入
有执行权限的用户只有在知道文件名,并拥有读权利的情况下才可以访问目录下的文件
必须有读和执行权限才可以ls列出目录清单,或使用cd命令进入目录
chmod -R 777 /home/mypackage 加入-R 参数,就可以将读写权限传递给子文件夹
chown
定义
这个指令只有是由系统管理者(root)所使用,一般使用者没有权限可以改变别人的档案拥有者,也没有权限可以自己的档案拥有者改设为别人。只有系统管理者(root)才有这样的权限。
命令
chown test:root test3.txt 将test3.txt文件的属主改为test用户,属组改为root用户
chown -R test:test testdir/ <==修改testdir及它的下级目录和所有文件到新的用户和用户组
chattr
定义
用来改变文件属性
命令
chattr +i /etc/fstab 用chattr命令防止系统中某个关键文件被修改
chattr +a /data1/user_act.log 让某个文件只能往里面追加内容,不能删除,一些日志文件适用于这种操作
chgrp
定义
用来改变文件或目录所属的用户组
命令
chgrp -R mengxin /usr/meng 将/usr/meng及其子目录下的所有文件的用户组改为mengxin
文件压缩与解压
zip
zip newfilename.zip filename1 filename2
zip -r newfilename.zip file1 file2 -r表示递归压缩子目录下所有文件
zip -d myfile.zip smart.txt 删除压缩文件中smart.txt文件
zip -m myfile.zip ./rpm_info.txt 向压缩文件中myfile.zip中添加rpm_info.txt文件
unzip
unzip -o -d /home/sunny myfile.zip 把myfile.zip文件解压到 /home/sunny/ -o:不提示的情况下覆盖文件;-d /home/sunny 指明将文件解压缩到/home/sunny目录下
tar
-c: 建立压缩档案
-x:解压
-t:查看内容
-r:向压缩归档文件末尾追加文件
-u:更新原压缩包中的文件
tar –cvf jpg.tar *.jpg //将目录里所有jpg文件打包成tar.jpg
tar –czf jpg.tar.gz *.jpg //将目录里所有jpg文件打包成jpg.tar后,并且将其用gzip压缩,生成一个gzip压缩过的包,命名为jpg.tar.gz
tar –cjf jpg.tar.bz2 *.jpg //将目录里所有jpg文件打包成jpg.tar后,并且将其用bzip2压缩,生成一个bzip2压缩过的包,命名为jpg.tar.bz2
rar a jpg.rar *.jpg //rar格式的压缩
tar –xvf file.tar //解压 tar包
tar -xzvf file.tar.gz //解压tar.gz
tar -xjvf file.tar.bz2 //解压 tar.bz2
unrar e file.rar //解压rar
磁盘空间
df
df -h 查看磁盘还剩多少空间
du
du -sh [目录名] 查看该目录大小
du -sm [文件夹] 返回该文件夹总M数
du -h --max-depth=1 [目录名] 查看指定文件夹下的所有文件大小(包含子文件夹)
fdisk -l 查看硬盘的分区
vim指令
输入模式
i: 在当前光标所在字符的前面,转为输入模式
a: 在当前光标所在字符的后面,转为输入模式
o: 在当前光标所在行的下方,新建一行,并转为输入模式
I:在当前光标所在行的行首,转换为输入模式
A:在当前光标所在行的行尾,转换为输入模式
O:在当前光标所在行的上方,新建一行,并转为输入模式
gI: 在当前行第一列插入
打开文件
vim -c cmd file: 在打开文件前,先执行指定的命令
vim -r file: 恢复上次异常退出的文件
vim -R file: 以只读的方式打开文件,但可以强制保存
vim -M file: 以只读的方式打开文件,不可以强制保存
vim -y num file: 将编辑窗口的大小设为num行
vim + file: 从文件的末尾开始
vim +num file: 从第num行开始
vim +/string file 打开file,并将光标停留在第一个找到的string上
光标移动
h或退格: 左移一个字符
l或空格: 右移一个字符
j 下移一个字符
k 上移一个字符
0: 移动到行首
$: 移动到行尾
gg: 到文件头部
G: 到文件尾部
(: 前移1句
): 后移1句
{: 前移1段
}: 后移1段
翻屏
ctrl+f: 下翻一屏
ctrl+b: 上翻一屏
ctrl+d: 下翻半屏
ctrl+u: 上翻半屏
ctrl+e: 向下滚动一行
ctrl+y: 向上滚动一行
zz: 将当前行移动到屏幕中央
zt: 将当前行移动到屏幕顶端
zb: 将当前行移动到屏幕底端
剪切、复制、粘贴
[n]x: 剪切光标右边n个字符,相当于d[n]l
[n]X: 剪切光标左边n个字符,相当于d[n]h
y: 复制在可视模式下选中的文本
yy or Y: 复制整行文本
y[n]w: 复制一(n)个词
y$: 从光标当前位置复制到行尾
y0: 从光标当前位置复制到行首
:m,ny<cr> 复制m行到n行的内容
y1G或ygg: 复制光标以上的所有行
yG: 复制光标以下的所有行
[n] dd: 删除(剪切)1(n)行
p: 在光标之后粘贴
P: 在光标之前粘贴
d0: 删除(剪切)当前位置到行首的内容
#dd」:从光标所在行开始删除#行
查找
/something: 在后面的文本中查找something
?something: 在前面的文本中查找something
n: 向后查找下一个
N: 向前查找下一个
/pattern/+number: 将光标停在包含pattern的行后面第number行上
/pattern/-number: 将光标停在包含pattern的行前面第number行上
整块操作
v,V 按字符、行进行选择
y 将选区复制
p 将选区粘贴
d 将选区删除
窗口分割
:sp 分割当前窗口
:sp 1.php 将其他文件分割进来
ctrl+w 然后 箭头 可以在多个窗口间移动
进程与端口
进程状态
R 运行 runnable (on run queue)
D 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
S 中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
T 停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行)
Z 僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
ps -aux | 显示所有程序,不以终端机来区分
pstree | 直接的看到相同的进程数量,最主要的还是我们可以看到所有进程的之间的相关性
ps -ef | 返回系统中所有用户的所有进程的完整列表
截图
分支主题
每一列含义
USER
PID 进程号
%CPU
%MEM
VSZ 虚拟内存大小
RSS内存大小
TTY 终端。若是 ? 表示系统启动的
STAT 状态
R 运行
S 休眠
Z 僵尸
T 停止
s(小写)表示还有子进程
+位于后台的进程组
START 进程启动时间
TIME 启动时间
要即时查看最活跃的进程,可使用 top
截图
分支主题
术语
第一行负载(load)
同一个时间cpu处理任务队列平均等待的长度
如果1核CPU load=1 是OK,4核CPU load=4也是OK
有种说法,不要超过0.6
第二行 任务/进程(Task)
表示多少个任务在进行,多少在等待
zombie 表示僵尸进程
第三行 CPU
wa CPU等待IO占用的时间,能反映出IO的瓶颈情况
sy 系统内核占用CPU占比
us 用户进程用CPU占比
Ni调高优先级进程的占比
id 空闲时间占比
第三、四行 内存和交换区
Linux本身特性,会尽量多使用内存,内存占比往往不能反映出什么
数据表格
IN:优先级 负数优先级高 -19优先级最高
PR也是优先级
VIRT 使用虚拟内存数
RES 使用实际内存数
SHR 使用功效内存数
快捷键
q 退出top查看
s 更新刷新时间,默认3s
P 按照CPU排序
T 按时间排序
M 按照内存排序
u 查看某一个用户
空格 立即刷新
1 看每个CPU的情况,可以看多少个核心
netstat -anp 查看所有的进程和端口使用情况
参数说明
-a,显示所有
-n,不用别名显示,只用数字显示
-p,显示进程号和进程名
-n,不用别名显示,只用数字显示
-p,显示进程号和进程名
每一列
传输控制协议,一般情况是tcp和udp
Recv-Q 接收队列,这些数字一般都应该是0。如果不是则表示软件包正在队列中堆积。这种情况只能在非常少的情况见到
Send-Q 发送队列
State LISTEN 表示端口被占用
查看进程打开的文件lsof
查看打开指定文件的所有进程:lsof 文件名
显示指定进程名的所有进程:lsof -c 进程名
显示用户能打开的所有进程:lsof -u 用户名
显示指定目录下被进程打开的文件:lsof +d 目录名
kill
bg
fg
jobs
crontab
* * * * * a.php 分 时 日 月 周
一些时间的设置
30 21 * * * 每天的21:30分执行
1 1 1,10,20 * * 每月的1、10、20号执行【逗号】
0,30 18-23 * * * 18点到23点之间,0或30分执行【-】
* */1 * * * 每隔一个小时执行【/】
* 23-7/1 * * *23到7点之间,每隔一个小时【-/】
命令
crontab -l 列出所有
crontab -e 编辑
crontab -r 删除
cat /etc/crontab 查看crontab配置
截图
分支主题
术语
第一行SHELL变量指定了系统要使用哪个shell,这里是bash
第二行PATH变量指定了系统执行 命令的路径
第三行MAILTO变量指定了crond的任务执行信息将通过电子邮件发送给root用户,如果MAILTO变量的值为空,则表示不发送任务 执行信息给用户
第四行的HOME变量指定了在执行命令或者脚本时使用的主目录
常用文件
/etc/passwd
就是linux的用户库,保存所有用户但是不保存密码
adm:x:3:4:adm:/var/adm:/bin/bash
用户名:密码占位x:UID:GID:home目录:使用的shell
/etc/shadow
用户密码,是加密过的
还包含密码修改&过期等信息
负载均衡
5种策略
轮询
理解
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
weight
理解
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的
情况,权重越高,在被访问的概率越大
情况,权重越高,在被访问的概率越大
ip_hash
理解
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
url_hash
理解
按访问url的hash结果来分配请求,使每个url定向到同一个(对应的)后端服务器,后端服务器为缓存时比较有效。
fair
理解
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
查看cpu个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
查看cpu核数
cat /proc/cpuinfo | grep "cpu cores" | uniq
查看内存大小
cat /proc/meminfo | grep MemTotal
大数据
数据仓库
定义
是一个面向主题的、集成的、相对稳定的、反映历史变化的数据集合,用于支持管理决策
mapreduce
理解
“分而治之” mapper负责分,把复杂的任务分解为若干个简单的任务,reducer负责对map阶段的结果进行汇总
执行流程
4个实体
1. JobClient
运行于client node,负责将MapReduce程序打成Jar包存储到HDFS,并把Jar包的路径提交到Jobtracker,由Jobtracker进行任务的分配和监控。
2. JobTracker
运行于name node,负责接收JobClient提交的Job,调度Job的每一个子task运行于TaskTracker上,并监控它们,如果发现有失败的task就重新运行它。
3. TaskTracker
运行于data node,负责主动与JobTracker通信,接收作业,并直接执行每一个任务。
4. HDFS
用来与其它实体间共享作业文件。
过程
1. JobClient通过RPC协议向JobTracker请求一个新应用的ID,用于MapReduce作业的ID
2. JobTracker检查作业的输出说明。例如,如果没有指定输出目录或目录已存在,作业就不提交,错误抛回给JobClient,否则,返回新的作业ID给JobClient
3. JobClient将作业所需的资源(包括作业JAR文件、配置文件和计算所得得输入分片)复制到以作业ID命名的HDFS文件夹中
4. JobClient通过submitApplication()提交作业
5. JobTracker收到调用它的submitApplication()消息后,进行任务初始化
6. JobTracker读取HDFS上的要处理的文件,开始计算输入分片,每一个分片对应一个TaskTracker
7. TaskTracker通过心跳机制领取任务(任务的描述信息)
8. TaskTracker读取HDFS上的作业资源(JAR包、配置文件等),启动一个java child子进程,用来执行具体的任务(MapperTask或ReducerTask)
9. TaskTracker将Reduce结果写入到HDFS当中
工作原理
map
1)设置 InputFormat 类, 将数据切分为 Key-Value(K1和V1) 对, 输入到第二步;
2)自定义 Map 逻辑, 将第一步的结果转换成另外的 Key-Value(K2和V2)对, 输出结果;
2)自定义 Map 逻辑, 将第一步的结果转换成另外的 Key-Value(K2和V2)对, 输出结果;
shuffle
3)对输出的 Key-Value 对进行分区;
4)对不同分区的数据按照相同的 Key 排序;
5)(可选) 对分组过的数据初步规约, 降低数据的网络拷贝;
6)对数据进行分组, 相同 Key 的 Value 放入一个集合中;
4)对不同分区的数据按照相同的 Key 排序;
5)(可选) 对分组过的数据初步规约, 降低数据的网络拷贝;
6)对数据进行分组, 相同 Key 的 Value 放入一个集合中;
reduce
7)对多个 Map 任务的结果进行排序以及合并, 编写 Reduce 函数实现自己的逻辑, 对输入的 Key-Value 进行处理, 转为新的 Key-Value(K3和V3)输出;
8)设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据;
8)设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据;
hive
使用场景
可以对hive的数据做即席查询,底层的引擎使用mapreduce比较耗时,可以替换成spark
离线数据分析
通过执行定时调度或者脚本去执行HQL语句,并将结果保存
构建数仓时用于组织管理数据库和表
理解
hive是基于Hadoop的一个数据仓库工具,用来进行数据提取、转化、加载。hive数据仓库工具能将结构化的数据文件映射为一张数据库表,并提供SQL查询功能,能将SQL语句转变成MapReduce任务来执行
构成
用户接口层
定义
可通过cli命令行的形式或者web界面形式访问
原数据存储
模式
内嵌式元数据存储
场景
主要用于单元测试
存储
默认存储在Derby数据库中
本地元数据存储
理解
每个hive客户端打开数据存储连接并在该连接上请求SQL查询
远程元数据存储
理解
所有客户端都建立与元数据服务器的连接,服务器依次查询数据,元数据服务器和客户端之间使用thrift协议
包括
数据库、表名、表的列、类型、分区、表数据目录等
Driver
作用
完成HQL查询语句的词法分析、语法分析、编译、优化以及查询计划的生成,生成的查询计划存储在HDFS上,并由mapreduce调用执行
执行过程
1.解释器完成词法、语法和语义的分析以及中间代码生成,最终转换成抽象语法树
2.编译器将语法树编译为逻辑执行计划
3.逻辑优化器对逻辑执行计划进行优化,map阶段和reduce阶段均由OperatorTree组成,优化器通过变化OperatorTree,合并操作符,达到减少job和减少shuffle数量的目的
4.物理层优化器进行mapreduce任务的变化,生成最终的物理执行计划
5.执行器调用底层的运行框架执行最终的物理执行计划
存储结构
数据库
理解
HDFS目录下的一个文件夹
数据表
内部表
理解
默认创建的都是内部表
删除内部表会直接删除元数据和存储数据
外部表
理解
由HDFS管理
删除外部表仅删除元数据,HDFS文件不会被删除
分区
理解
hive数据表根据字段进行分区,不同的分区对应不同的目录,让查询更快
分桶
理解
整个数据内容按照列属性值hash值区分,不同的桶对应不同的文件
视图
表数据
函数
窗口函数
row_number
rank
排序
order by
会对输入做全局排序,因此只有一个reducer,输入规模较大时,需要计算很久
sort by
数据进入reducer之前排序
cluster by
按照指定字段对数据划分,数据进入reducer之前排序
distrbute by
按照指定的字段对数据划分,输出到不同的reducer中
partition by
按照指定字段分区,用在窗口函数中
hive优化
数据倾斜
成因
mapreduce程序执行时,reduce节点大部分执行完毕,但是有一个或者几个reduce节点运行很慢,导致整个程序的处理时间很长,这是因为某一个key的条数比其他key多很多(有时是百倍或者千倍之多),这条key所在的reduce节点所处理的数据量比其他节点就大很多,从而导致某几个节点迟迟运行不完,如何将数据均匀的分配到各个reduce中
二是单个key对应的数据量太多
三是单条记录数据太大(比如数组中的值太多)
分类
Group by /distinct 倾斜
解决办法
hive.map.aggr = true
理解
他的意思是做map aggregation,也就是在mapper里面做聚合。这个方法不同于直接写mapreduce的时候可以实现的combiner,但是却实现了类似combiner的效果
hive.groupby.skewindata = true
做reduce操作的时候,拿到的key并不是所有相同值给同一个reduce,而是随机分发,然后reduce做聚合,做完之后再做一轮MR,拿前面聚合过的数据再算结果
Join倾斜
成因1
其中logs表里面会有一个特殊用户user_id = 0,代表未登录用户,假如这种用户占了相当的比例,那么个别reduce会收到比其他reduce多得多的数据,因为它要接收所有user_id = 0的记录进行处理,使得其处理效果会非常差,其他reduce都跑完很久了它还在运行。
解决办法
hive.optimize.skewjoin = true;
理解
hive给出的解决方案叫skew join,其原理把这种user_id = 0的特殊值先不在reduce端计算掉,而是先写入hdfs,然后启动一轮map join专门做这个特殊值的计算,期望能提高计算这部分值的处理速度
特殊值分开处理法
理解
一般都是通过改写sql解决。对于上面这个问题,我们已经知道user_id = 0是一个特殊key,那么可以把特殊值隔离开来单独做join,这样特殊值肯定会转化成map join,非特殊值就是没有倾斜的普通join了
随机数分配法
理解
分配到热门商品的reducer就会很慢,因为热门商品的行为日志肯定是最多的,而且我们也很难像上面处理特殊user那样去处理item。这个时候就会用到加随机数的方法,也就是在join的时候增加一个随机数,随机数的取值范围n相当于将item给分散到n个reducer:
空值产生的数据倾斜
成因
如日志中,常会有信息丢失的问题,比如日志中的 user_id,如果取其中的 user_id 和 用户表中的user_id 关联,会碰到数据倾斜的问题。
解决办法
user_id为空的不参与关联
不同数据类型关联产生数据倾斜
成因
用户表中user_id字段为int,log表中user_id字段既有string类型也有int类型。当按照user_id进行两个表的Join操作时,默认的Hash操作会按int型的id来进行分配,这样会导致所有string类型id的记录都分配到一个Reducer中。
解决办法
把数字类型转换成字符串类型
解决办法
调优参数
hive.map.aggr=true
在map中会做部分聚集操作,效率更高但需要更多的内存。
hive.groupby.skewindata=true
数据倾斜时负载均衡,当选项设定为true,生成的查询计划会有两个MRJob。第一个MRJob 中,Map的输出结果集合会随机分布到Reduce中,每个Reduce做部分聚合操作,并输出结果,这样处理的结果是相同的GroupBy Key有可能被分发到不同的Reduce中,从而达到负载均衡的目的;第二个MRJob再根据预处理的数据结果按照GroupBy Key分布到Reduce中(这个过程可以保证相同的GroupBy Key被分布到同一个Reduce中),最后完成最终的聚合操作。
针对key
在 map 阶段将造成倾斜的key 先分成多组,例如 aaa 这个 key,map 时随机在 aaa 后面加上 1,2,3,4 这四个数字之一,把 key 先分成四组,先进行一次运算,之后再恢复 key 进行最终运算。
group操作
能先进行 group 操作的时候先进行 group 操作,把 key 先进行一次 reduce,之后再进行 count 或者 distinct count 操作。
join操作
join 操作中,使用 map join 在 map 端就先进行 join ,免得到reduce 时卡住。
分布式
分布式锁
分布式事务
分布式主键生成器
操作系统
进程
定义
进程是资源的分配和调度的一个独立单元
通信
理解
进程之间交换数据必须通过内核,在内核中开辟一块缓存区,进程1把数据从用户空间拷贝到内核缓存区,进程2再从内核缓存区把数据读走
通信方式
管道
普通管道
理解
是一种半双工的通信方式,同一时刻,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
匿名管道
理解
是一种半双工的通信方式,同一时刻,数据只能单向流动,经典的形式就是管道由父进程创建,进程fork子进程之后,就可以在父子进程之间使用了
命名管道
理解
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
实现原理
内核管理的一个缓冲区,进程1负责写入,进程2负责读取
信号量
理解
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列
理解
消息队列可以认为是一个消息链表,存储在内核中,进程可以从中读写数据。与管道和FIFO不同,进程可以在没有另外一个进程等待读的情况下进行写。另外一方面,管道和FIFO一旦相关进程都关闭并退出后,里面的数据也就没有了,但是对于消息队列,一个进程往消息队列中写入数据后退出,另外一个进程仍然可以打开并读取消息。消息队列与后面介绍的UNIX域套接字相比,在速度上没有多少优势。
共享内存
理解
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字
理解
套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
线程
线程安全
理解
在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行
子主题
同步
理解
一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成。
异步
理解
别的程序来完成你的工作
进程、线程、协程的区别
进程
特点
1. 进程是资源分配的单位,一个进程可以内可以包含多个线程,进程资源不共享
2. 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
3. 进程的创建调用fork或者vfork
术语
调度
如何分配CPU去执行进程称之为调度
切换
内核有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,进程状态的记录,恢复,上下文切换(简称切换)
具体过程
1. 保存CPU上下文,包括程序计数器和其它寄存器
2. 更新PCB信息
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
4.选择另一个进程执行,并更新其PCB
5.更新内存管理的数据结构
6.恢复处理机上下文
线程
特点
1. 线程是CPU调度的基本单元,多个线程共享进程内的资源(寄存器、堆栈、上下文)
2. 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源
3. 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度
4. 线程的创建调用pthread_create
线程同步互斥
临界区(Critical Section)
理解
适合一个进程内的多线程访问公共区域或代码段时使用
互斥量 (Mutex)
理解
适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。
事件(Event)
理解
通过线程间触发事件实现同步互斥,为控制一个具有有限数量用户资源而设计
信号量(Semaphore)
理解
与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与操作系统中PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。
协程
特点
1. 用户态的线程,用户态调度,不同协程的模型实现可能是单线程,也可能是多线程
2. 协程的切换是由程序自行控制,没有线程切换的开销
3. 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
进程切换
进程上下文包含了进程执行需要的所有信息
用户地址空间:
包括程序代码,数据,用户堆栈等
控制信息
:进程描述符,内核堆栈等
用户地址空间:
包括程序代码,数据,用户堆栈等
控制信息
:进程描述符,内核堆栈等
I/O模式
阶段
1.从磁盘读取数据到内核空间缓存区或者直接从缓存区取数
延伸
操作系统和驱动程序运行在内核空间
2.内核空间缓存区拷贝数据到用户空间缓存区
延伸
应用程序运行在用户空间
方式
阻塞式
理解
当用户线程调用请求(如调用read(),write(),listen()等接口),内核就会等待数据的到来,数据到来时实行数据拷贝,然而在内核等待数据到来和实行数据拷贝这段时间用户线程就会被阻塞,直到数据到达线程是阻塞才解除。
特点
在两个阶段都被阻塞
非阻塞式
理解
如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码 ----- 非阻塞IO一般需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
特点
非阻塞IO时,用户进程需要不断的询问内核空间数据是否准备好
缺点
进程轮训,消耗CPU资源
做法
可以通过设置socket使其变为non-blocking
I/O复用模型
select/poll
理解
select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
时间复杂度
o(n)
缺点
1. 单个进程监控的文件描述符有数量限制,32位机默认是1024个,64位机默认是2048
2. 对socket进行扫描时是线性扫描,即采用轮询的方法,当套接字比较多时,浪费cpu的时间
3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
区别
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll
理解
epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
优点
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2. 不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
时间复杂度
o(1)
对文件描述符有两种模式
水平模式(LT)
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,下次调用epoll_wait时,会再次响应应用程序并通知此事件
垂直模式(ET)
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件,下次调用epoll_wait时,不会再响应应用程序并通知此事件
区别
1. 水平模式很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高,但是ET模式,必须使用非阻塞接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死
区别
epoll采用注册回调的方式,实现有差别的轮训
select/poll只要有文件描述符就续,就采用无差别的轮训方式
相同
都是I/O多路复用,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作
信号驱动I/O模型
理解
进程预先向内核注册一个函数,然后返回不阻塞,当内核数据准备好时,发送信号给进程,用户进程便调用信号处理函数获取数据
异步I/O模型
理解
告知内核启动某个操作,并让内核在整个操作完成后,通知我们
理解
进程A往内核缓存区写数据,进程B从内核缓存区读数据
不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
内核态和用户态
内核态(内核空间)
理解
内核空间是操作系统所在的内存区域
用户态(用户空间)
理解
用户空间是用户进程所在的内存区域
权限
为了保证内核的安全,处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。
0 条评论
下一页