BAT面试题汇总及详解(进大厂必看)03

该策略可以           大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定

时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到 优的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指   该

Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis key的过期时间和永久有效分别怎么设置?

EXPIRE和PERSIST命令。

我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  1. 定时去清理过期的缓存;
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据           自己的应用场景来权衡。

 内存相关

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。    全局的键空间选择性移除

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除 近 少使用的key。(这个是最常用的)

allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。设置过期时间的键空间选择性移除

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除     近   少使用的key。

volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis主要消耗什么物理资源?

内存。

Redis的内存用完了会发生什么?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表

(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不         要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面线程模型

Redis线程模型

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处

理器(file  event   handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

 
  

文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了

Redis 内部单线程设计的简单性。

什么是事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis事务的概念

Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支

持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务的三个阶段

\1. 事务开始 MULTI

\2. 命令入队

\3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

\1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。

\2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;

\3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。

WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

MULTI命令用于开启一个事务,它总是返回OK。     MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

 
  

EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。  当操作被打断时,返回空值  nil  。通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。

事务管理(ACID)概述

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。一致性(Consistency)

事务前后数据的完整性必须保持一致。

隔离性(Isolation)

多个事务并发执行时,一个事务的执行不应影响其他事务的执行持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据          库发生故障也不应该对其有任何影响

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

Redis事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完

 
  

基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐集群方案哨兵模式

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

集群监控:负责监控 redis master 和 slave 进程是否正常工作。

消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。

故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。

配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。

即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。哨兵的核心知识

哨兵至少需要 3 个实例,来保证自己的健壮性。

哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。

对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

 
  
官方Redis Cluster 方案(服务端路由查询)

redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?简介

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个

槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行方案说明

  1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值) 区间的数据,默认分配了16384 个槽位
  2. 每份数据分片会存储在多个互为主从的多节点上
  3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  4. 同一分片多个节点间的数据不保持一致性
  5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  6. 扩容时时需要需要把旧节点的数据迁移一部分到新节点

在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制基本通信原理

集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。

分布式寻址算法

hash 算法(大量缓存重建)

一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) redis cluster 的 hash slot 算法优点无中心架构,支持动态扩容,对业务透明

具备Sentinel的监控和自动Failover(故障转移)能力

客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可高性能,客户端直连redis运维也很复杂,数据迁移需要人工干预只能使用0号数据库        不支持批量操作(pipeline管道操作) 分布式逻辑和存储模块耦合等基于客户端分配

简介

Redis     Sharding是Redis     Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持

Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool 优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强缺点      由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。

 
  

客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化基于代理服务        器分片

简介客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点, 后将结果回复给客户端特征透明接入,业务程序不用关心后端Redis实例,切换成本低

Proxy 的逻辑和存储的逻辑是隔离的代理层多了一次转发,性能有所损耗业界开源方案

Twtter开源的Twemproxy 豌豆荚开源的Codis

Redis 主从架构

单机的 redis,能够承载的 QPS  大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高

redis replication -> 主从架构 -> 读写分离 -(redismasterslave)> 水平扩容支撑读高并发 redis replication 的核心机制redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量; 一个 master node 是可以配置多个 slave node 的; slave node 也可以连接其他的 slave node;

slave node 做复制的时候,不会 block master node 的正常工作;

slave node  在做复制的时候,也不会  block  对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;

slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

注意,如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化, 可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。

另外,master  的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份  rdb  去恢复  master,这样才能确保启动的时候,是有数据的,即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。

redis 主从复制的核心原理当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。如果这是 slave node 初次连接到 master node,那么会触发一次 full

resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份

RDB 快照文件,

同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

 
  

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

  1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
  2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令存起来
  3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从

Redis

  1. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
  2. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

缺点

所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决

Redis集群的主从复制模型是怎样的?

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品生产环境中的 redis 是怎么部署的?

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署

了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器 多是 25 万读写请求/s。机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的

是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。

5 台机器对外提供读写,一共有 50g 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是

20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

说说Redis哈希槽的概念?

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384 个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。Redis集群之间是如何复制的?

异步复制

Redis集群最大节点个数是多少? 16384个

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

 分区

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个

CPU,你可以考虑一下分片(shard)。

为什么要做Redis分区?

分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你       多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

你知道有哪些Redis分区实现方案?

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个 redis节点读取。大多数客户端已经实现了客户端分区。

代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。

redis和memcached的一种代理实现就是Twemproxy

查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

Redis分区有什么缺点?

涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。         同时操作多个key,则不能使用Redis事务.

分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The

partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set) 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis 实例和主机同时收集RDB / AOF文件。

分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis   节点,能做到      大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

 分布式问题

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户    端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则SETNX 不做任何动作

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

使用SETNX完成同步锁的流程及事项如下(img):使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功为了防止获取锁后程序出现异常,导致其   他线程/进程调用SETNX命令总是返回

0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除

如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是 后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。         判断是否获 取锁的方式很简单,只需要判断有序节点中序号 小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,        好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你        添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

什么是 RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫

Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  2. 避免死锁: 终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务缓存异常缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 一般并发量不是特别多的时候,使用 多的解决方案是加锁排队。
  3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。解决方案

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-

value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

  1. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的  bitmap  中,一个一定不存在的数据会被这个  bitmap  拦截掉,从而避免了对底层存储系统的查询压力附加对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。

Bitmap:     典型的就是哈希表缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。布隆过滤器(推荐)

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。

它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。

Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不     在集合中,那么该元素肯定不在集合中。只有在所有的Hash

函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是

Bloom-Filter的基本思想。

Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!解决方案

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的 终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的 大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,

Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

热点数据,缓存才有价值对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是 基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享

数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到

Redis缓存,减少数据库压力。

缓存热点key

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回        设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其   他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询常用工具

Redis支持的Java客户端都有哪些?官方推荐用哪个? Redisson、Jedis、lettuce等等,官方推荐使用Redisson。

RedisRedisson有什么关系?

Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap,

Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock,

ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

JedisRedisson对比有什么优缺点?

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支

持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。

Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

 其他问题

RedisMemcached的区别

两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

对比参数

Redis

Memcac hed

类型

1. 支持内存 2. 非关系型数据库

1. 支持内存 2. 键值对形式 3. 缓存形式

数据存储类型

1. String 2. List 3. Set 4. Hash 5. Sort Set 【俗称 ZSet】

1. 文本型 2. 二进制类型

查询

【操作】类型

1. 批量操作 2. 事务支持 3. 每个类型不同的 CRUD

1.常用的 CRUD 2. 少量的其他命令

附加功能

1. 发布/订阅模式 2. 主从分区 3. 序列化支持 4. 脚本支持 【Lua脚本】

1. 多线程服务支持

网络IO模型

1. 单线程的多路 IO 复用模型

1. 多线程,非阻塞IO模式

事件库

自封转简易事件库 AeEvent

贵族血统的 LibEvent 事件库

持久化支持

1. RDB 2. AOF

不支持

集群模式

原生支持 cluster 模式,可以实现主从复制,读写分离

没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据

内存管理机制

在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用 的

value 交换到磁盘

Memcach ed 的数据则会一直在内存中, Memcach ed 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。

复杂数据

纯key- value,数据量非常大,并发量非常大的业务

适用场景

结构,有持久化,高可用需求,value 存储内容较大

(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

(2) redis的速度比memcached快很多

(3) redis可以持久化其数据

如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况, 好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

问题场景

描述

解决

先写缓存,再写数据库, 缓存写成功,数据库写失败

缓存写成功,但写数据库失败或者响应延迟,则下次读取

(并发读)缓存时,就出现脏读

这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存

先写数据库,再写缓存, 数据库写成功,缓存写失败

写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据

缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现

需要缓存异步刷新

指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补

确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔

救措施)时候

Redis常见性能问题和解决方案?

  1. Master 好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
  2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
  3. 为了主从复制的速度和连接的稳定性,Slave和Master 好在同一个局域网内。
  4. 尽量避免在压力较大的主库上增加从库
  5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
  6. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题, 实现Slave对Master的替换,也即,如果

Master挂了,可以立马启用Slave1做Master,其他不变。

Redis官方为什么不提供Windows版本?

因为目前Linux版本已经相当稳定,而且用户量很大,无需开发windows版本,反而会带来兼容性等问题。

一个字符串类型的值能存储最大容量是多少?

512M

Redis如何做大量数据插入? Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

假如Redis里面有1亿个key,其中有10wkey是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻    塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概            率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop,   在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过 pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis如何实现延时队列

使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。

Redis回收进程如何工作的?

  1. 一个客户端运行了新的命令,添加了新的数据。
  2. Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
  3. 一个新的命令被执行,等等。
  4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

Redis回收使用的是什么算法?

LRU算法

 消息中间件如何保证消息的一致性

(1) 主动方应用先把消息发送给消息中间件,消息状态标记为待确认;

(2) 消息中间件收到消息之后,把消息持久化到消息存储中,但并不向被动方应用投递消息;

(3) 消息中间件返回消息持久化结果(成功,或者失效),主动方应用根据返回结果进行判断如何处理业务操作处理;

①失败:放弃业务操作处理,结束(必须向上层返回失败结果)

②成功:执行业务操作处理

(4) 业务操作完成后,把业务操作结果(成功/失败)发送给消息中间件; (5)消息中间件收到业务操作结果后,根据结果进行处理;

①失败:删除消息存储中的消息,结束;

②成功:更新消息存储中的消息状态为∙待发送(可发送)∙,紧接着执行消息投递;

 
  

(6)前面的正向流程都成功后,向被动方应用投递消息;

 如何进行消息的重试机制?

Rocket重试机制,消息模式,刷盘方式

一、Consumer 批量消费(推模式)
 
  

可以通过

这里需要分为2种情况

Consumer端先启动

Consumer端后启动. 正常情况下:应该是Consumer需要先启动

注意:如果broker采用推模式的话,consumer先启动,会一条一条消息的消费,consumer后启动会才用批量消费

Consumer端先启动

1Consumer.java
 
  

由于这里是Consumer先启动,所以他回去轮询MQ上是否有订阅队列的消息,由于每次producer插入一条,Consumer就拿一条所以测试结果如下(每次size都是1)

2Consumer端后启动,也就是Producer先启动
 
  

由于这里是Consumer后启动,所以MQ上也就堆积了一堆数据,Consumer的

二、消息重试机制:消息重试分为2

 1Producer端重试

 2Consumer端重试

1Producer端重试

也就是Producer往MQ上发消息没有发送成功,我们可以设置发送失败重试的次数,发送并触发回调函数

 
  
2Consumer端重试

2.1 exception的情况,一般重复1610s30s1分钟、2分钟、3分钟等等

上面的代码中消费异常的情况返回

return ConsumeConcurrentlyStatus.RECONSUME_LATER;//重试正常则返回:

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//成功

 
  

假如超过了多少次之后我们可以让他不再重试记录 日志。

if(msgs.get(0).getReconsumeTimes()==3){

//记录日志

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 成功

}

2.2 超时的情况,这种情况MQ会无限制的发送给消费端。

就是由于网络的情况,MQ发送数据之后,Consumer端并没有收到导致超时。也就是消费端没有给我返回return    任何状态,这样的就认为没有到达Consumer端。这里模拟Producer只发送一条数据。consumer端暂停1分钟并且不发送接收状态给MQ

 
  
三、消费模式
1、集群消费

2、广播消费

 
  

rocketMQ默认是集群消费,我们可以通过在Consumer来支持广播消费

 
  
四、conf下的配置文件说明

异步复制和同步双写主要是主和从的关系。消息需要实时消费的,就需要采用主从模式部署

异步复制:比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就算从producer端发送成功了,然后通过异步复制的方法将数据复制到从节点

同步双写:比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就并不算从producer端发送成功了,需要通过同步双写的方法将数据同步到从节点后,          才算数据发送成功。

如果rocketMq才用双master部署,Producer往MQ上写入20条数据 其中Master1中拉取了12条 。Master2中拉取了8 条,这种情况下,Master1宕机,那么我们消费数据的时候,只能消费到Master2中的8条,Master1中的12条默认持久化,不会丢失消息,需要Master1恢复之后这12条数据才能继续被消费,如果想保证消息实时消费,就才用双 Master双Slave的模式

五、刷盘方式

同步刷盘:在消息到达MQ后,RocketMQ需要将数据持久化,同步刷盘是指数据到达内存之后,必须刷到commitlog日志之后才算成功,然后返回producer数据已经发送成功。   异步刷盘:,同步刷盘是指数据到达内存之后,返回producer说数据已经发送成功。,然后再写入commitlog日志。

commitlog:

commitlog就是来存储所有的元信息,包含消息体,类似于MySQL、Oracle的redolog,所以主要有CommitLog在,Consume Queue即使数据丢失,仍然可以恢复出来。consumequeue:记录数据的位置,以便Consume快速通过consumequeue找到commitlog中的数据

 Redis数据类型

概述

Redis的键值可以使用物种数据类型:字符串,散列表,列表,集合,有序集合。本文详细介绍这五种数据类型的使用方法。本文命令介绍部分只是列举了基本的命令,至于具体          的使用示例,可以参考Redis官方文档:http://redis.readthedocs.org/en/latest

字符串类型

字符串是Redis中最基本的数据类型,它能够存储任何类型的字符串,包含二进制数据。可以用于存储邮箱,JSON化的对象,甚至是一张图片,一个字符串允许存储的最大容量        为512MB。字符串是其他四种类型的基础,与其他几种类型的区别从本质上来说只是组织字符串的方式不同而已。

基本命令
字符串操作
  1. SET 赋值,用法:
  2. GET 取值,用法:
  3. INCR 递增数字,仅仅对数字类型的键有用,相当于Java的i++运算,用法:
  4. INCRBY 增加指定的数字,仅仅对数字类型的键有用,相当于Java的i+=3,用法: INCRBY key increment ,意思是key自增increment,increment可以为负数,表示减少。
  5. DECR 递减数字,仅仅对数字类型的键有用,相当于Java的i–,用法: DECR key
  6. DECRBY 减少指定的数字,仅仅对数字类型的键有用,相当于Java的i-=3,用法: DECRBY key decrement ,意思是key自减decrement,decrement可以为正数,表示增加。
  7. INCRBYFLOAT 增加指定浮点数,仅仅对数字类型的键有用,用法: INCRBYFLOAT key increment
  8. APPEND 向尾部追加值,相当于Java中的”hello”.append(“ world”),用法: APPEND key value
  9. STRLEN 获取字符串长度,用法: STRLEN key
  10. MSET 同时设置多个key的值,用法: MSET key1 value1 [key2 value2 ...]
  11. MGET 同时获取多个key的值,用法: MGET key1 [key2 ...]
  12. GETBIT 获取一个键值的二进制位的指定位置的值(0/1),用法: GETBIT key offset
  13. SETBIT 设置一个键值的二进制位的指定位置的值(0/1),用法: SETBIT key offset value
  14. BITCOUNT 获取一个键值的一个范围内的二进制表示的1的个数,用法: BITCOUNT key [start end]
  15. BITOP 该命令可以对多个字符串类型键进行位运算,并将结果存储到指定的键中,BITOP支持的运算包含:OR,AND,XOR,NOT,用法: BITOP OP desKey key1 key2
  16. BITPOS 获取指定键的第一个位值为0或者1的位置,用法: BITPOS key 0/1 [start, end]
位操作

散列类型

散列类型相当于Java中的HashMap,他的值是一个字典,保存很多key,value对,每对key,value的值个键都是字符串类型,换句话说,散列类型不能嵌套其他数据类型。一个 散列类型键最多可以包含2的32次方-1个字段。

基本命令
  1. HSET 赋值,用法: HSET key field value
  2. HMSET 一次赋值多个字段,用法: HMSET key field1 value1 [field2 values]
  3. HGET 取值,用法: HSET key field
  4. HMGET 一次取多个字段的值,用法: HMSET key field1 [field2]
  5. HGETALL 一次取所有字段的值,用法: HGETALL key
  6. HEXISTS 判断字段是否存在,用法: HEXISTS key field
  7. HSETNX 当字段不存在时赋值,用法: HSETNX key field value
  8. HINCRBY 增加数字,仅对数字类型的值有用,用法: HINCRBY key field increment
  9. HDEL 删除字段,用法: HDEL key field
  10. HKEYS 获取所有字段名,用法: HKEYS key
  11. HVALS 获取所有字段值,用法: HVALS key
  12. HLEN 获取字段数量,用法: HLEN key

列表类型

列表类型(list)用于存储一个有序的字符串列表,常用的操作是向队列两端添加元素或者获得列表的某一片段。列表内部使用的是双向链表(double   linked   list)实现的,所以向列表两端添加元素的时间复杂度是O(1),获取越接近列表两端的元素的速度越快。但是缺点是使用列表通过索引访问元素的效率太低(需要从端点开始遍历元素)。所以列表的使           用场景一般如:朋友圈新鲜事,只关心最新的一些内容。借助列表类型,Redis还可以作为消息队列使用。

基本命令
  1. LPUSH 向列表左端添加元素,用法: LPUSH key value
  2. RPUSH 向列表右端添加元素,用法: RPUSH key value
  3. LPOP 从列表左端弹出元素,用法: LPOP key
  4. RPOP 从列表右端弹出元素,用法: RPOP key
  5. LLEN 获取列表中元素个数,用法: LLEN key
  6. LRANGE 获取列表中某一片段的元素,用法: LRANGE key start stop ,index从0开始,-1表示最后一个元素
  7. LREM 删除列表中指定的值,用法: LREM key count value ,删除列表中前count个值为value的元素,当count>0时从左边开始数,count<0时从右边开始数,count=0 时会删除所有值为value的元素
  8. LINDEX 获取指定索引的元素值,用法: LINDEX key index
  9. LSET 设置指定索引的元素值,用法: LSET key index value
  10. LTRIM 只保留列表指定片段,用法: LTRIM key start stop ,包含start和stop
  11. LINSERT 像列表中插入元素,用法: LINSERT key BEFORE|AFTER privot value ,从左边开始寻找值为privot的第一个元素,然后根据第二个参数是BEFORE还是AFTER 决定在该元素的前面还是后面插入value
  12. RPOPLPUSH 将元素从一个列表转义到另一个列表,用法: RPOPLPUSH source destination

集合类型

集合在概念在高中课本就学过,集合中每个元素都是不同的,集合中的元素个数最多为2的32次方-1个,集合中的元素师没有顺序的。

基本命令
  1. SADD 添加元素,用法: SADD key value1 [value2 value3 ...]
  2. SREM 删除元素,用法: SREM key value2 [value2 value3 ...]
  3. SMEMBERS 获得集合中所有元素,用法: SMEMBERS key
  4. SISMEMBER 判断元素是否在集合中,用法: SISMEMBER key value
  5. SDIFF 对集合做差集运算,用法: SDIFF key1 key2 [key3 ...] ,先计算key1和key2的差集,然后再用结果与key3做差集
  6. SINTER 对集合做交集运算,用法: SINTER key1 key2 [key3 ...]
  7. SUNION 对集合做并集运算,用法: SUNION key1 key2 [key3 ...]
  8. SCARD 获得集合中元素的个数,用法: SCARD key
  9. SDIFFSTORE 对集合做差集并将结果存储,用法: SDIFFSTORE destination key1 key2 [key3 ...]
  10. SINTERSTORE 对集合做交集运算并将结果存储,用法: SINTERSTORE destination key1 key2 [key3 ...]
  11. SUNIONSTORE 对集合做并集运算并将结果存储,用法: SUNIONSTORE destination key1 key2 [key3 ...]
  12. SRANDMEMBER 随机获取集合中的元素,用法: SRANDMEMBER key [count] ,当count>0时,会随机中集合中获取count个不重复的元素,当count<0时,随机中集合中获取|count|和可能重复的元素。
  13. SPOP 从集合中随机弹出一个元素,用法: SPOP key

有序集合类型

有序集合类型与集合类型的区别就是他是有序的。有序集合是在集合的基础上为每一个元素关联一个分数,这就让有序集合不仅支持插入,删除,判断元素是否存在等操作外, 还支持获取分数最高/最低的前N个元素。有序集合中的每个元素是不同的,但是分数却可以相同。有序集合使用散列表和跳跃表实现,即使读取位于中间部分的数据也很快,时间复杂度为O(log(N)),有序集合比列表更费内存。

基本命令
  1. ZADD 添加元素,用法: ZADD key score1 value1 [score2 value2 score3 value3 ...]
  2. ZSCORE 获取元素的分数,用法: ZSCORE key value
  3. ZRANGE 获取排名在某个范围的元素,用法: ZRANGE key start stop [WITHSCORE] ,按照元素从小到大的顺序排序,从0开始编号,包含start和stop对应的元素,

WITHSCORE选项表示是否返回元素分数

  1. ZREVRANGE 获取排名在某个范围的元素,用法: ZREVRANGE key start stop [WITHSCORE] ,和上一个命令用法一样,只是这个倒序排序的。
  2. ZRANGEBYSCORE 获取指定分数范围内的元素,用法: ZRANGEBYSCORE key min max ,包含min和max, (min 表示不包含min, (max 表示不包含max, +inf 表示无穷大
  3. ZINCRBY 增加某个元素的分数,用法: ZINCRBY key increment value
  4. ZCARD 获取集合中元素的个数,用法: ZCARD key
  5. ZCOUNT 获取指定分数范围内的元素个数,用法: ZCOUNT key min max ,min和max的用法和5中的一样
  6. ZREM 删除一个或多个元素,用法: ZREM key value1 [value2 ...]
  7. ZREMRANGEBYRANK 按照排名范围删除元素,用法: ZREMRANGEBYRANK key start stop
  8. ZREMRANGEBYSCORE 按照分数范围删除元素,用法: ZREMRANGEBYSCORE key min max ,min和max的用法和4中的一样
  9. ZRANK 获取正序排序的元素的排名,用法: ZRANK key value
  10. ZREVRANK 获取逆序排序的元素的排名,用法: ZREVRANK key value
  11. ZINTERSTORE 计算有序集合的交集并存储结果,用法: ZINTERSTORE destination numbers key1 key2 [key3 key4 ...] WEIGHTS weight1 weight2 [weight3 weight4 ...] AGGREGATE SUM | MIN | MAX ,numbers表示参加运算的集合个数,weight表示权重,aggregate表示结果取值
  12. ZUNIONSTORE 计算有序几个的并集并存储结果,用法和14一样,不再赘述。

 redis集群如何同步;

Redis 复制与集群

复制

为提高高可用性,排除单点故障,redis支持主从复制功能。 其整体结构是一个有向无环图。

同步方式

分为两种:

全同步

全同步是第一次从机连主机是进行的同步,主机会生成一个RDB文件给从机,然后从机加载该文件。       并且如果从机掉线时间很长时也会触发这个同步,掉线时间短时使用另外的策略

部分同步

当主机收到修改命令之后会把命令发给从机进行部分同步。

 
  

这里会有一个缓存区,主要是用来,如果有从机掉线,再次连接的时候会优先使用缓存区中的数据进行同步,是在不行才使用全同步同步过程

问题

单主单从的情况下,读写分离很好,但是如果万一主挂了,这样就无法写了

或者单主多从时,如果主挂了,也无法进行同步了。这样就需要选举出一个新的主来作为主机。

主从切换

使用Sentinel,其包含如下功能: 监控

监控服务器节点

提醒

当监控的节点出现问题时,可以通过api通知其他应用等  故障转移

当主挂掉时会选举新的从服务器为主服务器,代替原来主服务器的地位

集群

主从为了提高可用性,防止单点故障。集群则是为了伸缩性了

key映射到节点的算法

对于集群的情况,经常会涉及一个key存在哪个节点中去。

一般有hash/mod的方式,但是有着增删节点重算的致命问题。

另外还有一致性hash算法,memcache客户端的算法就是这种算法,分成一个2^31的槽圆环,对节点进行hash,对key进行hash,选择顺时针离key最近的节点保存key-value,这样可以最少的影响原数据,还可以具有hash的平衡性等好的优点。

Redis使用的是另外一种:

内置16384  个hash槽,把这些槽大致均匀的分到节点中,每个节点都记录哪些槽给了自己以及给了别人。然后对key算hash/16384,   放到对应的节点中,如果新的节点来了,那么重新分割他一些槽同时更新各个节点中的记录,并且把槽中的记录同时也发给新的节点。

握手
 
  

节点信息的结构

size为0, 则state为集群下线。

握手就是节点加入到已有集群的一种方式,主要是为了丰富nodeList。握手的过程如下:

B向A发送CLUSTER MEET

A为B创建Node结构,并且加到clusterState.nodes中A向B发送MEET消息,B再未A创建Node,加入自己

然后B向A发送PONG,A向B返回PING完成握手

集群行为

当集群接收到请求之后:

一个节点接收到了请求,会检查是否自己的槽,不是则返回MOVED,告诉客户端去哪个槽     重新分片是由客户端去做的, 把一个槽的所有键值转到另外的槽

如果正在进行转移,客户请求没有命中,则会返回ask消息,让客户端去另外的节点去找     集群中如果有主从,那么从节点复制主节点,主几点下线之后,从节点升级代替主节点

 redis的数据添加过程是怎样的:哈希槽;

redis集群实现(四) 数据的和槽位的分配

不知道有没有人思考过redis是如何把数据分配到集群中的每一个节点的,可能有人会说,把集群中的每一个节点编号,先放第一个节点,放满了就放第二个节点,以此类推。。           如果真的是这样的话,服务器的利用率和性能就太低了,因为先放第一个,其他的服务器节点就闲置下来了,单个节点的压力就会非常的大,其实就相当于退化成为了单机服务器,从而违背了集群发挥每一个节点的性能的初衷。

在redis官方给出的集群方案中,数据的分配是按照槽位来进行分配的,每一个数据的键被哈希函数映射到一个槽位,redis-3.0.0规定一共有16384个槽位,当然这个可以根据用      户的喜好进行配置。当用户put或者是get一个数据的时候,首先会查找这个数据对应的槽位是多少,然后查找对应的节点,然后才把数据放入这个节点。这样就做到了把数据均          匀的分配到集群中的每一个节点上,从而做到了每一个节点的负载均衡,充分发挥了集群的威力。

 
  

在redis中,把一个key-value键值对放入的最简单的方式就是set key value,如下所示:

可以看出,当我们把key的值设置成为value的时候,客户端被重定向到了另一个节点192.168.39.153:7002,这是因为key对应的槽位是12359,所以我们的key-value就被放到了              槽12359对应的节点,192.168.39.153:7002了。接下来,我们来看看redis是怎么把一个key-value键值对映射成槽,然后又如何存放进集群中的。

首先在redis.c文件里定义了客户端命令和函数的对应关系,

 
  

可以看出,set命令会执行setCommand函数进行解析,继续进入setCommand函数查看

继续进入setGenericCommand函数

 
  

接着看数据库的setKey函数

当没有在数据库中发现key的时候,我们需要执行dbAdd函数把key-value添加到数据库里。

 
  

继续进入slotToKeyAdd函数

keyHashSlot是一个哈希函数,通过key映射到一个0-16384的整数,我们来看一下实现

计算key字符串对应的映射值,redis采用了crc16函数然后与0x3FFF取低16位的方法。crc16以及md5都是比较常用的根据key均匀的分配的函数,就这样,用户传入的一个key我们就映射到一个槽上,然后经过gossip协议,周期性的和集群中的其他节点交换信息,最终整个集群都会知道key在哪一个槽上。

redis的淘汰策略有哪些;

volatile-lru -> 根据LRU算法删除带有过期时间的key。

allkeys-lru -> 根据LRU算法删除任何key。

volatile-random -> 根据过期设置来随机删除key, 具备过期时间的key。

allkeys->random -> 无差别随机删, 任何一个key。

volatile-ttl -> 根据最近过期时间来删除(辅以TTL), 这是对于有过期时间的key noeviction -> 谁也不删,直接在写操作时返回错误。

redis 集群基础

所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽. 节点的fail是通过集群中超过半数的master节点检测失效时才生效.

客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可

redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->key.

如果存入一个值,按照redis cluster哈希槽的算法: CRC16('key')384 = 6782。 那么就会把这个key 的存储分配到对应的master上

redis Cluster主从模式

如果进群超过半数以上master挂掉,无论是否有slave集群进入fail状态,所以集群中至少应该有奇数个节点,所以至少有三个节点,每个节点至少有一个备份节点,

redis   cluster    为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。

Redis为什么这么快?

(1) 绝大部分请求是纯粹的内存操作(非常快速)

(2) 采用单线程,避免了不必要的上下文切换和竞争条件

(3) 非阻塞IO - IO多路复用

redis采用多线程会有哪些问题?

1) 单线程的问题

无法发挥多核CPU性能,单进程单线程只能跑满一个CPU核可以通过在单机开多个Redis实例来完善

可以通过数据分片来增加吞吐量,问题(不支持批量操作、扩缩容复杂等)

2) 多线程的问题

多线程处理可能涉及到锁

多线程处理会涉及到线程切换而消耗CPU

阿里云Redis多线程性能提升思路解析

背景

众所周知redis是单进程单线程模型(不完全是单进程单线程,还有若干后端线程主要做刷脏数据,关闭文件描述符等后台清理工作)。redis中负责主要工作的是主线程,主线程          的工作包括但不限:接收客户端连接,处理连接读写事件,解析请求,处理命令,处理定时器事件,数据同步等相关工作。单进程单线程只能跑满一个CPU核,在小包场景下,单             个redis server的QPS在8~10万级别。如果QPS超过这个级别,单个redis server就无法满足需求。而常用的解决办法就是数据分片,采用多server的分布式架构予以解决。然而数据分片,多redis server方式也存在若干问题:redis server过多,难以管理;分片之后一些在单redis server上使用的命令无法支持;分片无法解决热点读写问题;分片后数据倾斜,数据重分布,数据扩缩容等也比较复杂。由于单进程单线程的局限,我们期望通过多线程的改造以期充分利用SMP多核架构的优势,从而达到提高单redis          server吞吐的目的。对redis做多线程化,最容易想到的方案是每个线程既做IO又做命令处理等工作,但由于redis处理的数据结构相对比较复杂,多线程需要锁来保证线程安全性,而锁粒度处理         不好性能反而可能会出现下降。

我们的思路是通过增加IO线程,将连接中数据的读写,命令的解析和数据包的回复放到单独的IO线程来处理,而对命令的处理,定时器事件的执行等仍让单一的线程来处理,以           此达到提高单redis server吞吐的目的。

单进程单线程的优点和不足优点

因为单进程单线程模型的限制,redis在实现上将耗时的操作分解成多步,多次来执行(例如dict   rehash,   过期key删除等操作),尽量避免长时间执行一个操作,从而避免长时间阻塞在一个操作上。单进程单线程代码编写简单,可以减少多进程多线程导致的上下文切换和锁的争抢。

不足

只能使用一个CPU核,无法发挥多核优势。

对于重IO应用来说,大量的cpu耗费在网络IO操作上。对于将redis做为缓存的应用,往往都是重IO的应用。这类应用基本上都是QPS很高,使用的命令相对比较简单(多为get,set,incr等操作),但是对RT响应很敏感。这类应用通常带宽占用很高,甚至会跑到百兆级别。当前由于万兆,25G网卡的普及,网络往往已不再是瓶颈,而如何发        挥多核优势,充分发挥网卡性能成为需要考虑的事情。

实现

线程划分

主线程(MAIN THREAD) IO线程(IO THREAD)

WORKER线程(WORKER THREAD)

线程模型

主线程:接受连接,创建client,将连接转发给IO线程。

IO线程:处理连接的读写事件,解析命令,将解析的完整命令转发给WORKER线程处理,发送response包,负责删除连接等。WORKER线程:负责命令的处理,生成客户端回包,定时器事件的执行等。

主线程,IO线程,WORKER线程都有单独的事件驱动。

线程之间通过无锁队列交换数据,通过管道进行消息通知。

收益

压测结果

从压测结果来看,小包场景下,读写性能差不多有三倍左右的性能提升。主从同步速度提升

主从同步优化

Master向Slave发送同步数据时,数据在IO线程中发送,Slave从主读取数据时,全量数据在WORKER线程中读取,增量数据在IO线程中读取,因此可以相对比较有效的增加 同步的速度。

后续工作

现在所做的第一部分工作是增加IO线程,优化IO读写能力。进一步的优化可以考虑对WORKER线程进行拆分:每个线程既负责IO读取,也负责WORKER工作处理。

IO线程数设置

从测试结果来看,IO线程数最大不要超过6个。超过之后对简单操作来说,WORKER线程往往已经成为瓶颈。

进程在启动时需要设置IO线程的个数,在进程运行期间IO线程个数无法修改,按当前的连接分配策略,修改IO线程的个数涉及到连接的重新分配,处理相对比较复杂。

展望

随着万兆网卡,25G网卡的普及,如何充分利用硬件的性能需要充分的考虑。多网络IO线程,By    pass内核的用户态协议栈等都是可利用的技术。通过IO线程实现数据的迁移,可以无阻塞,IO线程对数据进程Encode,或者命令转发,目标节点实现数据Decode,或者命令执行。

 Redis支持哪几种数据结构;

String 、List 、Set 、Hash 、ZSet

 Redis跳跃表的问题;

Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群结点中用作内部数 据结构

跳跃表以及跳跃表在redis中的实现

之前在阅读redis源码的时候了解到跳跃表这个数据结构,当时花了点时间了解了下,并做了记录;如今差不多一年过去了,被人问起,竟然一点印象都没有了。然后回头去看自           己的注解,重新梳理下。

跳跃表的原理

跳跃表在redis中主要是有序表的一种底层实现。对于普通链表的查找,即使有序,我们也不能使用二分法,需要从头开始,一个一个找,时间复杂度为O(n)。而对于跳跃表,从            名字可以看出跳跃表的优势就在于可以跳跃。如何做到呢?在于其特殊的层设计。比如我们查找46,普通链表只能从头开始查找,比对-3,2,17...直到46,要比对7次。但是对于跳        跃表,我们可以从最高层开始查找:

第一步:在L4层直接与55比对,发现大了,退回到第3层

第二步:在L3层与21比对,发现小了,继续往前比对55,发现大了,退回到第二层   第三步:在L2层与37比对,发现小了,往前,与55比对,发现大了,退回到第一层   第四步:在第1层,与46比对,查找成功。

 
  

共比对了6次,比普通链表只节省了一次,似乎没什么优势。但如果细想,当链表比较长的时候,在高层查找时,跳过的元素数量将相当可观,提速的效果将非常明显。比如如果            元素在55之后,在L4层,我们直接就跳过了7个元素,这是非常大的进步。

跳跃表在redis中的实现
2.1 
 
  
 
  
跳跃表节点定义和跳跃表描述符定义

2.2 新建跳跃表

主要是初始化描述符对象和初始化头结点。这里需要注意的是,头结点默认层数是为32的,但描述符中指示跳跃表层数初始化为1。

 
  
2.3 插入新节点

分为三步:

第一步,查找每一层的插入点,所谓插入点指新节点插入后作为新节点前继的节点

 
  

redis使用了一个数组来记录

另外一个数组来记录插入点前继节点排名,所谓排名就是就链表中的位置,这个有什么用呢?主要是用来更新span字段

第二步,新建插入点,层数随机

这一步,要注意的是,如果新节点的层数大于跳跃表的层数,需要更新跳跃表的层数外,还要做:

1 将大于原跳跃表层数的更新节点设置为头结点

2 将大于原跳跃表层数的更新点前前继节点排名设置为0

 
  

3将更新点的前继节点设置为跳跃表长度,为啥是长度?因为整个层除了头结点没有其他节点

第三步,修改节点指针和跨度之span

span指示节点与后继节点的距离,如下图,o1.L1 和o2.L1距离为1,o1.L3 和o3.L3距离为2

插入新节点必然涉及到插入处前继和后继           节点指针的改,这个跟普通链表没有什么区别。至于span值的修改,因为在两个节点之间插入新节点,那么原来两点的距离就会产生改变,新节点与后继节点的跨度span也需要设置,这个怎么理解呢?

比如对于上图,要在o2和o3之间插入新节点,

update[0]=o2, rank[0]=2 update[1]=o2, rank[1]=2 update[2]=o1,rank[2]=1 update[3]=o1,rank[3]=1 update[4]=header,rank[1]=0

要设置新节点在L4层与后继节点的距离:

在L4层,在插入新节点之前,update[3]即o1的后继节点为o3,o1.level[3].span=2,插入后,新节点的后继节点为o3,怎么计算新节点在L4层的span呢?

o.level[3].span 就是oo3的距离我们知道的是插入新节点前:

1) o1o3的距离(o1.level[3].span),

2) o1o2的排名(rank[3]***rank[0]***)

3) 间接知道o2o1的距离(rank[0]-rank[3])

4) 间接知道o2o3的距离(o1.level[3].span-(*rank[0]-rank[3])*)

看图可知:插入节点后,oo3的距离,实质就是是插入节点前o2o3的距离

 
  
 
  

下面是具体的源码实现:

* 记 录

* 1.日 期: 2017年06月13日

* 作 者:

* 修改内容: 新生成函数

*****************************************************************************/ zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {

// updata[]数组记录每一层位于插入节点的前一个节点

zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;

// rank[]记录每一层位于插入节点的前一个节点的排名

//在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位

unsigned int rank[ZSKIPLIST_MAXLEVEL]; int i, level;

serverAssert(!isnan(score));

// 表头节点

x = zsl->header;

// 从最高层开始查找(最高层节点少,跳越快) for (i = zsl->level-1; i >= 0; i--) {

/* store rank that is crossed to reach the insert position */

//rank[i]用来记录第i层达到插入位置的所跨越的节点总数,也就是该层最接近(小于)给定score的排名

//rank[i]初始化为上一层所跨越的节点总数,因为上一层已经加过

rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];

//后继节点不为空,并且后继节点的score比给定的score小while (x->level[i].forward &&

(x->level[i].forward->score < score ||

//score相同,但节点的obj比给定的obj小

(x->level[i].forward->score == score && compareStringObjects(x->level[i].forward->obj,obj) < 0))) {

//记录跨越了多少个节点

rank[i] += x->level[i].span;

//查找下一个节点

x = x->level[i].forward;

}

// 存储当前层上位于插入节点的前一个节点,找下一层的插入节点

update[i] = x;

}

/* we assume the key is not already inside, since we allow duplicated

* scores, and the re-insertion of score and redis object should never

* happen since the caller of zslInsert() should test in the hash table

* if the element is already inside or not. */

// 此处假设插入节点的成员对象不存在于当前跳跃表内,即不存在重复的节点

// 随机生成一个level值

level = zslRandomLevel();

// 如果level大于当前存储的最大level值

// 设定rank数组中大于原level层以上的值为0--为什么设置为0

// 同时设定update数组大于原level层以上的数据

if (level > zsl->level) {

for (i = zsl->level; i < level; i++) {

//因为这一层没有节点,所以重置rank[i]为0 rank[i] = 0;

//因为这一层还没有节点,所以节点的前一个节点都是头节点update[i] = zsl->header;

//在未添加新节点之前,需要更新的节点跨越的节点数目自然就是zsl->length---因为整个层只有一个头结点 >言外之意头结点的span都是链表长度

update[i]->level[i].span = zsl->length;

}

// 更新level值(max层数)

zsl->level = level;

}

// 创建插入节点

x = zslCreateNode(level,score,obj); for (i = 0; i < level; i++) {

// 针对跳跃表的每一层,改变其forward指针的指向

x->level[i].forward = update[i]->level[i].forward;

//插入位置节点的后继就是新节点update[i]->level[i].forward = x;

/* update span covered by update[i] as x is inserted here */

//rank[i]: 在第i层,update[i]->score的排名

//rank[0] - rank[i]: update[0]->score与update[i]->score之间间隔了几个数

2.4 删除指定节点

删除节点相对简单一些,提供了根据排名删除节点和根据分数删除节点两个API,主要涉及如下步骤:

1) 找到待删除节点在每一层的前继节点,存在updatte数组中

2) 调用zslDeleteNode处理因为删除节点而引发的指针修改和span修改,以及跳跃表层数和长度修改

3) 

 
  

释放待删除节点下面是源码的实现:

//指针前移的必要条件是前继指针不为空

while (x->level[i].forward && (traversed + x->level[i].span) < start) {

//排名累加

traversed += x->level[i].span; x = x->level[i].forward;

}

update[i] = x;

}

//下面的节点排名肯定大于等于start traversed++;

x = x->level[0].forward;

while (x && traversed <= end) {

//逐个删除后继节点,直到end为止

zskiplistNode *next = x->level[0].forward; zslDeleteNode(zsl,x,update); dictDelete(dict,x->obj);

zslFreeNode(x); removed++;

//每删除一个节点,排名加1

traversed++; x = next;

}

return removed;

}

/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */

/*****************************************************************************

* 函 数 名 : zslDeleteNode

* 函数功能 : 内置功能函数,被zslDelete等函数调用

* 输 入 参 数  : zskiplist *zsl 链表头指针

zskiplistNode *x 待删除节点指针

zskiplistNode **update 带删除节点的前一节点地址的指针

* 输 出 参 数 : 无

* 返   回   值   :

* 调 用 关 系 :

* 记 录

* 1.日 期: 2017年06月24日

* 作 者:

* 修改内容: 新生成函数

*****************************************************************************/

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { int i;

for (i = 0; i < zsl->level; i++) {

//如果待更新节点(待删除节点的前一节点)的后继节点是待删除节点,则需要处理待更新节点的后继指针if (update[i]->level[i].forward == x) {

update[i]->level[i].span += x->level[i].span - 1;

//这里有可能为NULL,比如删除最后一个节点

update[i]->level[i].forward = x->level[i].forward;

//待删除节点没有出现在此层--跨度减1即可

} else {

update[i]->level[i].span -= 1;

}

}

//处理待删除节点的后一节点(如果存在的话) if (x->level[0].forward) {

x->level[0].forward->backward = x->backward;

} else {

zsl->tail = x->backward;

}

//跳跃表的层数处理,如果表头层级的前向指针为空,说明这一层已经没有元素,层数要减一while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)

zsl->level--;

//跳跃表长度减一zsl->length--;

}

/*****************************************************************************

* 函 数 名 : zslDelete

* 函数功能 : 根据给定分值和成员来删除节点

* 输入参数 : zskiplist *zsl 表头指针

double score 节点分数

robj *obj 节点数据指针

* 输 出 参 数 : 无

* 返   回   值   :

* 调 用 关 系 :

* 记 录

* 1.日 期: 2017年06月13日

* 作 者 : zyz

* 修改内容: 新生成函数

*****************************************************************************/

2.5 跳跃表的查询
 
  

跳跃表提供了根据排名查询元素,以及根据分数或群排名的API,间接提供了根据分数获取元素的API,查询体现了跳跃表的优势,但实现相对简单,主要是判断在当前层比对的         元素是否是否小于给定元素,如果小于,且其后继指针不为空,则继续往前查找(这效率是很高的),否则往下一层找(效率相对低一点):

总结:redis为了能根据排名查询元素,在每一个层维护了一个span字段,相对来说增加了不少实现的复杂度,在我看来是得不偿失的,不知道redis的在这个上面还有其他奥妙        没有,或许我没有发现。

 Redis是单进程单线程的,如何能够高并发?

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消 耗)

 Redis如何使用Redis实现分布式锁?

Redis分布式锁的正确实现方式

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3.  基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
代码实现
组件依赖

首先我们要通过Maven引入 Jedis 开源组件,在pom.xml 文件加入下面的代码:

 
  
加锁代码正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

 
  

可以看到,我们加锁就一行代码: jedis.set(String key, String value, String nxxx, String expx, int time) ,这个set()方法一共有五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四    个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString() 方法生成。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1.  当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2.   已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客           户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生           死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署      的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx() 和Rjedis.expire() 组合实现加锁,代码如下:

 
  

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2
 
  

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()  命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1.    通过setnx() 方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行

jedis.getSet() 方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

 
  

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第            二行代码,我们将Lua代码传到jedis.eval() 方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因     为要确保上述操作是原子性的。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del() 方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

 
  
错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

 
  

如代码注释,问题在于如果调用jedis.del() 方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del() 之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足     可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

如果你的项目中Redis是多机部署的,那么可以尝试使用 Redisson 实现分布式锁,这是Redis官方提供的Java组件

 
  

 Redis分布式锁操作的原子性,Redis内部是如何实现的?

 网络(周老师,李老师)

 计算机网络体系结构

在计算机网络的基本概念中,分层次的体系结构是 基本的。计算机网络体系结构的抽象概念较多,在学习时要多思考。这些概念对后面的学习很有帮助。

网络协议是什么?

在计算机网络要做到有条不紊地交换数据,就必须遵守一些事先约定好的规则, 比如交换数据的格式、是否需要发送一个应答信息。这些规则被称为网络协议。

为什么要对网络协议分层?

简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。

灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受 影响。易于实现和维护。

促进标准化工作。分开后,每层功能可以相对简单地被描述。

网络协议分层的缺点: 功能可能出现在多个层里,产生了额外开销。 为了使不同体系结构的计算机网络都能互联,国际标准化组织 ISO 于1977年提 出了一个试图使各种计算机在世界范围内互联成网的标准框架,即著名的开放系统互联基本参考模型 OSI/RM,简称为OSI。

OSI 的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用,  TCP/IP 体系结构则不同,但它现在却得到了非常广泛的应用。TCP/IP  是一个四  层体系结构,它包含应用层,运输层,网际层和网络接口层(用网际层这个名字 是强调这一层是为了解决不同网络的互连问题),不过从实质上讲,TCP/IP 只 有上面的三层,因为下面的网络接口层并没有什么具体内容,因此在学习计 算机网络的原理时往往采用折中的办法,即综合 OSI 和 TCP/IP 的优点,采用 一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚,有时为了方 便,也可把底下两层称为网络接口层。

四层协议,五层协议和七层协议的关系如下:

TCP/IP是一个四层的体系结构,主要包括:应用层、运输层、网际层和网络接   口层。五层协议的体系结构主要包括:应用层、运输层、网络层,数据链路层和物理层。

OSI七层协议模型主要包括是:应用层(Application)、表示层 (Presentation)、会话层(Session)、运输层(Transport)、网络层 (Network)、数据链路层

(Data Link)、物理层(Physical)。

注:五层协议的体系结构只是为了介绍网络原理而设计的,实际应用还是 TCP/IP 四层体系结构。

 TCP/IP 四层体系结构。 TCP/IP 协议族

应用层

应用层(  application-layer  )的任务是通过应用进程间的交互来完成特定网络应  用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和  交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域 名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等 等。

运输层

运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通 用的数据传输服务。应用进程利用该服务传送应用层报文。 运输层主要使用一下两种协议

  1. 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
  2. 用户数据协议-UDP:提供无连接的,尽大努力的数据传输服务(不 保证数据传输的可靠性)。

UDP

TCP

是否连接

无连接

面向连接

是否可靠

不可靠传 输,不使 用流量控 制和拥塞 控制

可靠传 输,使用 流量控制 和拥塞控 制

连接对象 个数

支持一对 一,一对 多,多对 一和多对 多交互通 信

只能是一 对一通信

传输方式

面向报文

面向字节 流

首部开销

首部开销 小,仅8字 节

首部小 20字节, 大60字 节

场景

适用于实 时应用 (IP电 话、视频会议、直 播等)

适用于要 求可靠传 输的应 用,例如 文件传输

每一个应用层(TCP/IP参考模型的最高层)协议一般都会使用到两个传输层协   议之一: 运行在TCP协议上的协议:

HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏 览。HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。FTP(File Transfer Protocol,文件传输协议),用于文件传输。

POP3(Post Office Protocol, version 3,邮局协议),收邮件用。

SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子 邮件。

TELNET(Teletype over the Network,网络电传),通过一个终端 (terminal)登陆到网络。SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。 运行在UDP协议上的协议: BOOTP(Boot Protocol,启动协议),应用于无盘设备。

NTP(Network Time Protocol,网络时间协议),用于网络同步。

DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态 配置IP地址。 运行在TCP和UDP协议上:

DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等 工作。

网络层

网络层的任务就是选择合适的网间路由和交换结点,确保计算机通信的数据及时 传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和 包进行传送。在

TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。

互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连 接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Prococol) 和许多路由选择协议,因此互联网的网络层也叫做网际层或 IP 层。

数据链路层

数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总 是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。

在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装 成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息 (如同步信息,地址信息,差错控制等)。

在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特 结束。一般的web应用的通信传输流是这样的:

发送端在层与层之间传输数据时,每经过一层时会被打上一个该层所属的首部信 息。反之,接收端在层与层之间传输数据时,每经过一层时会把对应的首部信息 去除。

物理层

在物理层上所传送的数据单位是比特。  物理层(physical  layer)的作用是实现相  邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的   差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送 比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说, 这个电路好像是看不见的。

TCP/IP 协议族

在互联网使用的各种协议中重要和著名的就是 TCP/IP 两个协议。现在人们 经常提到的 TCP/IP 并不一定是单指 TCP 和 IP 这两个具体的协议,而往往是表 示互联网所使用的整个

TCP/IP 协议族。

互联网协议套件(英语:Internet Protocol Suite,缩写IPS)是一个网络通讯模型,  以及一整个网络传输协议家族,为网际网络的基础通讯架构。它常被通称为TCP/IP协议族(英语:TCP/IP Protocol Suite,或TCP/IP Protocols),简称TCP/IP。因为该 协定家族的两个核心协定:TCP(传输控制协议)和IP(网际协议),为该家族中早通过的标准。

划重点:

TCP(传输控制协议)和IP(网际协议) 是先定义的两个核心协议,所以才统称为TCP/IP协议族

TCP的三次握手四次挥手

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据     前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服      务端保存的一份关于对方的信息,如ip地址、端口号等。

TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问 题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP头部。

一个TCP连接由一个4元组构成,分别是两个IP地址和两个端口号。一个TCP连     接通常分为三个阶段:连接、数据传输、退出(关闭)。通过三次握手建立一个     链接,通过四次挥手来关闭一个连接。

当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据。

TCP报文的头部结构

 
  

在了解TCP连接之前先来了解一下TCP报文的头部结构。

上图中有几个字段需要重点介绍下:

(1) 序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流, 发起方发送数据时对此进行标记。

(2) 确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有 效,ack=seq+1。

(3) 标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如 下:

ACK:确认序号有效。FIN:释放一个连接。

PSH:接收方应该尽快将这个报文交给应用层。

RST:重置连接。

SYN: 发 起 一 个 新 连 接 。          URG:紧急指针(urgent pointer)有效。

需要注意的是:

不要将确认序号ack与标志位中的ACK搞混了。确认方ack=发起方seq+1,两端配对。

三次握手

三次握手的本质是确认通信双方收发数据的能力首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。

 
  

然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我 后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。这,就是三次握手,这样说,你理解了吗?

第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列    号seq=100。

第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比        如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。

第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,   于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报      文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

四次挥手

 
  

四次挥手的目的是关闭一个连接

比如客户端初始化的序列号ISA=100,服务端初始化的序列号ISA=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。

第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位 (FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。

第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号

seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。

第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。

第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端    结束TCP连接的时间要比客户端早一些。

 常见面试题

为什么TCP连接的时候是3次?2次不可以吗?

因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据, 而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端            发过来的数据。

如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进       行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。

为什么TCP连接的时候是3次,关闭的时候却是4次?

因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端          收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户           端发FIN报文(所以不能一次性将确认报文和

FIN报文发给客户端,就是这里多出来了一次)。

为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?

这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认    ack报文就会重发第三次挥手的报文,这样报文一去一回       长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP设有一个保活计时器,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小          时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故           障,接着就关闭连接。什么是HTTP,HTTP 与 HTTPS 的区别

HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

区别

HTTP

HTTPS

协议

运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份

身披 SSL( Secure Socket Layer )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之 上, 是添加了加密和认证机制的 HTTP。

端口

80

443

资源消耗

较少

由于加解密处理,会消耗更 多的 CPU 和内存资源

开销

无需证书

需要证书,而证书一般需要向认证机构购买

加密机制

共享密钥加密和公开密钥加密并用的混合加密机制

安全性

由于加密机制,安全性强

常用HTTP状态码

HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等。  状态码的类别:

类别

原因短语

1XX

Informational(信息性状态码)接受的请求正在处理

2XX

Success(成功状态码)请求正常处理完毕

3XX

Redirection(重定向状态码)需要进行附加操作以完成请求

4XX

Client Error(客户端错误状态码)服务器无法处理请求

5XX

Server Error(服务器错误状态码)服务器处理请求出错

常用HTTP状态码:

2XX

成功(这系列表明请求被正常处理了)

200

OK,表示从客户端发来的请求在服务器端被正确处理

204

No content,表示请求成功,但响应报文不含实体的主体部分

206

Partial Content ,进行范围请求成功

3XX

重定向 (表明浏览器要执行特殊处理)

301

moved permanently,永久 性重定向,表示资源已被分配了新的 URL

302

found,临时性重定向,表示资源临时被分配了新的 URL

303

see other,表示资源存在着另一个 URL, 应使用 GET 方法获取资源 (对于 301/302/ 303响应,几乎所有浏览器都会删除报文主体并 自动用 GET重新请求)

304

not modified ,表示服务器允许访问资源,但请求未满足条件的情况(与重定向无关)

307

temporary redirect,临时重定 向,和302 含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

4XX

客户端错误

400

bad request,请求报文存在语法错误

401

unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息

403

forbidden ,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述

404

not found,表示在服务器上没有找到请求的资源

5XX

服务器错误

500

internal sever error,表 示服务器端在执行请求时发生了错误

501

Not Implemented,表示服务器不支持当前请求所需要的某个功能

503

service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

GETPOST区别

说道GET和POST,就不得不提HTTP协议,因为浏览器和服务器的交互是通过

HTTP协议执行的,而GET和POST也是HTTP协议中的两种方法。

HTTP全称为Hyper Text Transfer Protocol,中文翻译为超文本传输协议,目的是保证浏览器与服务器之间的通信。HTTP的工作方式是客户端与服务器之间的请求-应答协议。HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,改,增,删。

GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。

PUT:英文含义是放置,也就是向服务器新添加数据,就是所谓的增。

DELETE:从字面意思也能看出,这种方式就是删除服务器数据的过程。GET和POST区别

  1. Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。        但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。
  2. Get请求提交的url中的数据        多只能是2048字节,这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。
  3. Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个

ISO10646字符集。

  1. Get执行效率却比Post方法好。Get是form提交的默认方法。
  2. GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http          header和data一并发送出去,服务器响应200(返回数据);而对于POST, 浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

什么是对称加密与非对称加密

对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。

由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢什么是HTTP2 HTTP2 可以提高了网页的性能。

在 HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome 下一般是六

个),当在请求很多资源的时候,由于队头阻塞当浏览器达到 大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。

HTTP2   中引入了多路复用的技术,这个技术可以只通过一个   TCP   连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。

SessionCookieToken的主要区别

HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。   什么是cookie

cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response      向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,   以此来辨认用户身份。

什么是session

session是依赖Cookie实现的。session是服务器端对象

session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输

cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。

cookie与session区别

存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;

存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点多保存20个cookie,session无此限制

占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。     什么是Token

Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景        下,Token便应运而生。

Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上    这个Token前来请求数据即可,无需再次带上用户名和密码。使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token  给前端。前端可以在每次请求的时候带上  Token  证明自己的合法地位 session与token区别

session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题; session存储在服务器端,token存储在客户端

token提供认证和授权功能,作为身份认证,token安全性比session好;

session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)

Servlet是线程安全的吗

Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()  和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。

注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类      型的。

Servlet接口中有哪些方法及Servlet生命周期探秘

在Java Web程序中,Servlet主要负责接收用户请求HttpServletRequest,在  doGet(),doPost()中做相应的处理,并将回应HttpServletResponse反馈给用户。Servlet可以设置初始化参数,供Servlet内部使用。

Servlet接口定义了5个方法,其中前三个方法与Servlet生命周期相关: void init(ServletConfig config) throws ServletException

void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException

void destory()

java.lang.String getServletInfo() ServletConfig getServletConfig()

生命周期:

Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其      init()方法进行Servlet的初始化; 请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求

对应的doGet或doPost等方法;

当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet 的destroy()方法。

init方法和destory方法只会执行一次,service方法客户端每次请求Servlet都会执行。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init

方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。

如果客户端禁止 cookie 能实现 session 还能用吗?

Cookie 与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。

但为什么禁用Cookie就不能得到Session呢?因为Session是用Session   ID来确定当前对话所对应的服务器Session,而Session    ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。

假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:

  1. 手动通过URL传值、隐藏表单传递Session ID。
  2. 用文件、数据库等形式保存Session ID,在跨页过程中手动调用。

 socket通信,以及长连接,分包,连接异常断开的处理。

Socket TCP/IP协议数据传输过程中的粘包和分包问题

一:通过图解法来描述一下分包和粘包,这样客户更清晰直观的了解:

下面对上面的图进行解释:

  1. 正常情况:如果Socket Client 发送的数据包,在Socket Server端也是一个一个完整接收的,那个就不会出现粘包和分包情况,数据正常读取。
  2. 粘包情况:Socket    Client发送的数据包,在客户端发送和服务器接收的情况下都有可能发送,因为客户端发送的数据都是发送的一个缓冲buffer,然后由缓冲buffer最后刷到数据链路层的,那么就有可能把数据包2的一部分数据结合数据包1的全部被一起发送出去了,这样在服务器端就有可能出现这样的情况,导致读取的数据包包含了数据包2的一部分           数据,这就产生粘包,当然也有可能把数据包1和数据包2全部读取出来。
    1. 分包情况:意思就是把数据包2或者数据包1都有可能被分开一部分发送出去,接着发另外的部分,在服务器端有可能一次读取操作只读到一个完整数据包的一部分。
    2. 在数据包发送的情况下,有可能后面的数据包分开成2个或者多个,但是最前面的部分包,黏住在前面的一个完整或者部分包的后面,也就是粘包和分包同时产生了。

二:产生上情况的内部原因有下面几点:

  1. 数据发送端发送数据给缓冲buffer太大,导致发送一个完整的数据包被分几次发送给缓存buffer,然而缓冲buffer等到数据满了以后会自动把数据发送的数据链路层去,这样就        导致分包了。
  2. TCP协议定义有一个选项叫做最大报文段长度(MSS,Maximum Segment Size),该选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。在一定程度上MSS应该能尽可能多地承载用户数据,用于在传输通路上又可能避免分片,但是在复杂的网络环境下确定这个长度值非常困难,那么在这样的情况下在传输过           程中产生分包,粘包就很常见了
    1. 以太网,IP,TCP,UDP数据包分析
    2. 数据帧的有效载荷(payload)比以太网的最大传输单元(MTU)大的时候,进行了IP分片。

三:解决数据分包和粘包的基本策略如下

  1. 消息定长,比如定一个100,那么读取端每次读取数据就截取100个长度的数据,然后交给业务成去做解析
  2. 在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。
  3. 读取缓存的数据是不定长的,所以我们把读取到的数据添加到我们自己的一个byte[]数组中,然后根据我们的业务逻辑来找到指定的特殊协议头部,协议长度,协议尾部,然后          从我们的byte[]中获取一个完整的数据包,然后再对数据包进行业务解析就可以得到正确结果。

深入理解socket网络异常

在各种网络异常情况的背后,TCP是怎么处理的?又是怎样把处理结果反馈给上层应用的?本文就来讨论这个问题。        分为两个场景来讨论

建立连接
正常情况下
 
  

进过三次握手,客户端连接成功,服务端有一个新连接到来。

客户端连接了服务端未监听的端口

在这种情况下,服务端会对收到的SYN回应一个RST(RFC 793 3.4),客户端收到RST之后,终止连接,并进入CLOSED状态。客户端的connect返回ECONNREFUSED 111 /* Connection refused */。

 
  
客户端与服务器之间的网络不通

这又分两种情况

  1. connect返回主机不可达

具体信息在不同系统上不一样,比如linux上的定义是EHOSTUNREACH 113 /* No route to host */。明显给出了一个不可访问的地址(例如,访问一个不存在的本地网络地址,或者DNS解析失败会导致这种情况。

  1. connect返回连接超时

这种情况下,客户端发送的SYN丢失在网络中,没有得到确认,客户端的TCP会超时重发SYN。以ubuntu 12.04为例,重发SYN的时间,系列是: 0,1,3,7,15,31,63(2n-1-1)。即发送7个SYN后等待一个超时时间(例如:127秒),如果在这段时间内仍然没有收到ACK,则connect返回超时。

在这两种情况下, 服务端的状态没有变化,对服务端来讲什么也没发生。

建立连接的过程中包丢失

三次握手发送的包系列是SYN > SYN-ACK > ACK

  1. SYN丢失

这种情况就是3种的第2种情况。

  1. SYN-ACK丢失

从客户端的角度来讲以前面一种情况类似。从服务端的角度来讲,由LISTEN状态进入SYN_REVD状态。服务端的TCP会重发SYN-ACK,直到超时。SYN攻击正是利用这一原理,攻击方伪造大量的SYN包发送到服务器,服务器对收到的SYN包不断回应SYN-ACK,直到超时。这会浪费服务

 
  

器大量的资源,甚至导致奔溃。对服务端的应用层来讲,什么也没有发生。因为TCP只有在经过3次握手之后才回通知应用层,有新的连接到来。

  1. ACK丢失

这对服务端来讲与2相同。对于客户端来讲,由SYN_SENT状态进入了ESTABLISED状态,即连接成功了。连接成功后客户端就可以发送数据了。但实际上数据是发送不到服务端的(我们假设客户端收到SYN-ACK之后,客户端与服务端之间的网络就断开了),客户端发送出去的数据得不         到确认,一般重发3次左右就会处于等待ACK的状态(win7)。而ubuntu 12.10下,调用send会返回成功,直到TCP的缓冲被填满(测试环

境:局域网,感觉这个不是很合理,按照书上所说:应该是使用“指数退避”进行重传  --  TCP/IP协议详解,  大概是我的测试环境中有NAT所致吧)。最终,客户端产生一个复位信号并终止连接。返回给应用程序的结果是Connection time out(errno: 110)

通信过程

下面来看看连接建立成功后出现异常情况的处理

客户端与服务器的网络断开,双方不再发送数据

这样,双方都不知道网络已经不通,会一直保持ESTABLISHDED状态,除非打开了SO_KEEPALIVE选项。

网络断开,一方给另一方发送数据

这种情况下,接收一方不知道网络出问题,会一直等待数据到来。对于发送方,理论上的情况是,重传一定次数后,返回连接超时。

不过实际,很可能是这样的情况,发送方显示发送数据成功(send返回发送的数据长度),但实际接收方还没有接收到数据。          对于已经发送成功的数据有3种可能情况:

1 在本机的TCP缓存中

2 在网络上的某个NAT的缓存中

3 对方已经成功接收到

在实验的过程中发现,即使网络断开了,发送方仍然收到了对数据的ACK(在有NAT的情况下),猜测是NAT把数据缓存起来并发送了ACK。

当网络恢复时,那些被缓存的数据会被发送到接收方。鉴于这样的结果,给我们一个提示:不能依赖于TCP的可靠性,认为我发送成功的数据,对方一        定能收到。

TCP可以保证可靠、有序的传输,这意思是说保证收到的数据时有序正确的,并没有说已经发送成功的数据,对方一定就收到了。       在ubuntu 12.10上,发送方一直在发送数据,直到缓冲区满。而在win7下,重发3次就会停止,进入等待ACK状态。

解决的办法是:应用层对数据是否接收完成进行确认(需要的时候)。

网络断开,一方等待着另一方发送数据

这种情况下,等待数据的一方将一直等待下去。接收方无法直接知道网络已经断开,一般是设置一个超时时间,超时时间到就判断为网络已断开。发送数据的一方的反应如2所述。

一方crash,另一方继续发送/接收数据

这依赖于TCP协议栈对crash的反应。与系统相关性很大 例如:

在windows下:按ctrl+c结束程序,会发送RST段。而在linux下,按ctrl+c结束程序,会调用close。     在wind7下,如果没有调用close而结束程序,TCP会发送RST。而Ubuntu12.10上,则会发送FIN段。

  1. crash的一端发送FIN,相当于调用了close

没有crash的一端接收数据具体的反应与系统有关

例如linux 3.8.0-29-generic调用recv返回-1,errno被设置为22, Invalid argument,而linux3.3.6-030306-generic调用recv返回0.

在TCP内部,调用recv时,发送FIN,终止连接(Linux)。

windows情况以此不同,recv返回0,表示对方调用了shutdown。TCP内部发送一个RST。

但共同点是recv都会立即返回失败。没有crash的一端发送数据

第一次调用send返回成功,数据会被发送到crash的一端,crash的一端会回应一个RST,再次调用send返回-1, errno被设置为32, Broken pipe。 注意:这会向应用程序发送SIGPIPE信号,你的程序会莫名其妙退出。这是因为程序对SIGPIPE的默认处理就是结束程序。这是编写服务器程序是最需要注意的一个问题。最简单的处理方法是忽略该信号 -- signal(SIGPIPE,SIG_IGN);

windows下行为是一样的, 不同的是返回的错误是10053 - WSAECONNABORTED, 由于软件错误,造成一个已经建立的连接被取消。

共同点第一次send成功,之后就出错。

  1. crash的一端发送RST

没有crash的一端接收数据

调用recv返回-1,errno被设置为104, Connection reset by peer。在TCP内部,当收到RST时,把错误号设为ECONNRESET。没有crash的一端发送数据

调用send返回-1,errno被设置为104, Connection reset by peer。在TCP内部,当收到RST时,把错误号设为ECONNRESET

  1. crash的一端即没发送FIN也没发送RST

没有crash的一端接收数据

调用recv会一直阻塞等待数据到来没有crash的一端发送数据

重传一定次数后,返回connection time out。

5 一端关闭连接

这种情况与一端crash并发送FIN 的情况相同,参看4.1

总结

上面分析的目的是:当程序出现网络异常时,能够知道问题的原因在哪?

作为开发者,我们主要关心应用层面的返回状态。一般出错的地方是调用connect, recv, send的时候。下面做一个总结

 
  

connect函数返回状态及其原因

recv函数返回状态及其原因

 
  

send函数返回状态及其原因

各种不同步的状态,都是通过发送RST来恢复的,理解这些状况的关键在于理解何时产生RST,以及在各种状态下,对RST段如何处理。

 http中,get post的区别

get: 从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。

post: 向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。

请求方式的区别: get 请求的数据会附加在URL之后,特定的浏览器和服务器对URL的长度有限制. post 更加安全数据不会暴漏在url上,而且长度没有限制.

 HTTP报文内容

HTTP请求报文和HTTP响应报文

简介:

HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的。HTTP有两类报文:请求报文和响应报文。HTTP请求报文

一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下面给出了请求报文的一般格式。

  1. 请求头

请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如,GET /index.html HTTP/1.1。HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。

而常见的有如下几种:

1).GET

最常见的一种请求方式,当客户端要从服务器中读取文档时,当点击网页上的链接或者通过在浏览器的地址栏输入网址来浏览网页的,使用的都是GET方式。GET方法要求服务器           将URL定位的资源放在响应报文的数据部分,回送给客户端。使用GET方法时,请求参数和对应的值附加在URL后面,利用一个问号(“?”)代表URL的结尾与请求参数的开始,传      递参数长度受限制。例如,/index.jsp?id=100&op=bind,这样通过GET方式传递的数据直接表示在地址中,所以我们可以把请求结果以链接的形式发送给好友。以用google搜索domety为例,Request格式如下:

 
  

可以看到,GET方式的请求一般不包含”请求内容”部分,请求数据以地址的形式表现在请求行。地址链接如下:

地址中”?”之后的部分就是通过GET发送的请求数据,我们可以在地址栏中清楚的看到,各个数据之间用”&”符号隔开。显然,这种方式不适合传送私密数据。另外,由于不同的浏       览器对地址的字符限制也有所不同,一般最多只能识别1024个字符,所以如果需要传送大量数据的时候,也不适合使用GET方式

2).POST
 
  

对于上面提到的不适合使用GET方式的情况,可以考虑使用POST方式,因为使用POST方法可以允许客户端给服务器提供信息较多。POST方法将请求参数封装在HTTP请求数据       中,以名称/值的形式出现,可以传输大量数据,这样POST方式对传送的数据大小没有限制,而且也不会显示在URL中。还以上面的搜索domety为例,如果使用POST方式的话,     格式如下:

可以看到,POST方式请求行中不包含数据字符串,这些数据保存在”请求内容”部分,各数据之间也是使用”&”符号隔开。POST方式大多用于页面的表单中。因为POST也能完成GET的功能,因此多数人在设计表单的时候一律都使用POST方式,其实这是一个误区。GET方式也有自己的特点和优势,我们应该根据不同的情况来选择是使用GET还是使用  POST。

3).HEAD

HEAD就像GET,只不过服务端接受到HEAD请求后只返回响应头,而不会发送响应内容。当我们只需要查看某个页面的状态的时候,使用HEAD是非常高效的,因为在传输的过     程中省去了页面内容。

2. 请求头部

请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:

User-Agent: 产 生 请 求 的 浏 览 器 类 型 。                    Accept:客户端可识别的内容类型列表。                               Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。

3. 空行

最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。

4. 请求数据

请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合。与请求数据相关的最常使用的请求头是Content-Type和Content-Length。

HTTP报文
 
  

HTTP响应也由三个部分组成,分别是:状态行、消息报头、响应正文。  如下所示,HTTP响应的格式与请求的格式十分类似:

正如你所见,在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status      line)通过提供一个状态码来说明所请求的资源情况。状态行格式如下:

HTTP-Version Status-Code Reason-Phrase CRLF

其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。

1xx:指示信息--表示请求已接收,继续处理。

2xx:成功--表示请求已被成功接收、理解、接受。

3xx:重定向--要完成请求必须进行更进一步的操作。

4xx:客户端错误--请求有语法错误或请求无法实现。

5xx:服务器端错误--服务器未能实现合法的请求。

常见状态代码、状态描述的说明如下。

200 OK:客户端请求成功。

400 Bad Request:客户端请求有语法错误,不能被服务器所理解。

401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。

403 Forbidden:服务器收到请求,但是拒绝提供服务。

404 Not Found:请求资源不存在,举个例子:输入了错误的URL。500 Internal Server Error:服务器发生不可预期的错误。

503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

 
  

下面给出一个HTTP响应报文例子

关于HTTP请求GETPOST的区别
1. GET提交,请求的数据会附在URL之后(就是把数据放置在HTTP协议头<request-line>中)

以?分割URL和传输数据,多个参数用&连接;例如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0 %E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如: %E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。

POST提交:把提交的数据放置在是HTTP包的包体<request-body>中。    上文示例中加粗字体标明的就是实际的传输数据

因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变
  1. 传输数据的大小:

首先声明,HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制。 而在实际开发中存在的限制主要有:

GET:特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决            于操作系统的支持。

因此对于GET提交时,传输数据就会受到URL长度的限制。

POST:由于不是通过URL传值,理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。3.安全性:

POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的   Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存,    (2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了。

 说说浏览器访问http://www.taobao.com经历了怎样的过程。

访问 www.taobao.com过程

首先是查找浏览器缓存,浏览器会保存一段时间你之前访问过的一些网址的DNS信息,不同浏览器保存的时常不等。         如果没有找到对应的记录,这个时候浏览器会尝试调用系统缓存来继续查找这个网址的对应DNS信息。

如果还是没找到对应的IP,那么接着会发送一个请求到路由器上,然后路由器在自己的路由器缓存上查找记录,路由器一般也存有DNS信息。  看到看到

如果还是没有,这个请求就会被发送到ISP(注:Internet Service Provider,互联网服务提供商,就是那些拉网线到你家里的运营商,中国电信中国移动什么的),ISP也会有相应的ISP DNS服务器,一听中国电信就知道这个DNS服务器的规模肯定不会小,所以基本上都能在这里找得到。题外话:会跑到这里进行查询是因为你没有改动过"网络中

心"的"ipv4"的DNS地址,万恶的电信联通可以改动了这个DNS服务器,换句话说他们可以让你的浏览器跳转到他们设定的页面上,这也就是人尽皆知的DNS和HTTP劫持,ISP们   还美名曰“免费推送服务”。强烈鄙视这种霸王行为。我们也可以自行修改DNS服务器来防止DNS被ISP污染。

如果还是没有的话,        你的ISP的DNS服务器会将请求发向根域名服务器进行搜索。根域名服务器就是面向全球的顶级DNS服务器,共有13台逻辑上的服务器,从A到M命名,真正的实体服务器则有几百台,分布于全球各大洲。所以这些服务器有真正完整的DNS数据库。如果到了这里还是找不到域名的对应信息,那只能说明一个问题:这个域名本来就不存在,它没有在网上正式注册过。或者卖域名的把它回收掉了(通常是因为欠费)。

这也就是为什么打开一个新页面会有点慢,因为本地没什么缓存,要这样递归地查询下去。

多说一句,例如"mp3.baidu.com",域名先是解析出这是个.com的域名,然后跑到管理.com域名的服务器上进行进一步查询,然后是.baidu,最后是mp3, 所以域名结构为:三级域名.二级域名.一级域名。

 
  

浏览器终于得到了IP以后,浏览器接着给这个IP的服务器发送了一个http请求,方式为get,例如访问nbut.cn

这个get请求包含了主机(host)、用户代理(User-Agent),用户代理就是自己的浏览器,它是你的"代理人",Connection(连接属性)中的keep-alive表示浏览器告诉对方服务器在传输完现在请求的内容后不要断开连接,不断开的话下次继续连接速度就很快了。其他的顾名思义就行了。还有一个重点是Cookies,Cookies保存了用户的登陆信息,在每       次向服务器发送请求的时候会重复发送给服务器。Corome上的F12与Firefox上的firebug(快捷键shift+F5)均可查看这些信息。

发送完请求接下来就是等待回应了

当然了,服务器收到浏览器的请求以后(其实是WEB服务器接收到了这个请求,WEB服务器有iis、apache等),它会解析这个请求(读请求头),然后生成一个响应头和具体响      应内容。接着服务器会传回来一个响应头和一个响应,响应头告诉了浏览器一些必要的信息,例如重要的Status       Code,2开头如200表示一切正常,3开头表示重定向,4开头, 如404,呵呵。响应就是具体的页面编码,就是那个 ,浏览器先读了关于这个响应的说明书(响应头),然后开始解析这个响应并在页面上显示出来。在下一次CF的时候(不

是穿越火线,是http://codeforces.com/),由于经常难以承受几千人的同时访问,所以CF页面经常会出现崩溃页面,到时候可以点开火狐的firebug或是Chrome的F12看看状态,不过这时候一般都急着看题和提交代码,似乎根本就没心情理会这个状态吧-.-。

如果是个静态页面,那么基本上到这一步就没了,但是如今的网站几乎没有静态的了吧,基本全是动态的。所以这时候事情还没完,根据我们的经验,浏览器打开一个网址的时候会慢慢加载这个页面,一部分一部分的显示,直到完全显示,最后标签栏上的圈圈就不转了。

 
  
 
  

这是因为,主页(index)页面框架传送过来以后,浏览器还要继续向服务器发送请求,请求的内容是主页里面包含的一些资源,如图片,视频,css样式等等。这些"非静态"的        东西要一点点地请求过来,所以标签栏转啊转,内容刷啊刷,最后全部请求并加载好了就终于好了。

需要说明的是,对于静态的页面内容,浏览器通常会进行缓存,而对于动态的内容,浏览器通常不会进行缓存。缓存的内容通常也不会保存很久,因为难保网站不会被改动。

 HTTP协议、  HTTPS协议,SSL协议及完整交互过程;

HTTPS协议,SSL协议及完整交互过程

SSL
  1. 安全套接字(Secure Socket Layer,SSL)协议是Web浏览器与Web服务器之间安全交换信息的协议。
  2. SSL协议的三个特性

Ø 保密:在握手协议中定义了会话密钥后,所有的消息都被加密。

Ø 鉴别:可选的客户端认证,和强制的服务器端认证。

Ø 完整性:传送的消息包括消息完整性检查(使用MAC)。

  1. SSL的位置
HTTPS
  1. HTTPS基于SSL的HTTP协议。
  2. HTTPS使用与HTTP不同的端口(,一个加密、身份验证层(HTTP与TCP之间))。
  3. 提供了身份验证与加密通信方法,被广泛用于互联网上安全敏感的通信。
交互过程

客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤

1) 客户端请求建立SSL连接,并将自己支持的一套加密规则发送给网站。

2) 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息

3) 获得网站证书之后浏览器要做以下工作:

Ø 验证证书的合法性

Ø 如果证书受信任,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。

Ø 使用约定好的HASH*计算握手消息*

Ø 使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。

4) 网站接收浏览器发来的数据之后要做以下的操作:

Ø 使用自己的私钥将信息解密取出密码

Ø 使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。

Ø *使用密码加密*一段握手消息,发送给浏览器

5) 浏览器解密并计算握手消息的*HASH*,如果与服务端发来的HASH一致,此时握手结束。

6) 使用随机密码对称加密算法对传输的数据加密,传输。

  1. 密与HASH算法如下:

1) 非对称加密算法:RSA,DSA/DSS,用于在握手过程中加密生成的密码。

2) 对称加密算法:AES,RC4,3DES,用于对真正传输的数据进行加密。

3) HASH算法:MD5,SHA1,SHA256,验证数据的完整性。

  1. HTTP与HTTPS的区别:

1) https协议需要申请证书。

2) http是超文本传输协议,明文传输;https使用的是具有安全性的SSL加密传输协议。

3) http端口80,;https端口443。

4) http连接简单无状态;https由SSL+HTTP协议构件的可进行加密传输、身份验证的网络协议。

 Ping过程 原理 详解

如果你想了解PING的原理,就看我的文章,不要去网上找,找不到什么好的内容。看了我文章,也许你会从对网络一窍不通,到豁然开朗。        我在这里讲拼的两情况,一种是同一网段内,一种是跨网段的ping ….

首先,如果主机A,要去    ping主机B,那么主机A,就要封装二层报文,他会先查自己的MAC地址表,如果没有B的MAC地址,就会向外发送一个ARP广播包其中ARP报文格式如下:

以太网目的MAC

以太网源MAC

帧类型

硬件类型

4

6

OP

发送端以太网

MAC

发送端IP地址

目的

MAC

目的

IP

FF-FF-FF-FF-FF- FF

00-50-56-C0-00-

01

0806

0800

1

00-50-56-C0-00-

01

1.1.1.1

00-00-00-00-00-

00

1.1.1.3

其中OP

1 :表示ARP请求
 
  

2:表示ARP应答

首先,交换机会收到这个报文后,交换机有学习MAC地址的功能,所以他会检索自己有没有保存主机B有MAC,如果有,就返回给主机A,       如果没有,就会向所有端口发送ARP广播        ,其它主机收到后,发现不是在找自己,就纷纷丢弃了该报文,不去理会。。直到主机B收到了报文后,就立即响应,我的MAC地址是多少,同时学到主机A的MAC地址,并按同样的ARP报文格式返回给主机A

ARP报文格式:

以太网目的MAC

以太网源MAC

帧类型

硬件类型

4

6

OP

发送端以太网

MAC

发送端IP地址

目的

MAC

目的

IP

00-50-56-C0-00-

01

00-50-56-C0-00-

03

0806

0800

2

00-50-56-C0-00-

03

1.1.1.3

00-50-56-C0-00-

01

1.1.1.1

这时候主机A学到了主机B的MAC,就把这个MAC封装到ICMP协议的二层报文中向主机B发送,报文格式如下:

目的地址

源地址

IP

目的IP

ICMP报文

00-50-56-C0-00-03

00-50-56-C0-00-01

1.1.1.1

1.1.1.3

Echo request

当主机B收到了这个报文后,发现是主机A 的ICPM回显请求,就按同样的格式,返回一个值给主机A,这样就完成了同一网段内的ping过程…

目的地址

源地址

IP

目的IP

ICMP报文

00-50-56-C0-00-01

00-50-56-C0-00-03

1.1.1.3

1.1.1.1

Echo answer

 
  

在这里,我讲了这么久的 *局域网内的PING* ,实际过程的发生不到 1毫秒….

报文格式如下:

目的地址

源地址

IP

目的IP

ICMP报文

00-50-56-C0-00-02

00-50-56-C0-00-01

1.1.1.1

2.1.1.1

Echo request

当路由器收到主机A发过来的ICMP报文,发现自己的目的地址是其本身MAC地址,根据目的的IP2.1.1.1,查路由表,发现2.1.1.1/24的路由表项,得到一个出口指针,去掉原来的MAC头部.加上自己的MAC地址向主机C转发…(      如果网关也没有主机CMAC地址,还是要向前面一个步骤一样,ARP广播一下即可相互学到….路由器2端口能学到主机DMAC,D也能学到路由器2端口的MAC. .) ,报文格式如下:

目的地址

源地址

IP

目的IP

ICMP报文

00-50-56-C0-00-05

00-50-56-C0-00-04

1.1.1.1

2.1.1.1

Echo request

最后,在主机C已学到路由器2端口MAC,路由器2端口转发给路由器1端口,路由1端口学到主机A的MAC的情况下,他们就不需要再做ARP解析,就将ICMP的回显请求回复过来.. 报文格式大致如下:

目的地址

源地址

IP

目的IP

ICMP报文

00-50-56-C0-00-04

00-50-56-C0-00-05

2.1.1.1

1.1.1.1

Echo Answer

 TCP/IP协议详解笔记——ARP协议和RARP协议

ARP:地址解析协议

对于以太网,数据链路层上是根据48bit的以太网地址来确定目的接口,设备驱动程序从不检查IP数据报中的目的IP地址。ARP协议为IP地址到对应的硬件地址之间提供动态映      射。

工作过程

在以太网(ARP协议只适用于局域网)中,如果本地主机想要向某一个IP地址的主机(路由表中的下一跳路由器或者直连的主机,注意此处IP地址不一定是IP数据报中的目的IP)       发包,但是并不知道其硬件地址,此时利用ARP协议提供的机制来获取硬件地址,具体过程如下:

1) 本地主机在局域网中广播ARP请求,ARP请求数据帧中包含目的主机的IP地址。意思是“如果你是这个IP地址的拥有者,请回答你的硬件地址”。

2) 目的主机的ARP层解析这份广播报文,识别出是询问其硬件地址。于是发送ARP应答包,里面包含IP地址及其对应的硬件地址。

3) 本地主机收到ARP应答后,知道了目的地址的硬件地址,之后的数据报就可以传送了。     点对点链路不使用ARP协议。

帧格式
 
  

以太网目的地址:目的主机的硬件地址。目的地址全为1的特殊地址是广播地址。以太网源地址:源主机的硬件地址。

帧类型:对于ARP协议,该字段为0x0806。对于RARP协议,该字段为0x8035。

硬件类型:表示硬件地址的类型。值为1时表示以太网地址。也就是说ARP协议不仅仅应用于以太网协议,还可以支持别的链路层协议。           协议类型:表示要映射的协议地址类型。值为0x0800时表示IP协议。

硬件地址长度:与硬件类型对应的硬件地址的长度,以字节为单位。如果是以太网,则是6字节(MAC长度)。        协议地址长度:与协议类型对应的协议地址长度,以字节为单位。如果是IP协议,则是4字节(IP地址长度)。      操作类型(op):四中操作类型。ARP请求(1),ARP应答(2),RARP请求(3),RARP应答(4)。

发送端硬件地址:如果是以太网,则是源主机以太网地址,此处和以太网头中的源地址对应。发送端协议地址:如果是IP协议,则表示源主机的IP地址。

目的端硬件地址:如果是以太网,则是目的以太网地址,和以太网头中的目的地址对应。目的端协议地址:如果是IP协议,则表示源主机要请求硬件地址的IP地址。

对应ARP请求包来说,目的端的硬件地址字段无须填充,其他字段都需要填充。对于ARP回复包来说,所有字段都需要填充。

APR请求包是广播的,但是ARP应答帧是单播的。

以太网数据报最小长度是60字节(14字节的以太网头,不包含4字节的FCS),ARP数据包长度为42字节(14字节的以太网头和28字节的ARP数据),需要加入填充字符到以太网   最小长度要求:60字节。

ARP高速缓存

每个主机都有一个ARP高速缓存表,这样避免每次发包时都需要发送ARP请求来获取硬件地址。默认老化时间是20分钟。利用arp -a命令可以查看显示系统中高速缓存的内容。Windows下“arp -d”命令可以清除arp高速缓存表。

有时候需要手动清除arp缓存,曾经就是因为arp缓存没有做清理,导致迷惑了很久。遇到的问题:

1) 制作了一个写路由器MAC地址的工具,每次写完MAC地址,重启路由器,会发现无法telnet登陆路由器。IP地址没变,但是MAC地址更改了,而ARP缓存表中IP地址映射的仍   然是旧的MAC地址。

2) 类似的问题,有两个路由器具有相同的IP地址。先连接一个路由器,登陆成功后,再去连接另一台路由器,却发现登陆不了。

ARP代理

如果ARP请求时从一个网络的主机发往另一个网络上的主机,那么连接这两个网络的路由器可以回答该请求,这个过程称作委托ARP或者ARP代理。这样可以欺骗发起ARP请求的      发送端,使它误以为路由器就是目的主机。

RARP:逆地址解析协议

将局域网中某个主机的物理地址转换为IP地址,比如局域网中有一台主机只知道物理地址而不知道IP地址,那么可以通过RARP协议发出征求自身IP地址的广播请求,然后由RARP    服务器负责回答。RARP协议广泛应用于无盘工作站引导时获取IP地址。

RARP允许局域网的物理机器从网管服务器ARP表或者缓存上请求其IP地址。

帧格式

帧格式同ARP协议,帧类型字段和操作类型不同,具体见ARP帧格式描述。

工作原理
  1. 主机发送一个本地的RARP广播,在此广播包中,声明自己的MAC地址并且请求任何收到此请求的RARP服务器分配一个IP地址。
  2. 本地网段上的RARP服务器收到此请求后,检查其RARP列表,查找该MAC地址对应的IP地址。
  3. 如果存在,RARP服务器就给源主机发送一个响应数据包并将此IP地址提供给对方主机使用。
  4. 如果不存在,RARP服务器对此不做任何的响应。
  5. 源主机收到从RARP服务器的响应信息,就利用得到的IP地址进行通讯;如果一直没有收到RARP服务器的响应信息,表示初始化失败。

 DNS域名解析的过程

 
  
一、主机解析域名的顺序

1、找缓存

2、找本机的hosts文件3、找DNS服务器

注意:

配置IP和主机名时,要记得修改/etc/hosts文件,因为有些应用程序在主机内的进程之间通信的时候,会本机的主机名,如果主机名不能正确解析到一个正常的IP地址,那么        就会导致进程通信有问题。

二、概念解释
 
  

DNS(Domain Name System,域名系统)

Q:浏览器如何通过域名去查询URL对应的IP(对应服务器地址)呢? A:

1、浏览器缓存:浏览器会按照一定的频率缓存DNS记录。

2、操作系统缓存:如果浏览器缓存中找不到需要的DNS记录,那就去操作系统中找。

3、路由缓存:路由器也有DNS缓存。

4、ISP的DNS服务器:ISP是互联网服务提供商(Internet Service Provider)的简称,ISP有专门的DNS服务器应对DNS查询请求。

 
  

5、根服务器:ISP的DNS服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS服务器先问根域名服务器.com域名服务器的IP地址,然后再问.com域名服    务器,依次类推)。

Q:在网上查到某个网站的IP地址,在自己的浏览器上输入,却为什么连接不上? A:

大的高并发网站可能不止一个IP地址,根据不同的网络他们会有很多的IP来做集群。有的是通过DNS来实现负载均衡,有的是用squid来实现的。

 Http会话的四个过程

建立连接,发送请求,返回响应,关闭连接。

 
  

 网络7层架构

7 层模型主要包括:

  1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由 1、0 转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的模数转换与数模转换)。这一层的数据叫做比特。
  2. 数据链路层:主要将从物理层接收的数据进行 MAC 地址(网卡的地址)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。
  3. 网络层:主要将从下层接收到的数据进行 IP 地址(例 192.168.0.1)的封装与解封装。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。
  4. 传输层:定义了一些传输数据的协议和端口号(WWW 端口 80 等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据), UDP(用户数据报协议,与 TCP 特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如 QQ 聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段进行传输,到达目的地址后在进行重组。常常把这一层数据叫做段。
  5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或或者接受会话请求(设备之间需要互相认识可以是 IP 也可以是

MAC 或者是主机名)

  1. 表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等))
  2. 应用层 主要是一些终端的应用,比如说FTP(各种文件下载),WEB(IE浏览),QQ之类的(你就把它理解成我们在电脑屏幕上可以看到的东西.就 是终端应用)。

 TCP/IP 原理

TCP/IP 协议不是 TCP 和 IP 这两个协议的合称,而是指因特网整个 TCP/IP 协议族。从协议分层模型方面来讲,TCP/IP 由四个层次组成:网络接口层、网络层、传输层、应用层。

 
  
网络访问层(Network Access Layer)
  1. 网络访问层(Network Access Layer)在 TCP/IP 参考模型中并没有详细描述,只是指出主机必须使用某种协议与网络相连。网络层(Internet Layer)
    1. 网络层(Internet      Layer)是整个体系结构的关键部分,其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。这些分组可能经由不同的网络,到达的顺序和发送的顺序也可能不同。高层如果需要顺序收发,那么就必须自行处理对分组的排序。互联网层使用因特网协议(IP,Internet Protocol)。
    2. 传输层(Tramsport Layer)使源端和目的端机器上的对等实体可以进行会话。在这一层定义了两个端到端的协议:传输控制协议(TCP,Transmission Control Protocol)和用户数据报协议(UDP,User Datagram Protocol)。TCP 是面向连接的协议,它提供可靠的报文传输和对上层应用的连接服务。为此,除了基本的数据传输外,它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。UDP 是面向无连接的不可靠传输的协议,主要用于不需要 TCP 的排序和流量控制等功能的应用程序。
    3. 应用层(Application Layer)包含所有的高层协议,包括:虚拟终端协议(TELNET,TELecommunications NETwork)、文件传输协议(FTP,File Transfer Protocol)、电子邮件传输协议(SMTP,Simple Mail Transfer Protocol)、域名服务(DNS,Domain Name Service)、网上新闻传输协议(NNTP,Net News Transfer Protocol)和超文本传送协议(HTTP,HyperText Transfer Protocol)等。
传输层(Tramsport Layer-TCP/UDP)
应用层(Application Layer)

 TCP  三次握手/四次挥手

TCP 三次握手/四次挥手

TCP 在传输之前会进行三次沟通,一般称为“三次握手”,传完数据断开的时候要进行四次沟通,一般称为“四次挥手”。

数据包说明

\1. 源端口号( 16 位):它(连同源主机 IP 地址)标识源主机的一个应用进程。

  1. 目的端口号( 16 位):它(连同目的主机 IP 地址)标识目的主机的一个应用进程。这两个值加上 IP 报头中的源主机 IP 地址和目的主机 IP 地址唯一确定一个 TCP 连接。
  2. 顺序号 seq( 32 位):用来标识从  TCP 源端向  TCP 目的端发送的数据字节流,它表示在这个报文段中的第一个数据字节的顺序号。如果将字节流看作在两个应用程序间的单向流动,则TCP 用顺序号对每个字节进行计数。序号是 32bit 的无符号数,序号到达 2 的 32 次方 - 1 后又从 0 开始。当建立一个新的连接时, SYN 标志变 1 ,顺序号字段包含由这个主机选择的该连接的初始顺序号 ISN ( Initial Sequence Number )。
  3. 确认号 ack( 32 位):包含发送确认的一端所期望收到的下一个顺序号。因此,确认序号应当是上次已成功收到数据字节顺序号加 1 。只有 ACK 标志为 1 时确认序号字段才有效。 TCP 为应用层提供全双工服务,这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据顺序号。
  4. TCP 报头长度( 4 位):给出报头中 32bit 字的数目,它实际上指明数据从哪里开始。需要这个值是因为任选字段的长度是可变的。这个字段占 4bit ,因此 TCP 最多有 60

字节的首部。然而,没有任选字段,正常的长度是 20 字节。

  1. 保留位( 6 位):保留给将来使用,目前必须置为 0 。
  2. 控制位( control flags , 6 位):在 TCP 报头中有 6 个标志比特,它们中的多个可同时被设置为 1 。依次为:

§ URG :为 1 表示紧急指针有效,为 0 则忽略紧急指针值。

§ ACK :为 1 表示确认号有效,为 0 表示报文中不包含确认信息,忽略确认号字段。

§ PSH :为 1 表示是带有 PUSH 标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。

§ RST :用于复位由于主机崩溃或其他原因而出现错误的连接。它还可以用于拒绝非法的报文段和拒绝连接请求。一般情况下,如果收到一个 RST 为 1 的报文,那么一定发生了某些问题。

§ SYN :同步序号,为 1 表示连接请求,用于建立连接和使顺序号同步( synchronize )。

§ FIN :用于释放连接,为 1 表示发送方已经没有数据发送了,即关闭本方数据流。

  1. 窗口大小(   16  位):数据字节数,表示从确认号开始,本报文的源方可以接收的字节数,即源方接收窗口大小。窗口大小是一个   16bit  字段,因而窗口大小最大为   65535

字节。

  1. 校验和( 16 位):此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证。
  2. 紧急指针( 16 位):只有当 URG 标志置 1 时紧急指针才有效。TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
  3. 选项:最常见的可选字段是最长报文大小,又称为 MSS(Maximum Segment Size) 。每个连接方通常都在通信的第一个报文段(为建立连接而设置 SYN 标志的那个段)中指明这个选项,它指明本端所能接收的最大长度的报文段。选项长度不一定是 32 位字的整数倍,所以要加填充位,使得报头长度成为整字数。
 
  

数据:  TCP  报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有  TCP  首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。

三次握手

第一次握手:主机 A 发送位码为 syn=1,随机产生 seq number=1234567 的数据包到服务器,主机 B 由 SYN=1 知道,A 要求建立联机; 第二次握手:主机 B 收到请求后要确认联机信息,向 A 发送 ack number=(主机 A 的 seq+1),syn=1,ack=1,随机产生 seq=7654321 的包

第三次握手:主机 A 收到后检查 ack number 是否正确,即第一次发送的 seq number+1,以及位码ack 是否为 1,若正确,主机 A 会再发送 ack number=(主机 B 的

 
  

seq+1),ack=1,主机 B 收到后确认 seq 值与 ack=1 则连接建立成功。

四次挥手

TCP 建立连接要进行三次握手,而断开连接要进行四次。这是由于 TCP  的半关闭造成的。因为  TCP  连接是全双工的(即数据可在两个方向上同时传递)所以进行关闭时每个方向上都要单独进行关闭。这个单方向的关闭就叫半关闭。当一方完成它的数据发送任务,就发送一个 FIN 来向另一方通告将要终止这个方向的连接。

1) 关闭客户端到服务器的连接:首先客户端 A 发送一个 FIN,用来关闭客户到服务器的数据传送,然后等待服务器的确认。其中终止标志位 FIN=1,序列号 seq=u

2) 服务器收到这个 FIN,它发回一个 ACK,确认号 ack 为收到的序号加 1。

3) 关闭服务器到客户端的连接:也是发送一个 FIN 给客户端。

4) 客户段收到 FIN 后,并发回一个 ACK 报文确认,并将确认序号 seq 设置为收到序号加 1。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

 HTTP 原理

HTTP是一个无状态的协议。无状态是指客户机(Web浏览器)和服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后服务器返回响应(response),连接就被关闭了,在服务器端不保留连接的有关信息.HTTP 遵循请求(Request)/应答(Response)模型。客户机(浏览器)向服务器发送请求,服务器处理请求并返回适当的应答。所有 HTTP 连接都被构造成一套请求和应答。

传输流程

1:地址解析

如用客户端浏览器请求这个页面:http://localhost.com:8080/index.htm 从中分解出协议名、主机名、端口、对象路径等部分,对于我们的这个地址,解析得到的结果如下: 协议名:http

主机名:localhost.com 端口:8080 对象路径:/index.htm

在这一步,需要域名系统 DNS 解析域名 localhost.com,得主机的 IP 地址。

2:封装HTTP请求数据包

把以上部分结合本机自己的信息,封装成一个 HTTP 请求数据包

3:封装成TCP包并建立连接

封装成 TCP 包,建立 TCP 连接(TCP 的三次握手)

4:客户机发送请求命

4)客户机发送请求命令:建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是    MIME    信息包括请求修饰符、客户机信息和可内容。

5:服务器响应

服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是 MIME 信息包括服务器信息、实体信息和可能的内容。

6:服务器关闭TCP连接

服务器关闭 TCP 连接:一般情况下,一旦 Web 服务器向浏览器发送了请求数据,它就要关闭 TCP 连接,然后如果浏览器或者服务器在其头信息加入了这行代码Connection:keep-alive,TCP   连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

HTTP 状态

状态码

原因短语

消息响应

100

Continue(继续)

101

Switching Protocol(切换协议)

成功响应

200

OK(成功)

201

Created(已创建)

202

Accepted(已创建)

203

Non-Authoritative Information(未授权信息)

204

No Content(无内容)

205

Reset Content(重置内容)

206

Partial Content(部分内容)

重定向

300

Multiple Choice(多种选择)

301

Moved Permanently(永久移动)

302

Found(临时移动)

303

See Other(查看其他位置)

304

Not Modified(未修改)

305

Use Proxy(使用代理)

306

unused(未使用)

307

Temporary Redirect(临时重定向)

308

Permanent Redirect(永久重定向)

客户端错误

400

Bad Request(错误请求)

401

Unauthorized(未授权)

402

Payment Required(需要付款)

403

Forbidden(禁止访问)

404

Not Found(未找到)

405

Method Not Allowed(不允许使用该方法)

406

Not Acceptable(无法接受)

407

Proxy Authentication Required(要求代理身份验证)

408

Request Timeout(请求超时)

409

Conflict(冲突)

410

Gone(已失效)

411

Length Required(需要内容长度头)

412

Precondition Failed(预处理失败)

413

Request Entity Too Large(请求实体过长)

414

Request-URI Too Long(请求网址过长)

415

Unsupported Media Type(媒体类型不支持)

416

Requested Range Not Satisfiable(请求范围不合要求)

417

Expectation Failed(预期结果失败)

服务器端错误

500

Internal Server Error(内部服务器错误)

501

Implemented(未实现)

502

Bad Gateway(网关错误)

503

Service Unavailable(服务不可用)

504

Gateway Timeout (网关超时)

 
  

状态码

原因短语

505

HTTP Version Not Supported(HTTP 版本不受支持)

HTTPS
 
  

建立连接获取证书

1) SSL 客户端通过 TCP 和服务器建立连接之后(443 端口),并且在一般的 tcp 连接协商(握手)过程中请求证书。即客户端发出一个消息给服务器,这个消息里面包含了自己可实现的算法列表和其它一些需要的消息,SSL         的服务器端会回应一个数据包,这里面确定了这次通信所需要的算法,然后服务器向客户端返回证书。(证书里面包含了服务器信息:域名。申请证书的公司,公共秘钥)。

证书验证

2) Client        在收到服务器返回的证书后,判断签发这个证书的公共签发机构,并使用这个机构的公共秘钥确认签名是否有效,客户端还会确保证书中列出的域名就是它正在连接的域名。

数据加密和传输

3) 如果确认证书有效,那么生成对称秘钥并使用服务器的公共秘钥进行加密。然后发送给服务器,服务器使用它的私钥对它进行解密,这样两台计算机可以开始进行对称加密进行通信。

 
  

 CDN 原理

CND 一般包含分发服务系统、负载均衡系统和管理系统

分发服务系统

其基本的工作单元就是各个    Cache     服务器。负责直接响应用户请求,将内容快速分发到用户;同时还负责内容更新,保证和源站内容的同步。根据内容类型和服务种类的不同, 分发服务系统分为多个子服务系统,如:网页加速服务、流媒体加速服务、应用加速服务等。每个子服务系统都是一个分布式的服务集群,由功能类似、地域接近的分布部署的Cache 集群组成。

在承担内容同步、更新和响应用户请求之外,分发服务系统还需要向上层的管理调度系统反馈各个Cache        设备的健康状况、响应情况、内容缓存状况等,以便管理调度系统能够根据设定的策略决定由哪个 Cache 设备来响应用户的请求。

负载均衡系统:

负载均衡系统是整个 CDN 系统的中枢。负责对所有的用户请求进行调度,确定提供给用户的最终访问地址。使用分级实现。最基本的两极调度体系包括全局负载均衡(GSLB)和本地负载均衡(SLB)。

GSLB 根据用户地址和用户请求的内容,主要根据就近性原则,确定向用户服务的节点。一般通过 DNS 解析或者应用层重定向(Http 3XX 重定向)的方式实现。

SLB 主要负责节点内部的负载均衡。当用户请求从 GSLB 调度到 SLB 时,SLB 会根据节点内各个

Cache 设备的工作状况和内容分布情况等对用户请求重定向。SLB 的实现有四层调度(LVS)、七层调度(Nginx)和链路负载调度等。

管理系统:分为运营管理和网络管理子系统。

网络管理系统实现对 CDN 系统的设备管理、拓扑管理、链路监控和故障管理,为管理员提供对全网资源的可视化的集中管理,通常用 web 方式实现。

运营管理是对 CDN 系统的业务管理,负责处理业务层面的与外界系统交互所必须的一些收集、整理、交付工作。包括用户管理、产品管理、计费管理、统计分析等。

 RPC

为什么要有RPC

http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果          是一个大型的网站,内部子系统较多、接口非常多的情况下,

RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;        发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。 后就是 近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑。

socket只是一个简单的网络通信方式,只是创建通信双方的通信通道,而要实现rpc的功能,还需要对其进行封装,以实现更多的功能。

RPC一般配合netty框架、spring自定义注解来编写轻量级框架,其实netty内部是封装了socket的,较新的jdk的IO一般是NIO,即非阻塞IO,在高并发网站中,RPC的优势会很明显

什么是RPC

 
  

RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等。

简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器

(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。

PRC架构组件

一个基本的RPC架构里面应该至少包含以下4个组件:

1、 客户端(Client):服务调用方(服务消费者)

2、 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端

3、 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理

4、 服务端(Server):服务的真正提供者

具体调用过程:

1、 服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;

2、 客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;

3、 客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;

4、 服务端存根(server stub)收到消息后进行解码(反序列化操作);

5、 服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;

6、 本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);

7、 服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;

8、 客户端存根(client stub)接收到消息,并进行解码(反序列化);

9、 服务消费方得到 终结果;而RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。

RPCSOASOAPREST的区别

1、REST

可以看着是HTTP协议的一种直接应用,默认基于JSON作为传输格式,使用简单, 学习成本低效率高,但是安全性较低。

2、SOAP

SOAP是一种数据交换协议规范,是一种轻量的、简单的、基于XML的协议的规范。而SOAP可以看着是一个重量级的协议,基于XML、SOAP在安全方面是通过使用XML-Security

和XML-Signature两个规范组成了WS-Security来实现安全控制的,当前已经得到了各个厂商的支持 。

它有什么优点?简单总结为:易用、灵活、跨语言、跨平台。

3、SOA

面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。服务层是SOA的基础,可以直接被应用调用,从而有效控制系统中与软件           代理交互的人为依赖性。

SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。SOA可以看作是B/S模型、XML(标准通用标记语言的子       集)/Web Service技术之后的自然延伸。

4、REST 和 SOAP、RPC 有何区别呢?

没什么太大区别,他们的本质都是提供可支持分布式的基础服务, 大的区别在于他们各自的的特点所带来的不同应用场景 。

RPC框架需要解决的问题?

1、 如何确定客户端和服务端之间的通信协议?

2、 如何更高效地进行网络通信?

3、 服务端提供的服务如何暴露给客户端?

4、 客户端如何发现这些暴露的服务?

5、 如何更高效地对请求对象和响应结果进行序列化和反序列化操作?

RPC的实现基础?

1、 需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架;

2、 需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架;

3、 可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等;

4、 如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能;

RPC使用了哪些关键技术?

1、动态代理

生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到 Java动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的:CGLib代理, Javassist字节码生成技术。

2、序列化和反序列化在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。           序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。

目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。

1、NIO通信

出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的

IO,即 NIO。Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以选择Netty或者MINA来解决NIO数据传输的问题。

2、服务注册中心

可选:Redis、Zookeeper、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。

主流RPC框架有哪些

1、RMI

利用java.rmi包实现,基于Java远程方法协议(Java Remote Method Protocol) 和java的原生序列化。

2、Hessian

是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。 基于HTTP协议,采用二进制编解码。

3、protobuf-rpc-pro是一个Java类库,提供了基于 Google 的 Protocol Buffers 协议的远程方法调用的框架。基于 Netty 底层的 NIO 技术。支持 TCP 重用/ keep-alive、SSL加密、RPC 调用取消操作、嵌入式日志等功能。

4、Thrift

是一种可伸缩的跨语言服务的软件框架。它拥有功能强大的代码生成引擎,无缝地支持C + +,C#,Java,Python和PHP和Ruby。thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。

初由facebook开发用做系统内个语言之间的RPC通信,2007年由facebook 贡献到apache基金 ,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信:

php语言client可以构造一个对象,调用相应的服务方法

来调用java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。5、Avro

出自Hadoop之父Doug Cutting, 在Thrift已经相当流行的情况下推出Avro的目标不仅是提供一套类似Thrift的通讯中间件,更是要建立一个新的,标准性的云计算的数据交换和存储的Protocol。支持HTTP,TCP两种协议。

6、Dubbo

Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。

 
  

RPC的实现原理架构图

PS:这张图非常重点,是PRC的基本原理,请大家一定记住!也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个        内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

比如说,A服务器想调用B服务器上的一个方法:

User getUserByName(String userName)

1、建立通信首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。

主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程           过程调用共享同一个连接。

通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。

2、服务寻址要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。        通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。

可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用

 
  

Redis或者Zookeeper来注册服务等等。

2.1 、 从服务提供者的角度看:当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找;当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务;服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。

2.2 、 从调用者的角度看:服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息;当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者;服务调用者下线的时候,则取消订阅。

3、网络传输3.1、序列化

当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要        先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。

3.2 、反序列化

当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方          式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用,

通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。

4、服务调用

B机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发            送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,            后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。通常,经过以上四个步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络抖动等原因需要重试等。

 Znode 有四种形式的目录节点

  1. PERSISTENT:持久的节点。
  2. EPHEMERAL:暂时的节点。
  3. PERSISTENT_SEQUENTIAL:持久化顺序编号目录节点。
  4. EPHEMERAL_SEQUENTIAL:暂时化顺序编号目录节点。

 负载均衡

负载均衡 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。

 四层负载均衡 vs 七层负载均衡

 
  
四层负载均衡(目标地址和端口交换)

主要通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。

以常见的 TCP 为例,负载均衡设备在接收到第一个来自客户端的 SYN 请求时,即通过上述方式选择一个最佳的服务器,并对报文中目标 IP 地址进行修改(改为后端服务器  IP), 直接转发给该服务器。TCP 的连接建立,即三次握手是客户端和服务器直接建立的,负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下,为保证服务器回包可以正确返回给负载均衡设备,在转发报文的同时可能还会对报文原来的源地址进行修改。实现四层负载均衡的软件有:

F5:硬件负载均衡器,功能很好,但是成本很高。lvs:重量级的四层负载软件。

nginx:轻量级的四层负载软件,带缓存功能,正则表达式较灵活。

haproxy:模拟四层转发,较灵活。

七层负载均衡(内容交换)

所谓七层负载均衡,也称为“内容交换”,也就是主要通过报文中的真正有意义的应用层内容,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器。七层应用           负载的好处,是使得整个网络更智能化。例如访问一个网站的用户流量,可以通过七层的方式,将对图片类的请求转发到特定的图片服务器并可以使用缓存技术;将对文字类的请求可以转发到特定的文字服务器并可以使用压缩技术。实现七层负载均衡的软件有:

haproxy:天生负载均衡技能,全面支持七层代理,会话保持,标记,路径转移; nginx:只在http 协议和mail协议上功能比较好,性能与haproxy差不多;  apache:功能较差

Mysql proxy:功能尚可。

 载均衡算法/策略

轮循均衡(Round Robin)

每一次来自网络的请求轮流分配给内部中的服务器,从 1 至 N 然后重新开始。此种均衡算法适合于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。

权重轮循均衡(Weighted Round Robin)

根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。例如:服务器 A 的权值被设计成 1,B 的权值是 3,C 的权值是 6,则服务器A、B、C 将分别接受到 10%、30%、60%的服务请求。此种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。

随机均衡(Random)把来自网络的请求随机分配给内部中的多个服务器。权重随机均衡(Weighted Random)

此种均衡算法类似于权重轮循算法,不过在处理请求分担时是个随机选择的过程。

响应速度均衡(Response Time 探测时间)

负载均衡设备对内部各服务器发出一个探测请求(例如           Ping),然后根据内部中各服务器对探测请求的最快响应时间来决定哪一台服务器来响应客户端的服务请求。此种均衡算法能较好的反映服务器的当前运行状态,但这最快响应时间仅仅指的是负载均衡设备与服务器间的最快响应时间,而不是客户端与服务器间的最快响应时间。

最少连接数均衡(Least Connection)

最少连接数均衡算法对内部中需负载的每一台服务器都有一个数据记录,记录当前该服务器正在处理的连接数量,当有新的服务连接请求时,将把当前请求分配给连接数最少的服务器,使均衡更加符合实际情况,负载更加均衡。此种均衡算法适合长时处理的请求服务,如 FTP。

处理能力均衡(CPU、内存)

此种均衡算法将把服务请求分配给内部中处理负荷(根据服务器   CPU   型号、CPU   数量、内存大小及当前连接数等换算而成)最轻的服务器,由于考虑到了内部服务器的处理能力及当前网络运行状况,所以此种均衡算法相对来说更加精确,尤其适合运用到第七层(应用层)负载均衡的情况下。

DNS响应均衡(Flash DNS)

在此均衡算法下,分处在不同地理位置的负载均衡设备收到同一个客户端的域名解析请求,并在同一时间内把此域名解析成各自相对应服务器的 IP 地址并返回给客户端,则客户端将以最先收到的域名解析 IP 地址来继续请求服务,而忽略其它的 IP 地址响应。在种均衡策略适合应用在全局负载均衡的情况下,对本地负载均衡是没有意义的。

哈希算法

一致性哈希一致性 Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

IP地址散列(保证客户端服务器对应关系稳定)

通过管理发送方 IP 和目的地 IP 地址的散列,将来自同一发送方的分组(或发送至同一目的地的分组)统一转发到相同服务器的算法。当客户端有一系列业务需要处理而必须和一个服务器反复通信时,该算法能够以流(会话)为单位,保证来自相同客户端的通信能够一直在同一服务器中进行处理。

URL散列

通过管理客户端请求 URL 信息的散列,将发送至相同 URL 的请求转发至同一服务器的算法。

 LVS

LVS 原理

IPVS

LVS 的 IP 负载均衡技术是通过 IPVS 模块来实现的,IPVS 是 LVS 集群系统的核心软件,它的主要作用是:安装在 Director Server 上,同时在 Director Server 上虚拟出一个 IP 地址,用户必须通过这个虚拟的 IP 地址访问服务器。这个虚拟 IP 一般称为 LVS 的 VIP,即 Virtual IP。访问的请求首先经过 VIP 到达负载调度器,然后由负载调度器从 Real Server 列表中选取一个服务节点响应用户的请求。 在用户的请求到达负载调度器后,调度器如何将请求发送到提供服务的 Real Server 节点,而 Real Server 节点如何返回数据给用户,是 IPVS 实现的重点技术。

 
  

ipvs : 工作于内核空间,主要用于使用户定义的策略生效 ipvsadm : 工作于用户空间,主要用于用户定义和管理集群服务的工具

ipvs 工作于内核空间的 INPUT 链上,当收到用户请求某集群服务时,经过 PREROUTING 链,经检查本机路由表,送往 INPUT 链;在进入 netfilter 的 INPUT 链时,ipvs 强行将请求报文通过ipvsadm 定义的集群服务策略的路径改为 FORWORD 链,将报文转发至后端真实提供服务的主机。

LVS NAT 模式
 
  

①.客户端将请求发往前端的负载均衡器,请求报文源地址是 CIP(客户端 IP),后面统称为 CIP),目标地址为 VIP(负载均衡器前端地址,后面统称为 VIP)。

②.负载均衡器收到报文后,发现请求的是在规则里面存在的地址,那么它将客户端请求报文的目标地址改为了后端服务器的 RIP 地址并将报文根据算法发送出去。

③.报文送到 Real Server 后,由于报文的目标地址是自己,所以会响应该请求,并将响应报文返还给 LVS。

④.然后 lvs 将此报文的源地址修改为本机并发送给客户端。

注意:在 NAT 模式中,Real Server 的网关必须指向 LVS,否则报文无法送达客户端特点:

1、NAT 技术将请求的报文和响应的报文都需要通过 LB 进行地址改写,因此网站访问量比较大的时候 LB 负载均衡调度器有比较大的瓶颈,一般要求最多之能 10-20 台节点

2、只需要在 LB 上配置一个公网 IP 地址就可以了。

3、每台内部的 realserver 服务器的网关地址必须是调度器 LB 的内网地址。

4、NAT 模式支持对 IP 地址和端口进行转换。即用户请求的端口和真实服务器的端口可以不一致。

优点:

集群中的物理服务器可以使用任何支持 TCP/IP 操作系统,只有负载均衡器需要一个合法的 IP 地址。

缺点:

扩展性有限。当服务器节点(普通 PC 服务器)增长过多时,负载均衡器将成为整个系统的瓶颈,因为所有的请求包和应答包的流向都经过负载均衡器。当服务器节点过多时,大量的数据包都交汇在负载均衡器那,速度就会变慢!

LVS DR 模式(局域网改写 mac 地址)

 
  

①.客户端将请求发往前端的负载均衡器,请求报文源地址是 CIP,目标地址为 VIP。

②.负载均衡器收到报文后,发现请求的是在规则里面存在的地址,那么它将客户端请求报文的源     MAC地址改为自己DIP的MAC地址,目标MAC改为了RIP的MAC地址,并将此包发送给RS。 ③.RS 发现请求报文中的目的 MAC 是自己,就会将次报文接收下来,处理完请求报文后,将响应报文通过 lo 接口送给 eth0 网卡直接发送给客户端。

注意:需要设置 lo 接口的 VIP 不能响应本地网络内的 arp 请求。

总结:

1、通过在调度器 LB 上修改数据包的目的 MAC 地址实现转发。注意源地址仍然是 CIP,目的地址仍然是 VIP 地址。

2、请求的报文经过调度器,而 RS 响应处理后的报文无需经过调度器 LB,因此并发访问量大时使用效率很高(和 NAT 模式比)

3、因为 DR 模式是通过 MAC 地址改写机制实现转发,因此所有 RS 节点和调度器 LB 只能在一个局域网里面

4、RS 主机需要绑定 VIP 地址在 LO 接口(掩码 32 位)上,并且需要配置 ARP 抑制。

5、RS 节点的默认网关不需要配置成 LB,而是直接配置为上级路由的网关,能让 RS 直接出网就可以。

6、由于 DR 模式的调度器仅做 MAC 地址的改写,所以调度器 LB 就不能改写目标端口,那么 RS 服务器就得使用和 VIP 相同的端口提供服务。

7、直接对外的业务比如 WEB 等,RS 的 IP 最好是使用公网 IP。对外的服务,比如数据库等最好使用内网 IP。

优点:

和  TUN(隧道模式)一样,负载均衡器也只是分发请求,应答包通过单独的路由方法返回给客户端。与  VS-TUN  相比,VS-DR  这种实现方式不需要隧道结构,因此可以使用大多数操作系统做为物理服务器。

DR 模式的效率很高,但是配置稍微复杂一点,因此对于访问量不是特别大的公司可以用

haproxy/nginx取代。日1000-2000W PV或者并发请求1万一下都可以考虑用haproxy/nginx。

缺点:所有 RS 节点和调度器 LB 只能在一个局域网里面

LVS TUN 模式(IP 封装、跨网段)

①.客户端将请求发往前端的负载均衡器,请求报文源地址是 CIP,目标地址为 VIP。

②.负载均衡器收到报文后,发现请求的是在规则里面存在的地址,那么它将在客户端请求报文的首部再封装一层  IP  报文,将源地址改为  DIP,目标地址改为  RIP,并将此包发送给RS。

③.RS 收到请求报文后,会首先拆开第一层封装,然后发现里面还有一层 IP 首部的目标地址是自己 lo 接口上的 VIP,所以会处理次请求报文,并将响应报文通过 lo 接口送给 eth0

网卡直接发送给客户端。

注意:需要设置 lo 接口的 VIP 不能在共网上出现。

总结:
  1. TUNNEL 模式必须在所有的 realserver 机器上面绑定 VIP 的 IP 地址
  2. TUNNEL 模式的 vip  >realserver 的包通信通过 TUNNEL 模式,不管是内网和外网都能通信,所以不需要 lvs vip 跟 realserver 在同一个网段内。
  3. TUNNEL 模式 realserver 会把 packet 直接发给 client 不会给 lvs 了4.TUNNEL 模式走的隧道模式,所以运维起来比较难,所以一般不用。优点:

负载均衡器只负责将请求包分发给后端节点服务器,而 RS 将应答包直接发给用户。所以,减少了负载均衡器的大量数据流动,负载均衡器不再是系统的瓶颈,就能处理很巨大的请求量,这种方式,一台负载均衡器能够为很多 RS 进行分发。而且跑在公网上就能进行不同地域的分发。

缺点:

隧道模式的 RS 节点需要合法 IP,这种方式需要所有的服务器支持”IP Tunneling”(IP Encapsulation)协议,服务器可能只局限在部分 Linux 系统上。

LVS FULLNAT 模式

无论是 DR 还是 NAT 模式,不可避免的都有一个问题:LVS 和 RS 必须在同一个 VLAN 下,否则

LVS 无法作为 RS 的网关。这引发的两个问题是:

1、同一个 VLAN 的限制导致运维不方便,跨 VLAN 的 RS 无法接入。

2、LVS 的水平扩展受到制约。当 RS 水平扩容时,总有一天其上的单点 LVS 会成为瓶颈。

Full-NAT 由此而生,解决的是 LVS 和 RS 跨 VLAN 的问题,而跨 VLAN 问题解决后,LVS 和 RS 不再存在 VLAN 上的从属关系,可以做到多个 LVS 对应多个 RS,解决水平扩容的问题。

Full-NAT 相比 NAT 的主要改进是,在 SNAT/DNAT 的基础上,加上另一种转换,转换过程如下:

  1. 在包从 LVS 转到 RS 的过程中,源地址从客户端 IP 被替换成了 LVS 的内网 IP。内网 IP 之间可以通过多个交换机跨 VLAN 通信。目标地址从 VIP 修改为 RS IP.
  2. 当 RS 处理完接受到的包,处理完成后返回时,将目标地址修改为 LVS ip,原地址修改为 RS IP,最终将这个包返回给 LVS 的内网 IP,这一步也不受限于 VLAN。
  3. LVS 收到包后,在 NAT 模式修改源地址的基础上,再把 RS 发来的包中的目标地址从 LVS 内网 IP 改为客户端的 IP,并将原地址修改为 VIP。

Full-NAT 主要的思想是把网关和其下机器的通信,改为了普通的网络通信,从而解决了跨 VLAN 的问题。采用这种方式,LVS 和 RS 的部署在 VLAN 上将不再有任何限制,大大提高了运维部署的便利性。

总结
  1. FULL NAT 模式不需要 LBIP 和 realserver ip 在同一个网段;
  2. full nat 因为要更新 sorce ip 所以性能正常比 nat 模式下降 10%

 Keepalive

keepalive 起初是为 LVS 设计的,专门用来监控 lvs 各个服务节点的状态,后来加入了 vrrp 的功能,因此除了 lvs,也可以作为其他服务(nginx,haproxy)的高可用软件。VRRP 是 virtual router redundancy protocal(虚拟路由器冗余协议)的缩写。VRRP 的出现就是为了解决静态路由出现的单点故障,它能够保证网络可以不间断的稳定的运行。所以 keepalive 一方面具有 LVS cluster node healthcheck 功能,另一方面也具有 LVS director failover。

 Nginx 反向代理负载均衡

普通的负载均衡软件,如LVS,其实现的功能只是对请求数据包的转发、传递,从负载均衡下的节点服务器来看,接收到的请求还是来自访问负载均衡器的客户端的真实用户;而           反向代理就不一样了,反向代理服务器在接收访问用户请求后,会代理用户 重新发起请求代理下的节点服务器,最后把数据返回给客户端用户。在节点服务器看来,访问的节点服务器的客户端用户就是反向代理服务器,而非真实的网站访问用户。

upstream_module 和健康检测

ngx_http_upstream_module 是负载均衡模块,可以实现网站的负载均衡功能即节点的健康检查,upstream 模块允许 Nginx 定义一组或多组节点服务器组,使用时可通过

proxy_pass 代理方式把网站的请求发送到事先定义好的对应 Upstream 组 的名字上。

upstream

模块内参数

参数说明

weight

服务器权重

max_fails

Nginx 尝试连接后端主机失败的此时,这是值是配合 proxy_next_upstream、 fastcgi_next_upstream 和 memcached_next_upstream 这三个参数来使用的。当 Nginx 接收后端服务器返回这三个参数定义的状态码时,会将这个请求转发给正常工作的的后端服务器。如 404、503、503,max_files=1

fail_timeout

max_fails 和 fail_timeout 一般会关联使用,如果某台 server 在 fail_timeout 时间内出现了 max_fails 次连接失败,那么 Nginx 会认为其已经挂掉,从而在 fail_timeout 时间内不再去请求它,fail_timeout 默认是 10s,max_fails 默认是 1,即默认情况只要是发生错误就认为服务器挂了,如果将 max_fails 设置为 0,则表示取消这项检查

backup

表示当前 server 是备用服务器,只有其它非 backup 后端服务器都挂掉了或很忙才会分配请求给它

down

标志服务器永远不可用,可配合 ip_hash 使用

 
  

proxy_pass 请求转发

 
  

proxy_pass 指令属于 ngx_http_proxy_module 模块,此模块可以将请求转发到另一台服务器,在实际的反向代理工作中,会通过 location 功能匹配指定的 URI,然后把接收到服务匹配 URI 的请求通过 proyx_pass 抛给定义好的 upstream 节点池。

#交给后端 upstream 为 download 的节点

proxy模块参数

说明

proxy_next_upstream

什么情况下将请求传递到下一个 upstream

proxy_limite_rate

限制从后端服务器读取响应的速率

proyx_set_header

设置 http 请求 header 传给后端服务器节点,如:可实现让代理后端的服务器节点获取访问客户端的这是 ip

client_body_buffer_size

客户端请求主体缓冲区大小

proxy_connect_timeout

代理与后端节点服务器连接的超时时间

proxy_send_timeout

后端节点数据回传的超时时间

proxy_read_timeout

设置 Nginx 从代理的后端服务器获取信息的时间,表示连接成功建立后,Nginx 等待后端服务器的响应时间

proxy_buffer_size

设置缓冲区大小

proxy_buffers

设置缓冲区的数量和大小

proyx_busy_buffers_size

用于设置系统很忙时可以使用的 proxy_buffers 大小,推荐为 proxy_buffers*2

proxy_temp_file_write_size

指定 proxy 缓存临时文件的大小

 Linux(李老师)

 Linux概述

1. 什么是Linux

Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和Unix 的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的Unix工  具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网 络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

2. UnixLinux有什么区别?

Linux和Unix都是功能强大的操作系统,都是应用广泛的服务器操作系统,有很   多相似之处,甚至有一部分人错误地认为Unix和Linux操作系统是一样的,然   而,事实并非如此,以下是两者的区别。

  1. 开源性 Linux是一款开源操作系统,不需要付费,即可使用;Unix是一款对源码实行知 识产权保护的传统商业软件,使用需要付费授权使用。
  2. 跨平台性 Linux操作系统具有良好的跨平台性能,可运行在多种硬件平台上;Unix操作系 统跨平台性能较弱,大多需与硬件配套使用。
  3. 可视化界面 Linux除了进行命令行操作,还有窗体管理系统;Unix只是命令行下的系统。
  4. 硬件环境 Linux操作系统对硬件的要求较低,安装方法更易掌握;Unix对硬件要求比较苛 刻,按照难度较大。
  5. 用户群体  Linux的用户群体很广泛,个人和企业均可使用;Unix的用户群体比较窄,多是  安全性要求高的大型企业使用,如银行、电信部门等,或者Unix硬件厂商使  用, 如Sun等。

相比于Unix操作系统,Linux操作系统更受广大计算机爱好者的喜爱,主要原因  是Linux操作系统具有Unix操作系统的全部功能,并且能够在普通PC计算机上实  现全部的Unix特性,开源免费的特性,更容易普及使用!

3. 什么是 Linux 内核?

Linux   系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要   时分配硬件,并根据需要执行软件。系统内存管理

应用程序管理硬件设备管理文件系统管理

4. Linux的基本组件是什么?

就像任何其他典型的操作系统一样,Linux拥有所有这些组件:内核,shell和   GUI,系统实用程序和应用程序。Linux比其他操作系统更具优势的是每个方面    都附带其他功能,所有代码都可以免费下载。

5. Linux 的体系结构

从大的方面讲,Linux 体系结构可以分为两块:

用户空间(User Space) :用户空间又包括用户的应用程序(User Applications)、C 库(C Library) 。

内核空间(Kernel Space) :内核空间又包括系统调用接口(System Call Interface)、内核(Kernel)、平台架构相关的代码(Architecture-Dependent Kernel Code) 。

为什么 Linux 体系结构要分为用户空间和内核空间的原因?

现代 CPU 实现了不同的工作模式,不同模式下 CPU 可以执行的指令和访问 的寄存器不同。

Linux 从 CPU 的角度出发,为了保护内核的安全,把系统分成了两部分。

用户空间和内核空间是程序执行的两种不同的状态,我们可以通过两种方式完成 用户空间到内核空间的转移:1)系统调用;2)硬件中断。

6. BASHDOS之间的基本区别是什么?

BASH和DOS控制台之间的主要区别在于3个方面:

BASH命令区分大小写,而DOS命令则不区分;

在BASH下,/ character是目录分隔符,\作为转义字符。在DOS下,/用作命令 参数分隔符,\是目录分隔符

DOS遵循命名文件中的约定,即8个字符的文件名后跟一个点,扩展名为3个字 符。BASH没有遵循这样的惯例。

7. Linux 开机启动过程?

了解即可。
  1. 主机加电自检,加载 BIOS 硬件信息。
  2. 读取 MBR 的引导文件(GRUB、LILO)。
  3. 引导 Linux 内核。
  4. 运行第一个进程 init (进程号永远为 1 )。
  5. 进入相应的运行级别。
  6. 运行终端,输入用户名和密码。

8. Linux系统缺省的运行级别?

关机。

单机用户模式。

字符界面的多用户模式(不支持网络)。字符界面的多用户模式。

未分配使用。

图形界面的多用户模式。重启。

9. Linux 使用的进程间通信方式?

了解即可,不需要太深入。
  1. 管道(pipe)、流管道(s_pipe)、有名管道(FIFO)。
  2. 信号(signal) 。
  3. 消息队列。
  4. 共享内存。
  5. 信号量。
  6. 套接字(socket) 。

10. Linux 有哪些系统日志文件?

比较重要的是 /var/log/messages 日志文件。

该日志文件是许多进程日志文件的汇总,从该文件可以看出任何入侵企图或成功的入 侵。另外,如果胖友的系统里有 ELK 日志集中收集,它也会被收集进去。

Linux系统安装多个桌面环境有帮助吗?

通常,一个桌面环境,如KDE或Gnome,足以在没有问题的情况下运行。尽管系统允许从一个环境切换到另一个环境,但这对用户来说都是优先考虑的问题。有些程序在一个环         境中工作而在另一个环境中无法工作,因此它也可以被视为选择使用哪个环境的一个因素。

什么是交换空间?

交换空间是Linux使用的一定空间,用于临时保存一些并发运行的程序。当RAM 没有足够的内存来容纳正在执行的所有程序时,就会发生这种情况。

什么是root帐户

root帐户就像一个系统管理员帐户,允许你完全控制系统。你可以在此处创建和维护用户帐户,为每个帐户分配不同的权限。每次安装Linux时都是默认帐户。

什么是LILO

LILO是Linux的引导加载程序。它主要用于将Linux操作系统加载到主内存中,以便它可以开始运行。什么是BASH? BASH是Bourne Again SHell的缩写。它由Steve Bourne编写,作为原始

Bourne Shell(由/ bin / sh表示)的替代品。它结合了原始版本的Bourne

Shell的所有功能,以及其他功能,使其更容易使用。从那以后,它已被改编为运行Linux的大多数系统的默认shell。

什么是CLI

命令行界面(英语command-line interface,缩写]:CLI)是在图形用

户界面得到普及之前使用 为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为字符用户界面

(CUI)。

通常认为,命令行界面(CLI)没有图形用户界面(GUI)那么方便用户操作。

因为,命令行界面的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行界面要较图形用户界面节约计算机系统的资源。在熟记命令的前提下,使用命令行界面往往要较使用图形用户界面的操作速度要快。所以,图形用户界面的操作系统中,都保留着可选的命令行界面。

什么是GUI

图形用户界面(Graphical User Interface,简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面。

图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。

开源的优势是什么?

开源允许你将软件(包括源代码)免费分发给任何感兴趣的人。然后,人们可以添加功能,甚至可以调试和更正源代码中的错误。它们甚至可以让它运行得更好,然后再次自由地重新分配这些增强的源代码。这 终使社区中的每个人受益。

GNU项目的重要性是什么?

这种所谓的自由软件运动具有多种优势,例如可以自由地运行程序以及根据你的需要自由学习和修改程序。它还允许你将软件副本重新分发给其他人,以及自由改进软件并将其发布给公众。

磁盘、目录、文件

简单 Linux 文件系统?

在 Linux  操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。

也就是说在 Linux 系统中有一个重要的概念:一切都是文件。其实这是

Unix 哲学的一个体现,而 Linux 是重写 Unix 而来,所以这个概念也就传承了下来。在 Unix 系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。

Linux 支持 5 种文件类型,如下图所示:

Linux 的目录结构是怎样的?

这个问题,一般不会问。更多是实际使用时,需要知道。

Linux 文件系统的结构层次鲜明,就像一棵倒立的树, 顶层是其根目录:

 
  

常见目录说明:

/bin: 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里;

/etc: 存放系统管理和配置文件;

/home: 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是/home/user,可以用~user表示;

/usr : 用于存放系统应用程序;

/opt: 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把

tomcat等都安装到这里;

/proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;

/root: 超级用户(系统管理员)的主目录(特权阶级o);

/sbin: 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等;

/dev: 用于存放设备文件;

/mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统;

/boot: 存放用于系统引导时使用的各种文件;

/lib : 存放着和系统运行相关的库文件 ;

/tmp: 用于存放各种临时文件,是公用的临时文件存储点;

/var: 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等;

/lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里。

什么是 inode

一般来说,面试不会问 inode 。但是 inode 是一个重要概念,是理解 Unix/Linux 文件系统和硬盘储存的基础。理解inode,要从文件储存说起。

文件储存在硬盘上,硬盘的 小存储单位叫做"扇区"(Sector)。每个扇区储存

512字节(相当于0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取         的 小单位。"块"的大小, 常见的是4KB,即连续八个 sector组成一个 block。

文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫            做inode,中文译名为"索引节点"。

每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

简述 Linux 文件系统通过 i 节点把文件的逻辑结构和物理结构转换的工作过程? 如果看的一脸懵逼,也没关系。一般来说,面试官不太会问这个题目。

Linux 通过 inode 节点表将文件的逻辑结构和物理结构进行转换。

inode 节点是一个 64 字节长的表,表中包含了文件的相关信息,其中有文件的大小、文件所有者、文件的存取许可方式以及文件的类型等重要信息。在 inode 节点表中 重要的内容是磁盘地址表。在磁盘地址表中有 13 个块号,文件将以块号在磁盘地址表中出现的顺序依次读取相应的块。

Linux 文件系统通过把 inode  节点和文件名进行连接,当需要读取该文件时,文件系统在当前目录表中查找该文件名对应的项,由此得到该文件相对应的  inode  节点号,通过该 inode 节点的磁盘地址表把分散存放的文件物理块连接成文件的逻辑结构

什么是硬链接和软链接?

1) 硬链接

由于  Linux  下的文件是通过索引节点(inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配  inode  。每添加一个一个硬链接,文件的链接数就加 1 。

不足:1)不可以在不同文件系统的文件间建立链接;2)只有超级用户才可以为目录创建硬链接。

2) 软链接软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。

不足:因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;还有它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。

实际场景下,基本是使用软链接。总结区别如下: 硬链接不可以跨分区,软件链可以跨分区。

硬链接指向一个 inode 节点,而软链接则是创建一个新的 inode 节点。

删除硬链接文件,不会删除原文件,删除软链接文件,会把原文件删除。

RAID 是什么?

RAID 全称为独立磁盘冗余阵列(Redundant Array of Independent Disks),基本思想就是把多个相对便宜的硬盘组合起来,成为一个硬盘阵列组,使性能达到甚至超过一个价格昂贵、 容量巨大的硬盘。RAID 通常被用在服务器电脑上,使用完全相同的硬盘组成一个逻辑扇区,因此操作系统只会把它当做一个硬盘。

RAID  分为不同的等级,各个不同的等级均在数据可靠性及读写性能上做了不同的权衡。在实际应用中,可以依据自己的实际需求选择不同的  RAID   方案。当然,因为很多公司都使用云服务,大家很难接触到 RAID 这个概念,更多的可能是普通云盘、SSD 云盘酱紫的概念。

 安全

一台 Linux 系统初始化环境后需要做一些什么安全工作?

1、添加普通用户登陆,禁止 root 用户登陆,更改 SSH 端口号。

修改 SSH  端口不一定绝对哈。当然,如果要暴露在外网,建议改下。l 2、服务器使用密钥登陆,禁止密码登陆。

3、开启防火墙,关闭 SElinux ,根据业务需求设置相应的防火墙规则。

4、 装 fail2ban 这种防止 SSH 暴力破击的软件。

5、 设置只允许公司办公网出口 IP 能登陆服务器(看公司实际需要) 也可以安装 VPN 等软件,只允许连接 VPN 到服务器上。

6、 只允许有需要的服务器可以访问外网,其它全部禁止。

7、 做好软件层面的防护。

7.1 设置 nginx_waf 模块防止 SQL 注入。

7.2 把 Web 服务使用 www 用户启动,更改网站目录的所有者和所属组为 www 。

什么叫 CC 攻击?什么叫 DDOS 攻击?

CC 攻击,主要是用来攻击页面的,模拟多个用户不停的对你的页面进行访问,从而使你的系统资源消耗殆尽。

DDOS 攻击,中文名叫分布式拒绝服务攻击,指借助服务器技术将多个计算机联合起来作为攻击平台,来对一个或多个目标发动 DDOS 攻击。

攻击,即是通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的。

怎么预防 CC 攻击和 DDOS 攻击?防 CC、DDOS 攻击,这些只能是用硬件防火墙做流量清洗,将攻击流量引入黑洞。

流量清洗这一块,主要是买 ISP 服务商的防攻击的服务就可以,机房一般有空余流量,我们一般是买服务,毕竟攻击不会是持续长时间。

什么是网站数据库注入?

由于程序员的水平及经验参差不齐,大部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断。

应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的 SQL 注入。

SQL注入,是从正常的 WWW 端口访问,而且表面看起来跟一般的 Web 页面访问没什么区别,如果管理员没查看日志的习惯,可能被入侵很长时间都不会发觉。

如何过滤与预防?

数据库网页端注入这种,可以考虑使用 nginx_waf 做过滤与预防。

Shell

本小节为选读。我也不太会写 Shell 脚本,都是写的时候,在网络上拼拼凑凑。。。

Shell 脚本是什么?

一个  Shell  脚本是一个文本文件,包含一个或多个命令。作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Shell   脚本) 来完成这些日常工作任务。

 
  

什么是默认登录 Shell ?在 Linux 操作系统,"/bin/bash" 是默认登录 Shell,是在创建用户时分配的。使用 chsh 命令可以改变默认的 Shell 。示例如下所示:

在 Shell 脚本中,如何写入注释?

 
  

注释可以用来描述一个脚本可以做什么和它是如何工作的。每一行注释以 ## 开头。例子如下:

语法级

可以在 Shell 脚本中使用哪些类型的变量?

在 Shell 脚本,我们可以使用两种类型的变量: 系统定义变量

系统变量是由系统系统自己创建的。这些变量通常由大写字母组成,可以通过 set 命令查看。

用户定义变量

用户变量由系统用户来生成和定义,变量的值可以通过命令 "echo $<变量名>" 查看。

Shell脚本中 $? 标记的用途是什么?在写一个 Shell 脚本时,如果你想要检查前一命令是否执行成功,在 if 条件中使用 $? 可以来检查前一命令的结束状态。

 
  

如果结束状态是 0 ,说明前一个命令执行成功。例如:

如果结束状态不是0,说明命令执行失败。例如:

 
  

Bourne Shell(bash) 中有哪些特殊的变量?下面的表列出了 Bourne Shell 为命令行设置的特殊变量。

如何取消变量或取消变量赋值? unset 命令用于取消变量或取消变量赋值。语法如下所示:

 
  

Shell 脚本中 if 语法如何嵌套?

 
  

在 Shell 脚本中如何比较两个数字?在 if-then 中使用测试命令( -gt 等)来比较两个数字。例如:

Shell 脚本中 case 语句的语法? 基础语法如下:

 
  

Shell 脚本中 for 循环语法?基础语法如下:

Shell 脚本中 while 循环语法?如同 for 循环,while 循环只要条件成立就重复它的命令块。不同于 for循环,while 循环会不断迭代,直到它的条件不为真。基础语法:

do-while 语句的基本格式?

 
  

do-while 语句类似于 while 语句,但检查条件语句之前先执行命令(LCTT 译注:意即至少执行一次。)。下面是用 do-while 语句的语法:

Shell 脚本中 break 命令的作用?

break 命令一个简单的用途是退出执行中的循环。我们可以在 while 和 until 循环中使用 break 命令跳出循环。 Shell 脚本中 continue 命令的作用? continue 命令不同于 break

命令,它只跳出当前循环的迭代,而不是整个循环。 continue 命令很多时候是很有用的,例如错误发生,但我们依然希望继续执行大循环的时候。

如何使脚本可执行?

使用 chmod 命令来使脚本可执行。例子如下:chmod a+x myscript.sh 。

#!/bin/bash 的作用?

#!/bin/bash 是 Shell 脚本的第一行,称为释伴(shebang)行。

这里 ## 符号叫做 hash ,而 ! 叫做 bang。它的意思是命令通过 /bin/bash 来执行。如何调试 Shell脚本?

使用 -x' 数(sh -x myscript.sh)可以调试 Shell脚本。另一个种方法是使用 -nv 参数(sh -nv myscript.sh)。

如何将标准输出和错误输出同时重定向到同一位置?

方法一:2>&1 (如### ls /usr/share/doc > out.txt 2>&1 ) 。方法二:&> (如### ls /usr/share/doc &> out.txt ) 。

在 Shell 脚本中,如何测试文件?

test 命令可以用来测试文件。基础用法如下表格:

在 Shell 脚本如何定义函数呢?

 
  

函数是拥有名字的代码块。当我们定义代码块,我们就可以在我们的脚本调用函数名字,该块就会被执行。示例如下所示:

如何让 Shell 就脚本得到来自终端的输入?

 
  

read 命令可以读取来自终端(使用键盘)的数据。read 命令得到用户的输入并置于你给出的变量中。例子如下:

如何执行算术运算?

有两种方法来执行算术运算:

1、使用 expr 命令:### expr 5 + 2 。

2、用一个美元符号和方括号($[ 表达式 ]):test=$[16 + 4] ; test=$[16 + 4]

编程题()

 
  

判断一文件是不是字符设备文件,如果是将其拷贝到 /dev 目录下?

添加一个新组为 class1 ,然后添加属于这个组的 30 个用户,用户名的形式为 stdxx ,其中 xx 从 01 到 30 ?

 
  

编写 Shell 程序,实现自动删除 50 个账号的功能,账号名为stud1 至 stud50?

写一个 sed 命令,修改 /tmp/input.txt 文件的内容? 要求:

删除所有空行。

 
  

一行中,如果包含 “11111”,则在 “11111” 前面插入 “AAA”,在 “11111” 后面插入 “BBB” 。比如:将内容为 `000111112222 的一行改为 0000AAA11111BBB2222 。

实战如何选择 Linux 操作系统版本?

一般来讲,桌面用户首选 Ubuntu ;服务器首选 RHEL 或 CentOS ,两者中首选 CentOS 。根据具体要求:

安全性要求较高,则选择 Debian 或者 FreeBSD 。

需要使用数据库高级服务和电子邮件网络应用的用户可以选择 SUSE 。

想要新技术新功能可以选择 Feddora ,Feddora 是 RHEL 和 CentOS 的一个测试版和预发布版本。

【重点】根据现有状况,绝大多数互联网公司选择 CentOS 。现在比较常用的是 6 系列,现在市场占有大概一半左右。另外的原因是 CentOS

更侧重服务器领域,并且无版权约束。

CentOS 7 系列,也慢慢使用的会比较多了。

如何规划一台 Linux 主机,步骤是怎样?

1、确定机器是做什么用的,比如是做 WEB 、DB、还是游戏服务器。不同的用途,机器的配置会有所不同。

2、确定好之后,就要定系统需要怎么安装,默认安装哪些系统、分区怎么做。

3、需要优化系统的哪些参数,需要创建哪些用户等等的。

请问当用户反馈网站访问慢,你会如何处理?

有哪些方面的因素会导致网站网站访问慢?

1、服务器出口带宽不够用

本身服务器购买的出口带宽比较小。一旦并发量大的话,就会造成分给每个用户的出口带宽就小,访问速度自然就会慢。

跨运营商网络导致带宽缩减。例如,公司网站放在电信的网络上,那么客户这边对接是长城宽带或联通,这也可能导致带宽的缩减。

2、服务器负载过大,导致响应不过来可以从两个方面入手分析:

分析系统负载,使用 w 命令或者 uptime 命令查看系统负载。如果负载很高,则使用 top 命令查看 CPU ,MEM 等占用情况,要么是 CPU 繁忙,要么是内存不够。

如果这二者都正常,再去使用 sar 命令分析网卡流量,分析是不是遭到了攻击。一旦分析出问题的原因,采取对应的措施解决,如决定要不要杀死一些进程,或者禁止一些访问等。

3、数据库瓶颈

如果慢查询比较多。那么就要开发人员或 DBA 协助进行 SQL 语句的优化。

如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等。然后,也可以搭建 MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。

4、网站开发代码没有优化好

例如 SQL 语句没有优化,导致数据库读写相当耗时。针对网站访问慢,怎么去排查?

1、首先要确定是用户端还是服务端的问题。当接到用户反馈访问慢,那边自己立即访问网站看看,如果自己这边访问快,基本断定是用户端问题,就需要耐心跟客户解释,协            助客户解决问题。不要上来就看服务端的问题。一定要从源头开始,逐步逐步往下。

2、如果访问也慢,那么可以利用浏览器的调试功能,看看加载那一项数据消耗时间过多,是图片加载慢,还是某些数据加载慢。

3、针对服务器负载情况。查看服务器硬件(网络、CPU、内存)的消耗情况。如果是购买的云主机,比如阿里云,可以登录阿里云平台提供各方面的监控,比如         CPU、内存、带宽的使用情况。

4、如果发现硬件资源消耗都不高,那么就需要通过查日志,比如看看 MySQL慢查询的日志,看看是不是某条 SQL 语句查询慢,导致网站访问慢。怎么去解决?

1、 如果是出口带宽问题,那么久申请加大出口带宽。

2、 如果慢查询比较多,那么就要开发人员或 DBA 协助进行 SQL 语句的优化。

3、 如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等等。然后也可以搭建MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。

4、 申请购买 CDN 服务,加载用户的访问。

5、 如果访问还比较慢,那就需要从整体架构上进行优化咯。做到专角色专用,多台服务器提供同一个服务。

Linux 性能调优都有哪几种方法?

1、Disabling daemons (关闭 daemons)。2、Shutting down the GUI (关闭 GUI)。

3、Changing kernel parameters (改变内核参数)。

4、Kernel parameters (内核参数)。

5、Tuning the processor subsystem (处理器子系统调优)。6、Tuning the memory subsystem (内存子系统调优)。

7、Tuning the file system (文件系统子系统调优)。

8、Tuning the network subsystem(网络子系统调优)。

文件管理命令 cat 命令

cat 命令用于连接文件并打印到标准输出设备上。

cat 主要有三大功能:

 
  

一次显示整个文件:

  1. 从键盘创建一个文件
 
  

只能创建新文件,不能编辑已有文件

 
  

将几个文件合并为一个文件:

-b 对非空输出行号

-n 输出所有行号实例:

(1) 

 
  

把 log2012.log 的文件内容加上行号后输入 log2013.log 这个文件里

(2) 把 log2012.log 和 log2013.log 的文件内容加上行号(空白行不加)之后将内容附加到 log.log 里

(3) 

 
  

使用 here doc 生成新文件

(4) 反向列示

chmod 命令

Linux/Unix 的文件调用权限分为三级 : 文件拥有者、群组、其他。利用 chmod 可以控制文件如何被他人所调用。

用于改变 linux 系统文件或目录的访问权限。用它控制文件或目录的访问权限。该命令有两种用法。一种是包含字母和操作符表达式的文字设定法;另一种是包含数字的数字设定法。

每一文件或目录的访问权限都有三组,每组用三位表示,分别为文件属主的读、写和执行权限;与属主同组的用户的读、写和执行权限;系统中其他用户的读、写和执行权限。可使用 ls -l test.txt 查找。

 
  

以文件 log2012.log 为例:

第一列共有 10 个位置,第一个字符指定了文件类型。在通常意义上,一个目录也是一个文件。如果第一个字符是横线,表示是一个非目录的文件。如果是 d,表示是一个目录。从第二个字符开始到第十个 9 个字符,3 个字符一组,分别表示了 3 组用户对文件或者目录的权限。权限字符用横线代表空许可,r 代表只读,w 代表写,x 代表可执行。常用参数:

 
  

权限范围:

权限代号:

实例:

(1) 

 
  

增加文件 t.log 所有用户可执行权限

(2) 撤销原来所有的权限,然后使拥有者具有可读权限,并输出处理信息

(3) 给 file 的属主分配读、写、执行(7)的权限,给file的所在组分配读、执行

 
  

(5)的权限,给其他用户分配执行(1)的权限

(4) 将 test 目录及其子目录所有文件添加可读权限

chown 命令

 
  

chown 将指定文件的拥有者改为指定的用户或组,用户可以是用户名或者用户 ID;组可以是组名或者组 ID;文件是以空格分开的要改变权限的文件列表,支持通配符。

实例:

 
  

(1) 改变拥有者和群组 并显示改变信息

(1) 改变文件群组

(2) 

 
  

改变文件夹及子文件目录属主及属组为 mail

cp 命令

将源文件复制至目标文件,或将多个源文件复制至目标目录。

 
  

注意:命令行复制,如果目标文件已经存在会提示是否覆盖,而在 shell 脚本中,如果不加 -i 参数,则不会提示,而是直接覆盖!

实例:

(1) 

 
  

复制 a.txt 到 test 目录下,保持原文件时间,如果原文件存在提示是否覆盖。

(2) 为 a.txt 建议一个链接(快捷方式)

find 命令

 
  

用于在文件树中查找文件,并作出相应的处理。命令格式:

命令参数:

 
  

命令选项:

实例:

(1) 

 
  

查找 48 小时内修改过的文件

(2) 在当前目录查找 以 .log 结尾的文件。 . 代表当前目录

(3) 查找 /opt 目录下 权限为 777 的文件

 
  

(4) 查找大于 1K 的文件

 
  

查找等于 1000 字符的文件

-exec 参数后面跟的是 command 命令,它的终止是以 ; 为结束标志的,所以

这句命令后面的分号是不可缺少的,考虑到各个系统中分号会有不同的意义,所以前面加反斜杠。{} 花括号代表前面find查找出来的文件名。

head 命令

 
  

head 用来显示档案的开头至标准输出中,默认 head 命令打印其相应文件的开头 10 行。常用参数:

实例:

(1) 

 
  

显示 1.log 文件中前 20 行

(2) 显示 1.log 文件前 20 字节

(3) 

 
  

显 示 t.log 后 10 行

less 命令

 
  

less 与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件。常用命令参数:

实例:

(1) 

 
  

ps 查看进程信息并通过 less 分页显示

(2) 查看多个文件

可以使用 n 查看下一个,使用 p 查看前一个。

ln 命令

功能是为文件在另外一个位置建立一个同步的链接,当在不同目录需要该问题时,就不需要为每一个目录创建同样的文件,通过     ln     创建的链接(link)减少磁盘占用量。链接分类:软件链接及硬链接软链接:

  1. 软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式
  2. 软链接可以 跨文件系统 ,硬链接不可以
  3. 软链接可以对一个不存在的文件名进行链接
  4. 软链接可以对目录进行链接硬链接:
  5. 硬链接,以文件副本的形式存在。但不占用实际空间。
  6. 不允许给目录创建硬链接
  7. 硬链接只有在同一个文件系统中才能创建需要注意:

第一:ln命令会保持每一处链接文件的同步性,也就是说,不论你改动了哪一处,其它的文件都会发生相同的变化;

第二:ln的链接又分软链接和硬链接两种,软链接就是ln –s 源文件 目标文件,它只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间,硬链接 ln 源文件 目标文件, 没有参数-s, 它会在你选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。

第三:ln指令用在链接文件或目录,如同时指定两个以上的文件或目录,且 后的目的地是一个已经存在的目录,则会把前面指定的所有文件或目录复制到该目录中。若同时指定多个文件或目录,且 后的目的地并非是一个已存在的目录,则会出现错误信息。

 
  

常用参数:

实例:

(1) 

 
  

给文件创建软链接,并显示操作信息

(2) 给文件创建硬链接,并显示操作信息

(3) 

 
  

给目录创建软链接

locate 命令

locate 通过搜寻系统内建文档数据库达到快速找到档案,数据库由 updatedb

程序来更新,updatedb 是由 cron daemon 周期性调用的。默认情况下

locate 命令在搜寻数据库时比由整个由硬盘资料来搜寻资料来得快,但较差劲的是  locate 所找到的档案若是  近才建立或  刚更名的,可能会找不到,在内定值中,updatedb 每天会跑一次,可以由修改 crontab 来更新设定值

(etc/crontab)。

 
  

locate 与 find 命令相似,可以使用如 *、? 等进行正则匹配查找常用参数:

实例:

(1) 

 
  

查找和 pwd 相关的所有文件(文件名中包含 pwd)

(2) 搜索 etc 目录下所有以 sh 开头的文件

(3) 

 
  

查找 /var 目录下,以 reason 结尾的文件

more 命令

 
  

功能类似于 cat, more 会以一页一页的显示方便使用者逐页阅读,而 基本的指令就是按空白键(space)就往下一页显示,按 b 键就会往回(back)一页显示。命令参数:

常用操作命令:

实例:

(1) 

 
  

显示文件中从第3行起的内容

(2) 在所列出文件目录详细信息,借助管道使每次显示 5 行

按空格显示下 5 行。

mv 命令

移动文件或修改文件名,根据第二参数类型(如目录,则移动文件;如为文件则重命令该文件)。

当第二个参数为目录时,第一个参数可以是多个以空格分隔的文件或目录,然后移动第一个参数指定的多个文件到第二个参数指定的目录中。实例:

(1) 

 
  

将文件 test.log 重命名为 test1.txt

(2) 将文件 log1.txt,log2.txt,log3.txt 移动到根的 test3 目录中

(3) 

 
  

将文件 file1 改名为 file2,如果 file2 已经存在,则询问是否覆盖

(4) 移动当前文件夹下的所有文件到上一级目录

rm 命令

 
  

删除一个目录中的一个或多个文件或目录,如果没有使用 -r 选项,则 rm 不会删除目录。如果使用 rm 来删除文件,通常仍可以将该文件恢复原状。

实例:

(1) 

 
  

删除任何 .log 文件,删除前逐一询问确认:

(2) 删除 test 子目录及子目录中所有档案删除,并且不用一一确认:

(3) 删除以 -f 开头的文件

tail 命令

 
  

用于显示指定文件末尾内容,不指定文件时,作为输入信息进行处理。常用查看日志文件。常用参数:

(1)循环读取逐渐增加的文件内容

 
  

后台运行:可使用 jobs -l 查看,也可使用 fg 将其移到前台运行。

(查看日志) touch 命令

Linux    touch命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。ls -l 可以显示档案的时间记录。

 
  

语法

参数说明:

a 改变档案的读取时间记录。 m 改变档案的修改时间记录。 c 假如目的档案不存在,不会建立新的档案。与 --no-create 的效果一样。

f 不使用,是为了与其他 unix 系统的相容性而保留。

r 使用参考档的时间记录,与 --file 的效果一样。d 设定时间与日期,可以使用各种不同的格式。 t 设定档案的时间记录,格式与 date 指令相同。

–no-create 不会建立新档案。

–help 列出指令格式。

–version 列出版本讯息。

实例

 
  

使用指令"touch"修改文件"testfile"的时间属性为当前系统时间,输入如下命令:

首先,使用ls命令查看testfile文件的属性,如下所示:

 
  

执行指令"touch"修改文件属性以后,并再次查看该文件的时间属性,如下所示:

使用指令"touch"时,如果指定的文件不存在,则将创建一个新的空白文件。例如,在当前目录下,使用该指令创建一个空白文件"file",输入如下命令:

vim 命令

Vim是从  vi   发展出来的一个文本编辑器。代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。打开文件并跳到第 10 行:vim +10 filename.txt 。

打开文件跳到第一个匹配的行:vim +/search-term filename.txt 。

以只读模式打开文件:vim -R /etc/passwd 。

基本上 vi/vim 共分为三种模式,分别是命令模式(Command mode),输入模式(Insert mode)和底线命令模式(Last line mode)。简单的说,我们可以将这三个模式想成

底下的图标来表示:

whereis 命 令

whereis 命令只能用于程序名的搜索,而且只搜索二进制文件(参数-b)、

man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。whereis 及  locate  都是基于系统内建的数据库进行搜索,因此效率很高,而find则是遍历硬盘查找文件。

 
  

常用参数:

实例:

(1) 查找 locate 程序相关文件

 
  

(2) 查找 locate 的源码文件

(3) 

 
  

查找 lcoate 的帮助文件

which 命令

 
  

在 linux 要查找某个文件,但不知道放在哪里了,可以使用下面的一些命令来搜索:

which 是在 PATH  就是指定的路径中,搜索某个系统命令的位置,并返回第一个搜索结果。使用  which  命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。

 
  

常用参数:

实例:

(1) 

 
  

查看 ls 命令是否存在,执行哪个

(2) 查看 which

(3) 

 
  

查看 cd

查看当前 PATH 配置:

`1 echo$PATH

或使用 env 查看所有环境变量及对应值文档编辑命令 grep 命令

强大的文本搜索命令,grep(Global Regular Expression Print) 全局正则表达式搜索。

grep         的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到标准输出,不影响原文件内容。

 
  

命令格式:

常用参数:

 
  

grep 的规则表达式:

实例:(1)查找指定进程

(2) 

 
  

查找指定进程个数

(3) 从文件中读取关键词

(4) 

 
  

从文件夹中递归查找以grep开头的行,并只列出文件

(5) 查找非x开关的行内容

 
  

(6) 显示包含 ed 或者 at 字符的内容行

wc 命令

 
  

wc(word count)功能为统计指定的文件中字节数、字数、行数,并将统计结果输出命令格式:

命令参数:

实例:

(1) 

 
  

查找文件的 行数 单词数 字节数 文件名

结果:

(2) 

 
  

统计输出结果的行数

 磁盘管理命令 cd 命令

 
  

cd(changeDirectory) 命令语法:

说明:切换当前目录至 dirName。实例:(1)进入要目录

(2) 

 
  

进入 “home” 目录

(3) 进入上一次工作路径

(4) 

 
  

把上个命令的参数作为cd参数使用。

df 命令

显示磁盘空间使用情况。获取硬盘被占用了多少空间,目前还剩下多少空间等信息,如果没有文件名被指定,则所有当前被挂载的文件系统的可用空间将被显示。默认情况下,磁盘空间将以 1KB 为单位进行显示,除非环境变量

 
  

POSIXLY_CORRECT 被指定,那样将以512字节为单位进行显示:

实例:

(1) 显示磁盘使用情况

1 df ‐l

(2) 以易读方式列出所有文件系统及其类型

1 df ‐haT

du 命令

du 命令也是查看使用空间的,但是与 df 命令不同的是 Linux du 命令是对文件和目录磁盘使用的空间的查看:命令格式:

1 du [选项] [文件]

 
  

常用参数:

实例:

(1) 

 
  

以易读方式显示文件夹内及子文件夹大小

(2) 以易读方式显示文件夹内所有文件大小

(3) 

 
  

显示几个文件或目录各自占用磁盘空间的大小,还统计它们的总和

(4) 输出当前目录下各个子目录所使用的空间

ls命令

 
  

就是 list 的缩写,通过 ls 命令不仅可以查看 linux 文件夹包含的文件,而且可以查看文件权限(包括目录、文件夹、文件权限) 查看目录信息等等。常用参数搭配:

实例:

(1) 

 
  

按易读方式按时间反序排序,并显示文件详细信息

(2) 按大小反序显示文件详细信息

(3) 

 
  

列出当前目录中所有以"t"开头的目录的详细内容

(4) 列出文件绝对路径(不包含隐藏文件)

1 ls|sed"s:^: pwd /:"

(5) 

 
  

列出文件绝对路径(包含隐藏文件)

mkdir 命令

mkdir 命令用于创建文件夹。可用选项:

-m: 对新建目录设置存取权限,也可以用 chmod 命令设置;

-p: 可以是一个路径名称。此时若路径中的某些目录尚不存在,加上此选项后,系统将自动建立好那些尚不在的目录,即一次可以建立多个目录。实例:

(1) 

 
  

当前工作目录下创建名为 t的文件夹

(2) 在 tmp 目录下创建路径为 test/t1/t 的目录,若不存在,则创建:

pwd 命令

pwd 命令用于查看当前工作目录路径。实例:

(1) 

 
  

查看当前路径

(2) 查看软链接的实际路径

rmdir 命令

从一个目录中删除一个或多个子目录项,删除某目录时也必须具有对其父目录的写权限。注意:不能删除非空目录实例:

 
  

(1)当 parent 子目录被删除后使它也成为空目录的话,则顺便一并删除:

网络通讯命令 ifconfig 命令

ifconfig 用于查看和配置 Linux 系统的网络接口。查看所有网络接口及其状态:ifconfig -a 。

使用 up 和 down 命令启动或停止某个接口:ifconfig eth0 up 和 ifconfig eth0 down 。

iptables 命令

iptables ,是一个配置 Linux 内核防火墙的命令行工具。功能非常强大,对于我们开发来说,主要掌握如何开放端口即可。例如:

把来源 IP 为 192.168.1.101 访问本机 80 端口的包直接拒绝: iptables -I INPUT -s 192.168.1.101 -p tcp --dport 80 -j REJECT 。

开启 80 端口,因为web对外都是这个端口

iptables -A INPUT -p tcp --dport 80 -j ACCEP

 
  

另外,要注意使用 iptables save 命令,进行保存。否则,服务器重启后,配置的规则将丢失。

netstat 命令

Linux netstat命令用于显示网络状态。

 
  

利用netstat指令可让你得知整个Linux系统的网络情况。语法

参数说明:

-a或–all 显示所有连线中的Socket。

-A<网络类型>或–<网络类型> 列出该网络类型连线中的相关地址。

-c或–continuous 持续列出网络状态。

-C或–cache 显示路由器配置的快取信息。

-e或–extend 显示网络其他相关信息。

-F或–fib 显示FIB。

-g或–groups 显示多重广播功能群组组员名单。

-h或–help 在线帮助。

-i或–interfaces 显示网络界面信息表单。

-l或–listening 显示监控中的服务器的Socket。

-M或–masquerade 显示伪装的网络连线。

-n或–numeric 直接使用IP地址,而不通过域名服务器。

-N或–netlink或–symbolic 显示网络硬件外围设备的符号连接名称。

-o或–timers 显示计时器。

-p或–programs 显示正在使用Socket的程序识别码和程序名称。

-r或–route 显示Routing Table。

-s或–statistice 显示网络工作信息统计表。

-t或–tcp 显示TCP传输协议的连线状况。

-u或–udp 显示UDP传输协议的连线状况。

-v或–verbose 显示指令执行过程。

-V或–version 显示版本信息。

-w或–raw 显示RAW传输协议的连线状况。

-x或–unix 此参数的效果和指定"-A unix"参数相同。

–ip或–inet 此参数的效果和指定"-A inet"参数相同。

 
  

实例如何查看系统都开启了哪些端口?

如何查看网络连接状况?

如何统计系统当前进程连接数?输入命令 netstat -an | grep ESTABLISHED | wc -l 。输出结果 177 。一共有 177 连接数。用 netstat 命令配合其他命令,按照源 IP 统计所有到

80 端口的

 
  

ESTABLISHED 状态链接的个数?严格来说,这个题目考验的是对 awk 的使用。首先,使用 netstat -an|grep ESTABLISHED 命令。结果如下:

ping 命令

Linux ping命令用于检测主机。

 
  

执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。指定接收包的次数

telnet 命令

 
  

Linux telnet命令用于远端登入。执行telnet指令开启终端机阶段作业,并登入远端主机。语法

参数说明:

8 允许使用8位字符资料,包括输入与输出。 -a 尝试自动登入远端系统。

-b<主机别名> 使用别名指定远端主机名称。

-c 不读取用户专属目录里的.telnetrc文件。

-d 启动排错模式。

-e<脱离字符> 设置脱离字符。

-E 滤除脱离字符。

-f 此参数的效果和指定"-F"参数相同。

-F 使用Kerberos V5认证时,加上此参数可把本地主机的认证数据上传到远端主机。

-k<域名> 使用Kerberos认证时,加上此参数让远端主机采用指定的领域名,而非该主机的域名。

-K 不自动登入远端主机。

-l<用户名称> 指定要登入远端主机的用户名称。

-L 允许输出8位字符资料。

-n<记录文件> 指定文件记录相关信息。

-r 使用类似rlogin指令的用户界面。

-S<服务类型> 设置telnet连线所需的IP TOS信息。

-x 假设主机有支持数据加密的功能,就使用它。

-X<认证形态> 关闭指定的认证形态。

实例

 
  

登录远程主机

系统管理命令 date 命令

 
  

显示或设定系统的日期与时间。命令参数:

实例:

(1) 

 
  

显示下一天

(2) -d参数使用

free 命令

 
  

显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。  命令参数:

实例:

 
  

(1) 显示内存使用情况

(1) 以总和的形式显示内存的使用信息

(2) 

 
  

周期性查询内存使用情况

kill 命令

发送指定的信号到相应进程。不指定型号将发送SIGTERM(15)终止指定进

程。如果任无法终止该程序可用"-KILL" 参数,其发送的信号为SIGKILL(9) ,将强制结束进程,使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。

 
  

常用参数:

实例:

(1)先使用ps查找进程pro1,然后用kill杀掉

1 kill ‐9 $(ps ‐ef |grep pro1)

ps 命令

ps(process status),用来查看当前运行的进程状态,一次性查看,如果需要动态连续结果使用 top linux上进程有5种状态:

  1. 运行(正在运行或在运行队列中等待)
  2. 中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
  3. 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
  4. 僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
  5. 停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运

行)

 
  

ps 工具标识进程的5种状态码:

命令参数:

实例:

(1) 

 
  

显示当前所有进程环境变量及进程间关系

(2) 显示当前所有进程

(3) 

 
  

与grep联用查找某进程

(4) 找出与 cron 与 syslog 这两个服务有关的 PID 号码

rpm 命令

Linux rpm 命令用于管理套件。

rpm(redhat package manager) 原本是 Red Hat Linux 发行版专门用来管理

Linux 各项套件的程序,由于它遵循 GPL 规则且功能强大方便,因而广受欢迎。逐渐受到其他发行版的采用。RPM 套件管理方式的出现,让 Linux 易于安装,升级,间接提升了

 
  

Linux 的适用度。

top 命令

 
  

显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率等常用参数:

实例:

前五行是当前系统情况整体的统计信息区。

第一行,任务队列信息,同 uptime 命令的执行结果,具体参数说明情况如下:

14:06:23 — 当前系统时间

up 70 days, 16:44 — 系统已经运行了70天16小时44分钟(在这期间系统没有重启过的吆!)

2 users — 当前有2个用户登录系统

load average: 1.15, 1.42, 1.44 — load average后面的三个数分别是1分钟、5 分钟、15分钟的负载情况。

load average数据是每隔5秒钟检查一次活跃的进程数,然后按特定算法计算出的数值。如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。

第二行,Tasks — 任务(进程),具体信息说明如下:系统现在共有206个进程,其中处于运行中的有1个,205个在休眠(sleep), stoped状态的有0个,zombie状态(僵尸)的有0个。

 
  

第三行,cpu状态信息,具体属性说明如下:

备注:在这里CPU的使用比率和windows概念不同,需要理解linux系统用户空间和内核空间的相关知识! 第四行,内存状态,具体信息如下:

 
  

第五行,swap交换分区信息,具体信息说明如下:

第六行,空行。

 
  

第七行以下:各进程(任务)的状态监控,项目列信息说明如下:

top 交互命令

yum 命令

yum( Yellow dog Updater, Modified)是一个在Fedora和RedHat以及SUSE中的Shell前端软件包管理器。

基於RPM包管理,能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖性关系,并且一次安装所有依赖的软体包,无须繁琐地一次次下载、安装。

yum提供了查找、安装、删除某一个、一组甚至全部软件包的命令,而且命令简洁而又好记。        1.列出所有可更新的软件清单命令:yum check-update

  1. 更新所有软件命令:yum update
  2. 仅安装指定的软件命令:yum install 4.仅更新指定的软件命令:yum update
    1. 列出所有可安裝的软件清单命令:yum list
    2. 删除软件包命令:

yum remove 7.查找软件包 命令:

yum search 8.清除缓存命令:

yum clean packages: 清除缓存目录下的软件包 yum clean headers: 清除缓存目录下的 headers yum clean oldheaders: 清除缓存目录下旧的 headers

yum clean, yum clean all (= yum clean packages; yum clean oldheaders) :清除缓存目录下的软件包及旧的headers

实例

安装 pam-devel

1 [root@www ~]## yum install pam‐devel

备份压缩命令 bzip2 命令

创建 *.bz2 压缩文件:bzip2 test.txt 。解压 *.bz2 文件:bzip2 -d test.txt.bz2 。

gzip 命令

创建一个 *.gz 的压缩文件:gzip test.txt 。解压 *.gz 文件:gzip -d test.txt.gz 。显示压缩的比率:gzip -l *.gz 。

tar 命令

用来压缩和解压文件。tar 本身不具有压缩功能,只具有打包功能,有关压缩及解压是调用其它的功能来完成。

弄清两个概念:打包和压缩。打包是指将一大堆文件或目录变成一个总的文件;压缩则是将一个大的文件通过一些压缩算法变成一个小文件常用参数:

 
  

有关 gzip 及 bzip2 压缩:

实例:

(1) 

 
  

将文件全部打包成 tar 包

(2) 将 /etc 下的所有文件及目录打包到指定目录,并使用 gz 压缩

(3) 

 
  

查看刚打包的文件内容(一定加z,因为是使用  gzip  压缩的)

(4) 要压缩打包 /home, /etc ,但不要 /home/dmtsai

unzip 命令

解压 *.zip 文件:unzip test.zip 。查看 *.zip 文件的内容:unzip -l jasper.zip 。

 swap分区

Swap交换分区概念

什么是Linux swap space呢?我们先来看看下面两段关于Linux swap space的英文介绍资料:

Linux divides its physical RAM (random access memory) into chucks of memory called pages. Swapping is the process whereby a page of memory is copied to the preconfigured space on the hard disk, called swap space, to free up that page of memory. The combined sizes of the physical memory and the swap space is the amount of virtual memory available.

Swap space in Linux is used when the amount of physical memory (RAM) is full. If the system needs more memory resources and the RAM is full, inactive pages in memory are moved to the swap space. While swap space can help machines with a small amount of RAM, it should not be considered a replacement for more RAM.

Swap space is located on hard drives, which have a slower access time than physical memory.Swap space can be a dedicated swap partition (recommended), a swap file, or a combination of swap partitions and swap files.

Linux内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是Cache  Memory(缓存内存)。即使你的程序运行结束后,Cache  Memory也不会自动释放。这就会导致你在Linux系统中程序频繁读写文件后,你会发现可用物理内存变少。当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程           序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap分区中恢复保存的数           据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换。

关于Swap分区,其实我们有很多疑问,如果能弄清楚这些疑问,那么你对Swap的了解掌握就差不多了。如何查看Swap分区大小?     Swap分区大小应该如何设置?系统在什么时候会使用Swap分区? 是否可以调整? 如何调整Swap分区的大小?Swap分区有什么优劣和要注意的地方? Swap分区是否必要?那么我一个一个来看看这些疑问吧!

查看Swap分区大小

查看Swap分区的大小以及使用情况,一般使用free命令即可,如下所示,Swap大小为2015M,目前没有使用Swap分区

total

used

free

shared

buffers

cached

Mem:

1000

855

145

0

28

296

-/+ buffers/cache:

530

470

Swap: 2015

0

2015

另外我们还可以使用swapon命令查看当前swap相关信息:例如swap空间是swap partition,Swap size,使用情况等详细信息

[root@DB-Server ~]# swapon -s

Filename

Type

Size

Used

Priority

/dev/sda3

partition

2064344

0

-1

[root@DB-Server ~]# cat /proc/swaps

Filename

Type

Size

Used

Priority

/dev/sda3

partition

2064344

0

-1

[root@DB-Server ~]#

 
  
Swap分区大小设置

系统的Swap分区大小设置多大才是最优呢?        关于这个问题,应该说只能有一个统一的参考标准,具体还应该根据系统实际情况和内存的负荷综合考虑,像ORACLE的官方文档就推荐如下设置,这个是根据物理内存来做参考的。

RAM

Swap Space

Up to 512 MB

2 times the size of RAM

Between 1024 MB and 2048 MB

1.5 times the size of RAM

Between 2049 MB and 8192 MB

Equal to the size of RAM

More than 8192 MB

0.75 times the size of RAM

另外在其它博客中看到下面一个推荐设置,当然我不清楚其怎么得到这个标准的。是否合理也无从考证。可以作为一个参考。

4G以内的物理内存,SWAP 设置为内存的2倍。

4-8G的物理内存,SWAP 等于内存大小。

8-64G 的物理内存,SWAP 设置为8G。

64-256G物理内存,SWAP 设置为16G。

上下两个标准确实也很让人无所适从。我就有一次在一台ORACLE数据库服务器(64G的RAM),按照官方推荐设置了一个很大的Swap分区,但是我发现其实这个Swap几乎很少   用到,其实是浪费了磁盘空间。所以如果根据系统实际情况和内存的负荷综合考虑,其实应该按照第二个参考标准设置为8G即可。当然这个只是个人的一些认知。

释放Swap分区空间

total

used

free

shared

buffers

cached

Mem:

64556

55368

9188

0

926

51405

-/+ buffers/cache:

3036

61520

Swap: 65535

13

65522

[root@testlnx ~]# swapon

-s

Filename

Type

Size

Used

Priority

使用swapoff关闭交换分区

[root@testlnx ~]# swapoff /dev/mapper/VolGroup00-LogVol01

使用swapon启用交换分区,此时查看交换分区的使用情况,你会发现used为0了

total

used

free

shared

buffers

cached

Mem:

64556

55385

9171

0

926

51406

-/+ buffers/cache:

3052

61504

Swap: 65535

0

65535

Swap分区空间什么时候使用

系统在什么情况或条件下才会使用Swap分区的空间呢? 其实是Linux通过一个参数swappiness来控制的。当然还涉及到复杂的算法。

这个参数值可为    0-100,控制系统    swap   的使用程度。高数值可优先系统性能,在进程不活跃时主动将其转换出物理内存。低数值可优先互动性并尽量避免将进程转换处物理内存,并降低反应延迟。默认值为           60。注意:这个只是一个权值,不是一个百分比值,涉及到系统内核复杂的算法。关于该参数请参考这篇文章[转载]调整虚拟内存,在此不做过多赘述。下面是关于swappiness的相关资料

The Linux 2.6 kernel added a new kernel parameter called swappiness to let administrators tweak the way Linux swaps. It is a number from 0 to 100. In essence, higher values lead to more pages being swapped, and lower values lead to more applications being kept in memory, even if they are idle. Kernel maintainer Andrew Morton

has said that he runs his desktop machines with a swappiness of 100, stating that "My point is that decreasing the tendency of the kernel to swap stuff out is wrong. You really don't want hundreds of megabytes of BloatyApp's untouched memory floating about in the machine. Get it out on the disk, use the memory for something useful."

Swappiness is a property of the Linux kernel that changes the balance between swapping out runtime memory, as opposed to dropping pages from the system page cache. Swappiness can be set to values between 0 and 100 inclusive. A low value means the kernel will try to avoid swapping as much as possible where a higher value instead will make the kernel aggressively try to use swap space. The default value is 60, and for most desktop systems, setting it to 100 may affect the overall

performance, whereas setting it lower (even 0) may improve interactivity (by decreasing response latency.

 
  

有两种临时修改swappiness参数的方法,系统重启后失效

永久修改swappiness参数的方法就是在配置文件/etc/sysctl.conf里面修改vm.swappiness的值,然后重启系统

如果有人会问是否物理内存使用到某个百分比后才会使用Swap交换空间,可以明确的告诉你不是这样一个算法,如下截图所示,及时物理内存只剩下8M了,但是依然没有使用

 
  

Swap交换空间,而另外一个例子,物理内存还剩下19G,居然用了一点点Swap交换空间。

另外调整/proc/sys/vm/swappiness这个参数,如果你没有绝对把握,就不要随便调整这个内核参数,这个参数符合大多数情况下的一个最优值。

Swap交换分区对性能的影响

我们知道Linux可以使用文件系统中的一个常规文件或独立分区作为Swap交换空间,相对而言,交换分区要快一些。但是和RAM比较而言,Swap交换分区的性能依然比不上物理      内存,目前的服务器上RAM基本上都相当充足,那么是否可以考虑抛弃Swap交换分区,是否不需要保留Swap交换分区呢?这个其实是我的疑问之一。在这篇What Is a Linux SWAP Partition, And What Does It Do?博客中,作者给出了swap交换空间的优劣

Advantages:
  1. Provides overflow space when your memory fills up completely
  2. Can move rarely-needed items away from your high-speed memory
  3. Allows you to hibernate
Disadvantages:
  1. Takes up space on your hard drive as SWAP partitions do not resize dynamically
  2. Can increase wear and tear to your hard drive
  3. Does not necessarily improve performance (see below)

其实保留swap分区概括起来可以从下面来看:

首先,当物理内存不足以支撑系统和应用程序(进程)的运作时,这个Swap交换分区可以用作临时存放使用率不高的内存分页,把腾出的内存交给急需的应用程序(进程)使          用。有点类似机房的UPS系统,虽然正常情况下不需要使用,但是异常情况下, Swap交换分区还是会发挥其关键作用。

其次,即使你的服务器拥有足够多的物理内存,也有一些程序会在它们初始化时残留的极少再用到的内存分页内容转移到     swap     空间,以此让出物理内存空间。对于有发生内存泄漏几率的应用程序(进程),Swap交换分区更是重要,因为谁也不想看到由于物理内存不足导致系统崩溃。

最后,现在很多个人用户在使用Linux,有些甚至是PC的虚拟机上跑Linux系统,此时可能常用到休眠(Hibernate),这种情况下也是推荐划分Swap交换分区的。

其实少量使用Swap交换空间是不会影响性能,只有当RAM资源出现瓶颈或者内存泄露,进程异常时导致频繁、大量使用交换分区才会导致严重性能问题。另外使用Swap交换分       区频繁,还会引起kswapd0进程(虚拟内存管理中, 负责换页的)耗用大量CPU资源,导致CPU飙升。

关于Swap分区的优劣以及是否应该舍弃,我有点恶趣味的想到了这个事情:人身上的两个器官,阑尾和扁桃体。切除阑尾或扁桃体是否也是争论不休。另外,其实不要Swap交         换分区,Linux也是可以正常运行的(有人提及过这个问题)

调整Swap分区的大小

如下测试案例所示,Swap分区大小为65535M,我现在想将Swap分区调整为8G,那么我们来看看具体操作吧

1:查看Swap的使用情况以及相关信息

[root@getlnx14uat ~]# swapon -s

Filename

Type

Size

Used

Priority

/dev/mapper/VolGroup00-LogVol01

partition

67108856

878880 -1

[root@getlnx14uat ~]# free -m

total

used

free

shared

buffers

cached

Mem:

3957

3920

36

0

39

3055

-/+ buffers/cache:

825

3132

Swap: 65535

858

64677

 
  

2: 关闭Swap交换分区

3: 这里是缩小Swap分区大小,如果是增大Swap分区大小,那么就需要扩展正在使用的swap分区的逻辑卷,此处使用lvreduce命令收缩逻辑卷。

 
  

4:格式化swap分区

5:启动swap分区,并增加到/etc/fstab自动挂载

Filename

Type

Size

Used

Priority

/dev/mapper/VolGroup00-LogVol01

partition

8388600

0

-1

Linux系统swappiness参数在内存与交换分区之间优化作用

swappiness的值的大小对如何使用swap分区是有着很大的联系的。swappiness=0的时候表示最大限度使用物理内存,然后才是            swap空间,swappiness=100的时候表示积极的使用swap分区,并且把内存上的数据及时的搬运到swap空间里面。linux的基本默认设置为60,具体如下:

一般默认值都是60

```

[root@timeserver ~]# cat /proc/sys/vm/swappiness 60

```

也就是说,你的内存在使用到100-60=40%的时候,就开始出现有交换分区的使用。大家知道,内存的速度会比磁盘快很多,这样子会加大系统io,同时造的成大量页的换进换        出,严重影响系统的性能,所以我们在操作系统层面,要尽可能使用内存,对该参数进行调整。

临 时 调 整 的 方 法 如 下 , 我 们 调 成 10: [root@timeserver ~]# sysctl vm.swappiness=10 vm.swappiness = 10

[root@timeserver ~]# cat /proc/sys/vm/swappiness 10

这只是临时调整的方法,重启后会回到默认设置的

要想永久调整的话,需要将

需要在/etc/sysctl.conf修改,加上:

[root@timeserver ~]# cat /etc/sysctl.conf

# Controls the maximum number of shared memory segments, in pages kernel.shmall = 4294967296

vm.swappiness=10

激活设置

[root@timeserver ~]# sysctl -p

在linux中,可以通过修改swappiness内核参数,降低系统对swap的使用,从而提高系统的性能。

遇到的问题是这样的,新版本产品发布后,每小时对内存的使用会有一个尖峰。虽然这个峰值还远没有到达服务器的物理内存,但确发现内存使用达到峰值时系统开始使用swap。在swap的过程中系统性能会有所下降,表现为较大的服务延迟。对这种情况,可以通过调节swappiness内核参数降低系统对swap的使用,从而避免不必要的swap对性 能造成的影响。

简单地说这个参数定义了系统对swap的使用倾向,默认值为60,值越大表示越倾向于使用swap。可以设为0,这样做并不会禁止对swap的使用,只是最大限度地降低了使用

swap的可能性。

通过sysctl -q vm.swappiness可以查看参数的当前设置。

修改参数的方法是修改/etc/sysctl.conf文件,加入vm.swappiness=xxx,并重起系统。这个操作相当于是修改虚拟系统中的/proc/sys/vm/swappiness文件,将值改为XXX数         值。

如果不想重起,可以通过sysctl -p动态加载/etc/sysctl.conf文件,但建议这样做之前先清空swap。

 overcommit_memory

取值为0,系统在为应用进程分配虚拟地址空间时,会判断当前申请的虚拟地址空间大小是否超过剩余内存大小,如果超过,则虚拟地址空间分配失败。因此,也就是如果进程本            身占用的虚拟地址空间比较大或者剩余内存比较小时,fork、malloc等调用可能会失败。

取值为1,系统在为应用进程分配虚拟地址空间时,完全不进行限制,这种情况下,避免了fork可能产生的失败,但由于malloc是先分配虚拟地址空间,而后通过异常陷入内核分         配真正的物理内存,在内存不足的情况下,这相当于完全屏蔽了应用进程对系统内存状态的感知,即malloc总是能成功,一旦内存不足,会引起系统OOM杀进程,应用程序对于          这种后果是无法预测的

取值为2,则是根据系统内存状态确定了虚拟地址空间的上限,由于很多情况下,进程的虚拟地址空间占用远大小其实际占用的物理内存,这样一旦内存使用量上去以后,对于一            些动态产生的进程(需要复制父进程地址空间)则很容易创建失败,如果业务过程没有过多的这种动态申请内存或者创建子进程,则影响不大,否则会产生比较大的影响

又一次内存分配失败(关于overcommit_memory)

1、问题现象和分析:

测试时发现当系统中空闲内存还有很多时,就报内存分配失败了,所有进程都报内存分配失败:

sshd@localhost:/var/log>free

total used free shared buffers cached

Mem: 12183700 8627972 3555728 0 289252 584444

-/+ buffers/cache: 7754276 4429424

Swap: 0 0 0 sshd@localhost:/var/log>free

-bash: fork: Cannot allocate memory sshd@localhost:/var/log>cat /proc/meminfo

-bash: fork: Cannot allocate memory

而messages日志中,也没有OOM相关的记录。最后确认原因为:/proc/sys/vm/overcommit_memory参数导致。           该环境中该参数设置为2,表示“No overcommit”,即系统中所有进程占用的虚拟内存空间不能超过上限:

cat /proc/meminfo

CommitLimit: 12061860 kB //虚拟地址空间的上限Committed_AS: 8625360 kB //当前的使用量

而该参数应该默认是0,这种情况下,只有还有空闲的物理内存,就可以继续分配,不受虚拟地址空间的限制。

echo 0 > /proc/sys/vm/overcommit_memory

如此修正后解决。

2、关于overcommit_memory说明:

取值为0,系统在为应用进程分配虚拟地址空间时,会判断当前申请的虚拟地址空间大小是否超过剩余内存大小,如果超过,则虚拟地址空间分配失败。因此,也就是如果进程            本身占用的虚拟地址空间比较大或者剩余内存比较小时,fork、malloc等调用可能会失败。

取值为1,系统在为应用进程分配虚拟地址空间时,完全不进行限制,这种情况下,避免了fork可能产生的失败,但由于malloc是先分配虚拟地址空间,而后通过异常陷入内核            分配真正的物理内存,在内存不足的情况下,这相当于完全屏蔽了应用进程对系统内存状态的感知,即malloc总是能成功,一旦内存不足,会引起系统OOM杀进程,应用程序对          于这种后果是无法预测的

取值为2,则是根据系统内存状态确定了虚拟地址空间的上限,由于很多情况下,进程的虚拟地址空间占用远大小其实际占用的物理内存,这样一旦内存使用量上去以后,对于            一些动态产生的进程(需要复制父进程地址空间)则很容易创建失败,如果业务过程没有过多的这种动态申请内存或者创建子进程,则影响不大,否则会产生比较大的影响

 
  

3、相应代码分析:

15.

  1. free = global_page_state(NR_FILE_PAGES);
  2. free += nr_swap_pages; 18.

19. /*

  1. \* Any slabs which are created with the
  2. \* SLAB_RECLAIM_ACCOUNT flag claim to have contents
  3. \* which are reclaimable, under pressure. The dentry
  4. \* cache and most inode caches should fall into this 24. */

25. free += global_page_state(NR_SLAB_RECLAIMABLE); 26.

27. /*

28. \* Leave the last 3% for root 29. */

  1. if (!cap_sys_admin)
  2. free -= free / 32; //root用户可以在free更少(3%)的时候,分配内存。32.

33. if (free > pages) // pages为需要分配的内存大小,free为根据一定规则算出来的“空闲内存大小”,第一次free仅为NR_FILE_PAGES+NR_SLAB_RECLAIMABLE,由于直接或者系统中“实际空闲”内存代价比较大,所以进行分阶判断,提高效率。

34.

return 0;

35.

36.

/*

37.

\* nr_free_pages() is very expensive on large systems,

38.

\* only call if we're about to fail.

39.

*/

40.

n = nr_free_pages(); //当第一次判断不满足内存分配条件时,再进行“实际空闲”内存的获取操作。

41.

42.

/*

43.

\* Leave reserved pages. The pages are not for anonymous pages.

44.

*/

45.

if (n <= totalreserve_pages)

46.

goto error;

47.

else

48.

n -= totalreserve_pages;

49.

50.

/*

51.

\* Leave the last 3% for root

52.

*/

53.

if (!cap_sys_admin)

54.

n -= n / 32;

55.

free += n;

56.

57.

if (free > pages)

58.

return 0;

59.

60.

goto error;

61.

}

62.

  1. allowed = (totalram_pages - hugetlb_total_pages()) //当overcommit_memory=2时,根据系统中虚拟地址空间的总量来进行限制。
  2. \* sysctl_overcommit_ratio / 100; 65. /*

66. \* Leave the last 3% for root 67. */

  1. if (!cap_sys_admin)
    1. allowed -= allowed / 32;
    2. allowed += total_swap_pages; 71.
      1. /* Don't let a single process grow too big:
        1. leave 3% of the size of this process for other processes */
        2. if (mm)
        3. allowed -= mm->total_vm / 32; 76.
          1. if (percpu_counter_read_positive(&vm_committed_as) < allowed)
            1. return 0;
            2. error:
            3. vm_unacct_memory(pages); 81.

82. return -ENOMEM;

83. }

 linux系统下查看CPU、内存负载情况

$ vmstat

procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu------

r b swpd free buff cache si so bi bo in cs us sy id wa st

1 4 329796 26040 4528 3379824 1 1 50 160 36 17 2 10 85 3 0

结果解释如下:

procs

r 列表示运行和等待cpu时间片的进程数,如果长期大于1,说明cpu不足,需要增加cpu。

b 列表示在等待资源的进程数,比如正在等待I/O、或者内存交换等。

cpu 表示cpu的使用状态

us 列显示了用户方式下所花费 CPU 时间的百分比。us的值比较高时,说明用户进程消耗的cpu时间多,但是如果长期大于50%,需要考虑优化用户的程序。

sy 列显示了内核进程所花费的cpu时间的百分比。这里us + sy的参考值为80%,如果us+sy 大于 80%说明可能存在CPU不足。

wa   列显示了IO等待所占用的CPU时间的百分比。这里wa的参考值为30%,如果wa超过30%,说明IO等待严重,这可能是磁盘大量随机访问造成的,也可能磁盘或者磁盘访问控制器的带宽瓶颈造成的(主要是块操作)。

id 列显示了cpu处在空闲状态的时间百分比

system 显示采集间隔内发生的中断数

in 列表示在某一时间间隔中观测到的每秒设备中断数。

cs列表示每秒产生的上下文切换次数,如当 cs 比磁盘 I/O 和网络信息包速率高得多,都应进行进一步调查。

memory

swpd 切换到内存交换区的内存数量(k表示)。如果swpd的值不为0,或者比较大,比如超过了100m,只要si、so的值长期为0,系统性能还是正常

free 当前的空闲页面列表中内存数量(k表示)

buff 作为buffer cache的内存数量,一般对块设备的读写才需要缓冲。

cache: 作为page cache的内存数量,一般作为文件系统的cache,如果cache较大,说明用到cache的文件较多,如果此时IO中bi比较小,说明文件系统效率比较好。

swap

si 由内存进入内存交换区数量。so由内存交换区进入内存数量。IO

bi 从块设备读入数据的总量(读磁盘)(每秒kb)。

bo 块设备写入数据的总量(写磁盘)(每秒kb)

 经典面试题

问题一:绝对路径用什么符号表示?当前目录、上层目录用什么表示?主目录用什么表示 ? 切换目录用什么命令? 答案:

绝对路径:如/etc/init.d

当前目录和上层目录:./ ../ 主目录:~/ 切换目录:cd

问题二:怎么查看当前进程?怎么执行退出?怎么查看当前路径?答案:

查看当前进程:ps 执行退出:exit 查看当前路径:pwd

问题三:怎么清屏?怎么退出当前命令?怎么执行睡眠?怎么查看当前用户 id?查看指定帮助用什么命令?

答案:

清屏:clear

退出当前命令:ctrl+c 彻底退出执行睡眠 :ctrl+z 挂起当前进程 fg 恢复后台查看当前用户 id:”id“:查看显示目前登陆账户的 uid 和 gid 及所属分组及用户名查看指定帮助:如 man adduser 这个很全 而且有例子;adduser --help 这个告诉你一些常用参数;info adduesr;

问题四:Ls 命令执行什么功能?可以带哪些参数,有什么区别?

答案: ls 执行的功能:列出指定目录中的目录,以及文件哪些参数以及区别:a 所有文件 l 详细信息,包括大小字节数,可读可写可执行的权限等

问题五:建立软链接(快捷方式),以及硬链接的命令。答案:

软链接:ln -s slink source 硬链接:ln link source

问题六:目录创建用什么命令?创建文件用什么命令?复制文件用什么命令?答案:

创建目录:mkdir

创建文件:典型的如 touch,vi  也可以创建文件,其实只要向一个不存在的文件输出,都会创建文件复制文件:cp  7.  文件权限修改用什么命令?格式是怎么样的?文件权限修改:chmod

格式如下:

$ chmod u+x file 给 file 的属主增加执行权限

$ chmod 751 file 给 file 的属主分配读、写、执行(7)的权限,给 file 的所在组分配读、执行(5)的权限,给其他用户分配执行(1)的权限

$ chmod u=rwx,g=rx,o=x file 上例的另一种形式

$ chmod =r file 为所有用户分配读权限

$ chmod 444 file 同上例

$ chmod a-wx,a+r file 同上例

$ chmod -R u+r directory 递归地给 directory 目录下所有文件和子目录的属主分配读的权限

问题八:查看文件内容有哪些命令可以使用?答案:

vi 文件名 #编辑方式查看,可修改 cat 文件名 #显示全部文件内容 more 文件名 #分页显示文件内容 less 文件名 #与 more 相似,更好的是可以往前翻页 tail 文件名 #仅查看尾部,还可以指定行数 head 文件名 #仅查看头部,还可以指定行数

问题九:

随意写文件命令?怎么向屏幕输出带空格的字符串,比如”hello world”?

答案:

写文件命令:vi

向屏幕输出带空格的字符串:echo hello world

问题十:终端是哪个文件夹下的哪个文件?黑洞文件是哪个文件夹下的哪个命令?答案:

终端 /dev/tty

黑洞文件 /dev/null

问题十一:移动文件用哪个命令?改名用哪个命令?答案: mv mv

问题十二:

[root@localhost ~]## whatis zcat zcat [gzip] (1) – compress or expand files

问题十三:使用哪一个命令可以查看自己文件系统的磁盘空间配额呢?

答案:使用命令 repquota 能够显示出一个文件系统的配额信息

 MQ(一明老师,周老师)

 为什么使用MQMQ的优点

简答

异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。

流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请 求。日志处理 - 解决大量日志传输。

消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通 讯。比如实现点对点消息队列,或者聊天室等。

详答

主要是:解耦、异步、削峰。

解耦:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要 这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统 跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系 统都需要 A 系统将这个数据发送过来。如果使用 MQ,A 系统产生一条数据, 发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要 数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数 据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。

异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写 库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、 200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户 感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从 接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。

削峰:减少高峰时期对服务器压力。

 消息队列有什么优缺点?RabbitMQ有什么优缺点?

优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。缺点有以下几个:

系统可用性降低

本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的 系统不是呵呵了。因此,系统可用性会降低; 系统复杂度提高

加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息 不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂 性增大。一致性问题

A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是, 要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了, 咋整?你这数据就不一致了。

所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对 它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈 呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用, 还是得用的。

 你们公司生产环境用的是什么消息中间件?

这个首先你可以说下你们公司选用的是什么消息中间件,比如用的是       RabbitMQ,然后可以初步给一些你对不同MQ中间件技术的选型分析。举个例子:比如说ActiveMQ是老牌的消息中间件,国内很多公司过去运用的还 是非常广泛的,功能很强大。

但是问题在于没法确认ActiveMQ可以支撑互联网公司的高并发、高负载以及高   吞吐的复杂场景,在国内互联网公司落地较少。而且使用较多的是一些传统企    业,用ActiveMQ做异步调用和系统解耦。

然后你可以说说RabbitMQ,他的好处在于可以支撑高并发、高吞吐、性能很      高,同时有非常完善便捷的后台管理界面可以使用。另外,他还支持集群化、高可用部署架构、消息高可靠支持,功能较为完善。

而且经过调研,国内各大互联网公司落地大规模RabbitMQ集群支撑自身业务的    case较多,国内各种中小型互联网公司使用RabbitMQ的实践也比较多。除此之外,RabbitMQ的开源社区很活跃,较高频率的迭代版本,来修复发现的 bug以及进行各种优化,因此综合考虑过后,公司采取了RabbitMQ。

但是RabbitMQ也有一点缺陷,就是他自身是基于erlang语言开发的,所以导致   较为难以分析里面的源码,也较难进行深层次的源码定制和改造,毕竟需要较为    扎实的erlang语言功底才可以。

然后可以聊聊RocketMQ,是阿里开源的,经过阿里的生产环境的超高并发、      高吞吐的考验,性能卓越,同时还支持分布式事务等特殊场景。而且RocketMQ是基于Java语言开发的,适合深入阅读源码,有需要可以站在 源码层面解决线上生产问题,包括源码的二次开发和改造。

另外就是Kafka。Kafka提供的消息中间件的功能明显较少一些,相对上述几款    MQ中间件要少很多。但是Kafka的优势在于专为超高吞吐量的实时日志采集、实时数据同步、实时数 据计算等场景来设计。

因此Kafka在大数据领域中配合实时计算技术(比如Spark Streaming、 Storm、Flink)使用的较多。但是在传统的MQ中间件使用场景中较少采用。

 KafkaActiveMQRabbitMQRocketMQ  什么优缺点?

ActiveMQ

RabbitMQ

RocketMQ

Kafka

ZeroMQ

单机吞吐量

比 RabbitM Q低

2.6w/s( 消息做持 久化)

11.6w/s

17.3w/s

29w/s

开发语言

Java

Erlang

Java

Scala/Java

C

主要维护者

Apache

Mozilla/Spring

Alibaba

Apache

iMatix创始人已去世

成熟度

成熟

成熟

开源版本不够成熟

比较成熟

只有C、PHP等版本成熟

订阅形式

点对点 (p2p)、广 播(发布订阅)

提 供 了 4 种 : direct, topic,Headers 和 fanout。fanout就 是广播模 式

基于 topic/me ssageTag 以及按照消息类 型、属性 进行正则 匹配的发 布订阅模 式

基于topic 以及按照 topic进行正则匹配的发布订 阅模式

点对点

(P2P)

持久化

支持少量 堆积

支持少量 堆积

支持大量 堆积

支持大量 堆积

不支持

顺序消息

不支持

不支持

支持

支持

不支持

性能稳定性

一般

较差

很好

集群方式

支持简单 集群模 式,比

如’主备’,对 高级集群 模式支持 不好。

支持简单 集群,'复 制’模 式, 对高 级集群模 式支持不 好。

常用 多 对’Mast erSlave’ 模 式,开源版本需手 动切换 Slave变成 Master

天然 的‘Lead erSlave’无 状态集群,每台 服务器既 是Master 也是Slave

不支持

管理界面

一般

较好

一般

综上,各种对比之后,有如下建议:

一般的业务系统要引入  MQ,最早大家都用  ActiveMQ,但是现在确实大家用   的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是   算了吧,我个人不推荐用这个了;

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的  Java 工程师  去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开  源的,比较稳定的支持,活跃度也高;

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出 品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝 对不会黄。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是 不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选 择。

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝 对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性 规范。

 MQ 有哪些常见问题?如何解决这些问题?

MQ 的常见问题有:

  1. 消息的顺序问题
  2. 消息的重复问题
消息的顺序问题

消息有序指的是可以按照消息的发送顺序来消费。

 
  

假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,怎么做?

解决方案: (1)保证生产者 - MQServer - 消费者是一对一对一的关系

缺陷:

并行度就会成为消息系统的瓶颈(吞吐量不够)

更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我 们不得不花费更多的精力来解决阻塞的问题。(2)通过合理的设计或者将问题分解 来规避。

不关注乱序的应用实际大量存在

队列无序并不意味着消息无序 所以从业务层面来保证消息的顺序而不仅仅是依 赖于消息系统,是一种更合理的方式。

消息的重复问题

造成消息重复的根本原因是:网络不可达。

所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收 到两条一样的消息,应该怎样处理?

消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消 息,最后处理的结果都一样。保证每条消息都有唯一编号且保证消息处理成功与 去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如 果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。

 什么是RabbitMQ

RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件

 Rabbitmq 的使用场景

(1) 服务间异步通信

(2) 顺序消费

(3) 定时任务

(4) 请求削峰

 RabbitMQ基本概念

Broker: 简单来说就是消息队列服务器实体

Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列

Queue: 消息队列载体,每个消息都会被投入到一个或多个队列

Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来

Routing Key: 路由关键字,exchange根据这个关键字进行消息投递

VHost: vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部 均含有独立的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的 权限系统, 可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度, vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同 的 vhost 中)。Producer: 消息生产者,就是投递消息的程序

Consumer: 消息消费者,就是接受消息的程序

Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个 channel代表一个会话任务

由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的 唯一的线路。

 RabbitMQ的工作模式

 
  

.simple模式(即最简单的收发模式)

  1. 消息产生消息,将消息放入队列
  2. 消息的消费者(consumer)  监听  消息队列,如果队列中有消息,就消费掉,消息被  拿走后,自动从队列中删除(隐患   消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失,这里可以设置成手动的ack,但如果设置成手动ack,处 理完后要及时发送ack消息给队列,否则会造成内存溢出)。
 
  

.work工作模式(资源的竞争)

1.消息产生者将消息放入队列消费者可以有多个,消费者1,消费者2同时监听同一   个队列,消息被消费。C1   C2共同争抢当前的消息队列内容,谁先拿到谁负责消费   消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设 置一个开关(syncronize) 保证一条消息只能被一个消费者使用)。

.publish/subscribe发布订阅(共享资源)

publish_subscribe发布订阅(../../../../../0马士兵/新建文件夹/BAT面试突击资料(1)/整理/BAT面试突击资料/15-消息中间件MQ面试题(2020最新版).assets/publish_subscribe发布订阅(共享资源).png)

1、每个消费者监听自己的队列;

2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队 列,每个绑定交换机的队列都将接收到消息。

 
  

.routing路由模式

  1. 消息生产者将消息发送给交换机按照路由判断,路由是字符串(info)   当前产生的   消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;
    1. 根据业务功能定义路由字符串
    2. 从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中。
    3. 业务场景:error 通知;EXCEPTION;错误通知的功能;传统意义的错误通知;客户 通知;利用key路由,可以将程序中的错误封装成消息传入到消息队列中,开发者可  以自定义消费者,实时接收错误;
      1. 星号井号代表通配符
      2. 星号代表多个单词,井号代表一个单词
      3. 路由功能添加模糊匹配
      4. 消息产生者产生消息,把消息交给交换机
      5. 交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消 费

.topic 主题模式(路由模式的一种)

(在我的理解看来就是routing查询的一种模糊匹配,就类似sql的模糊查询方 式)

如何保证RabbitMQ消息的顺序性?

拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确 实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

消息如何分发?

若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。          通过路由可实现多消费的功能

消息怎么路由?

消息提供方->路由->一至多个队列消息发布到交换器时,消息将拥有一个路由键(routing       key),在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。消息到达 交换器后,RabbitMQ   会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则);常用的交换器主要分为一下三种:    fanout:如果交换器收到消息, 将会广播到所有绑定的队列上 direct:如果路由键完全匹配,消息就被投递到相应的队列

topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符

消息基于什么传输?

由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ  使用信道的方式来传输数据。信道是建立在真实的  TCP  连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。

如何保证消息不被重复消费?或者说,如何保证消息消费时的幂等性?

先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。

针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;比如:在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。

如何确保消息正确地发送至RabbitMQ?       如何确保消息接收方消费了消息?

发送方确认模式将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。

一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。

发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。接收方确认机制消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。

这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了

Consumer 足够长的时间来处理消息。保证数据的最终一致性;下面罗列几种特殊情况

如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ       会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患, 需要去重)

如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。

如何保证RabbitMQ消息的可靠传输?

消息不可靠的情况可能是消息丢失,劫持等原因;丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;生产者丢失消息:从生产者弄丢数据这个角度来看,

RabbitMQ提供 transaction和confirm模式来确保生产者不丢消息;

transaction机制就是说:发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚

(channel.txRollback()),如果发送成功则提交事务

(channel.txCommit())。然而,这种方式有个缺点:吞吐量下降;

confirm模式用的居多:一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后; rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;

如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。 消息队列丢数据:消息持久化。

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。    这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢?

这里顺便说一下吧,其实也很容易,就下面两步

\1. 将queue的持久化标识durable设置为true,则代表是一个持久的队列

\2. 发送消息的时候将deliveryMode=2

这样设置以后,即使rabbitMQ挂了,重启后也能恢复数据消费者丢失消息:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可!消费者在收到消息之          后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;解决方案:处理消息成功后,手动回复确认消息。

为什么不应该对所有的 message 都使用持久化机制?

首先,必然导致性能的下降,因为写磁盘比写 RAM 慢的多,message 的吞吐量可能有 10 倍的差距。

其次,message 的持久化机制用在 RabbitMQ 的内置 cluster 方案时会出现“坑爹”问题。矛盾点在于,若 message 设置了 persistent 属性,但 queue 未设置 durable 属性,那么当该 queue 的 owner node 出现异常后,在未重建该 queue 前,发往该 queue 的 message 将被 blackholed ;若 message 设置了 persistent 属性,同时 queue 也设置了durable 属性,那么当 queue 的 owner node 异常且无法重启的情况下,则该 queue 无法在其他 node 上重建,只能等待其 owner node 重启后,才能恢复该 queue 的使用, 而在这段时间内发送给该 queue 的 message 将被 blackholed 。

所以,是否要对 message 进行持久化,需要综合考虑性能需要,以及可能遇到的问题。若想达到 100,000 条/秒以上的消息吞吐量(单 RabbitMQ 服务

器),则要么使用其他的方式来确保 message 的可靠 delivery ,要么使用非常快速的存储系统以支持全持久化(例如使用 SSD)。另外一种处理原则是: 仅对关键消息作持久化处理(根据业务重要程度),且应该保证关键消息的量不会导致性能瓶颈。

如何保证高可用的?RabbitMQ 的集群

RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,

我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。

单机模式,就是   Demo  级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式普通集群模式,意思就是在多台机器上启动多个   RabbitMQ  实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实 例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含  queue 的全部数据的意思。然后每次你写消息到  queue 的时候,都会自动把消息同步到多个实例的 queue 上。

RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个  queue  的完整数据,别的  consumer  都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个queue 的完整数据。

如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? 消息积压处理办法:临时紧急扩容:

先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的

数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。

接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的

10 倍速度来消费数据。

等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间

的,也就是 TTL。如果消息在  queue 中积压超过一定的时间就会被  RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在  mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要

了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

 设计MQ思路

比如说这个消息队列系统,我们从以下几个角度来考虑一下:首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,

broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。

如果现在资源不够了,简单啊,给 topic 增加  partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?其次你得考虑一下这个  mq  的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有

磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。

其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。

能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。

 消息中间件如何保证消息的一致性

(1) 主动方应用先把消息发送给消息中间件,消息状态标记为待确认;

(2) 消息中间件收到消息之后,把消息持久化到消息存储中,但并不向被动方应用投递消息;

(3) 消息中间件返回消息持久化结果(成功,或者失效),主动方应用根据返回结果进行判断如何处理业务操作处理;

①失败:放弃业务操作处理,结束(必须向上层返回失败结果)

②成功:执行业务操作处理

(4) 业务操作完成后,把业务操作结果(成功/失败)发送给消息中间件; (5)消息中间件收到业务操作结果后,根据结果进行处理;

①失败:删除消息存储中的消息,结束;

②成功:更新消息存储中的消息状态为∙待发送(可发送)∙,紧接着执行消息投递;

(6)前面的正向流程都成功后,向被动方应用投递消息;

 如何进行消息的重试机制?

Rocket重试机制,消息模式,刷盘方式

一、Consumer 批量消费(推模式)
 
  

可以通过

这里需要分为2种情况

Consumer端先启动

Consumer端后启动. 正常情况下:应该是Consumer需要先启动

注意:如果broker采用推模式的话,consumer先启动,会一条一条消息的消费,consumer后启动会才用批量消费

Consumer端先启动

1Consumer.java
 
  

由于这里是Consumer先启动,所以他回去轮询MQ上是否有订阅队列的消息,由于每次producer插入一条,Consumer就拿一条所以测试结果如下(每次size都是1)

2Consumer端后启动,也就是Producer先启动
 
  

由于这里是Consumer后启动,所以MQ上也就堆积了一堆数据,Consumer的

二、消息重试机制:消息重试分为2

 1Producer端重试

 2Consumer端重试

1Producer端重试

也就是Producer往MQ上发消息没有发送成功,我们可以设置发送失败重试的次数,发送并触发回调函数

 
  
2Consumer端重试

2.1 exception的情况,一般重复1610s30s1分钟、2分钟、3分钟等等

上面的代码中消费异常的情况返回

return ConsumeConcurrentlyStatus.RECONSUME_LATER;//重试正常则返回:

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;//成功

 
  

假如超过了多少次之后我们可以让他不再重试记录 日志。

if(msgs.get(0).getReconsumeTimes()==3){

//记录日志

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 成功

}

2.2 超时的情况,这种情况MQ会无限制的发送给消费端。

就是由于网络的情况,MQ发送数据之后,Consumer端并没有收到导致超时。也就是消费端没有给我返回return    任何状态,这样的就认为没有到达Consumer端。这里模拟Producer只发送一条数据。consumer端暂停1分钟并且不发送接收状态给MQ

 
  
三、消费模式
1、集群消费

2、广播消费

 
  

rocketMQ默认是集群消费,我们可以通过在Consumer来支持广播消费

 
  
四、conf下的配置文件说明

异步复制和同步双写主要是主和从的关系。消息需要实时消费的,就需要采用主从模式部署

异步复制:比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就算从producer端发送成功了,然后通过异步复制的方法将数据复制到从节点

同步双写:比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就并不算从producer端发送成功了,需要通过同步双写的方法将数据同步到从节点后,          才算数据发送成功。

如果rocketMq才用双master部署,Producer往MQ上写入20条数据 其中Master1中拉取了12条 。Master2中拉取了8 条,这种情况下,Master1宕机,那么我们消费数据的时候,只能消费到Master2中的8条,Master1中的12条默认持久化,不会丢失消息,需要Master1恢复之后这12条数据才能继续被消费,如果想保证消息实时消费,就才用双 Master双Slave的模式

五、刷盘方式

同步刷盘:在消息到达MQ后,RocketMQ需要将数据持久化,同步刷盘是指数据到达内存之后,必须刷到commitlog日志之后才算成功,然后返回producer数据已经发送成功。   异步刷盘:,同步刷盘是指数据到达内存之后,返回producer说数据已经发送成功。,然后再写入commitlog日志。

commitlog:

commitlog就是来存储所有的元信息,包含消息体,类似于MySQL、Oracle的redolog,所以主要有CommitLog在,Consume Queue即使数据丢失,仍然可以恢复出来。consumequeue:记录数据的位置,以便Consume快速通过consumequeue找到commitlog中的数据

传递保证语义:

At most once:消息可能会丢,但绝不会重复传递。At least once:消息绝不会丢,但可能会重复传递。Exactly once: 每条消息只会被传递一次。

生产者的“Exactly once”语义方案

当生产者向Kafka发送消息,且正常得到响应的时候,可以确保生产者不会产生重复的消息。但是,如果生产者发送消息后,遇到网络问题,无法获取响应,生产者就无法判断该          消息是否成功提交给了Kafka。根据生产者的机制,我们知道,当出现异常时,会进行消息重传,这就可能出现“At least one”语义。为了实现“Exactly once”语义,这里提供两个可选方案:

每个分区只有一个生产者写入消息,当出现异常或超时的情况时,生产者就要查询此分区的最后一个消息,用来决定后续操作是消息重传还是继续发送。为每个消息添加一个全局唯一主键,生产者不做其他特殊处理,按照之前分析方式进行重传,由消费者对消息进行去重,实现“Exactly once”语义。

如果业务数据产生消息可以找到合适的字段作为主键,或是有一个全局ID生成器,可以优先考虑选用第二种方案。

消费者的“Exactly once”语义方案

为了实现消费者的“Exactly once”语义,在这里提供一种方案,供读者参考:消费者将关闭自动提交offset的功能且不再手动提交offset,这样就不使用Offsets Topic这个内部

Topic记录其offset,而是由消费者自己保存offset。这里利用事务的原子性来实现“Exactly once”语义,我们将offset和消息处理结果放在一个事务中,事务执行成功则认为此消息被消费,否则事务回滚需要重新消费。当出现消费者宕机重启或Rebalance操作时,消费者可以从关系型数据库中找到对应的offset,然后调用KafkaConsumer.seek()方法手动设置消费位置,从此offset处开始继续消费。

ISR集合

ISR(In-SyncReplica)集合表示的是目前“可用”(alive)且消息量与Leader相差不多的副本集合,这是整个副本集合的一个子集。“可用”和“相差不多”都是很模糊的描述,其实际含义是ISR集合中的副本必须满足下面两个条件:

  1. 副本所在节点必须维持着与ZooKeeper的连接。
  2. 副本最后一条消息的offset与Leader副本的最后一条消息的offset之间的差值不能超出指定的阈值。

每个分区中的Leader副本都会维护此分区的ISR集合。写请求首先由Leader副本处理,之后Follower副本会从Leader上拉取写入的消息,这个过程会有一定的延迟,导致 Follower副本中保存的消息略少于Leader副本,只要未超出阈值都是可以容忍的。如果一个Follower副本出现异常,比如:宕机,发生长时间GC而导致Kafka僵死或是网络断开 连接导致长时间没有拉取消息进行同步,就会违反上面的两个条件,从而被Leader副本踢出ISR集合。当Follower副本从异常中恢复之后,会继续与Leader副本进行同步,当Follower副本“追上”(即最后一条消息的offset的差值小于指定阈值)Leader副本的时候,此Follower副本会被Leader副本重新加入到ISR中。

 请说明什么是Apache Kafka?

Apache Kafka是由Apache开发的一种发布订阅消息系统,它是一个分布式的、分区的和重复的日志服务。

 请说明什么是传统的消息传递方法?

传统的消息传递方法包括两种:

排队:在队列中,一组用户可以从服务器中读取消息,每条消息都发送给其中一个人。发布-订阅:在这个模型中,消息被广播给所有的用户。

 请说明Kafka相对传统技术有什么优势?

Apache Kafka与传统的消息传递技术相比优势之处在于:

快速:单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作。   可伸缩:在一组机器上对数据进行分区和简化,以支持更大的数据

持久:消息是持久性的,并在集群中进行复制,以防止数据丢失设计:它提供了容错保证和持久性

 Kafkabroker的意义是什么?

在Kafka集群中,broker术语用于引用服务器。

 Kafka服务器能接收到的最大信息是多少?

Kafka服务器可以接收到的消息的最大大小是1000000字节。

 解释KafkaZookeeper是什么?我们可以在没有Zookeeper的情况下使用Kafka?

Zookeeper是一个开放源码的、高性能的协调服务,它用于Kafka的分布式应用。

不,不可能越过Zookeeper,直接联系Kafka broker。一旦Zookeeper停止工作,它就不能服务客户端请求。Zookeeper主要用于在集群中不同节点之间进行通信

在Kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取

除此之外,它还执行其他活动,如: leader检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

 解释Kafka的用户如何消费信息?

在Kafka中传递消息是通过使用sendfile API完成的。它支持将字节从套接口转移到磁盘,通过内核空间保存副本,并在内核用户之间调用内核。

 解释如何提高远程用户的吞吐量?

如果用户位于与broker不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。

 解释一下,在数据制作过程中,你如何能从Kafka得到准确的信息?

在数据中,为了精确地获得Kafka的消息,你必须遵循两件事:    在数据消耗期间避免重复,在数据生产过程中避免重复。这里有两种方法,可以在数据生成时准确地获得一个语义:

每个分区使用一个单独的写入器,每当你发现一个网络错误,检查该分区中的最后一条消息,以查看您的最后一次写入是否成功在消息中包含一个主键(UUID或其他),并在用户中进行反复制

 解释如何减少ISR中的扰动?broker什么时候离开ISR?

ISR是一组与leaders完全同步的消息副本,也就是说ISR中包含了所有提交的消息。ISR应该总是包含所有的副本,直到出现真正的故障。如果一个副本从leader中脱离出来,将会   从ISR中删除。

 Kafka为什么需要复制?

Kafka的信息复制确保了任何已发布的消息不会丢失,并且可以在机器错误、程序错误或更常见些的软件升级中使用。

 如果副本在ISR中停留了很长时间表明什么?

如果一个副本在ISR中保留了很长一段时间,那么它就表明,跟踪器无法像在leader收集数据那样快速地获取数据。

 请说明如果首选的副本不在ISR中会发生什么?

如果首选的副本不在ISR中,控制器将无法将leadership转移到首选的副本。

 有可能在生产后发生消息偏移吗?

在大多数队列系统中,作为生产者的类无法做到这一点,它的作用是触发并忘记消息。broker将完成剩下的工作,比如使用id进行适当的元数据处理、偏移量等。

作为消息的用户,你可以从Kafka broker中获得补偿。如果你注视SimpleConsumer类,你会注意到它会获取包括偏移量作为列表的MultiFetchResponse对象。此外,当你对Kafka消息进行迭代时,你会拥有包括偏移量和消息发送的MessageAndOffset对象。

 kafka与传统的消息中间件对比

kafka与传统的消息中间件对比

RabbitMQkafka从几个角度简单的对比

业界对于消息的传递有多种方案和产品,本文就比较有代表性的两个MQ(rabbitMQ,kafka)进行阐述和做简单的对比,   在应用场景方面,

RabbitMQ,遵循AMQP协议,由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上。  kafka是Linkedin于2010年12月份开源的消息发布订阅系统,它主要用于处理活跃的流式数据,大数据量的数据处理上。1)在架构模型方面,

RabbitMQ遵循AMQP协议,RabbitMQ的broker由Exchange,Binding,queue组成,其中exchange和binding组成了消息的路由键;客户端Producer通过连接channel和server进  行通信,Consumer从queue获取消息进行消费(长连接,queue有消息会推送到consumer端,consumer循环从输入流读取数据)。rabbitMQ以broker为中心;有消息的确认机制。

kafka遵从一般的MQ结构,producer,broker,consumer,以consumer为中心,消息的消费信息保存的客户端consumer上,consumer根据消费的点,从broker上批量pull

数据;无消息确认机制。

2) 在吞吐量,

kafka具有高的吞吐量,内部采用消息的批量处理,zero-copy机制,数据的存储和获取是本地磁盘顺序批量操作,具有O(1)的复杂度,消息处理的效率很高。

rabbitMQ在吞吐量方面稍逊于kafka,他们的出发点不一样,rabbitMQ支持对消息的可靠的传递,支持事务,不支持批量的操作;基于存储的可靠性的要求存储可以采用内存或      者硬盘。

3) 在可用性方面,

rabbitMQ支持miror的queue,主queue失效,miror queue接管。kafka的broker支持主备模式。

4) 在集群负载均衡方面,

kafka采用zookeeper对集群中的broker、consumer进行管理,可以注册topic到zookeeper上;通过zookeeper的协调机制,producer保存对应topic的broker信息,可以随机       或者轮询发送到broker上;并且producer可以基于语义指定分片,消息发送到broker的某分片上。

rabbitMQ的负载均衡需要单独的loadbalancer进行支持。

Kafka 对比 ActiveMQ

Kafka 是LinkedIn 开发的一个高性能、分布式的消息系统,广泛用于日志收集、流式数据处理、在线和离线消息分发等场景。虽然不是作为传统的MQ来设计,在大部分情况,

Kafaka 也可以代替原先ActiveMQ 等传统的消息系统。

Kafka 将消息流按Topic 组织,保存消息的服务器称为Broker,消费者可以订阅一个或者多个Topic。为了均衡负载,一个Topic 的消息又可以划分到多个分区(Partition),分区越多,Kafka并行能力和吞吐量越高。

Kafka 集群需要zookeeper 支持来实现集群,最新的kafka 发行包中已经包含了zookeeper,部署的时候可以在一台服务器上同时启动一个zookeeper Server 和 一个Kafka Server,也可以使用已有的其他zookeeper集群。

和传统的MQ不同,消费者需要自己保留一个offset,从kafka 获取消息时,只拉去当前offset 以后的消息。Kafka 的scala/java 版的client 已经实现了这部分的逻辑,将offset 保存到zookeeper 上。每个消费者可以选择一个id,同样id 的消费者对于同一条消息只会收到一次。一个Topic 的消费者如果都使用相同的id,就是传统的  Queue;如果每个消费者都使用不同的id, 就是传统的pub-sub.

ActiveMQ和Kafka,前者完全实现了JMS的规范,后者看上去有一些“野路子”,并没有纠结于JMS规范,剑走偏锋的设计了另一套吞吐非常高的分布式发布-订阅消息系统,目 前非常流行。接下来我们结合三个点(消息安全性,服务器的稳定性容错性以及吞吐量)来分别谈谈这两个消息中间件。今天我们谈Kafka

01 性能怪兽Kafka

Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache定级项目。”Apache Kafka is publish-subscribe messaging rethought as a distributed commit log.”,官网首页的一句话高度概括其职责。Kafka并没有遵守JMS规范,他只用文件系统来管理消息的生命周期。Kafka的设计目标是:

(1) 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。

(2) 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输。

(3) 支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输。

(4) 同时支持离线数据处理和实时数据处理。

(5) Scale out:支持在线水平扩展。

所以,不像AMQ,Kafka从设计开始极为高可用为目的,天然HA。broker支持集群,消息亦支持负载均衡,还有副本机制。同样,Kafka也是使用Zookeeper管理集群节点信息,包括consumer的消费信息也是保存在zk中,下面我们分话题来谈:

1) 消息的安全性

Kafka集群中的Leader负责某一topic的某一partition的消息的读写,理论上consumer和producer只与该Leader        节点打交道,一个集群里的某一broker即是Leader的同时也可以担当某一partition的follower,即Replica。Kafka分配Replica的算法如下:

(1) 将所有Broker(假设共n个Broker)和待分配的Partition排序

(2) 将第i个Partition分配到第(i mod n)个Broker上

(3)将第i个Partition的第j个Replica分配到第((i + j) mode n)个Broker上

同时,Kafka与Replica既非同步也不是严格意义上的异步。一个典型的Kafka发送-消费消息的过程如下:首先首先Producer消息发送给某Topic的某Partition的Leader,Leader          先是将消息写入本地Log,同时follower(如果落后过多将会被踢出出 Replica列表)从Leader上pull消息,并且在未写入log的同时即向Leader发送ACK的反馈,所以对于某一条已经算作commit的消息来讲,在某一时刻,其存在于Leader的log中,以及Replica的内存中。这可以算作一个危险的情况(听起来吓人),因为如果此时集群挂了这条消息就算    丢失了,但结合producer的属性(request.required.acks=2 当所有follower都收到消息后返回ack)可以保证在绝大多数情况下消息的安全性。当消息算作commit的时候才会暴露给consumer,并保证at-least-once的投递原则。

2) 服务的稳定容错性

前面提到过,Kafka天然支持HA,整个leader/follower机制通过zookeeper调度,它在所有broker中选出一个 controller,所有Partition的Leader选举都由controller决定,同时controller也负责增删Topic以及 Replica的重新分配。如果Leader挂了,集群将在ISR(in-sync replicas)中选出新的Leader,选举基本原则是:新的Leader必须拥有原来的

Leader    commit过的所有消息。假如所有的follower都挂了,Kafka会选择第一个“活”过来的Replica(不一定是ISR中的)作为    Leader,因为如果此时等待ISR中的Replica是有风险的,假如所有的ISR都无法“活”,那此partition将会变成不可用。

3) 吞吐量

Leader节点负责某一topic(可以分成多个partition)的某一partition的消息的读写,任何发布到此partition的消息都会被直接追加到log文件的尾部,因为每条消息都被append 到该partition中,是顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证),同时通过合理的partition,消息     可以均匀的分布在不同的partition里面。 Kafka基于时间或者partition的大小来删除消息,同时broker是无状态的,consumer的消费状态(offset)是由 consumer自己控制的

(每一个consumer实例只会消费某一个或多个特定partition的数据,而某个partition的数据只会被某一个特定的consumer实例所消费),也不需要broker通过锁机制去控制消息的消费,所以吞吐量惊人,这也是Kafka吸引人的地方。

最后说下由于zookeeper引起的脑裂(Split  Brain)问题:每个consumer分别单独通过Zookeeper判断哪些partition   down了,那么不同consumer从Zookeeper“看”到的view就可能不一样,这就会造成错误的reblance尝试。而且有可能所有的 consumer都认为rebalance已经完成了,但实际上可能并非如此。

如果在MQ的场景下,将Kafka 和 ActiveMQ 相比:

Kafka 的优点

分布式可高可扩展。Kafka 集群可以透明的扩展,增加新的服务器进集群。

高性能。Kafka 的性能大大超过传统的ActiveMQ、RabbitMQ等MQ 实现,尤其是Kafka 还支持batch 操作。下图是linkedin 的消费者性能压测结果:

容错。Kafka每个Partition的数据都会复制到几台服务器上。当某个Broker故障失效时,ZooKeeper服务将通知生产者和消费者,生产者和消费者转而使用其它Broker。

Kafka 的不利

重复消息。Kafka 只保证每个消息至少会送达一次,虽然几率很小,但一条消息有可能会被送达多次。

消息乱序。虽然一个Partition 内部的消息是保证有序的,但是如果一个Topic 有多个Partition,Partition 之间的消息送达不保证有序。复杂性。Kafka需要zookeeper 集群的支持,Topic通常需要人工来创建,部署和维护较一般消息队列成本更高

 KAFKA:如何做到1秒发布百万级条消息

KAFKA:如何做到1秒发布百万级条消息

KAFKA:如何做到1秒发布百万级条消息。KAFKA是分布式发布-订阅消息系统,是一个分布式的,可划分的,冗余备份的持久性的日志服务。它主要用于处理活跃的流式数据。

现在被广泛地应用于构建实时数据管道和流应用的场景中,具有横向扩展,容错,快等优点,并已经运行在众多大中型公司的生产环境中,成功应用于大数据领域,本文分享一下我所了解的KAFKA。

1 KAFKA高吞吐率性能揭秘

KAFKA的第一个突出特定就是“快”,而且是那种变态的“快”,在普通廉价的虚拟机器上,比如一般SAS盘做的虚拟机上,据LINDEDIN统计,最新的数据是每天利用KAFKA处理的消息超过1万亿条,在峰值时每秒钟会发布超过百万条消息,就算是在内存和CPU都不高的情况下,Kafka的速度最高可以达到每秒十万条数据,并且还能持久化存储。

作为消息队列,要承接读跟写两块的功能,首先是写,就是消息日志写入KAFKA,那么,KAFKA在“写”上是怎么做到写变态快呢?

1.1 KAFKA让代码飞起来之写得快

首先,可以使用KAFKA提供的生产端API发布消息到1个或多个Topic(主题)的一个(保证数据的顺序)或者多个分区(并行处理,但不一定保证数据顺序)。Topic可以简单理解成一个 数据类别,是用来区分不同数据的。

KAFKA维护一个Topic中的分区log,以顺序追加的方式向各个分区中写入消息,每个分区都是不可变的消息队列。分区中的消息都是以k-v形式存在。

? k表示offset,称之为偏移量,一个64位整型的唯一标识,offset代表了Topic分区中所有消息流中该消息的起始字节位置。

?    v就是实际的消息内容,每个分区中的每个offset都是唯一存在的,所有分区的消息都是一次写入,在消息未过期之前都可以调整offset来实现多次读取。以上提到KAFKA“快”的第一个因素:消息顺序写入磁盘。

我们知道现在的磁盘大多数都还是机械结构(SSD不在讨论的范围内),如果将消息以随机写的方式存入磁盘,就会按柱面、磁头、扇区的方式进行(寻址过程),缓慢的机械运动(相         对内存)会消耗大量时间,导致磁盘的写入速度只能达到内存写入速度的几百万分之一,为了规避随机写带来的时间消耗,KAFKA采取顺序写的方式存储数据,如下图所示:

 
  

新来的消息只能追加到已有消息的末尾,并且已经生产的消息不支持随机删除以及随机访问,但是消费者可以通过重置offset的方式来访问已经消费过的数据。

即使顺序读写,过于频繁的大量小I/O操作一样会造成磁盘的瓶颈,所以KAFKA在此处的处理是把这些消息集合在一起批量发送,这样减少对磁盘IO的过度读写,而不是一次发送        单个消息。

另一个是无效率的字节复制,尤其是在负载比较高的情况下影响是显着的。为了避免这种情况,KAFKA采用由Producer,broker和consumer共享的标准化二进制消息格式,这  样数据块就可以在它们之间自由传输,无需转换,降低了字节复制的成本开销。

同时,KAFKA采用了MMAP(Memory Mapped Files,内存映射文件)技术。很多现代操作系统都大量使用主存做磁盘缓存,一个现代操作系统可以将内存中的所有剩余空间用作磁盘缓存,而当内存回收的时候几乎没有性能损失。

由于KAFKA是基于JVM的,并且任何与Java内存使用打过交道的人都知道两件事:

? 对象的内存开销非常高,通常是实际要存储数据大小的两倍;

? 随着数据的增加,java的垃圾收集也会越来越频繁并且缓慢。

基于此,使用文件系统,同时依赖页面缓存就比使用其他数据结构和维护内存缓存更有吸引力:

? 不使用进程内缓存,就腾出了内存空间,可以用来存放页面缓存的空间几乎可以翻倍。

? 如果KAFKA重启,进行内缓存就会丢失,但是使用操作系统的页面缓存依然可以继续使用。可能有人会问KAFKA如此频繁利用页面缓存,如果内存大小不够了怎么办?

KAFKA会将数据写入到持久化日志中而不是刷新到磁盘。实际上它只是转移到了内核的页面缓存。

利用文件系统并且依靠页缓存比维护一个内存缓存或者其他结构要好,它可以直接利用操作系统的页缓存来实现文件到物理内存的直接映射。完成映射之后对物理内存的操作在适当时候会被同步到硬盘上。

1.2 KAFKA让代码飞起来之读得快

KAFKA除了接收数据时写得快,另外一个特点就是推送数据时发得快。

KAFKA这种消息队列在生产端和消费端分别采取的push和pull的方式,也就是你生产端可以认为KAFKA是个无底洞,有多少数据可以使劲往里面推送,消费端则是根据自己的消    费能力,需要多少数据,你自己过来KAFKA这里拉取,KAFKA能保证只要这里有数据,消费端需要多少,都尽可以自己过来拿。

▲零拷贝

具体到消息的落地保存,broker维护的消息日志本身就是文件的目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。维护这个公共的格式并允许优化最重             要的操作:网络传输持久性日志块。 现代的unix操作系统提供一个优化的代码路径,用于将数据从页缓存传输到socket;在Linux中,是通过sendfile系统调用来完成的。Java提供了访问这个系统调用的方法:FileChannel.transferTo API。

 
  

要理解senfile的影响,重要的是要了解将数据从文件传输到socket的公共数据路径,如下图所示,数据从磁盘传输到socket要经过以下几个步骤:

? 操作系统将数据从磁盘读入到内核空间的页缓存

? 应用程序将数据从内核空间读入到用户空间缓存中

? 应用程序将数据写回到内核空间到socket缓存中

? 操作系统将数据从socket缓冲区复制到网卡缓冲区,以便将数据经网络发出

这里有四次拷贝,两次系统调用,这是非常低效的做法。如果使用sendfile,只需要一次拷贝就行:允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中,          只有最后一步将数据拷贝到网卡缓存中是需要的。

 
  

常规文件传输和zeroCopy方式的性能对比:

假设一个Topic有多个消费者的情况,          并使用上面的零拷贝优化,数据被复制到页缓存中一次,并在每个消费上重复使用,而不是存储在存储器中,也不在每次读取时复制到用户空间。 这使得以接近网络连接限制的速度消费消息。

这种页缓存和sendfile组合,意味着KAFKA集群的消费者大多数都完全从缓存消费消息,而磁盘没有任何读取活动。

▲批量压缩

在很多情况下,系统的瓶颈不是CPU或磁盘,而是网络带宽,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。所以数据压缩就很重要。可以每个消息都压           缩,但是压缩率相对很低。所以KAFKA使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩。

KAFKA允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩。KAFKA支持Gzip和Snappy压缩协议。

 
  

KAFKA数据可靠性深度解读

KAFKA的消息保存在Topic中,Topic可分为多个分区,为保证数据的安全性,每个分区又有多个Replia。

? 多分区的设计的特点:

  1. 为了并发读写,加快读写速度;
  2. 是利用多分区的存储,利于数据的均衡;
  3. 是为了加快数据的恢复速率,一但某台机器挂了,整个集群只需要恢复一部分数据,可加快故障恢复的时间。

每个Partition分为多个Segment,每个Segment有.log和.index 两个文件,每个log文件承载具体的数据,每条消息都有一个递增的offset,Index文件是对log文件的索引,

Consumer查找offset时使用的是二分法根据文件名去定位到哪个Segment,然后解析msg,匹配到对应的offset的msg。

2.1 Partition recovery过程

每个Partition会在磁盘记录一个RecoveryPoint,,记录已经flush到磁盘的最大offset。当broker      失败重启时,会进行loadLogs。首先会读取该Partition的RecoveryPoint,找到包含RecoveryPoint的segment及以后的segment,    这些segment就是可能没有完全flush到磁盘segments。然后调用segment的recover,重新读取各个segment的msg,并重建索引。每次重启KAFKA的broker时,都可以在输出的日志看到重建各个索引的过程。

2.2 数据同步

Producer和Consumer都只与Leader交互,每个Follower从Leader拉取数据进行同步。

 
  

如上图所示,ISR是所有不落后的replica集合,不落后有两层含义:距离上次FetchRequest的时间不大于某一个值或落后的消息数不大于某一个值,Leader失败后会从ISR中随机选取一个Follower做Leader,该过程对用户是透明的。

当Producer向Broker发送数据时,可以通过request.required.acks参数设置数据可靠性的级别。

此配置是表明当一次Producer请求被认为完成时的确认值。特别是,多少个其他brokers必须已经提交了数据到它们的log并且向它们的Leader确认了这些信息。

?典型的值:

0: 表示Producer从来不等待来自broker的确认信息。这个选择提供了最小的时延但同时风险最大(因为当server宕机时,数据将会丢失)。

1:表示获得Leader replica已经接收了数据的确认信息。这个选择时延较小同时确保了server确认接收成功。

-1:Producer会获得所有同步replicas都收到数据的确认。同时时延最大,然而,这种方式并没有完全消除丢失消息的风险,因为同步replicas的数量可能是1。如果你想确保某   些replicas接收到数据,那么你应该在Topic-level设置中选项min.insync.replicas设置一下。

仅设置 acks= -1 也不能保证数据不丢失,当ISR列表中只有Leader时,同样有可能造成数据丢失。要保证数据不丢除了设置acks=-1,还要保证ISR的大小大于等于2。具体参数设置:

request.required.acks:设置为-1 等待所有ISR列表中的Replica接收到消息后采算写成功。

min.insync.replicas: 设置为>=2,保证ISR中至少两个Replica。

Producer:要在吞吐率和数据可靠性之间做一个权衡。

KAFKA作为现代消息中间件中的佼佼者,以其速度和高可靠性赢得了广大市场和用户青睐,其中的很多设计理念都是非常值得我们学习的,本文所介绍的也只是冰山一角,希望         能够对大家了解KAFKA有一定的作用。

 kafka文件存储

据kafka官网吹,如果随机写入磁盘,速度就只有100KB每秒。顺序写入的话,7200转/s的磁盘就能达到惊人的600MB每秒!

操作系统对文件访问做了优化,文件会在内核空间分页做缓存(pageCache)。写入时先写入pageCache。由操作系统来决定何时统一写入磁盘。操作系统会使用顺序写入。

Kafka深入理解-1Kafka高效的文件存储设计

Kafka是什么

Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx

日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。

1. 前言

一个商业化消息队列的性能好坏,其文件存储机制设计是衡量一个消息队列服务技术水平和最关键指标之一。下面将从Kafka文件存储机制和物理结构角度,分析Kafka是如何实现高效文件存储,及实际应用效果。

2. Kafka文件存储机制

Kafka部分名词解释如下:

Broker:消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。           Topic:一类消息,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发。Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。

Segment:partition物理上由多个segment组成,下面2.2和2.3有详细说明。

offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息.

分析过程分为以下4个步骤:

topic中partition存储分布partiton中文件存储方式

partiton中segment文件存储结构

在partition中如何通过offset查找message

通过上述4过程详细分析,我们就可以清楚认识到kafka文件存储机制的奥秘。

2.1 topicpartition存储分布

假设实验环境中Kafka集群只有一个broker,xxx/message-folder为数据文件存储根目录,在Kafka  broker中server.properties文件配置(参数log.dirs=xxx/message-folder),例如创建2个topic名称分别为report_push、launch_info, partitions数量都为partitions=4

存储路径和目录规则为:

xxx/message-folder

 
  

在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始,序号最大值为

partitions数量减1。

2.2 partiton中文件存储方式

下面示意图形象说明了partition中文件存储方式:

每个partion(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。

每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。

这样做的好处就是能快速删除无用文件,有效提高磁盘利用率。

2.3 partitonsegment文件存储结构

读者从2.2节了解到Kafka文件系统partition存储方式,本节深入分析partion中segment file组成和物理结构。

segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀".index"和“.log”分别表示为segment索引文件、数据文件. segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19         位数字字符长度,没有数字用0填充。

下面文件列表是笔者在Kafka  broker上做的一个实验,创建一个topicXXX包含1   partition,设置每个segment大小为500MB,并启动producer向Kafka   broker写入大量数据,如下图2所示segment文件列表形象说明了上述2个规则:

以上述一对segment file文件为例,说明segment中index<—->data file对应关系物理结构如下:

上述索引文件存储大量元数据,数据文件存储大量消息,索引文件中元数据指向对应数据文件中message的物理偏移地址。

其中以索引文件中元数据3,497为例,依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移地址为497。

从上述了解到segment data file由许多message组成,下面详细说明message物理结构: 参数说明:

关键字

解释说明

8 byte offset

在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示

partiion的第多少message

4 byte message size

message大小

4 byte CRC32

用crc32校验message

1 byte “magic"

表示本次发布Kafka服务程序协议版本号

1 byte “attributes"

表示为独立版本、或标识压缩类型、或编码类型。

4 byte key length

表示key的长度,当key为-1时,K byte key字段不填

K byte key

可选

value bytes payload

表示实际消息数据。

2.4 partition中如何通过offset查找message

例如读取offset=368776的message,需要通过下面2个步骤查找。   第一步查找segment file

上述图2为例,其中00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0.第二个文件00000000000000368769.index的消息量起始偏移量为368770

= 368769 + 1.同样,第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset 二分查找文件列表,就可以快速定位到具体文件。

当offset=368776时定位到00000000000000368769.index|log 第二步通过segment file查找message

通过第一步定位到segment   file,当offset=368776时,依次定位到00000000000000368769.index的元数据物理位置和00000000000000368769.log的物理偏移地址,然后再通过00000000000000368769.log顺序查找直到offset=368776为止。

从上述图3可知这样做的优点,segment index file采取稀疏索引存储方式,它减少索引文件大小,通过mmap可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。

Kafka文件存储机制实际运行效果

实验环境:

Kafka集群:由2台虚拟机组成cpu:4核

物理内存:8GB 网卡:千兆网卡

jvm heap: 4GB

从上述可以看出,Kafka运行时很少有大量读磁盘的操作,主要是定期批量写磁盘操作,因此操作磁盘很高效。这跟Kafka文件存储中读写message的设计是息息相关的。Kafka  中读写message有如下特点:

写message

消息从java堆转入page cache(即物理内存)。由异步线程刷盘,消息从page cache刷入磁盘。

读message

消息直接从page cache转入socket发送出去。

当从page cache没有找到相应数据时,此时会产生磁盘IO,从磁盘Load消息到page cache,然后直接从socket发出去

4. 总结

Kafka高效文件存储设计特点

Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。    通过索引信息可以快速定位message和确定response的最大大小。

通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

kafkalog存储解析——topic的分区partition分段segment以及索引等

引言

Kafka中的Message是以topic为基本单位组织的,不同的topic之间是相互独立的。每个topic又可以分成几个不同的partition(每个topic有几个partition是在创建topic时指定         的),每个partition存储一部分Message。借用官方的一张图,可以直观地看到topic和partition的关系。

 
  

partition是以文件的形式存储在文件系统中,比如,创建了一个名为page_visits的topic,其有5个partition,那么在Kafka的数据目录中(由配置文件中的log.dirs指定的)中就有这            样5个目录: page_visits-0, page_visits-1,page_visits-2,page_visits-3,page_visits-4,其命名规则为<topic_name>-<partition_id>,里面存储的分别就是这5个partition的数据。

接下来,本文将分析partition目录中的文件的存储格式和相关的代码所在的位置。

Partition的数据文件

Partition中的每条Message由offset来表示它在这个partition中的偏移量,这个offset不是该Message在partition数据文件中的实际存储位置,而是逻辑上一个值,它唯一确定了partition中的一条Message。因此,可以认为offset是partition中Message的id。partition中的每条Message包含了以下三个属性:

offset MessageSize data

其中offset为long型,MessageSize为int32,表示data有多大,data为message的具体内容。它的格式和Kafka通讯协议中介绍的MessageSet格式是一致。

 
  

Partition的数据文件则包含了若干条上述格式的Message,按offset由小到大排列在一起。它的实现类为FileMessageSet,类图如下:

它的主要方法如下:

append: 把给定的ByteBufferMessageSet中的Message写入到这个数据文件中。

searchFor: 从指定的startingPosition开始搜索找到第一个Message其offset是大于或者等于指定的offset,并返回其在文件中的位置Position。它的实现方式是从startingPosition开始读取12个字节,分别是当前MessageSet的offset和size。如果当前offset小于指定的offset,那么将position向后移动LogOverHead+MessageSize(其中LogOverHead为offset+messagesize,为12个字节)。

read:准确名字应该是slice,它截取其中一部分返回一个新的FileMessageSet。它不保证截取的位置数据的完整性。sizeInBytes: 表示这个FileMessageSet占有了多少字节的空间。

truncateTo: 把这个文件截断,这个方法不保证截断位置的Message的完整性。

readInto: 从指定的相对位置开始把文件的内容读取到对应的ByteBuffer中。

我们来思考一下,如果一个partition只有一个数据文件会怎么样?

  1. 新数据是添加在文件末尾(调用FileMessageSet的append方法),不论文件数据文件有多大,这个操作永远都是O(1)的。
  2. 查找某个offset的Message(调用FileMessageSet的searchFor方法)是顺序查找的。因此,如果数据文件很大的话,查找的效率就低。

那Kafka是如何解决查找效率的的问题呢?有两大法宝:1) 分段 2) 索引。

数据文件的分段

Kafka解决查询效率的手段之一是将数据文件分段,比如有100条Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。

为数据文件建索引

数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。

索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。

相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其所属数据文件中最小的offset的大小。举例,分段后的一个数 据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20   =   5。存储相对offset可以减小索引文件占用的空间。 position,表示该条Message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的Message了。

index文件中并没有为数据文件中的每条Message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将        索引文件保留在内存中。但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。

 
  

在Kafka中,索引文件的实现类为OffsetIndex,它的类图如下:

主要的方法有:

append方法,添加一对offset和position到index文件中,这里的offset将会被转成相对的offset。lookup, 用二分查找的方式去查找小于或等于给定offset的最大的那个offset

小结

我们以几张图来总结一下Message是如何在Kafka中存储的,以及如何查找指定offset的Message的。

Message是按照topic来组织,每个topic可以分成多个的partition,比如:有5个partition的名为为page_visits的topic的目录结构为:

partition是分段的,每个段叫LogSegment,包括了一个数据文件和一个索引文件,下图是某个partition目录下的文件:

可以看到,这个partition有4个LogSegment。

借用博主@lizhitao博客上的一张图来展示是如何查找Message的。

 
  

比如:要查找绝对offset为7的Message:

  1. 首先是用二分查找确定它是在哪个LogSegment中,自然是在第一个Segment中。
  2. 打开这个Segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件我们知道offset为6的Message在数据文件中的位置为9807。
  3. 打开数据文件,从位置为9807的那个地方开始顺序扫描直到找到offset为7的那条Message。

这套机制是建立在offset是有序的。索引文件被映射到内存中,所以查找的速度还是很快的。

一句话,Kafka的Message存储采用了分区(partition),分段(LogSegment)和稀疏索引这几个手段来达到了高效性。

Kafka 的设计时什么样的呢?

Kafka 将消息以 topic 为单位进行归纳

将向 Kafka topic 发布消息的程序成为 producers.

将预订 topics 并消费消息的程序成为 consumer.

Kafka 以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 broker. producers 通过网络将消息发送到 Kafka 集群,集群向消费者提供消息

数据传输的事物定义有哪三种?

数据传输的事务定义通常有以下三种级别:

(1) 最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输

(2) 最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.

(3) 精确的一次(Exactly once):不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的

Kafka 判断一个节点是否还活着有那两个条件?

(1) 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接

(2) 如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久

producer 是否直接将数据发送到 broker leader(主节点)

producer 直接将数据发送到 broker 的 leader(主节点),不需要在多个节点进行分发,为了帮助 producer 做到这点,所有的 Kafka 节点都可以及时的告知:哪些节点是活动的, 目标topic 目标分区的 leader 在哪。这样 producer 就可以直接将消息发送到目的地了

Kafa consumer 是否可以消费指定分区消息?

Kafaconsumer 消费消息时,向 broker 发出"fetch"请求去消费特定分区的消息,consumer 指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,

customer 拥有了 offset 的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的

Kafka 消息是采用 Pull 模式,还是 Push 模式?

Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消息推送到

consumer,也就是 pull 还 push。在这方面,Kafka 遵循了一种大部分消息系统共同的传统的设计:producer 将消息推送到 broker,consumer 从 broker 拉取消息

一些消息系统比如 Scribe 和 ApacheFlume 采用了 push 模式,将消息推送到下游的 consumer。这样做有好处也有坏处:由 broker 决定消息推送的速率,对于不同消费速率的consumer 就不太好处理了。消息系统都致力于让 consumer 以最大的速率最快速的消费消息,但不幸的是,push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时, consumer 恐怕就要崩溃了。最终 Kafka 还是选取了传统的 pull 模式

Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据。Push 模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免  consumer  崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull  模式下,consumer  就可以根据自己的消费能力去决定这些策略

Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询, 直到新消息到 t 达。为了避免这点,Kafka 有个参数可以让 consumer 阻塞知道新消息到达

(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发

Kafka 存储在硬盘上的消息格式是什么?

消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和 CRC32

校验码。

  • 消息长度: 4 bytes (value: 1+4+n)
  • 版本号: 1 byte

·CRC 校验码: 4 bytes

  • 具体的消息: n bytes

Kafka 高效文件存储设计特点:

(1).Kafka 把 topic 中一个 parition 大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。

(2). 通过索引信息可以快速定位 message 和确定 response 的最大大小。

(3). 通过 index 元数据全部映射到 memory,可以避免 segment file 的 IO 磁盘操作。(4).通过索引文件稀疏存储,可以大幅降低 index 文件元数据占用空间大小。

Kafka 与传统消息系统之间有三个关键区别

(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留

(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性

(3).Kafka 支持实时的流式处理

Kafka 创建 Topic 时如何将分区放置到不同的 Broker

  • 副本因子不能大于 Broker 的个数;
  • 第一个分区(编号为 0)的第一个副本放置位置是随机从 brokerList 选择的;
  • 其他分区的第一个副本放置位置相对于第 0 个分区依次往后移。也就是如果我们有 5 个

Broker,5 个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个

Broker上;第三个分区将会放在第一个Broker上;第四个分区将会放在第二个Broker 上,依次类推;

  • 剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的

Kafka 新建的分区会在哪个目录下创建

在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是  Kafka 数据的存放目录,  这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。

Zookeeper(周老师)

ZooKeeper 是什么?

ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性 服务的软件,分布式应用程序可以基于  Zookeeper  实现诸如数据发布/订阅、  负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和 分布式队列等功能。

ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性 能高效、功能稳定的系统提供给用户。

Zookeeper 保证了如下分布式一致性特性:

(1) 顺序一致性

(2) 原子性

(3) 单一视图

(4) 可靠性

(5) 实时性(最终一致性)

客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了  监听器,这个监听器也是由所连接的  zookeeper   机器来处理。对于写请求,这   些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。

因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞 吐会下降。

有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每 个更新都有一个唯一的时间戳,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中 会带有这个zookeeper 最新的 zxid。

ZooKeeper 提供了什么?

文件系统通知机制

Zookeeper 文件系统

Zookeeper  提供一个多层级的节点命名空间(节点称为  znode)。与文件系统  不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存   放数据而目录节点不行。

Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构, 这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限 为1M。

Zookeeper 怎么保证主从节点的状态同步?

Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。 实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模 式和广播模式。恢复模式

当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出 来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状 态同步保证了

leader 和 server 具有相同的系统状态。

广播模式

一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息 了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在 恢复模式下启动, 发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的followers 支持。

四种类型的数据节点 Znode

(1) PERSISTENT-持久节点

除非手动删除,否则节点一直存在于 Zookeeper 上

(2) EPHEMERAL-临时节点

临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与 zookeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点 都会被移除。

(3) PERSISTENT_SEQUENTIAL-持久顺序节点

基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维 护的自增整型数字。

(4) EPHEMERAL_SEQUENTIAL-临时顺序节点

基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的 自增整型数字。

Zookeeper Watcher 机制 数据变更通知

Zookeeper 允许客户端向服务端的某个 Znode 注册一个 Watcher 监听,当服 务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事 件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类 型做出业务上的改变。

工作机制:

(1) 客户端注册 watcher

(2) 服务端处理 watcher

(3) 客户端回调 watcher Watcher 特性总结:

(1) 一次性

无论是服务端还是客户端,一旦一个 Watcher 被 触  发  ,Zookeeper 都会将其  从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务 端的压力都非常大。

(2) 客户端串行执行 客户端 Watcher 回调的过程是一个串行同步的过程。

(3) 轻量

3.1 、Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的 具体内容。

3.2 、客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端,仅仅是在客户端请求中使用 boolean 类型属性进行了 标记。

(4) watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是 异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket 进行通 信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知 它所监视 znode发生了变化。所以我们使用Zookeeper 不能期望能够监控到节 点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。

(5) 注册 watcher getData、exists、getChildren

(6) 触发 watcher create、delete、setData

(7) 当一个客户端连接到一个新的服务器上时,watch 将会被以任意会话事件 触发。当与一个服务器失去连接的时候,是无法接收到  watch 的。而当  client  重新连接时,如果需要的话,所有先前注册过的 watch,都会被重新注册。通 常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未 创建的 znode的 exist watch,如果在客户端断开连接期间被创建了,并且随后 在客户端连接上之前又删除了,这种情况下,这个 watch 事件可能会被丢失。

客户端注册 Watcher 实现

(1) 调用 getData()/getChildren()/exist()三个 API,传入 Watcher 对象

(2) 标记请求 request,封装 Watcher 到 WatchRegistration

(3) 封装成 Packet 对象,发服务端发送 request

(4) 收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管 理

(5) 请求返回,完成注册。

服务端处理 Watcher 实现

(1) 服务端接收 Watcher 并存储

接收到客户端请求,处理请求判断是否需要注册 Watcher,需要的话将数据节 点的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接, 实现了 Watcher 的

process 接口,此时可以看成一个 Watcher 对象)存储在 WatcherManager 的 WatchTable 和 watch2Paths 中去。

(2) Watcher 触发

以服务端接收到 setData() 事务请求触发 NodeDataChanged 事件为例:

2.1 封装 WatchedEvent 将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点 路径封装成一个 WatchedEvent 对象

2.2 查询 Watcher 从 WatchTable 中根据节点路径查找 Watcher 2.3 没找到;说明没有客户端在该数据节点上注册过 Watcher 2.4 找到;提取并从 WatchTable 和 Watch2Paths

中删除对应 Watcher(从 这里可以看出 Watcher 在服务端是一次性的,触发一次就失效了)

(3) 调用 process 方法来触发 Watcher 这里 process 主要就是通过 ServerCnxn 对应的 TCP 连接发送 Watcher 事件 通知。

客户端回调 Watcher

客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效 了。

ACL 权限控制机制

UGO(User/Group/Others)

目前在 Linux/Unix 文件系统中使用,也是使用最广泛的权限控制方式。是一种 粗粒度的文件系统权限控制模式。

ACL(Access Control List)访问控制列表包括三个方面:

权限模式(Scheme)

(1) IP:从 IP 地址粒度进行权限控制

(2) Digest:最常用,用类似于 username:password 的权限标识来进行权限 配置,便于区分不同应用来进行权限控制

(3) World:最开放的权限控制方式,是一种特殊的 digest 模式,只有一个 权限标识“world:anyone”

(4) Super:超级用户

授权对象

授权对象指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器灯。

权限 Permission

(1) CREATE:数据节点创建权限,允许授权对象在该 Znode 下创建子节点

(2) DELETE:子节点删除权限,允许授权对象删除该数据节点的子节点

(3) READ:数据节点的读取权限,允许授权对象访问该数据节点并读取其数 据内容或子节点列表等

(4) WRITE:数据节点更新权限,允许授权对象对该数据节点进行更新操作

(5) ADMIN:数据节点管理权限,允许授权对象对该数据节点进行 ACL 相关 设置操作

Chroot 特性

3.2.0  版本后,添加了  Chroot  特性,该特性允许每个客户端为自己设置一个命名空间。如果一个客户端设置了  Chroot,那么该客户端对服务器的任何操作,都将会被限制在其自己的命名空间下。

通过设置 Chroot,能够将一个客户端应用于 Zookeeper 服务端的一颗子树相对应,在那些多个应用公用一个  Zookeeper 进群的场景下,对实现不同应用间的相互隔离非常有帮助。

会话管理

分桶策略:将类似的会话放在同一区块中进行管理,以便于 Zookeeper 对会话进行不同区块的隔离处理以及同一区块的统一处理。分配原则:每个会话的“下次超时时间点”

(ExpirationTime)

计算公式:

ExpirationTime_ = currentTime + sessionTimeout ExpirationTime = (ExpirationTime_ / ExpirationInrerval + 1) *

ExpirationInterval , ExpirationInterval 是指 Zookeeper 会话超时检查时间间隔,默认 tickTime

服务器角色

Leader

(1) 事务请求的唯一调度和处理者,保证集群事务处理的顺序性

(2) 集群内部各服务的调度者

Follower

(1) 处理客户端的非事务请求,转发事务请求给 Leader 服务器

(2) 参与事务请求 Proposal 的投票

(3) 参与 Leader 选举投票

Observer

(1)3.0   版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力

(2) 处理客户端的非事务请求,转发事务请求给 Leader 服务器

(3) 不参与任何形式的投票

Zookeeper Server 工作状态

服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、 OBSERVING。

(1) LOOKING:寻 找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。

(2) FOLLOWING:跟随者状态。表明当前服务器角色是 Follower。(3)LEADING:领导者状态。表明当前服务器角色是 Leader。

(4)OBSERVING:观察者状态。表明当前服务器角色是 Observer。

数据同步

整个集群完成 Leader 选举之后,Learner(Follower 和 Observer 的统称)回向Leader 服务器进行注册。当 Learner 服务器想 Leader 服务器完成注册后,进入数据同步环节。数据同步流程:(均以消息传递的方式进行)

Learner 向 Learder 注册数据同步同步确认

Zookeeper 的数据同步通常分为四类:

(1) 直接差异化同步(DIFF 同步)

(2) 先回滚再差异化同步(TRUNC+DIFF 同步)

(3) 仅回滚同步(TRUNC 同步)(4)全量同步(SNAP 同步)

在进行数据同步前,Leader 服务器会完成数据同步初始化: peerLastZxid:

∙ 从 learner 服务器注册时发送的 ACKEPOCH 消息中提取 lastZxid(该Learner 服务器最后处理的 ZXID) minCommittedLog:

∙ Leader 服务器 Proposal 缓存队列 committedLog 中最小

ZXIDmaxCommittedLog:

∙ Leader 服务器 Proposal 缓存队列 committedLog 中最大 ZXID直接差异化同步(DIFF 同步)

∙ 场景:peerLastZxid 介于 minCommittedLog 和 maxCommittedLog之间先回滚再差异化同步(TRUNC+DIFF 同步)

∙ 场景:当新的 Leader 服务器发现某个 Learner 服务器包含了一条自己没有的事务记录,那么就需要让该 Learner 服务器进行事务回滚–回滚到 Leader服务器上存在的,同时也是最接近于 peerLastZxid 的 ZXID仅回滚同步(TRUNC 同步)

∙ 场景:peerLastZxid 大于 maxCommittedLog 全量同步(SNAP 同步)

∙ 场景一:peerLastZxid 小于 minCommittedLog

∙ 场景二:Leader 服务器上没有 Proposal 缓存队列且 peerLastZxid 不等于 lastProcessZxid

zookeeper 是如何保证事务的顺序一致性的?

zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是

epoch( 时期; 纪元; 世; 新时代)用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

分布式集群中为什么会有 Master主节点?

在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行 leader 选举。

zk 节点宕机如何处理?

Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失;

如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。

ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在

ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。  所以

3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5)

2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票<=1) 19. zookeeper 负载均衡和 nginx 负载均衡区别

zk 的负载均衡是可以调控,nginx 只是能调权重,其他需要可控的都需要自己写插件;但是 nginx 的吞吐量比 zk 大很多,应该说按业务选择用哪种方式。

Zookeeper 有哪几种几种部署模式?

Zookeeper 有三种部署模式:

  1. 单机部署:一台集群上运行;
  2. 集群部署:多台集群运行;
  3. 伪集群部署:一台集群启动多个 Zookeeper 实例运行。

集群最少要几台机器,集群规则是怎样的?集群中有 3 台服务器,其中一个节点宕机,这个时候

Zookeeper 还可以使用吗?

集群规则为 2N+1 台,N>0,即 3 台。可以继续使用,单数服务器只要没超过一半的服务器宕机就可以继续使用。

集群支持动态添加机器吗?

其实就是水平扩容了,Zookeeper 在这方面不太好。两种方式:

全部重启:关闭所有 Zookeeper 服务,修改配置之后启动。不影响之前客户端的会话。

逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。

3.5 版本开始支持动态扩容。

Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的?

不是。官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch 的数据发生了改变的时候,则服务器将这个改变发送给设置了  Watch 的客户端,以便通知它们。为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况

下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。

一般是客户端执行 getData(“/节点 A”,true),如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。

Zookeeper java 客户端都有哪些?

java 客户端:zk 自带的 zkclient 及 Apache 开源的 Curator。

chubby 是什么,和 zookeeper 比你怎么看?

chubby 是 google 的,完全实现 paxos 算法,不开源。zookeeper 是 chubby的开源实现,使用 zab 协议,paxos 算法的变种。

说几个 zookeeper 常用的命令。

常用命令:ls get set create delete 等。

ZAB Paxos 算法的联系与区别?

相同点:

(1) 两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行

(2) Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交

(3) ZAB 协议中,每个 Proposal 中都包含一个 epoch 值来代表当前的

Leader周期,Paxos 中名字为 Ballot 不同点:

ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。

Zookeeper 的典型应用场景

Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布和订阅。

通过对 Zookeeper 中丰富的数据节点进行交叉使用,配合 Watcher 事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能,如:

(1) 数据发布/订阅

(2) 负载均衡

(3) 命名服务

(4) 分布式协调/通知

(5) 集群管理

(6) Master 选举

(7) 分布式锁

(8) 分布式队列数据发布/订阅介绍数据发布/订阅系统,即所谓的配置中心,顾名思义就是发布者发布数据供订阅者进行数据订阅。目的动态获取数据(配置信息)实现数据(配置信息)的集中式管理和数据的动态更新设计模式

Push 模式

Pull 模式

数据(配置信息)特性

(1) 数据量通常比较小

(2) 数据内容在运行时会发生动态更新

(3) 集群中各机器共享,配置一致

如:机器列表信息、运行时开关配置、数据库配置信息等基于 Zookeeper 的实现方式

∙ 数据存储:将数据(配置信息)存储到 Zookeeper 上的一个数据节点

∙ 数据获取:应用在启动初始化节点从 Zookeeper 数据节点读取数据,并在该节点上注册一个数据变更 Watcher

∙ 数据变更:当变更数据时,更新  Zookeeper   对应节点数据,Zookeeper会将数据变更通知发到各客户端,客户端接到通知后重新读取变更后的数据即可。负载均衡

zk 的命名服务命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址, 或者一个远程的对象等等。

分布式通知和协调

对于系统调度来说:操作人员发送通知实际是通过控制台改变某个节点的状态,然后 zk 将这些变化发送给注册了这个节点的 watcher 的所有客户端。

对于执行情况汇报:每个工作进程都在某个目录下创建一个临时节点。并携带工作的进度数据,这样汇总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。

zk 的命名服务(文件系统)命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,即是唯一的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。

zk 的配置管理(文件系统、通知机制)程序分布式的部署在不同的机器上,将程序的配置信息放在 zk 的 znode 下,当有配置发生改变时,也就是 znode 发生变化时,可以通过改变 zk 中某个目录节点的内容,利用 watcher 通知给各个客户端,从而更改配置。

Zookeeper 集群管理(文件系统、通知机制)

所谓集群管理无在乎两点:是否有机器退出和加入、选举 master。

对于第一点,所有机器约定在父目录下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与     zookeeper    的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。

新机器加入也是类似,所有机器收到通知:新兄弟目录加入,highcount 又有

了,对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为 master 就好。

Zookeeper 分布式锁(文件系统、通知机制)

有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。

对于第一类,我们将 zookeeper 上的一个 znode 看作是一把锁,通过 createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的 distribute_lock 节点就释放出锁。

对于第二类,  /distribute_lock  已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选 master 一样,编号最小的获得锁,用完删除,依次方便。Zookeeper 队列管理(文件系统、通知机制)两种类型的队列:

(1) 同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。

(2) 队列按照 FIFO 方式进行入队和出队操作。

第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建 PERSISTENT_SEQUENTIAL 节点,创建成功时 Watcher 通知等待的队列,队列删除序列号最小的节点用以消费。此场景下

Zookeeper 的 znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。

 Zookeeper 都有哪些功能?

  1. 集群管理:监控节点存活状态、运行请求等;
  2. 主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 Zookeeper 可以协助完成这个过程;
  3. 分布式锁:Zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线

线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。

Zookeeper 可以对分布式锁进行控制。

  1. 命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。

 说一下 Zookeeper 的通知机制?

client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些 client 会收到 zk 的通知,然后 client 可以根据 znode 变化来做出业务上的改变等。

 Zookeeper  Dubbo 的关系?

Zookeeper的作用:

zookeeper用来注册服务和进行负载均衡,哪一个服务由哪一个机器来提供必需让调用者知道,简单来说就是ip地址和服务名称的对应关系。当然也可以通过硬编码的方式把这种         对应关系在调用方业务代码中实现,但是如果提供服务的机器

挂掉调用者无法知晓,如果不更改代码会继续请求挂掉的机器提供服务。

zookeeper通过心跳机制可以检测挂掉的机器并将挂掉机器的ip和服务对应关系从列表中删除。至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提         高运算能力。通过添加新的机器向zookeeper注册服务,服务的提供者多了能服务的客户就多了。

dubbo:

是管理中间层的工具,在业务层到数据仓库间有非常多服务的接入和服务提供者需要调度,dubbo提供一个框架解决这个问题。

注意这里的dubbo只是一个框架,至于你架子上放什么是完全取决于你的,就像一个汽车骨架,你需要配你的轮子引擎。这个框架中要完成调度必须要有一个分布式的注册中           心,储存所有服务的元数据,你可以用zk,也可以用别的,只是大家都用zk。

zookeeper和dubbo的关系:

Dubbo 的将注册中心进行抽象,它可以外接不同的存储媒介给注册中心提供服务,有 ZooKeeper,Memcached,Redis 等。

引入了 ZooKeeper 作为存储媒介,也就把  ZooKeeper  的特性引进来。首先是负载均衡,单注册中心的承载能力是有限的,在流量达到一定程度的时  候就需要分流,负载均衡就是为了分流而存在的,一个 ZooKeeper 群配合相应的

Web   应用就可以很容易达到负载均衡;资源同步,单单有负载均衡还不   够,节点之间的数据和资源需要同步,ZooKeeper   集群就天然具备有这样的功能;命名服务,将树状结构用于维护全局的服务地址列表,服务提供者在启动 的时

 
  

候,向 ZooKeeper 上的指定节点 /dubbo/${serviceName}/providers 目录下写入自己的 URL 地址,这个操作就完成了服务的发布。 其他特性还有 Mast 选举,分布式锁等。

 zookeeper集群之间如何通讯

zookeeper系列之通信模型

 
  

本文的主题就是讲解Zookeeper通信模型,本节将通过一个概要图来说明Zookeeper的通信模型。

Zookeeper的通信架构

在Zookeeper整个系统中,有3中角色的服务,client、Follower、leader。其中client负责发起应用的请求,Follower接受client发起的请求,参与事务的确认过程,在leader  crash后的leader选择。而leader主要承担事务的协调,当然leader也可以承担接收客户请求的功能,为了方便描述,后面的描述都是client与Follower之间的通信,如果

Zookeeper的配置支持leader接收client的请求,client与leader的通信跟client与Follower的通信模式完全一样。Follower与leader之间的角色可能在某一时刻进行转换。一个Follower在leader          crash掉以后可能被集群(Quorum)的Follower选举为leader。而一个leader在crash后,再次加入集群(Quorum)将作为Follower角色存在。在一个集群

(Quorum)中,除了在选举leader的过程中没有Follower和leader的区分外,其他任何时刻都只有1个leader和多个Follower。Client、Follower和leader之间的通信架构如下:

Client与Follower之间

 
  

为了使客户端具有较高的吞吐量,Client与Follower之间采用NIO的通信方式。当client需要与Zookeeper service打交道时,首先读取配置文件确定集群内的所有server列表,按照一定的load          balance算法选取一个Follower作为一个通信目标。这样client和Follower之间就有了一条由NIO模式构成的通信通道。这条通道会一直保持到client关闭session或者因为client或Follower任一方因某种原因异常中断通信连接。正常情况下, client与Follower在没有请求发起的时候都有心跳检测。

Follower与leader之间

Follower与leader之间的通信主要是因为Follower接收到像(create, delete, setData, setACL, createSession, closeSession, sync)这样一些需要让leader来协调最终结果的命令,将会导致Follower与leader之间产生通信。由于leader与Follower之间的关系式一对多的关系,非常适合client/server模式,因此他们之间是采用c/s模式,由leader创建一            个socket server,监听各Follower的协调请求。

集群在选择leader过程中

由于在选择leader过程中没有leader,在集群中的任何一个成员都需要与其他所有成员进行通信,当集群的成员变得很大时,这个通信量是很大的。选择leader的过程发生在

Zookeeper系统刚刚启动或者是leader失去联系后,选择leader过程中将不能处理用户的请求,为了提高系统的可用性,一定要尽量减少这个过程的时间。选择哪种方式让他们      可用快速得到选择结果呢?Zookeeper在这个过程中采用了策略模式,可用动态插入选择leader的算法。系统默认提供了3种选择算法,AuthFastLeaderElection, FastLeaderElection,LeaderElection。其中AuthFastLeaderElection和LeaderElection采用UDP模式进行通信,而FastLeaderElection仍然采用tcp/ip模式。在Zookeeper新的         版本中,新增了一个learner角色,减少选择leader的参与人数。使得选择过程更快。一般说来Zookeeper leader的选择过程都非常快,通常<200ms。

Zookeeper的通信流程

要详细了解Zookeeper的通信流程,我们首先得了解Zookeeper提供哪些客户端的接口,我们按照具有相同的通信流程的接口进行分组:

Zookeeper系统管理命令

Zookeeper的系统管理接口是指用来查看Zookeeper运行状态的一些命令,他们都是具有4字母构成的命令格式。主要包括:

  1. ruok:发送此命令可以测试zookeeper是否运行正常。
  2. dump:dump server端所有存活session的Ephemeral(临时)node信息。
  3. stat:获取连接server的服务器端的状态及连接该server的所有客服端的状态信息。
  4. reqs: 获取当前客户端已经提交但还未返回的请求。
  5. stmk:开启或关闭Zookeeper的trace level.
  6. gtmk:获取当前Zookeeper的trace level是否开启。
  7. envi: 获取Zookeeper的java相关的环境变量。
  8. srst:重置server端的统计状态

当用户发送这些命令的到server时,由于这些请求只与连接的server相关,没有业务处理逻辑,非常简单。Zookeeper对这些命令采用最快的效率进行处理。这些命令发送到

server端只占用一个4字节的int类型来表示不同命令,没有采用字符串处理。当服务器端接收到这些命令,立刻返回结果。

Session创建

任何客户端的业务请求都是基于session存在的前提下。Session是维持client与Follower之间的一条通信通道,并维持他们之间从创建开始后的所有状态。当启动一个Zookeeper client的时候,首先按照一定的算法查找出follower, 然后与Follower建立起NIO连接。当连接建立好后,发送create session的命令,让server端为该连接创建一个维护该连接状态的对象session。当server收到create     session命令,先从本地的session列表中查找看是否已经存在有相同sessionId,则关闭原session重新创建新的session。创建session的过程将需要发送到Leader,再由leader通知其他follower,大部分Follower都将此操作记录到本地日志再通知leader后,leader发送commit命令给所有Follower,连接客户端的Follower返回创建成功的session响应。Leader与Follower之间的协调过程将在后面的做详细讲解。当客户端成功创建好session后,其他的业务命令就可以正常处理了。

Zookeeper查询命令

Zookeeper查询命令主要用来查询服务器端的数据,不会更改服务器端的数据。所有的查询命令都可以即刻从client连接的server立即返回,不需要leader进行协调,因此查询命   令得到的数据有可能是过期数据。但由于任何数据的修改,leader都会将更改的结果发布给所有的Follower,因此一般说来,Follower的数据是可以得到及时的更新。这些查询       命令包括以下这些命令:

  1. exists:判断指定path的node是否存在,如果存在则返回true,否则返回false.
  2. getData:从指定path获取该node**的数据
  3. getACL:获取指定pathACL**
  4. getChildren:获取指定pathnode**的所有孩子结点。

所有的查询命令都可以指定watcher,通过它来跟踪指定path的数据变化。一旦指定的数据发生变化(create,delete,modified,children_changed),服务器将会发送命令来回            调注册的watcher. Watcher详细的讲解将在Zookeeper的Watcher中单独讲解。

Zookeeper修改命令

Zookeeper修改命令主要是用来修改节点数据或结构,或者权限信息。任何修改命令都需要提交到leader进行协调,协调完成后才返回。修改命令主要包括:

  1. createSession:请求server创建一个**session
  2. create:创建一个节点
  3. delete:删除一个节点
  4. setData:修改一个节点的数据
  5. setACL:修改一个节点的**ACL
  6. closeSession:请求server关闭**session

我们根据前面的通信图知道,任何修改命令都需要leader协调。    在leader的协调过程中,需要3次leader与Follower之间的来回请求响应。并且在此过程中还会涉及事务日志的记录,更糟糕的情况是还有take   snapshot的操作。因此此过程可能比较耗时。但Zookeeper的通信中最大特点是异步的,如果请求是连续不断的,Zookeeper的处理是集中处理逻辑,然后批量发送,批量的大小也是有控制的。如果请求量不大,则即刻发送。这样当负载很大时也能保证很大的吞吐量,时效性也在一定程度上进行了保证。

zookeeper server端的业务处理-processor链

Zookeeper通过链式的processor来处理业务请求,每个processor负责处理特定的功能。不同的Zookeeper角色的服务器processor链是不一样的,以下分别介绍standalone Zookeeper server, leader和Follower不同的processor链。

Zookeeper中的processor

  1. AckRequestProcessor:当leader从向Follower发送proposal后,Follower将发送一个Ack响应,leader收到Ack响应后,将会调用这个Processor进行处理。它主要负责检查  请求是否已经达到了多数Follower的确认,如果满足条件,则提交commitProcessor进行commit处理
  2. CommitProcessor:从commited队列中处理已经由leader协调好并commit的请求或者从请求队列中取出那些无需leader协调的请求进行下一步处理。
  3. FinalRequestProcessor:任何请求的处理都需要经过这个processor,这是请求处理的最后一个Processor,主要负责根据不同的请求包装不同的类型的响应包。当然Follower与leader之间协调后的请求由于没有client连接,将不需要发送响应(代码体现在if (request.cnxn == null) {return;})。
  4. FollowerRequestProcessor:Follower processor链上的第一个,主要负责将修改请求和同步请求发往leader进行协调。
  5. PrepRequestProcessor:在leader和standalone server上作为第一Processor,主要作用对于所有的修改命令生成changelog。
  6. ProposalRequestProcessor:leader用来将请求包装为proposal向Follower请求确认。
  7. SendAckRequestProcessor:Follower用来向leader发送Ack响应的处理。
  8. SyncRequestProcessor:负责将已经commit的事务写到事务日志以及take snapshot.
  9. ToBeAppliedRequestProcessor:负责将tobeApplied队列的中request转移到下一个请求进行处理。

Standalone zookeeper processor链

 
  
 
  

Leader processor链

Follower processor链

 Zookeeper面试题

ZooKeeper 是什么?

ZooKeeper 是一个*分布式*的,开放源码的分布式*应用程序协调服务*,是 Google 的 Chubby 一个开源的实现,它是*集群的管理者**监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作*。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

客户端的*读请求*可以被集群中的*任意一台机器处理*,如果读请求在节点上注册了监听器,这个监听器也是由所

连接的 zookeeper 机器来处理。对于*写请求*,这些请求会同*时发给其他zookeeper机器并且达成一致后,请求才会返回成功*。因此,随着zookeeper*的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降*。有序性是 zookeeper 中非常重要的一个特性,所有的*更新都是全局有序的*,每个更新都有一个*唯一的时间戳*,这个时间戳称为zxid*(Zookeeper Transaction Id)*。而*读请求只会相对于更新有序*,也就是读请求的返回结果中会带有这个zookeeper*最新的zxid*

ZooKeeper 提供了什么?

1、 *文件系统*

2、 *通知机制*

Zookeeper 文件系统

Zookeeper    提供一个多层级的节点命名空间(节点称为    znode)。与文件系统不同的是,这些节点*都可以设置关联的数据*,而文件系统中只有文件节点可以存放数据而目录节点不行。Zookeeper 为了保证高吞吐和低延

迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper*不能用于存放大量的数据*,每个节点的存放数据上限为*1M*

四种类型的 znode

1、 PERSISTENT-*持久化目录节点*

客户端与 zookeeper 断开连接后,该节点依旧存在

2、 PERSISTENT_SEQUENTIAL-*持久化顺序编号目录节点*

客户端与 zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号

3、 EPHEMERAL-*临时目录节点*

客户端与 zookeeper 断开连接后,该节点被删除

4、 EPHEMERAL_SEQUENTIAL-*临时顺序编号目录节点*

客户端与 zookeeper 断开连接后,该节点被删除,只是 Zookeeper 给该节点名称进行顺序编号

Zookeeper 通知机制

client 端会对某个 znode 建立一个watcher*事件*,当该 znode 发生变化时,这些 client 会收到 zk 的通知,然后 client 可以根据 znode 变化来做出业务上的改变等。

Zookeeper 做了什么?

1、命名服务

2、配置管理

3、集群管理

4、分布式锁

5、队列管理

zk 的命名服务(文件系统)

命名服务是指通过指定的名字来*获取资源*或者*服务的地址*,利用 zk 创建一个全局的路径,即是*唯一*的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。

zk 的配置管理(文件系统、通知机制)

程序分布式的部署在不同的机器上,将程序的配置信息放在 zk 的*znode*下,当有配置发生改变时,也就是

znode 发生变化时,可以通过改变 zk 中某个目录节点的内容,利用*watcher*通知给各个客户端,从而更改配置。

Zookeeper   集群管理(文件系统、通知机制)

所谓集群管理无在乎两点:*是否有机器退出和加入、选举master*

对于第一点,所有机器约定在父目录下*创建临时目录节点*,然后监听父目录节点的子节点变化消息。一旦有机

器挂掉,该机器与 zookeeper 的连接断开,其所创建的临时目录节点被删除,*所有其他机器都收到通知:某个兄弟目录被删除*,于是,所有人都知道:它上船了。

新机器加入也是类似,*所有机器收到通知:新兄弟目录加入*,highcount 又有了,对于第二点,我们稍微改变一下,*所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好*

Zookeeper 分布式锁(文件系统、通知机制)

有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是*保持独占*,另一个是*控制时序*。对于第一类,我们将 zookeeper 上的一个znode*看作是一把锁*,通过 createznode 的方式来实现。所有客户

端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的 distribute_lock 节点就释放出锁。

对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选 master 一样,*编号最小的获得锁*,用完删除,依次方便。

获取分布式锁的流程

在获取分布式锁的时候在 locker 节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用 createNode 方法在 locker 下创建临时顺序节点,

 
  

然后调用 getChildren(“locker”)来获取 locker 下面的所有子节点,注意此时不用设置任何 Watcher。客户端获取到所有的子节点 path 之后,如果发现自己创建的节点在所有创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非 locker 所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到*比自己小的那个节点*,然后对其调用*exist()*方法,同时对其注册事件监听器。之后,让这个被关注的节点删除,则客户端的 Watcher 会收到相应通知,此时再次判断自己创建的节点是否是 locker 子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。当前这个过程中还需要许多的逻辑判断。

代码的实现主要是基于互斥锁,获取分布式锁的重点逻辑在于*BaseDistributedLock*,实现了基于Zookeeper 实现分布式锁的细节。

Zookeeper 队列管理(文件系统、通知机制)

两种类型的队列:

1、同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。

2、队列按照 FIFO 方式进行入队和出队操作。

第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建

*PERSISTENT_SEQUENTIAL*节点,创建成功时*Watcher*通知等待的队列,队列删除*序列号最小的节点*用以消费。此场景下 Zookeeper  的  znode  用于消息存储,znode 存储的数据就是消息队列中的消息内容,

SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以*不必担心队列消息的丢失问题*

Zookeeper 数据复制

Zookeeper 作为一个集群提供一致的数据服务,自然,它要在*所有机器间*做数据复制。数据复制的好处:

1、容错:一个节点出错,不致于让整个系统停止工作,别的节点可以接管它的工作;

2、提高系统的扩展能力 :把负载分布到多个节点上,或者增加节点来提高系统的负载能力;

3、提高性能:让*客户端本地访问就近的节点,提高用户访问速度*。从客户端读写访问的透明度来看,数据复制集群系统分下面两种:

1、 *写主*(WriteMaster) :对数据的*修改提交给指定的节点*。读无此限制,可以读取任何一个节点。这种情况下客户端需要对读与写进行区别,俗称*读写分离*

2、 *写任意*(Write Any):对数据的*修改可提交给任意的节点*,跟读一样。这种情况下,客户端对集群节点的角色与变化透明。

对 zookeeper 来说,它采用的方式是*写任意*。通过增加机器,它的读吞吐能力和响应能力扩展性非常好,而写,随着机器的增多吞吐能力肯定下降(这也是它建立 observer

的原因),而响应能力则取决于具体实现方式,是*延迟复制保持最终一致性*,还是*立即复制快速响应*

Zookeeper 工作原理

Zookeeper 的核心是*原子广播*,这个机制保证了*各个Server之间的同步*。实现这个机制的协议叫做Zab*协议*。Zab 协议有两种模式,它们分别是*恢复模式(选主)** 广播模式(同步)*。当服务启动或者在领导者崩溃后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 Server  完成了和  leader  的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 Server 具有相同的系统状态。

zookeeper 是如何保证事务的顺序一致性的?

zookeeper 采用了*递增的事务Id*来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch(时期; 纪元; 世; 新时代)用来标识 leader 是否发生改变,如果有新的 leader 产生出来,epoch 会自增,*32位用来递增计数*。当新产生  proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

Zookeeper Server 工作状态

每个 Server 在工作过程中有三种状态:

LOOKING:当前 Server*不知道leader是谁*,正在搜寻LEADING:当前 Server 即为选举出来的 leader

FOLLOWING:leader 已经选举出来,当前 Server 与之同步

zookeeper 是如何选取主 leader 的?

当 leader 崩溃或者 leader 失去大多数的 follower,这时 zk 进入恢复模式,恢复模式需要重新选举出一个新的 leader,让所有的 Server 都恢复到一个正确的状态。Zk 的选举算法有两种:一种是基于 basic paxos 实现的,另外一种是基于 fast paxos 算法实现的。系统默认的选举算法为*fast paxos*

1、Zookeeper 选主流程(basic paxos)

(1) 选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;

(2) 选举线程首先向所有 Server 发起一次询问(包括自己);

(3) 选举线程收到回复后,验证是否是自己发起的询问(验证 zxid 是否一致),然后获取对方的 id(myid),并存储到当前询问对象列表中,最后获取对方提议的  leader 相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;

(4) 收到所有 Server 回复以后,就计算出 zxid 最大的那个 Server,并将这个 Server 相关信息设置成下一次要投票的 Server;

(5) 线程将当前 zxid 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数,设置当前推荐的 leader 为获胜的 Server, 将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 leader 被选举出来。 通过流程分析我们可以得出:要使 Leader 获得多数Server 的支持,则 Server 总数必须是奇数 2n+1,且存活的 Server 的数目不得少于 n+1. 每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zk 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

 微服务

 服务注册发现

服务注册就是维护一个登记簿,它管理系统内所有的服务地址。当新的服务启动后,它会向登记簿交待自己的地址信息。服务的依赖方直接向登记簿要Service      Provider地址就行了。当下用于服务注册的工具非常多 ZooKeeper,Consul,Etcd, 还有 Netflix 家的 eureka 等。服务注册有两种形式:客户端注册和第三方注册。

客户端注册(zookeeper)
 
  

客户端注册是服务自身要负责注册与注销的工作。当服务启动后向注册中心注册自身,当服务下线时注销自己。期间还需要和注册中心保持心跳。心跳不一定要客户端来做,也可以由注册中心负责(这个过程叫探活)。这种方式的缺点是注册工作与服务耦合在一起,不同语言都要实现一套注册逻辑。

第三方注册(独立的服务 Registrar)

第三方注册由一个独立的服务Registrar负责注册与注销。当服务启动后以某种方式通知Registrar,然后 Registrar  负责向注册中心发起注册工作。同时注册中心要维护与服务之间的心跳,当服务不可用时,向注册中心注销服务。这种方式的缺点是 Registrar 必须是一个高可用的系统,否则注册工作没法进展。

客户端发现
 
  

客户端发现是指客户端负责查询可用服务地址,以及负载均衡的工作。这种方式最方便直接,而且也方便做负载均衡。再者一旦发现某个服务不可用立即换另外一个,非常直接。缺点也在于多语言时的重复工作,每个语言实现相同的逻辑。

服务端发现

服务端发现需要额外的 Router 服务,请求先打到 Router,然后 Router 负责查询服务与负载均衡。这种方式虽然没有客户端发现的缺点,但是它的缺点是保证 Router 的高可用。

Consul Eureka

SmartStack

Etcd

 API 网关

 
  

API Gateway 是一个服务器,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的 Facade 模式很像。API Gateway 封装内部系统的架构,并且提供 API 给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等。下图展示了一个适应当前架构的 API Gateway。

API Gateway 负责请求转发、合成和协议转换。所有来自客户端的请求都要先经过 API Gateway,

然后路由这些请求到对应的微服务。API Gateway 将经常通过调用多个微服务来处理一个请求以及聚合多个服务的结果。它可以在 web 协议与内部使用的非 Web 友好型协议间进行转换,如

HTTP 协议、WebSocket 协议。

请求转发

服务转发主要是对客户端的请求安装微服务的负载转发到不同的服务上

响应合并

把业务上需要调用多个服务接口才能完成的工作合并成一次调用对外统一提供服务。

协议转换

重点是支持 SOAP,JMS,Rest 间的协议转换。

数据转换

重点是支持 XML 和 Json 之间的报文格式转换能力(可选)

安全认证
  1. 基于 Token 的客户端访问控制和安全策略
  2. 传输数据和报文加密,到服务端解密,需要在客户端有独立的 SDK 代理包
  3. 基于 Https 的传输加密,客户端和服务端数字证书支持
  4. 基于 OAuth2.0 的服务安全认证(授权码,客户端,密码模式等)

配置中心

配置中心一般用作系统的参数配置,它需要满足如下几个要求:高效获取、实时感知、分布式访问。

zookeeper 配置中心
 
  

实现的架构图如下所示,采取数据加载到内存方式解决高效获取的问题,借助 zookeeper 的节点监听机制来实现实时感知。

配置中心数据分类

事件调度(kafka)

消息服务和事件的统一调度,常用用 kafka ,activemq 等。

服务跟踪(starter-sleuth)

随着微服务数量不断增长,需要跟踪一个请求从一个微服务到下一个微服务的传播过程, Spring Cloud  Sleuth  正是解决这个问题,它在日志中引入唯一  ID,以保证微服务调用之间的一致性,这样你就能跟踪某个请求是如何从一个微服务传递到下一个。

  1. 为了实现请求跟踪,当请求发送到分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的跟踪标识,同时在分布式系统内部流转的时候,框架始终保持传递该唯一标识,直到返回给请求方为止,这个唯一标识就是前文中提到的 Trace ID。通过 Trace ID 的记录,我们就能将所有请求过程日志关联起来。
  2. 为了统计各处理单元的时间延迟,当请求达到各个服务组件时,或是处理逻辑到达某个状态时,也通过一个唯一标识来标记它的开始、具体过程以及结束,该标识就是我们前文中提到的 Span ID,对于每个 Span 来说,它必须有开始和结束两个节点,通过记录开始 Span 和结束 Span 的时间戳,就能统计出该 Span 的时间延迟,除了时间戳记录之外,它还可以包含一些其他元数据,比如:事件名称、请求信息等。
  3. 在快速入门示例中,我们轻松实现了日志级别的跟踪信息接入,这完全归功于spring-cloudstarter-sleuth 组件的实现。在 Spring Boot 应用中,通过在工程中引入 spring- cloudstarter-sleuth 依赖之后, 它会自动的为当前应用构建起各通信通道的跟踪机制,比如:

通过诸如 RabbitMQ、Kafka(或者其他任何 Spring Cloud Stream 绑定器实现的消息中间件)传递的请求。通过 Zuul 代理传递的请求。

通过 RestTemplate 发起的请求。

服务熔断(Hystrix)

在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务            提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。

熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费 CPU 时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

Hystrix 断路器机制

断路器很好理解, 当 Hystrix Command 请求后端服务失败数量超过一定比例(默认 50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路 器保持在开路状态一段时间后(默认 5 秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix 的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力。

API 管理

SwaggerAPI 管理工具。

Zookeeper

Zookeeper概念

Zookeeper 是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。

Zookeeper  提供了一个类似于   Linux   文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。

Zookeeper角色

Zookeeper 集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色中的一种

Leader
  1. 一个 Zookeeper 集群同一时间只会有一个实际工作的 Leader,它会发起并维护与各 Follwer 及 Observer 间的心跳。
  2. 所有的写操作必须要通过 Leader 完成再由 Leader 将写操作广播给其它服务器。只要有超过半数节点(不包括 observeer 节点)写入成功,该写请求就会被提交(类 2PC

协议)。

Follower
  1. 一个 Zookeeper 集群可能同时存在多个 Follower,它会响应 Leader 的心跳,
  2. Follower 可直接处理并返回客户端的读请求,同时会将写请求转发给 Leader 处理,
  3. 并且负责在 Leader 处理写请求时对请求进行投票。
Observer
 
  

角色与 Follower 类似,但是无投票权。Zookeeper 需保证高可用和强一致性,为了支持更多的客户端,需要增加更多 Server;Server 增多,投票阶段延迟增大,影响性能;引入 Observer, Observer 不参与投票; Observers 接受客户端的连接,并将写请求转发给 leader 节点; 加入更多 Observer 节点,提高伸缩性,同时不影响吞吐率。

ZAB协议

事务编号 Zxid(事务请求计数器+ epoch)

在 ZAB ( ZooKeeper Atomic Broadcast , ZooKeeper 原子消息广播协议) 协议的事务编号 Zxid 设计中,Zxid 是一个 64 位的数字,其中低 32 位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加 1;而高 32 位则代表 Leader 周期 epoch 的编号,每个当选产生一个新的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中最大事务的 ZXID,并从中读取 epoch 值,然后加 1,以此作为新的 epoch,并将低 32 位从 0 开始计数。

Zxid(Transaction id)类似于 RDBMS 中的事务 ID,用于标识一次更新操作的 Proposal(提议) ID。为了保证顺序性,该 zkid 必须单调递增。

epoch

epoch:可以理解为当前集群所处的年代或者周期,每个  leader  就像皇帝,都有自己的年号,所以每次改朝换代,leader  变更之后,都会在前一个年代的基础上加  1。这样就算旧的 leader 崩溃恢复之后,也没有人听他的了,因为 follower 只听从当前年代的 leader 的命令。

Zab 协议有两种模式-恢复模式(选主)、广播模式(同步)

Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab   就进入了恢复模式,当领导者被选举出来,且大多数   Server   完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 Server 具有相同的系统状态。

ZAB 协议4阶段

Leader election(选举阶段-选出准Leader)

  1. Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。只有到达 广播阶段(broadcast) 准

leader 才会成为真正的 leader。这一阶段的目的是就是为了选出一个准 leader,然后进入下一个阶段。

Discovery(发现阶段-接受提议、生成epoch、接受epoch)

  1. Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准 leader 生成新的 epoch,让 followers 接受,更新它们的 accepted Epoch一个 follower 只会连接一个 leader,如果有一个节点 f 认为另一个 follower p 是

leader,f 在尝试连接 p 时会被拒绝,f 被拒绝之后,就会进入重新选举阶段。

Synchronization(同步阶段-同步follower副本)

  1. Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。只有当 大多数节点都同步完成,准  leader 才会成为真正的 leader。

follower 只会接收 zxid 比自己的 lastZxid 大的提议。

Broadcast(广播阶段-leader消息广播)

  1. Broadcast(广播阶段):到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader  可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。

ZAB 提交事务并不像 2PC 一样需要全部 follower 都 ACK,只需要得到超过半数的节点的 ACK 就可以了。

ZAB 协议JAVA 实现(FLE-发现阶段和同步合并为 Recovery Phase(恢复阶段)协议的 Java 版本实现跟上面的定义有些不同,选举阶段使用的是 Fast Leader Election(FLE),它包含了 选举的发现职责。因为 FLE 会选举拥有最新提议历史的节点作为 leader,这样就省去了发现最新提议的步骤。实际的实现将 发现阶段 和 同步合并为 Recovery Phase(恢复阶段)。所以,ZAB 的实现只有三个阶段:Fast Leader Election;Recovery Phase;Broadcast Phase。

 投票机制

每个 sever 首先给自己投票,然后用自己的选票和其他 sever 选票对比,权重大的胜出,使用权重较大的更新自身选票箱。具体选举过程如下:

  1. 每个 Server 启动以后都询问其它的 Server 它要投票给谁。对于其他 server 的询问, server 每次根据自己的状态都回复自己推荐的 leader 的 id 和上一次处理事务的

zxid(系统启动时每个 server 都会推荐自己)

  1. 收到所有 Server 回复以后,就计算出 zxid 最大的哪个 Server,并将这个 Server 相关信息设置成下一次要投票的 Server。
  2. 计算这过程中获得票数最多的的 sever 为获胜者,如果获胜者的票数超过半数,则改 server 被选为 leader。否则,继续这个过程,直到 leader 被选举出来
  3. leader 就会开始等待 server 连接
  4. Follower 连接 leader,将最大的 zxid 发送给 leader
  5. Leader 根据 follower 的 zxid 确定同步点,至此选举阶段完成。
  6. 选举阶段完成 Leader 同步后通知 follower 已经成为 uptodate 状态
  7. Follower 收到 uptodate 消息后,又可以重新接受 client 的请求进行服务了

目前有 5 台服务器,每台服务器均没有数据,它们的编号分别是 1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

  1. 服务器 1 启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器 1 的状态一直属于 Looking。
  2. 服务器 2 启动,给自己投票,同时与之前启动的服务器 1 交换结果,由于服务器 2 的编号大所以服务器 2 胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是

LOOKING。

  1. 服务器 3 启动,给自己投票,同时与之前启动的服务器 1,2 交换信息,由于服务器 3 的编号最大所以服务器 3 胜出,此时投票数正好大于半数,所以服务器 3 成为领导者, 服务器1,2 成为小弟。
  2. 服务器 4 启动,给自己投票,同时与之前启动的服务器 1,2,3 交换信息,尽管服务器 4 的编号大,但之前服务器 3 已经胜出,所以服务器 4 只能成为小弟。
  3. 服务器 5 启动,后面的逻辑同服务器 4 成为小弟。
  4. Zookeeper 的核心是原子广播,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。
  5. 当服务启动或者在领导者崩溃后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 的完成了和 leader 的状态同步以后,恢复模式就结束了。
  6. 状态同步保证了 leader 和 server 具有相同的系统状态
  7. 一旦 leader 已经和多数的 follower 进行了状态同步后,他就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 zookeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。Zookeeper服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。
  8. 广播模式需要保证 proposal 被按顺序处理,因此 zk 采用了递增的事务 id 号(zxid)来保证。所有的提议(proposal)都在被提出的时候加上了 zxid。
  9. 实现中 zxid 是一个 64 为的数字,它高 32 位是 epoch 用来标识 leader 关系是否改变,每次一个 leader 被选出来,它都会有一个新的 epoch。低 32 位是个递增计数。
  10. 当 leader 崩溃或者 leader 失去大多数的 follower,这时候 zk 进入恢复模式,恢复模式需要重新选举出一个新的 leader,让所有的 server 都恢复到一个正确的状态。

 Zookeeper 工作原理(原子广播)

 Netty(周老师)

 
  

 Netty简介

Netty是 一个异步事件驱动的网络应用程(img)序框架,用于快速开发可维护的高性能协议服务器和客户端。

JDK原生NIO程序的问题

JDK原生也有一套网络应用程序API,但是存在一系列问题,主要如下:

NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等

需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序

可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发           相对容易,但是可靠性能力补齐工作量和难度都非常大

JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询, 终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决

Netty的特点

Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特点有:

设计优雅 适用于各种传输类型的统一API - 阻塞和非阻塞Socket 基于灵活且可扩展的事件模型,可以清晰地分离关注点 高度可定制的线程模型 - 单线程,一个或多个线程池真正的无连接数据报套接字支持(自3.1起)

使用方便 详细记录的Javadoc,用户指南和示例 没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了高性能 吞吐量更高,延迟更低 减少资源消耗 小化不必要的内存复制安全 完整的SSL / TLS和StartTLS支持

社区活跃,不断更新 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入

Netty常见使用场景

Netty常见的使用场景如下:

互联网行业    在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC   框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC   框架使用。     典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。

游戏行业  无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。  非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信

大数据领域 经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现有兴趣的读者可以了解一下目前有哪些开源项目使用了 Netty:Related projects

 Netty高性能设计

Netty作为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据

I/O模型

用什么样的通道将数据发送给对方,BIO、NIO或者AIO,I/O模型在很大程度上决定了框架的性能阻塞I/O 传统阻塞型I/O(BIO)可以用下图表示:

特点

每个请求都需要独立的线程完成数据read,业务处理,数据write的完整操作问题  当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大

连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read操作上,造成线程资源浪费

 
  
I/O复用模型

在I/O复用模型中,会用到select,这个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O       函数进行检测,直到有数据可读或可写时,才真正调用

I/O操作函数

Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector对象表示:

Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时, 该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。

由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解           决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

基于buffer

传统的I/O是面向字节流或字符流的,以流式的方式顺序地从一个Stream  中读取一个或多个字节,   因此也就不能随意改变读取指针的位置。在NIO中, 抛弃了传统的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中,

只能从Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel。

基于buffer操作不像传统IO的顺序操作, NIO 中可以随意地读取任意位置的数据线程模型

数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能的影响也非常大。

事件驱动模型

通常,我们设计一个事件处理模型的程序有两种思路

轮询方式 线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑。

事件驱动方式 发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式, 其实是设计模式中观察者模式的思路。

以GUI的逻辑处理为例,说明两种逻辑的不同:

轮询方式 线程不断轮询是否发生按钮点击事件,如果发生,调用处理逻辑

事件驱动方式 发生点击事件把事件放入事件队列,在另外线程消费的事件列表中的事件,根据事件类型调用相关事件处理逻辑这里借用O’Reilly 大神关于事件驱动模型解释图

主要包括4个基本组件:

事件队列(event  queue):接收事件的入口,存储待处理事件

分发器(event mediator):将不同的事件分发到不同的业务逻辑单元事件通道(event channel):分发器与处理器之间的联系渠道

事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作

可以看出,相对传统轮询模式,事件驱动有如下优点:

可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑高性能,基于队列暂存事件,能方便并行异步处理事件

Reactor线程模型

Reactor是反应堆的意思,Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。       服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。

Reactor模型中有2个关键组成:

Reactor   Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。   它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人

Handlers     处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作

取决于Reactor的数量和Hanndler线程数量的不同,Reactor模型有3个变种    单Reactor单线程

单Reactor多线程

主从Reactor多线程

可以这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。

Netty线程模型

Netty主要基于主从Reactors多线程模型(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:

MainReactor负责客户端的连接请求,并将请求转交给SubReactor    SubReactor负责相应通道的IO读写请求非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进

行处理

这里引用Doug Lee大神的Reactor介绍:Scalable IO in Java里面关于主从 Reactor多线程模型的图

特别说明的是: 虽然Netty的线程模型基于主从Reactor多线程,借用了

 
  

MainReactor和SubReactor的结构,但是实际实现上,SubReactor和Worker 线程在同一个线程池中:

bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的accept事件,每个端口对应一个boss线程workerGroup线程池会被各个SubReactor和worker线程充分利用

异步处理

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主            动获取或者通过通知机制获得IO操作结果。

当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操,常见有如下操作:     通过isDone方法来判断当前操作是否完成

通过isSuccess方法来判断已完成的当前操作是否成功

通过getCause方法来获取已完成的当前操作失败的原因  通过isCancelled方法来判断已完成的当前操作是否被取消

通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果future对象已完成,则理解通知指定的监听器

 
  

例如下面的的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑

相比传统阻塞I/O,执行I/O操作后线程会被阻塞住, 直到操作完成;异步处理的 好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情 形下会更稳定和更高的吞吐量。

 Netty架构设计

前面介绍完Netty相关一些理论介绍,下面从功能特性、模块组件、运作过程来介绍Netty的架构设计功能特性

传输服务 支持BIO和NIO

容器集成 支持OSGI、JBossMC、Spring、Guice容器

协议支持 HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。 还支持通过实行编码解码逻辑来实现自定义协议

Core核心 可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象

模块组件

BootstrapServerBootstrap

Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类, ServerBootstrap是服务端启动引导类。

FutureChannelFuture

正如前面介绍,在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和

ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

Channel

Netty网络通信的组件,能够用于执行网络I/O操作。 Channel为用户提供: 当前网络连接的通道的状态(例如是否打开?是否已连接?)

网络连接的配置参数 (例如接收缓冲区大小)

提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I / O调用都将立即返回,并且不保证在调用结束时所请求的I / O操作已完成。调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I / O操作成功、失败或取消时回调通知调用方。

支持关联I/O操作与对应的处理程序

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型

NioSocketChannel,异步的客户端 TCP Socket 连接NioServerSocketChannel,异步的服务器端 TCP Socket 连接NioDatagramChannel,异步的 UDP 连接NioSctpChannel,异步的客户端 Sctp 连接

NioSctpServerChannel,异步的 Sctp 服务器端连接 这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO.

Selector

Netty基于Selector对象实现I/O多路复用,通过 Selector,  一个线程可以监听多个连接的Channel事件,  当向一个Selector中注册Channel  后,Selector  内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

NioEventLoop

NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务: I/O任务 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。

非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。

两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个 Channel只对应于一个线程。 ChannelHandler

ChannelHandler是一个接口,处理I / O事件或拦截I / O操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:

ChannelInboundHandler用于处理入站I / O事件ChannelOutboundHandler用于处理出站I / O操作

或者使用以下适配器类:

ChannelInboundHandlerAdapter用于处理入站I / O事件ChannelOutboundHandlerAdapter用于处理出站I / O操作ChannelDuplexHandler用于处理入站和出站事件

ChannelHandlerContext 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象 ChannelPipline

保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。

下图引用Netty的Javadoc4.1中ChannelPipline的说明,描述了 ChannelPipeline中ChannelHandler通常如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并通过调用

ChannelHandlerContext中定义的事件传播方法(例如

 
  

ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))转发到其 近的处理程序。

入站事件由自下而上方向的入站处理程序处理,如图左侧所示。 入站Handler处理程序通常处理由图底部的I / O线程生成的入站数据。 通常通过实际输入操作(例如

SocketChannel.read(ByteBuffer))从远程读取入站数据。

出站事件由上下方向处理,如图右侧所示。 出站Handler处理程序通常会生成或转换出站传输,例如write请求。 I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。

 
  

在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:

一个 Channel 包含了一个 ChannelPipel(img)ine, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站

事件在一个双向链表中,入站事件会从链表head往后传递到 后一个入站的

handler,出站事件会从链表tail往前传递到 前一个出站的handler,两种类型的handler互不干扰。

工作原理架构

 
  

初始化并启动Netty服务端过程如下:

基本过程如下:

1 初始化创建2个NioEventLoopGroup,其中boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑

2 基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler

3 绑定端口,开始工作结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工作架构图:

server 端 包 含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组

里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1  个事件循环线程。每个Boss NioEventLoop循环执行的任务包含3步:

1 轮询accept事件

2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上 *3 处理任务队列中的任务,

runAllTasks。任务队列中的任务包括用户调用eventloop.execute或 schedule执行的任务,或者其它线程提交到该eventloop的任务。

每个Worker  NioEventLoop循环执行的任务包含3步: 1 轮询read、write事件;

 
  

2  处I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理

其中任务队列中的task有3种典型使用场景

 
  

2 非当前reactor线程调用channel的各种方法  例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write类方法向该用户推送消息,就会进入到这种场景。 终的write会提交到任务队列中后被异步消费。

总结

现在稳定推荐使用的主流版本还是Netty4,Netty5 中使用了 ForkJoinPool,

增加了代码的复杂度,但是对性能的改善却不明显,所以这个版本不推荐使用,官网也没有提供下载链接。

Netty 入门门槛相对较高,其实是因为这方面的资料较少,并不是因为他有多

难,大家其实都可以像搞透 Spring 一样搞透 Netty。在学习之前,建议先理解透整个框架原理结构,运行过程,可以少走很多弯路。

 Netty面试题

1. Netty 是什么?

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能  协议服务器和客户端。Netty是基于nio的,它封装了jdk的nio,让我们使用起  来更加方法灵活。

2. Netty 特点是什么?

高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通 信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。传输快:Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了 更高效率的传输。

封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。

3. Netty 的优势有哪些?

使用简单:封装了 NIO 的很多细节,使用更简单。

功能强大:预置了多种编解码功能,支持多种主流协议。

定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能优。

稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务 本身。社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。

4. Netty 的应用场景有哪些?

典型的应用有:阿里分布式服务框架 Dubbo,默认使用 Netty 作为基础通信组 件,还有 RocketMQ 也是使用 Netty 作为通讯的基础。

5. Netty 高性能表现在哪些方面?

IO 线程模型:同步非阻塞,用少的资源做更多的事。

内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。

内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查 找树管理内存分配情况。串形化处理读写:避免使用锁带来的性能开销。

高性能序列化协议:支持 protobuf 等高性能序列化协议。

6. BIONIOAIO的区别?

BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进     行处理。线程开销大。伪异步IO:将请求连接放入线程池,一对多,但线程还是很宝贵的资源。

NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上, 多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

AIO:一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务 器应用去启动线程进行处理,

BIO是面向流的,NIO是面向缓冲区的;BIO的各种流是阻塞的。而NIO是非阻 塞的;BIO的Stream是单向的,而NIO的channel是双向的。

NIO的特点:事件驱动模型、单线程处理多任务、非阻塞I/O,I/O读写不再阻   塞,而是返回0、基于block的传输比基于流的传输更高效、更高级的IO函数   zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性。基于 Reactor线程模型。

在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,     事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来      做实际的读写操作。如在Reactor中实现读:注册读就绪事件和相应的事件处理     器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理      器、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还 控制权。

7. NIO的组成?

Buffer:与Channel进行交互,数据是从Channel读入缓冲区,从缓冲区写入 Channel中的flip方法 : 反转此缓冲区,将position给limit,然后将position置为0,其实就 是切换读写模式clear方法 :清除此缓冲区,将position置为0,把capacity的值给limit。

rewind方法 : 重绕此缓冲区,将position置为0

DirectByteBuffer可减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁     的成本更高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的 中小应用情况下,可以考虑使用heapBuffer,由JVM进行管理。

Channel:表示 IO 源与目标打开的连接,是双向的,但不能直接访问数据,只 能与Buffer 进行交互。通过源码可知,FileChannel的read方法和write方法都 导致数据复制了两次!

Selector可使一个单独的线程管理多个Channel,open方法可创建Selector, register方法向多路复用器器注册通道,可以监听的事件类型:读、写、连接、 accept。注册事件后会产生一个SelectionKey:它表示SelectableChannel 和 Selector 之间的注册关系,wakeup方法:使尚未返回的第一个选择操作立即返 回,唤醒的

原因是:注册了新的channel或者事件;channel关闭,取消注册;优先级更高 的事件触发(如定时器事件),希望及时处理。

Selector在Linux的实现类是EPollSelectorImpl,委托给EPollArrayWrapper实 现,其中三个native方法是对epoll的封装,而EPollSelectorImpl.

implRegister方法,通过调用epoll_ctl向epoll实例中注册事件,还将注册的文 件描述符(fd)与SelectionKey的对应关系添加到fdToKey中,这个map维护了文 件描述符与

SelectionKey的映射。

fdToKey有时会变得非常大,因为注册到Selector上的Channel非常多(百万连 接);过期或失效的Channel没有及时关闭。fdToKey总是串行读取的,而读取 是在select方法中进行的,该方法是非线程安全的。

Pipe:两个线程之间的单向数据连接,数据会被写到sink通道,从source通道 读取

NIO的服务端建立过程:Selector.open():打开一个Selector;

ServerSocketChannel.open():创建服务端的Channel;bind():绑定到某个  端口上。并配置非阻塞模式;register():注册Channel和关注的事件到  Selector上;select()轮询拿到已经就绪的事件

8. Netty的线程模型?

Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个 线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的 accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read 和write事件,由对应的Handler处理。

单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都    是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连      接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的 链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高 负载、大并发的应用场景不合适。

多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和 发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线 程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证 时,一个Acceptor 线程可能会存在性能不足问题。

主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上, 用于处理I/O 的读写等操作,从而保证mainReactor只负 责接入认证、握手等操作;

9. TCP 粘包/拆包的原因及解决方法?

TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发 送,也可能把小的封装成一个大的数据包发送。

TCP粘包/分包的原因:

应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应 用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络 上,这将会发生粘包现象;

进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆     包以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。

解决方法

消息定长:FixedLengthFrameDecoder类包尾增加特殊字符分割:

行分隔符类:LineBasedFrameDecoder

或自定义分隔符类 :DelimiterBasedFrameDecoder

将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头 部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘 包。

10. 什么是 Netty 的零拷贝?

Netty 的零拷贝主要包含三个方面:

Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进 行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存 (HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内 存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓 冲区的内存拷贝。

Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操 作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式 将几个小 Buffer 合并成一个大的 Buffer。

Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发 送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。

11. Netty 中有哪种重要组件?

Channel:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、 connect、read、write 等。EventLoop:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。ChannelFuture:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要

ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。

ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。

ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。

ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。

12. Netty 发送消息有几种方式?

Netty 有两种发送消息的方式:

直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动;

写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从ChannelPipeline 中的下一个 ChannelHandler 中移动。

13. 默认情况 Netty 起多少线程?何时启动?

Netty 默认是 CPU 处理器数的两倍,bind 完之后启动。

14. 了解哪几种序列化协议?

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象, 主要用于网络传输对象的解码,以便完成远程调用。

影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能(CPU资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。

Java默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差        XML,优点:人机可读性好,可指定元素或特性的名称。缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。适用场景:当做配置文件存储数据,实时数据转换。

JSON,是一种轻量级的数据交换格式,优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度        比较快。缺点:数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。

Fastjson,采用一种“假定有序快速匹配”的算法。优点:接口简单易用、目前   java语言中    快的json库。缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。适用场景:协议交互、Web输出、Android客户端

Thrift,不仅是序列化协议,还是一个RPC框架。优点:序列化后的体积小,         速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如HTTP)、无法支持向持久层直接读写数据,          即不适合做数据持久化序列化协议。适用场景:分布式系统的RPC解决方案

Avro,Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可      压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。缺点:对于习惯于静态类型语言的用户不直观。适用场景:在

Hadoop中做Hive、Pig和MapReduce的持久化数据格式。

Protobuf,将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。优点:序列化后码流小,性能高、结构化数     据存储格式(XML       JSON等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python。适用场景:对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化

其它

protostuff 基于protobuf协议,但不需要配置proto文件,直接导包即可Jboss marshaling 可以直接序列化java类, 无须实java.io.Serializable接口Message pack 一个高效的二进制序列化格式

Hessian 采用二进制协议的轻量级remoting onhttp工具

kryo 基于protobuf协议,只支持java语言,需要注册(Registration),然后序列化(Output),反序列化(Input)

15. 如何选择序列化协议?

具体场景

对于公司间的系统调用,如果性能要求在100ms以上的服务,基于XML的SOAP 协议是一个值得考虑的方案。

基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景, JSON也是非常不错的选择。

对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的   竞争关系。

对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在hadoop子项目里,Avro会是更好的选择。

对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。由于Avro的设计理念偏向于动态类型语言,对于动态语言为       主的应用场景,Avro是更好的选择。

如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。

如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。

protobuf的数据类型有多种:bool、double、float、int32、int64、string、  bytes、enum、message。protobuf的限定符:required:   必须赋值,不能为空、optional:字段可以赋值,也可以不赋值、repeated: 该字段可以重复任意次数(包括0次)、枚举;只能用指定的常量集中的一个值作为其值;

protobuf的基本规则:每个消息中必须至少留有一个required类型的字段、包含0个或多个optional类型的字段;repeated表示的字段可以包含0个或多个数据;[1,15]之内的标 识号在编码的时候会占用一个字节(常用),[16,2047]之内的标识号则占用2个字节,标识号一定不能重复、使用消息类型,也可以将消息嵌套任意多层,可用嵌套消息类型来         代替组。

protobuf的消息升级原则:不要更改任何已有的字段的数值标识;不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但要保留标号不能被重用。    新添加的字段必须是optional或repeated。因为旧版本程序无法读取或写入新增的required限定符的字段。

编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();

Netty中的使用:ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类;ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的 UserProto.java文件中的解码类;ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类将StringBuilder转换为ByteBuf类型:copiedBuffer()方法

16. Netty 支持哪些心跳类型设置?

readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。allIdleTime:所有类型的超时时间。

17. Netty Tomcat 的区别?

作用不同:Tomcat 是 Servlet 容器,可以视为 Web 服务器,而 Netty 是异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。

协议不同:Tomcat 是基于 http 协议的 Web 服务器,而 Netty 能通过编程自定义各种协议,因为 Netty 本身自己能编码/解码字节流,所有 Netty 可以实现, HTTP 服务器、FTP 服务器、UDP 服务器、RPC 服务器、WebSocket 服务器、 Redis 的 Proxy 服务器、MySQL 的 Proxy 服务器等等。

18. NIOEventLoopGroup源码?

NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化

EventExecutor时NioEventLoopGroup重载newChild方法,所以children元素的实际类型为NioEventLoop。

线程启动时调用SingleThreadEventExecutor的构造方法,执行NioEventLoop   类的run方法,首先会调用hasTasks()方法判断当前taskQueue是否有元素。如果taskQueue中有元素,执行 selectNow() 方法, 终执行 selector.selectNow(),该方法会立即返回。如果taskQueue没有元素,执行 select(oldWakenUp) 方法

select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录 selector.select方法的执行次数和标识是否执行过selector.selectNow(),若触发了epoll的空轮询bug,则会反复执行selector.select(timeoutMillis),变量 selectCnt 会逐渐变大,当selectCnt 达到阈值(默认512),则执行 rebuildSelector方法,进行selector重建,解决cpu占用100%的bug。

rebuildSelector方法先通过openSelector方法创建一个新的selector。然后将

old selector的selectionKey执行cancel。 后将old selector的channel重新注册到新的selector中。rebuild后,需要重新执行方法selectNow,检查是否有已ready的selectionKey。

接下来调用processSelectedKeys 方法(处理I/O任务),当selectedKeys != null时,调用processSelectedKeysOptimized方法,迭代 selectedKeys 获取就绪的 IO 事件的

selectkey存放在数组selectedKeys中, 然后为每个事件都调用 processSelectedKey 来处理它,processSelectedKey 中分别处理OP_READ; OP_WRITE;OP_CONNECT事件。

后调用runAllTasks方法(非IO任务),该方法首先会调用

fetchFromScheduledTaskQueue方法,把scheduledTaskQueue中已经超过延迟执行时间的任务移到taskQueue中等待被执行,然后依次从taskQueue中取任务执行,每执行      64个任务,进行耗时检查,如果已执行时间超过预先设定的执行时间,则停止执行非IO任务,避免非IO任务太多,影响IO任务的执行。每个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel 会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。

Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事件进行通知,传播方向是 tail到head。Inbound 事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头到尾。内存管理机制,首先会预申请一大块内存Arena,Arena由许多Chunk组成,而每个Chunk默认由2048个page组成。Chunk通过AVL树的形式组织Page,每个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个       Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于8k的内存分配在

poolChunkList中,而PoolSubpage用于分配小于8k的内存,它会把一个page 分割成多段,进行内存分配。

ByteBuf的特点:支持自动扩容(4M),保证put方法不会抛出异常、通过内置的复合缓冲类型,实现零拷贝(zero-copy);不需要调用flip()来切换读/写模

式,读取和写入索引分开;方法链;引用计数基于AtomicIntegerFieldUpdater 用于内存回收;PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。

 Netty

为什么选择 Netty

> 1) API使用简单,开发门槛低;

> 2) 功能强大,预置了多种编解码功能,支持多种主流协议;

> 3) 定制能力强,可以通过 ChannelHandler 对通信框架进行灵活的扩展;

> 4)   性能高,通过与其它业界主流的NIO框架对比,Netty的综合性能最优;

> 5) 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

> 6) 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入;

> 7) 经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。

> 正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架。

说说业务中,Netty 的使用场景

> 构建高性能、低时延的各种Java中间件,例如MQ、分布式服务框架、ESB消息总线等,

Netty主要作为基础通信框架提供高性能、低时延的通信服务;

> 公有或者私有协议栈的基础通信框架,例如可以基于Netty构建异步、高性能的

WebSocket协议栈;

> 各领域应用,例如大数据、游戏等,Netty作为高性能的通信框架用于内部各模块的数据分发、传输和汇总等,实现模块之间高性能通信。

原生的 NIO JDK 1.7 版本存在 epoll bug

> 它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有得到根本性解决。

什么是 TCP 粘包/拆包

> 1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。

> 2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

> 3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。

> 4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

TCP 粘包/拆包的解决办法

> 1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

> 2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

> 3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

Netty 线程模型

> 首先,Netty使用EventLoop来处理连接上的读写事件,而一个连接上的所有请求都保证在一个EventLoop中被处理,一个EventLoop中只有一个Thread,所以也就实现了一个连接上的所有事件只会在一个线程中被执行。一个EventLoopGroup包含多个EventLoop,可以把一个EventLoop当做是Reactor线程模型中的一个线程,而一个EventLoopGroup类似于一个ExecutorService

说说 Netty 的零拷贝

> “零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而   它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。

Netty 内部执行流程

> 1. Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

> 2. Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

> 3. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到

 Netty经典面试题

BIONIO AIO 的区别?

BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。        伪异步 IO:将请求连接放入线程池,一对多,但线程还是很宝贵的资源。

NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。

AIO:一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理,

BIO 是面向流的,NIO 是面向缓冲区的;BIO 的各种流是阻塞的。而 NIO 是非阻塞的;BIO 的 Stream 是单向的,而 NIO 的 channel 是双向的。

NIO 的特点:事件驱动模型、单线程处理多任务、非阻塞 I/O,I/O 读写不再阻塞,而是返回 0、基于 block 的传输比基于流的传输更高效、更高级的 IO 函数 zero-copy、IO 多路复用大大提高了 Java 网络应用的可伸缩性和实用性。基于 Reactor 线程模型。

在     Reactor    模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。如在     Reactor    中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

NIO 的组成?

Buffer:与 Channel 进行交互,数据是从 Channel 读入缓冲区,从缓冲区写入 Channel 中的

flip 方法 : 反转此缓冲区,将 position 给 limit,然后将 position 置为 0,其实就是切换读写模式clear 方法 :清除此缓冲区,将 position 置为 0,把 capacity 的值给 limit。

rewind 方法 : 重绕此缓冲区,将 position 置为 0

DirectByteBuffer 可减少一次系统空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更

高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下,可以考虑使用 heapBuffer,由 JVM 进行管理。

Channel:表示 IO 源与目标打开的连接,是双向的,但不能直接访问数据,只能与 Buffer 进行交互。通过源码可知,FileChannel 的 read 方法和 write 方法都导致数据复制了两次!

Selector 可使一个单独的线程管理多个 Channel,open 方法可创建 Selector,register 方法向多路复用器器注册通道,可以监听的事件类型:读、写、连接、accept。注册事件后会产

生一个 SelectionKey:它表示 SelectableChannel 和 Selector 之间的注册关系,wakeup 方法:使尚未返回的第一个选择操作立即返回,唤醒的原因是:注册了新的 channel 或者事件;channel 关闭,取消注册;优先级更高的事件触发(如定时器事件),希望及时处理。

Selector 在 Linux 的实现类是 EPollSelectorImpl,委托给 EPollArrayWrapper 实现,其中三个 native 方法是对 epoll 的封装,而 EPollSelectorImpl. implRegister 方法,通过调用 epoll_ctl 向 epoll 实例中注册事件,还将注册的文件描述符(fd)与 SelectionKey 的对应关系添加到 fdToKey 中,这个 map 维护了文件描述符与 SelectionKey 的映射。

fdToKey 有时会变得非常大,因为注册到 Selector 上的 Channel 非常多(百万连接);过期或失效的 Channel 没有及时关闭。fdToKey 总是串行读取的,而读取是在 select 方法中进行的,该方法是非线程安全的。

Pipe:两个线程之间的单向数据连接,数据会被写到 sink 通道,从 source 通道读取

NIO 的服务端建立过程:Selector.open():打开一个 Selector;ServerSocketChannel.open():创建服务端的 Channel;bind():绑定到某个端口上。并配置非阻塞模式; register():注册

Channel 和关注的事件到 Selector 上;select()轮询拿到已经就绪的事件

Netty 的特点?

一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持使用更高效的 socket 底层,对 epoll 空轮询引起的 cpu 占用飙升在内部进行了处理,避免了直接使用 NIO 的陷阱,简化了 NIO 的处理方式。

采用多种 decoder/encoder 支持,对 TCP 粘包/分包进行自动化处理可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持

可配置 IO 线程数、TCP 参数, TCP 接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用 ByteBuf

通过引用计数器及时申请释放不再引用的对象,降低了 GC 频率使用单线程串行化的方式,高效的 Reactor 线程模型大量使用了 volitale、使用了 CAS 和原子类、线程安全类的使用、读写锁的使用

Netty 的线程模型?

Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,

boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work

线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。

单线程模型:所有  I/O  操作都由一个线程完成,即多路复用、事件分发和处理都是在一个  Reactor  线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度

慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。

多线程模型:有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性能不足问题。

主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上, 用于处理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作;

TCP 粘包/拆包的原因及解决方法?

TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。

TCP 粘包/分包的原因:

应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;进行 MSS 大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包以太网帧的 payload(净荷)大于 MTU(1500 字节)进行 ip 分片。

解决方法

消息定长:FixedLengthFrameDecoder 类

包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder 或自定义分隔符类 :

DelimiterBasedFrameDecoder

将消息分为消息头和消息体:LengthFieldBasedFrameDecoder 类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。

了解哪几种序列化协议?

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象, 主要用于网络传输对象的解码,以便完成远程调用。

影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能

(CPU 资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。

Java 默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差

XML,优点:人机可读性好,可指定元素或特性的名称。缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序           列化方法;文件庞大,文件格式复杂,传输占带宽。适用场景:当做配置文件存储数据,实时数据转换。

JSON,是一种轻量级的数据交换格式,优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与    XML    相比,其协议比较简单,解析速度比较快。缺点:数据的描述性比 XML 差、不适合性能要求为 ms 级别的情况、额外空间开销比较大。适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web

browser 的 Ajax 请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。

Fastjson,采用一种“假定有序快速匹配”的算法。优点:接口简单易用、目前 java 语言中最快的  json  库。缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。适用场景:协议交互、Web 输出、Android 客户端 Thrift,不仅是序列化协议,还是一个 RPC 框架。优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如 HTTP)、无法支持向持久层直接读写数据,即

不适合做数据持久化序列化协议。适用场景:分布式系统的 RPC 解决方案

Avro,Hadoop 的一个子项目,解决了 JSON  的冗长和没有  IDL  的问题。优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用 RPC、支持跨编程语言实现。缺点:对于习惯于静态类型语言的用户不直观。适用场景:在 Hadoop 中做 Hive、Pig 和MapReduce 的持久化数据格式。

Protobuf,将数据结构以.proto 文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。优点:序列化后码流小,性能高、结构化数据存储格式(XML     JSON     等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持

Java 、C++ 、python。适用场景:对性能要求高的 RPC 调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化其它

protostuff 基于 protobuf 协议,但不需要配置 proto 文件,直接导包即可

Jboss marshaling 可以直接序列化 java 类, 无须实 java.io.Serializable 接口

Message pack 一个高效的二进制序列化格式

Hessian 采用二进制协议的轻量级 remoting onhttp 工具

kryo 基于 protobuf 协议,只支持 java 语言,需要注册(Registration),然后序列化

(Output),反序列化(Input)

如何选择序列化协议?

具体场景

对于公司间的系统调用,如果性能要求在 100ms 以上的服务,基于 XML 的 SOAP 协议是一个值得考虑的方案。

基于 Web browser 的 Ajax,以及 Mobile app 与服务端之间的通讯,JSON 协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON 也是非常不错的选择。

对于调试环境比较恶劣的场景,采用 JSON 或 XML 能够极大的提高调试效率,降低系统开发成本。

当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro 之间具有一定的竞争关系。对于 T 级别的数据的持久化应用场景,Protobuf 和 Avro 是首要选择。如果持久化后的数据存储在 hadoop 子项目里,Avro 会是更好的选择。

对于持久层非 Hadoop 项目,以静态类型语言为主的应用场景,Protobuf  会更符合静态类型语言工程师的开发习惯。由于  Avro  的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro 是更好的选择。

如果需要提供一个完整的 RPC 解决方案,Thrift 是一个好的选择。

如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,

Protobuf 可以优先考虑。

protobuf 的数据类型有多种:bool、double、float、int32、int64、string、bytes、enum、  message。protobuf  的限定符:required:  必须赋值,不能为空、optional:字段可以赋值,也可以不赋值、repeated: 该字段可以重复任意次数(包括 0 次)、枚举;只能用指定的常量集中的一个值作为其值;

protobuf 的基本规则:每个消息中必须至少留有一个 required 类型的字段、包含 0 个或多个 optional 类型的字段;repeated 表示的字段可以包含 0 个或多个数据;[1,15]之内的标识号在编码的时候会占用一个字节(常用),[16,2047]之内的标识号则占用     2    个字节,标识号一定不能重复、使用消息类型,也可以将消息嵌套任意多层,可用嵌套消息类 型来代替组。

protobuf 的消息升级原则:不要更改任何已有的字段的数值标识;不能移除已经存在的 required 字段,optional 和 repeated 类型的字段可以被移除,但要保留标号不能被重用。新添加的字段必须是 optional 或 repeated。因为旧版本程序无法读取或写入新增的 required 限定符的字段。

编译器为每一个消息类型生成了一个.java 文件,以及一个特殊的 Builder 类(该类是用来创建消息类接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();

Netty 中的使用:ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类;

ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的 UserProto.java 文件中的解码类;ProtobufVarint32LengthFieldPrepender 对 protobuf 协议的消息头上加上一个长度为

32 的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类将 StringBuilder 转换为 ByteBuf 类型:copiedBuffer()方法

Netty 的零拷贝实现?

Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM 会将堆内存

Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。ByteBuffer 由 ChannelConfig 分配, 而 ChannelConfig 创建 ByteBufAllocator 默认使用 Direct Buffer

CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。addComponents 方法将

header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在 CompositeByteBuf 内部都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体

通过 FileRegion 包装的 FileChannel.tranferTo 方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。通过 wrap 方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty

ByteBuf 对象, 进而避免了拷贝操作。

Selector BUG:若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮询,CPU 使用率 100%,

Netty 的解决办法:对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进行一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 epoll 死循环 bug。重建

Selector,判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的

Selector 上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。

Netty 的高性能表现在哪些方面?

心跳,对服务端:会定时清除闲置会话 inactive(netty5),对客户端:用来检测会话是否断开,是否重来,检测网络延迟,其中 idleStateHandler 类 用来检测会话状态

串行无锁化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,

这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

可靠性,链路有效性检测:链路空闲检测机制,读/写空闲超时机制;内存保护机制:通过内存池重用    ByteBuf;ByteBuf    的解码保护;优雅停机:不再接收新消息、退出前的预处理操作、资源的释放操作。

Netty 安全性:支持的安全协议:SSL V2 和 V3,TLS,SSL 单向认证、双向认证和第三方 CA 认证。

高效并发编程的体现:volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。IO 通信性能三原则:传输(AIO)、协议

(Http)、线程(主从多线程)

流量整型的作用(变压器):防止由于上下游网元性能不均衡导致下游网元被压垮,业务流中断;防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。

TCP 参数配置:SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K;

SO_TCPNODELAY:NAGLE   算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;

NIOEventLoopGroup 源码?

NioEventLoopGroup(其实是 MultithreadEventExecutorGroup) 内部维护一个类型为

EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化 EventExecutor 时 NioEventLoopGroup 重载 newChild 方法,所以 children 元素的实际类型为

NioEventLoop。

线程启动时调用 SingleThreadEventExecutor 的构造方法,执行 NioEventLoop 类的 run 方法,首先会调用 hasTasks()方法判断当前 taskQueue 是否有元素。如果 taskQueue

中有元素,执行 selectNow() 方法,最终执行 selector.selectNow(),该方法会立即返回。如果 taskQueue 没有元素,执行 select(oldWakenUp) 方法

select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录 selector.select 方法的执行次数和标识是否执行过 selector.selectNow(),若触发了 epoll 的空轮询 bug, 则会反复

执行 selector.select(timeoutMillis),变量 selectCnt 会逐渐变大,当 selectCnt 达到阈值(默认 512),则执行 rebuildSelector 方法,进行 selector 重建,解决 cpu 占用

100%的 bug。

rebuildSelector 方法先通过 openSelector 方法创建一个新的 selector。然后将 old selector 的 selectionKey 执行 cancel。最后将 old selector 的 channel 重新注册到新的

selector 中。 rebuild 后,需要重新执行方法 selectNow,检查是否有已 ready 的 selectionKey。

接下来调用 processSelectedKeys 方法(处理 I/O 任务),当 selectedKeys != null 时,调用 processSelectedKeysOptimized 方法,迭代 selectedKeys 获取就绪的 IO 事件的selectkey 存放在数组 selectedKeys 中, 然后为每个事件都调用 processSelectedKey 来处理它, processSelectedKey 中分别处理 OP_READ;OP_WRITE;OP_CONNECT 事件。

最后调用 runAllTasks 方法(非 IO 任务),该方法首先会调用 fetchFromScheduledTaskQueue 方法,把 scheduledTaskQueue 中已经超过延迟执行时间的任务移到taskQueue 中等待被执行,然后依次从  taskQueue 中取任务执行,每执行  64 个任务,进行耗时检查,如果已执行时间超过预先设定的执行时间,则停止执行非 IO 任务,避免非 IO 任务太多,影响 IO 任务的执行。

每个 NioEventLoop 对应一个线程和一个 Selector,NioServerSocketChannel 会主动注册到某一个 NioEventLoop 的 Selector 上,NioEventLoop 负责事件轮询。

Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事件进行通知,传播方向是 tail 到 head。Inbound 事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头到尾。

内存管理机制,首先会预申请一大块内存 Arena,Arena 由许多 Chunk 组成,而每个 Chunk 默认由 2048 个 page 组成。Chunk 通过 AVL 树的形式组织 Page,每个叶子节点表示一个 Page,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于 8k 的内存分配在 poolChunkList 中,而 PoolSubpage 用于分配小于 8k 的内存,它会把一个 page 分割成多段,进行内存分配。

ByteBuf 的特点:支持自动扩容(4M),保证 put  方法不会抛出异常、通过内置的复合缓冲类型,实现零拷贝(zero-copy);不需要调用  flip()来切换读/写模式,读取和写入索引分开;方法链;引用计数基于 AtomicIntegerFieldUpdater 用于内存回收;PooledByteBuf 采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf 每次都会新建一个缓冲区对象。

 Netty 原理

Netty 是一个高性能、异步事件驱动的 NIO 框架,基于 JAVA NIO 提供的 API 实现。它提供了对

TCP、UDP 和文件传输的支持,作为一个异步 NIO 框架,Netty 的所有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得

IO 操作结果。

 Netty 高性能

在IO编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者IO多路复用技术进行处理。IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞上,       从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel 两种不同的套接字通道实现。多路复用通讯方式

 
  

Netty 架构按照 Reactor 模式设计和实现,它的服务端通信序列图如下:

客户端通信序列图如下:

Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。

异步通讯NIO

由于 Netty 采用了异步通信模式,一个 IO 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

零拷贝(DIRECT BUFFERS 使用堆外直接内存)

  1. Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,

JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

  1. Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的

Buffer。

  1. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环 write 方式导致的内存拷贝问题

内存池(基于内存池的缓冲区重用机制)

随着 JVM 虚拟机和  JIT  即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区  Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。

高效的 Reactor 线程模型

常用的 Reactor 线程模型有三种,Reactor 单线程模型, Reactor 多线程模型, 主从 Reactor 多线程模型。

Reactor单线程模型

Reactor 单线程模型,指的是所有的 IO 操作都在同一个 NIO 线程上面完成,NIO 线程的职责如下:

1) 作为 NIO 服务端,接收客户端的 TCP 连接;

2) 作为 NIO 客户端,向服务端发起 TCP 连接;

3) 读取通信对端的请求或者应答消息;

4) 向通信对端发送消息请求或者应答消息。

 
  

由于 Reactor 模式使用的是异步非阻塞 IO,所有的 IO 操作都不会导致阻塞,理论上一个线程可以独立处理所有 IO 相关的操作。从架构层面看,一个 NIO 线程确实可以完成其承担的职责。例如,通过

Acceptor 接收客户端的 TCP 连接请求消息,链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户 Handler 可以通过 NIO 线程将消息发送给客户端。

Reactor多线程模型

 
  

Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操作。 有专门一个NIO 线程-Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求; 网络 IO 操作-读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送;

主从Reactor多线程模型

服务端用于接收客户端连接的不再是个 1 个单独的 NIO 线程,而是一个独立的 NIO 线程池。

 
  

Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel 注册到 IO 线程池(sub reactor 线程池)的某个 IO 线程上,由它负责SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。

无锁设计、线程绑定

Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整

NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列多个工作线程模型性能更优。

 
  
高性能的序列化框架

Netty 默认提供了对 Google Protobuf 的支持,通过扩展 Netty 的编解码接口,用户可以实现其它的高性能序列化框架,例如 Thrift 的压缩二进制编解码框架。

  1. SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K。小包封大包,防止网络阻塞
    1. SO_TCPNODELAY:NAGLE   算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法。

软中断Hash 值和CPU绑定

  1. 软中断:开启 RPS 后可以实现软中断,提升网络吞吐量。RPS 根据数据包的源地址,目的地址以及目的和源端口,计算出一个 hash 值,然后根据这个  hash 值来选择软中断运行的 cpu,从上层来看,也就是说将每个连接和 cpu 绑定,并通过这个 hash 值,来均衡软中断在多个 cpu 上,提升网络并行处理性能。

 Netty RPC 实现

概念

RPC,即 Remote Procedure Call(远程过程调用),调用远程计算机上的服务,就像调用本地服务一样。RPC 可以很好的解耦系统,如 WebService 就是一种基于 Http 协议的

RPC。这个 RPC 整体框架如下:

关键技术
  1. 服务发布与订阅:服务端使用 Zookeeper 注册服务地址,客户端从 Zookeeper 获取可用的服务地址。
  2. 通信:使用 Netty 作为通信框架。
  3. Spring:使用 Spring 配置服务,加载 Bean,扫描注解。
  4. 动态代理:客户端使用代理模式透明化服务调用。
  5. 消息编解码:使用 Protostuff 序列化和反序列化消息。
  6. 服务消费方(client)调用以本地调用方式调用服务;
  7. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  8. client stub 找到服务地址,并将消息发送到服务端;
  9. server stub 收到消息后进行解码;
  10. server stub 根据解码结果调用本地的服务;
  11. 本地服务执行并将结果返回给 server stub;
  12. server stub 将返回结果打包成消息并发送至消费方;
  13. client stub 接收到消息,并进行解码;
  14. 服务消费方得到最终结果。
核心流程
 
  

RPC 的目标就是要 2~8 这些步骤都封装起来,让用户对这些细节透明。JAVA 一般使用动态代理方式实现远程调用。

消息编解码

息数据结构(接口名称+方法名+参数类型和参数值+超时时间+ requestID) 客户端的请求消息结构一般需要包括以下内容:

  1. 接口名称:在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;
  2. 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;
  3. 参数类型和参数值:参数类型有很多,比如有 bool、int、long、double、string、map、list,甚至如 struct(class);以及相应的参数值;
  4. 超时时间:
  5. requestID,标识唯一请求 id,在下面一节会详细描述 requestID 的用处。
  6. 服务端返回的消息 : 一般包括以下内容。返回值+状态 code+requestID

序列化

目前互联网公司广泛使用 Protobuf、Thrift、Avro 等成熟的序列化解决方案来搭建 RPC 框架,这些都是久经考验的解决方案。

通讯过程

核心问题(线程暂停、消息乱序)

如果使用 netty 的话,一般会用 channel.writeAndFlush()方法来发送消息二进制串,这个方法调用后对于整个远程调用(从发出请求到接收到结果)来说是一个异步的,即对于当前线程来说,将请求发送出来后,线程就可以往后执行了,至于服务端的结果,是服务端处理完成后,再以消息的形式发送给客户端的。于是这里出现以下两个问题:

  1. 怎么让当前线程“暂停”,等结果回来后,再向后执行?
  2. 如果有多个线程同时进行远程方法调用,这时建立在 client server 之间的 socket 连接上会有很多双方发送的消息传递,前后顺序也可能是随机的,server 处理完结果后, 将结果消息发送给 client,client 收到很多消息,怎么知道哪个消息结果是原先哪个线程调用

的?如下图所示,线程 A 和线程 B 同时向 client socket 发送请求 requestA 和 requestB,

socket 先后将 requestB 和 requestA 发送至 server,而 server 可能将 responseB 先返回,尽管 requestB 请求到达时间更晚。我们需要一种机制保证 responseA 丢给

 
  

ThreadA,responseB 丢给 ThreadB。

通讯流程

requestID 生成-AtomicLong
  1. client 线程每次通过 socket 调用一次远程接口前,生成一个唯一的 ID,即 requestID(requestID 必需保证在一个 Socket 连接里面是唯一的),一般常常使用

AtomicLong 从 0 开始累计数字生成唯一 ID;存放回调对象callback到全局ConcurrentHashMap

  1. 将 处 理 结 果 的 回 调 对 象 callback , 存 放 到 全 局 ConcurrentHashMap 里 面 put(requestID, callback);synchronized 获取回调对象callback的锁并自旋wait
  2. 当线程调用 channel.writeAndFlush()发送消息后,紧接着执行  callback 的  get()方法试图获取远程返回的结果。在 get()内部,则使用  synchronized 获取回调对象  callback 的锁,再先检测是否已经获取到结果,如果没有,然后调用 callback 的 wait()方法,释放 callback 上的锁,让当前线程处于等待状态。 监听消息的线程收到消息,找到callback上的锁并唤醒
 
  

服务端接收到请求并处理后,将response结果(此结果中包含了前面的requestID)发送给客户端,客户端 socket 连接上专门监听消息的线程收到消息,分析结果,取到requestID,再从前面的 ConcurrentHashMap 里面 get(requestID),从而找到 callback 对象,再用 synchronized 获取 callback 上的锁,将方法调用结果设置到 callback 对象里,再调用 callback.notifyAll()唤醒前面处于等待状态的线程。

 大数据(周老师)

 Hadoop

概念

就是一个大数据解决方案。它提供了一套分布式系统基础架构。 核心内容包含 hdfs 和 mapreduce。hadoop2.0 以后引入 yarn. hdfs 是提供数据存储的,mapreduce 是方便数据计算的。

  1. hdfs 又对应 namenode 和 datanode. namenode 负责保存元数据的基本信息, datanode 直接存放数据本身;
  2. mapreduce 对应 jobtracker 和 tasktracker. jobtracker 负责分发任务,tasktracker 负责执行具体任务;
  3. 对应到 master/slave 架构,namenode 和 jobtracker 就应该对应到 master, datanode 和 tasktracker 就应该对应到 slave.

HDFS

Client

Client(代表用 户) 通过与 NameNode 和 DataNode 交互访问 HDFS 中 的文件。 Client 提供了一个类似 POSIX 的文件系统接口供用户调用。

NameNode

整个 Hadoop 集群中只有一个 NameNode。 它是整个系统的“ 总管”, 负责管理 HDFS 的目录树和相关的文件元数据信息。 这些信息是以“ fsimage”( HDFS 元数据镜像文件) 和 “ editlog”(HDFS 文件改动日志)两个文件形式存放在本地磁盘,当 HDFS 重启时重新构造出来的。此外, NameNode 还负责监控各个 DataNode 的健康状态, 一旦发现某个 DataNode 宕掉,则将该 DataNode 移出 HDFS 并重新备份其上面的数据。

Secondary NameNode

Secondary NameNode 最重要的任务并不是为 NameNode 元数据进行热备份, 而是定期合并 fsimage 和 edits 日志, 并传输给 NameNode。 这里需要注意的是,为了减小

NameNode 压力, NameNode 自己并不会合并 fsimage 和 edits, 并将文件存储到磁盘上, 而是交由

Secondary NameNode 完成。

DataNode

一般而言, 每个 Slave 节点上安装一个 DataNode, 它负责实际的数据存储, 并将数据信息定期汇报给 NameNode。 DataNode 以固定大小的 block 为基本单位组织文件内容, 默认情况下 block 大小为 64MB。 当用户上传一个大的文件到 HDFS 上时, 该文件会被切分成若干个 block,分别存储到不同的 DataNode ; 同时,为了保证数据可靠, 会将同一个 block 以流水线方式写到若干个(默认是 3,该参数可配置)不同的 DataNode 上。 这种文件切割后存储的过程是对用户透明的。

MapReduce

 
  

同 HDFS 一样,Hadoop MapReduce 也采用了 Master/Slave(M/S)架构,具体如图所示。它主要由以下几个组件组成:Client、JobTracker、TaskTracker 和 Task。 下面分别对这几个组件进行介绍。

Client

用户编写的 MapReduce 程序通过 Client 提交到 JobTracker 端; 同时, 用户可通过 Client 提供的一些接口查看作业运行状态。 在 Hadoop 内部用“作业”(Job) 表示

MapReduce 程序。一个 MapReduce 程序可对应若干个作业,而每个作业会被分解成若干个 Map/Reduce 任务

(Task)。

JobTracker

JobTracker  主要负责资源监控和作业调度。JobTracker  监控所有  TaskTracker  与作业的健康状况,一旦发现失败情况后,其会将相应的任务转移到其他节点;同时   JobTracker 会跟踪任务的执行进度、资源使用量等信息,并将这些信息告诉任务调度器,而调度器会在资源出现空闲时,选择合适的任务使用这些资源。在     Hadoop     中,任务调度器是一个可插拔的模块,用户可以根据自己的需要设计相应的调度器。

TaskTracker

TaskTracker 会周期性地通过 Heartbeat 将本节点上资源的使用情况和任务的运行进度汇报给 JobTracker, 同时接收 JobTracker 发送过来的命令并执行相应的操作(如启动新任务、 杀死任务等)。TaskTracker 使用“slot” 等量划分本节点上的资源量。“slot” 代表计算资源(CPU、内存等)。一个 Task 获取到一个 slot 后才有机会运行,而 Hadoop 调度器的作用就是将各个

TaskTracker 上的空闲 slot 分配给 Task 使用。 slot 分为 Map slot 和 Reduce slot 两种,分别供 MapTask 和 Reduce Task 使用。 TaskTracker 通过 slot 数目(可配置参数)限定 Task 的并发度。

Task

Task 分为 Map Task 和 Reduce Task 两种, 均由 TaskTracker 启动。 HDFS 以固定大小的 block 为基本单位存储数据, 而对于 MapReduce 而言, 其处理单位是 split。split 与 block 的对应关系如图所示。 split 是一个逻辑概念, 它只包含一些元数据信息, 比如数据起始位置、数据长度、数据所在节点等。它的划分方法完全由用户自己决定。 但需要注意的是,split 的多少决定了 Map

Task 的数目 ,因为每个 split 会交由一个 Map Task 处理。

Map Task 执行过程如图所示。 由该图可知,Map Task 先将对应的 split 迭代解析成一个个 key/value 对,依次调用用户自定义的 map() 函数进行处理,最终将临时结果存放到本地磁盘上,其中临时数据被分成若干个 partition,每个 partition 将被一个 Reduce Task 处理。

Reduce Task执行过程

该过程分为三个阶段

  1. 从远程节点上读取 MapTask 中间结果(称为“Shuffle 阶段”);
  2. 按照 key 对 key/value 对进行排序(称为“ Sort 阶段”);
  3. 依次读取<key, value list>,调用用户自定义的 reduce() 函数处理,并将最终结果存到 HDFS 上(称为“ Reduce 阶段”)。
  4. 作业提交与初始化
    1. 用户提交作业后, 首先由 JobClient 实例将作业相关信息, 比如将程序 jar 包、作业配置文件、 分片元信息文件等上传到分布式文件系统( 一般为 HDFS)上,其中,分片元信息文件记录了每个输入分片的逻辑位置信息。 然后 JobClient 通过 RPC 通知 JobTracker。 JobTracker 收到新作业提交请求后, 由 作业调度模块对作业进行初始化: 为作业创建一个 JobInProgress 对象以跟踪作业运行状况, 而 JobInProgress 则会为每个 Task 创建一个 TaskInProgress 对象以跟踪每个任务的运行状态,

Hadoop MapReduce 作业的生命周期

TaskInProgress 可能需要管理多个

“ Task 运行尝试”( 称为“ Task Attempt”)。

  1. 任务调度与监控。
    1. 前面提到,任务调度和监控的功能均由 JobTracker 完成。TaskTracker 周期性地通过

Heartbeat 向 JobTracker 汇报本节点的资源使用 情况, 一旦出 现空闲资源, JobTracker 会按照一定的策略选择一个合适的任务使用该空闲资源, 这由任务调度器完成。 任务调度器是一个可插拔的独立模块, 且为双层架构, 即首先选择作业,  然后从该作业中选择任务,  其中,选择任务时需要重点考虑数据本地性。  此外,JobTracker  跟踪作业的整个运行过程,并为作业的成功运行提供全方位的保障。 首先, 当 TaskTracker 或者 Task 失败时, 转移计算任务 ; 其次, 当某个 Task 执行进度远落后于同一作业的其他 Task 时,为之启动一个相同

Task, 并选取计算快的 Task 结果作为最终结果。

  1. 任务运行环境准备
    1. 运行环境准备包括 JVM 启动和资源隔 离, 均由 TaskTracker 实现。 TaskTracker 为每个

Task 启动一个独立的 JVM 以避免不同 Task 在运行过程中相互影响 ; 同时,TaskTracker 使用了操作系统进程实现资源隔离以防止 Task 滥用资源。

  1. 任务执行
  2. TaskTracker 为 Task 准备好运行环境后, 便会启动 Task。 在运行过程中, 每个 Task 的最新进度首先由 Task 通过 RPC 汇报给 TaskTracker, 再由 TaskTracker 汇报给 JobTracker。
    1. 作业完成。

5. 待所有 Task 执行完毕后, 整个作业执行成功。

 Spark

概念

Spark 提供了一个全面、统一的框架用于管理各种有着不同性质(文本数据、图表数据等)的数据集和数据源(批量数据或实时的流数据)的大数据处理的需求。

 
  

核心架构

Spark SQL

提供通过 Apache Hive 的 SQL 变体 Hive 查询语言(HiveQL)与 Spark 进行交互的 API。每个数据库表被当做一个 RDD,Spark SQL 查询被转换为 Spark 操作。

Spark Streaming

对实时数据流进行处理和控制。Spark Streaming 允许程序能够像普通 RDD 一样处理实时数据

Mllib

一个常用机器学习算法库,算法被实现为对 RDD 的 Spark 操作。这个库包含可扩展的学习算法,比如分类、回归等需要对大量数据集进行迭代的操作。

GraphX

控制图、并行图操作和计算的一组算法和工具的集合。GraphX 扩展了 RDD API,包含控制图、创建子图、访问路径上所有顶点的操作

核心组件

 
  
Cluster Manager-制整个集群,监控worker

在 standalone 模式中即为 Master 主节点,控制整个集群,监控 worker。在 YARN 模式中为资源管理器

Worker节点-负责控制计算节点从节点,负责控制计算节点,启动 Executor 或者 Driver。

Driver: 运行Application main()函数

Executor:执行器,是为某个Application 运行在worker node 上的一个进程

SPARK编程模型

 
  

Spark 应用程序从编写到提交、执行、输出的整个过程如图所示,图中描述的步骤如下:

  1. 用户使用SparkContext提供的API(常用的有textFile、sequenceFile、runJob、stop等)编写 Driver application 程序。此外 SQLContext、HiveContext 及

StreamingContext 对 SparkContext 进行封装,并提供了 SQL、Hive 及流式计算相关的 API。

  1. 使用SparkContext提交的用户应用程序,首先会使用BlockManager和BroadcastManager 将任务的 Hadoop 配置进行广播。然后由 DAGScheduler 将任务转换为 RDD 并组织成 DAG, DAG 还将被划分为不同的 Stage。最后由 TaskScheduler 借助 ActorSystem 将任务提交给集群管理器(Cluster Manager)。
  2. 集群管理器(ClusterManager)给任务分配资源,即将具体任务分配到Worker上,Worker 创建  Executor  来处理任务的运行。Standalone、YARN、Mesos、EC2  等都可以作为 Spark 的集群管理器。

SPARK 计算模型

RDD 可以看做是对各种数据计算模型的统一抽象,Spark 的计算过程主要是 RDD 的迭代计算过程。RDD 的迭代计算过程非常类似于管道。分区数量取决于 partition 数量的设定,每个分区的数据只会在一个 Task 中计算。所有分区可以在多个机器节点的 Executor 上并行执行。

 
  

SPARK 运行流程

  1. 构建Spark Application 的运行环境,启动SparkContext
  2. SparkContext 向资源管理器(可以是StandaloneMesosYarn)申请运行Executor 资源,并启动StandaloneExecutorbackend
  3. SparkContext 将应用程序分发给Executor
  4. SparkContext 构建成DAG 图,将DAG图分解成Stage、将Taskset 发送给Task Scheduler,最后由Task Scheduler Task发送给Executor运行
  5. TaskExecutor 上运行,运行完释放所有资源
    1. 创建 RDD 对象
    2. DAGScheduler 模块介入运算,计算 RDD 之间的依赖关系,RDD 之间的依赖关系就形成了DAG
    3. 每一个 Job 被分为多个 Stage。划分 Stage 的一个主要依据是当前计算因子的输入是否是确定的,如果是则将其分在同一个 Stage,避免多个 Stage 之间的消息传递开销
3. ExecutorSparkContext 申请Task
 
  

SPARK RDD 流程

SPARK RDD

(1) RDD 的创建方式

1) 从Hadoop文件系统(或与Hadoop兼容的其他持久化存储系统,如Hive、Cassandra、 HBase)输入(例如 HDFS)创建。2) 从父 RDD 转换得到新 RDD。

3) 通过 parallelize 或 makeRDD 将单机数据创建为分布式 RDD。

(2) RDD 的两种操作算子(转换(Transformation)与行动(Action)) 对于 RDD 可以有两种操作算子:转换(Transformation)与行动(Action)。

 
  

转换(Transformation):Transformation操作是延迟计算的,也就是说从一个RDD转换生成另一个 RDD 的转换操作不是马上执行,需要等到有 Action 操作的时候才会真正触发运算。

行动(Action):Action 算子会触发 Spark 提交作业(Job),并将数据输出 Spark 系统。

 Storm

概念

Storm 是一个免费并开源的分布式实时计算系统。利用 Storm 可以很容易做到可靠地处理无限的数据流,像 Hadoop 批量处理大数据一样,Storm 可以实时处理数据。

 
  

集群架构

Nimbus(master-代码分发给 Supervisor)

Storm 集群的 Master 节点,负责分发用户代码,指派给具体的 Supervisor 节点上的 Worker 节点,去运行 Topology 对应的组件(Spout/Bolt)的 Task。

Supervisor(slave-管理 Worker 进程的启动和终止)

Storm 集群的从节点,负责管理运行在 Supervisor 节点上的每一个 Worker 进程的启动和终止。通过 Storm 的配置文件中的 supervisor.slots.ports 配置项,可以指定在一个

Supervisor 上最大允许多少个 Slot,每个 Slot 通过端口号来唯一标识,一个端口号对应一个 Worker 进程(如果该 Worker 进程被启动)。

Worker(具体处理组件逻辑的进程)

运行具体处理组件逻辑的进程。Worker 运行的任务类型只有两种,一种是 Spout 任务,一种是

Bolt 任务。

Task

worker中每一个spout/bolt的线程称为一个task. 在storm0.8之后,task不再与物理线程对应,不同 spout/bolt 的 task 可能会共享一个物理线程,该线程称为 executor。

ZooKeeper

用来协调 Nimbus 和 Supervisor,如果 Supervisor 因故障出现问题而无法运行 Topology, Nimbus 会第一时间感知到,并重新分配 Topology 到其它可用的 Supervisor 上运行

编程模型(spout->tuple->bolt)

strom 在运行中可分为 spout 与 bolt 两个组件,其中,数据源从 spout 开始,数据以 tuple 的方式发送到 bolt,多个 bolt 可以串连起来,一个 bolt 也可以接入多个 spot/bolt.

 
  

运行时原理如下图:

Topology

Storm 中运行的一个实时应用程序的名称。将 Spout、 Bolt 整合起来的拓扑图。定义了 Spout 和 Bolt 的结合关系、并发数量、配置等等。

Spout

在一个 topology 中获取源数据流的组件。通常情况下 spout 会从外部数据源中读取数据,然后转换为 topology 内部的源数据。

Bolt

接受数据然后执行处理的组件,用户可以在其中执行自己想要的操作。

Tuple

一次消息传递的基本单元,理解为一组消息就是一个 Tuple。

Stream

Tuple 的集合。表示数据的流向。

Topology 运行

在 Storm 中,一个实时应用的计算任务被打包作为 Topology 发布,这同 Hadoop MapReduce 任务相似。但是有一点不同的是:在 Hadoop 中,MapReduce 任务最终会执行完成后结束;而在 Storm 中,Topology 任务一旦提交后永远不会结束,除非你显示去停止任务。计算任务

Topology 是由不同的 Spouts 和 Bolts,通过数据流(Stream)连接起来的图。一个 Storm 在集群上运行一个 Topology 时,主要通过以下 3 个实体来完成 Topology 的执行工作:

(1). Worker(进程)

(2). Executor(线程)

(3). Task

Worker(1worker 进程执行的是 1 topology 的子集) 1 个 worker 进程执行的是 1 个 topology 的子集(注:不会出现 1 个 worker 为多个 topology 服务)。1 个 worker 进程会启动 1 个或多个 executor 线程来执行 1 个 topology 的 component(spout 或 bolt)。因此,1 个运行中的 topology 就是由集群中多台物理机上的多个 worker 进程组成的。

Executor(executor1 个被 worker 进程启动的单独线程)

executor 是 1 个被 worker 进程启动的单独线程。每个 executor 只会运行 1 个 topology 的 1 个 component(spout 或 bolt)的 task(注:task 可以是 1 个或多个,storm 默认是 1 个 component 只生成 1 个 task,executor 线程里会在每次循环里顺序调用所有 task 实例)。

Task(最终运行 spout bolt 中代码的单元)
 
  

是最终运行 spout 或 bolt 中代码的单元(注:1 个 task 即为 spout 或 bolt 的 1 个实例, executor 线程在执行期间会调用该 task 的 nextTuple 或 execute 方法)。topology 启动后,1 个 component(spout 或 bolt)的 task 数目是固定不变的,但该 component 使用的 executor 线程数可以动态调整(例如:1 个 executor 线程可以执行该component 的 1 个或多个 task 实例)。这意味着,对于 1 个 component 存在这样的条件:#threads<=#tasks(即:线程数小于等于 task 数目)。默认情况下 task 的数目等于 executor 线程数目,即 1 个 executor 线程只运行 1 个 task。

Storm Streaming Grouping

Storm 中最重要的抽象,应该就是 Stream grouping 了,它能够控制 Spot/Bolt 对应的 Task 以什么样的方式来分发 Tuple,将 Tuple 发射到目的 Spot/Bolt 对应的 Task.

目前,Storm Streaming Grouping 支持如下几种类型:

huffle Grouping

随机分组,尽量均匀分布到下游 Bolt 中将流分组定义为混排。这种混排分组意味着来自 Spout 的输入将混排,或随机分发给此 Bolt 中的任务。shuffle grouping 对各个 task 的

tuple 分配的比较均匀。

Fields Grouping

按字段分组,按数据中 field 值进行分组;相同 field 值的 Tuple 被发送到相同的 Task 这种 grouping 机制保证相同 field 值的 tuple 会去同一个 task。

All grouping :广播

广播发送, 对于每一个 tuple 将会复制到每一个 bolt 中处理。

Global grouping

全局分组,Tuple 被分配到一个 Bolt 中的一个 Task,实现事务性的 Topology。Stream 中的所有的 tuple 都会发送给同一个 bolt 任务处理,所有的 tuple 将会发送给拥有最小

task_id 的 bolt 任务处理。

None grouping :不分组

不关注并行处理负载均衡策略时使用该方式,目前等同于 shuffle grouping,另外 storm 将会把 bolt 任务和他的上游提供数据的任务安排在同一个线程下。

Direct grouping :直接分组 指定分组

由 tuple 的发射单元直接决定 tuple 将发射给那个 bolt,一般情况下是由接收 tuple 的 bolt 决定接收哪个 bolt 发射的 Tuple。这是一种比较特别的分组方法,用这种分组意味着消息的发送者指定由消息接收者的哪个 task 处理这个消息。 只有被声明为 Direct Stream 的消息流可以声明这种分组方法。而且这种消息 tuple 必须使用 emitDirect 方法来发射。消息处理者可以通过TopologyContext 来获取处理它的消息的 taskid (OutputCollector.emit 方法也会返回 taskid)。

 YARN

概念

YARN  是一个资源管理、任务调度的框架,主要包含三大模块:ResourceManager(RM)、  NodeManager(NM)、ApplicationMaster(AM)。其中,ResourceManager 负责所有资源的监控、分配和管理;ApplicationMaster 负责每一个具体应用程序的调度和协调; NodeManager 负责每一个节点的维护。对于所有的applications,RM拥有绝对的控制权和对资源的分配权。而每个 AM 则会和 RM 协商资源,同时和 NodeManager 通信来执行和监控 task。几个模块之间的关系如图所示。

ResourceManager

  1. ResourceManager 负责整个集群的资源管理和分配,是一个全局的资源管理系统。
  2. NodeManager 以心跳的方式向 ResourceManager 汇报资源使用情况(目前主要是 CPU 和内存的使用情况)。RM 只接受 NM 的资源回报信息,对于具体的资源处理则交给 NM 自己处理。
  3. YARN Scheduler 根据 application 的请求为其分配资源,不负责 application job 的监控、追踪、运行状态反馈、启动等工作。
  4. NodeManager 是每个节点上的资源和任务管理器,它是管理这台机器的代理,负责该节点程序的运行,以及该节点资源的管理和监控。YARN集群每个节点都运行一个

NodeManager

NodeManager。

  1. NodeManager 定时向 ResourceManager 汇报本节点资源(CPU、内存)的使用情况和 Container 的运行状态。当 ResourceManager 宕机时 NodeManager 自动连接

RM 备用节点。

  1. NodeManager 接收并处理来自 ApplicationMaster 的 Container 启动、停止等各种请求。

ApplicationMaster

用户提交的每个应用程序均包含一个ApplicationMaster,它可以运行在ResourceManager以外的机器上。

  1. 负责与 RM 调度器协商以获取资源(用 Container 表示)。
  2. 将得到的任务进一步分配给内部的任务(资源的二次分配)。
  3. 与 NM 通信以启动/停止任务。
  4. 监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务。
  5. 当前 YARN 自带了两个 ApplicationMaster 实现,一个是用于演示 AM 编写方法的实例程序 DistributedShell,它可以申请一定数目的 Container 以并行运行一个 Shell 命令或者 Shell 脚本;另一个是运行 MapReduce 应用程序的 AM—MRAppMaster。

注:RM 只负责监控 AM,并在 AM 运行失败时候启动它。RM 不负责 AM 内部任务的容错,任务的容错由 AM 完成。

YARN运行流程

  1. client 向 RM 提交应用程序,其中包括启动该应用的 ApplicationMaster 的必须信息,例如 ApplicationMaster 程序、启动 ApplicationMaster 的命令、用户程序等。
  2. ResourceManager 启动一个 container 用于运行 ApplicationMaster。
  3. 启动中的ApplicationMaster向ResourceManager注册自己,启动成功后与RM保持心跳。
  4. ApplicationMaster 向 ResourceManager 发送请求,申请相应数目的 container。
  5. ResourceManager 返回 ApplicationMaster 的申请的 containers 信息。申请成功的 container,由 ApplicationMaster 进行初始化。container 的启动信息初始化后,AM 与对应的 NodeManager 通信,要求 NM 启动 container。AM 与 NM 保持心跳,从而对 NM 上运行的任务进行监控和管理。
  6. container 运行期间,ApplicationMaster 对 container 进行监控。container 通过 RPC 协议向对应的 AM 汇报自己的进度和状态等信息。
  7. 应用运行期间,client 直接与 AM 通信获取应用的状态、进度更新等信息。
  8. 应用运行结束后,ApplicationMaster 向 ResourceManager 注销自己,并允许属于它的 container 被收回。

 负载均衡的原理

六大Web负载均衡原理与实现

开头先理解一下所谓的“均衡”

不能狭义地理解为分配给所有实际服务器一样多的工作量,因为多台服务器的承载能力各不相同,这可能体现在硬件配置、网络带宽的差异,也可能因为某台服务器身兼多职, 我们所说的“均衡”,也就是希望所有服务器都不要过载,并且能够最大程序地发挥作用。

一、http重定向

当http代理(比如浏览器)向web服务器请求某个URL后,web服务器可以通过http响应头信息中的Location标记来返回一个新的URL。这意味着HTTP代理需要继续请求这个新的URL,完成自动跳转。

性能缺陷:

1、吞吐率限制

主站点服务器的吞吐率平均分配到了被转移的服务器。现假设使用RR(Round Robin)调度策略,子服务器的最大吞吐率为1000reqs/s,那么主服务器的吞吐率要达到3000reqs/s才能完全发挥三台子服务器的作用,那么如果有100台子服务器,那么主服务器的吞吐率可想而知得有大?相反,如果主服务的最大吞吐率为6000reqs/s,那么平均      分配到子服务器的吞吐率为2000reqs/s,而现子服务器的最大吞吐率为1000reqs/s,因此就得增加子服务器的数量,增加到6个才能满足。

2、重定向访问深度不同

有的重定向一个静态页面,有的重定向相比复杂的动态页面,那么实际服务器的负载差异是不可预料的,而主站服务器却一无所知。因此整站使用重定向方法做负载均衡不太好。

我们需要权衡转移请求的开销和处理实际请求的开销,前者相对于后者越小,那么重定向的意义就越大,例如下载。你可以去很多镜像下载网站试下,会发现基本下载都使用了

Location做了重定向。

二、DNS负载均衡

DNS负责提供域名解析服务,当访问某个站点时,实际上首先需要通过该站点域名的DNS服务器来获取域名指向的IP地址,在这一过程中,DNS服务器完成了域名到IP地址的映        射,同样,这样映射也可以是一对多的,这时候,DNS服务器便充当了负载均衡调度器,它就像http重定向转换策略一样,将用户的请求分散到多台服务器上,但是它的实现机          制完全不同。

 
  

使用dig命令来看下"baidu"的DNS设置

可见baidu拥有三个A记录

相比http重定向,基于DNS的负载均衡完全节省了所谓的主站点,或者说DNS服务器已经充当了主站点的职能。但不同的是,作为调度器,DNS服务器本身的性能几乎不用担            心。因为DNS记录可以被用户浏览器或者互联网接入服务商的各级DNS服务器缓存,只有当缓存过期后才会重新向域名的DNS服务器请求解析。也说是DNS不存在http的吞吐率       限制,理论上可以无限增加实际服务器的数量。

特性:

1、可以根据用户IP来进行智能解析。DNS服务器可以在所有可用的A记录中寻找离用记最近的一台服务器。

2、动态DNS:在每次IP地址变更时,及时更新DNS服务器。当然,因为缓存,一定的延迟不可避免。

不足:

1、没有用户能直接看到DNS解析到了哪一台实际服务器,加服务器运维人员的调试带来了不便。

2、策略的局限性。例如你无法将HTTP请求的上下文引入到调度策略中,而在前面介绍的基于HTTP重定向的负载均衡系统中,调度器工作在HTTP层面,它可以充分理解HTTP请    求后根据站点的应用逻辑来设计调度策略,比如根据请求不同的URL来进行合理的过滤和转移。

3、如果要根据实际服务器的实时负载差异来调整调度策略,这需要DNS服务器在每次解析操作时分析各服务器的健康状态,对于DNS服务器来说,这种自定义开发存在较高的门          槛,更何况大多数站点只是使用第三方DNS服务。

4、DNS记录缓存,各级节点的DNS服务器不同程序的缓存会让你晕头转向。

5、基于以上几点,DNS服务器并不能很好地完成工作量均衡分配,最后,是否选择基于DNS的负载均衡方式完全取决于你的需要。

三、反向代理负载均衡

这个肯定大家都有所接触,因为几乎所有主流的Web服务器都热衷于支持基于反向代理的负载均衡。它的核心工作就是转发HTTP请求。      相比前面的HTTP重定向和DNS解析,反向代理的调度器扮演的是用户和实际服务器中间人的角色:

1、任何对于实际服务器的HTTP请求都必须经过调度器

2、调度器必须等待实际服务器的HTTP响应,并将它反馈给用户(前两种方式不需要经过调度反馈,是实际服务器直接发送给用户)

特性:

1、调度策略丰富。例如可以为不同的实际服务器设置不同的权重,以达到能者多劳的效果。

2、对反向代理服务器的并发处理能力要求高,因为它工作在HTTP层面。

3、反向代理服务器进行转发操作本身是需要一定开销的,比如创建线程、与后端服务器建立TCP连接、接收后端服务器返回的处理结果、分析HTTP头部信息、用户空间和内核         空间的频繁切换等,虽然这部分时间并不长,但是当后端服务器处理请求的时间非常短时,转发的开销就显得尤为突出。例如请求静态文件,更适合使用前面介绍的基于DNS的           负载均衡方式。

4、反向代理服务器可以监控后端服务器,比如系统负载、响应时间、是否可用、TCP连接数、流量等,从而根据这些数据调整负载均衡的策略。

5、反射代理服务器可以让用户在一次会话周期内的所有请求始终转发到一台特定的后端服务器上(粘滞会话),这样的好处一是保持session的本地访问,二是防止后端服务器         的动态内存缓存的资源浪费。

四、IP负载均衡(LVS-NAT)

因为反向代理服务器工作在HTTP层,其本身的开销就已经严重制约了可扩展性,从而也限制了它的性能极限。那能否在HTTP层面以下实现负载均衡呢?

NAT服务器:它工作在传输层,它可以修改发送来的IP数据包,将数据包的目标地址修改为实际服务器地址。

从Linux2.4内核开始,其内置的Neftilter模块在内核中维护着一些数据包过滤表,这些表包含了用于控制数据包过滤的规则。可喜的是,Linux提供了iptables来对过滤表进行插  入、修改和删除等操作。更加令人振奋的是,Linux2.6.x内核中内置了IPVS模块,它的工作性质类型于Netfilter模块,不过它更专注于实现IP负载均衡。

 
  

想知道你的服务器内核是否已经安装了IPVS模块,可以

有输出意味着IPVS已经安装了。IPVS的管理工具是ipvsadm,它为提供了基于命令行的配置界面,可以通过它快速实现负载均衡系统。这就是大名鼎鼎的LVS(Linux Virtual Server,Linux虚拟服务器)。

1、打开调度器的数据包转发选项

echo 1 > /proc/sys/net/ipv4/ip_forward

2、检查实际服务器是否已经将NAT服务器作为自己的默认网关,如果不是,如添加route add default gw xx.xx.xx.xx

3、使用ipvsadm配置

ipvsadm -A -t 111.11.11.11:80 -s rr

添加一台虚拟服务器,-t   后面是服务器的外网ip和端口,-s    rr是指采用简单轮询的RR调度策略(这属于静态调度策略,除此之外,LVS还提供了系列的动态调度策略,比如最小连接(LC)、带权重的最小连接(WLC),最短期望时间延迟(SED)等)

ipvsadm -a -t 111.11.11.11:80 -r 10.10.120.210:8000 -m ipvsadm -a -t 111.11.11.11:80 -r 10.10.120.211:8000 -m

添加两台实际服务器(不需要有外网ip),-r后面是实际服务器的内网ip和端口,-m表示采用NAT方式来转发数据包

运行ipvsadm -L -n可以查看实际服务器的状态。这样就大功告成了。

实验证明使用基于NAT的负载均衡系统。作为调度器的NAT服务器可以将吞吐率提升到一个新的高度,几乎是反向代理服务器的两倍以上,这大多归功于在内核中进行请求转发          的较低开销。但是一旦请求的内容过大时,不论是基于反向代理还是NAT,负载均衡的整体吞吐量都差距不大,这说明对于一睦开销较大的内容,使用简单的反向代理来搭建负           载均衡系统是值考虑的。

这么强大的系统还是有它的瓶颈,那就是NAT服务器的网络带宽,包括内部网络和外部网络。当然如果你不差钱,可以去花钱去购买千兆交换机或万兆交换机,甚至负载均衡硬           件设备,但如果你是个屌丝,咋办?

一个简单有效的办法就是将基于NAT的集群和前面的DNS混合使用,比如5个100Mbps出口宽带的集群,然后通过DNS来将用户请求均衡地指向这些集群,同时,你还可以利用

DNS智能解析实现地域就近访问。这样的配置对于大多数业务是足够了,但是对于提供下载或视频等服务的大规模站点,NAT服务器还是不够出色。

五、直接路由(LVS-DR)

NAT是工作在网络分层模型的传输层(第四层),而直接路由是工作在数据链路层(第二层),貌似更屌些。它通过修改数据包的目标MAC地址(没有修改目标IP),将数据包         转发到实际服务器上,不同的是,实际服务器的响应数据包将直接发送给客户羰,而不经过调度器。

1、网络设置

这里假设一台负载均衡调度器,两台实际服务器,购买三个外网ip,一台机一个,三台机的默认网关需要相同,最后再设置同样的ip别名,这里假设别名为10.10.120.193。这样       一来,将通过10.10.120.193这个IP别名来访问调度器,你可以将站点的域名指向这个IP别名。

2、将ip别名添加到回环接口lo上

这是为了让实际服务器不要去寻找其他拥有这个IP别名的服务器,在实际服务器中运行:

另外还要防止实际服务器响应来自网络中针对IP别名的ARP广播,为此还要执行:

echo "1" > /proc/sys/net/ipv4/conf/lo/arp_ignore echo "2" > /proc/sys/net/ipv4/conf/lo/arp_announce echo "1" > /proc/sys/net/ipv4/conf/all/arp_ignore echo "1" > /proc/sys/net/ipv4/conf/all/arp_announce

配置完了就可以使用ipvsadm配置LVS-DR集群了

ipvsadm -A -t 10.10.120.193:80 -s rr

ipvsadm -a -t 10.10.120.193:80 -r 10.10.120.210:8000 -g ipvsadm -a -t 10.10.120.193:80 -r 10.10.120.211:8000 -g

-g 就意味着使用直接路由的方式转发数据包

LVS-DR   相较于LVS-NAT的最大优势在于LVS-DR不受调度器宽带的限制,例如假设三台服务器在WAN交换机出口宽带都限制为10Mbps,只要对于连接调度器和两台实际服务器的LAN交换机没有限速,那么,使用LVS-DR理论上可以达到20Mbps的最大出口宽带,因为它的实际服务器的响应数据包可以不经过调度器而直接发往用户端啊,所以它与调度         器的出口宽带没有关系,只能自身的有关系。而如果使用LVS-NAT,集群只能最大使用10Mbps的宽带。所以,越是响应数据包远远超过请求数据包的服务,就越应该降低调度器       转移请求的开销,也就越能提高整体的扩展能力,最终也就越依赖于WAN出口宽带。

总的来说,LVS-DR适合搭建可扩展的负载均衡系统,不论是Web服务器还是文件服务器,以及视频服务器,它都拥有出色的性能。前提是你必须为实际器购买一系列的合法IP地        址。

六、IP隧道(LVS-TUN)

基于IP隧道的请求转发机制:将调度器收到的IP数据包封装在一个新的IP数据包中,转交给实际服务器,然后实际服务器的响应数据包可以直接到达用户端。目前Linux大多支

持,可以用LVS来实现,称为LVS-TUN,与LVS-DR不同的是,实际服务器可以和调度器不在同一个WANt网段,调度器通过IP隧道技术来转发请求到实际服务器,所以实际服务器  也必须拥有合法的IP地址。

总体来说,LVS-DR和LVS-TUN都适合响应和请求不对称的Web服务器,如何从它们中做出选择,取决于你的网络部署需要,因为LVS-TUN可以将实际服务器根据需要部署在不同  的地域,并且根据就近访问的原则来转移请求,所以有类似这种需求的,就应该选择LVS-TUN。

 Tomcat并发

Tomcat的性能与最大并发配置

当一个进程有 500 个线程在跑的话,那性能已经是很低很低了。Tomcat 默认配置的最大请求数是 150,也就是说同时支持 150 个并发,当然了,也可以将其改大。

当某个应用拥有 250 个以上并发的时候,应考虑应用服务器的集群。

具体能承载多少并发,需要看硬件的配置,CPU 越多性能越高,分配给 JVM 的内存越多性能也就越高,但也会加重 GC 的负担。操作系统对于进程中的线程数有一定的限制:

Windows 每个进程中的线程数不允许超过 2000

Linux 每个进程中的线程数不允许超过 1000

另外,在 Java 中每开启一个线程需要耗用 1MB 的 JVM 内存空间用于作为线程栈之用。

Tomcat的最大并发数是可以配置的,实际运用中,最大并发数与硬件性能和CPU数量都有很大关系的。更好的硬件,更多的处理器都会使Tomcat支持更多的并发。

Tomcat 默认的 HTTP 实现是采用阻塞式的 Socket 通信,每个请求都需要创建一个线程处理。这种模式下的并发量受到线程数的限制,但对于 Tomcat 来说几乎没有 BUG 存在了。

Tomcat 还可以配置 NIO 方式的 Socket 通信,在性能上高于阻塞式的,每个请求也不需要创建一个线程进行处理,并发能力比前者高。但没有阻塞式的成熟。

这个并发能力还与应用的逻辑密切相关,如果逻辑很复杂需要大量的计算,那并发能力势必会下降。如果每个请求都含有很多的数据库操作,那么对于数据库的性能也是非常高的。

对于单台数据库服务器来说,允许客户端的连接数量是有限制的。并发能力问题涉及整个系统架构和业务逻辑。

系统环境不同,Tomcat版本不同、JDK版本不同、以及修改的设定参数不同。并发量的差异还是满大的。

maxThreads="1000" 最大并发数

minSpareThreads="100"///初始化时创建的线程数

maxSpareThreads="500"///一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。

acceptCount="700"// 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理

配置实例:

<Connector port="8080" protocol="HTTP/1.1" minSpareThreads="100" maxSpareThreads="500" maxThreads="1000" acceptCount="700"

 
  

 Exchange 类型

Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外

headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:

Direct键(routing key)分布:
 
  

Direct:消息中的路由键(routing key)如果和 Binding 中的 binding key 一致,交换器就将消息发到对应的队列中。它是完全匹配、单播的模式。

Fanout(广播分发)
 
  

Fanout:每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。

topic交换器(模式匹配)
  1. topic   交换器:topic    交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配 0 个或多个单词,匹配不多不少一个单词。

 base概念

base 是分布式、面向列的开源数据库(其实准确的说是面向列族)。HDFS 为 Hbase 提供可靠的底层数据存储服务,MapReduce 为 Hbase 提供高性能的计算能力,

Zookeeper 为 Hbase 提供稳定服务和 Failover 机制,因此我们说 Hbase 是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。

 列式存储

 
  

列方式所带来的重要好处之一就是,由于查询中的选择规则是通过列来定义的,因此整个数据库是自动索引化的。

这里的列式存储其实说的是列族存储,Hbase   是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定。为了加深对   Hbase   列族的理解,下面是一个简单的关系型数据库的表和 Hbase 数据库的表:

 Hbase 核心概念

Column Family列族

Column Family 又叫列族,Hbase  通过列族划分数据的存储,列族下面可以包含任意多的列,实现灵活的数据存取。Hbase  表的创建的时候就必须指定列族。就像关系型数据库创建的时候必须指定具体的列是一样的。Hbase的列族不是越多越好,官方推荐的是列族最好小于或者等于3。我们使用的场景一般是 1 个列族。

Rowkey(Rowkey查询,Rowkey 范围扫描,全表扫描)

Rowkey 的概念和 mysql 中的主键是完全一样的,Hbase 使用 Rowkey 来唯一的区分某一行的数据。Hbase 只支持 3 中查询方式:基于 Rowkey 的单行查询,基于 Rowkey 的范围扫描,全表扫描。

Region分区

§ Region:Region 的概念和关系型数据库的分区或者分片差不多。Hbase 会将一个大表的数据基于 Rowkey 的不同范围分配到不通的 Region 中,每个 Region 负责一定范围的数据访问和存储。这样即使是一张巨大的表,由于被切割到不通的 region,访问起来的时延也很低。

TimeStamp多版本

§ TimeStamp 是实现 Hbase 多版本的关键。在 Hbase 中使用不同的 timestame 来标识相同 rowkey 行对应的不通版本的数据。在写入数据的时候,如果用户没有指定对应的

timestamp,Hbase 会自动添加一个 timestamp,timestamp 和服务器时间保持一致。在

Hbase 中,相同 rowkey 的数据按照 timestamp 倒序排列。默认查询的是最新的版本,用户可同指定 timestamp 的值来读取旧版本的数据。

 Hbase 核心架构

 
  

Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等几个组建组成。

Client

§ Client 包含了访问 Hbase 的接口,另外 Client 还维护了对应的 cache 来加速 Hbase 的访问,比如 cache 的.META.元数据的信息。

Zookeeper

§ Hbase 通过 Zookeeper 来做 master 的高可用、RegionServer 的监控、元数据的入口以及集群配置的维护等工作。具体工作如下:

  1. 通过 Zoopkeeper 来保证集群中只有 1 个 master 在运行,如果 master 异常,会通过竞争机制产生新的 master 提供服务
  2. 通过 Zoopkeeper 来监控 RegionServer 的状态,当 RegionSevrer 有异常的时候,通过回调的形式通知 Master RegionServer 上下限的信息
  3. 通过 Zoopkeeper 存储元数据的统一入口地址。
14.1.4.3.Hmaster

§ master 节点的主要职责如下:

  1. 为 RegionServer 分配 Region
  2. 维护整个集群的负载均衡
  3. 维护集群的元数据信息发现失效的 Region,并将失效的 Region 分配到正常 RegionServer 上当 RegionSever 失效的时候,协调对应 Hlog 的拆分
HregionServer

§ HregionServer 直接对接用户的读写请求,是真正的“干活”的节点。它的功能概括如下:

\1. 管理 master 为其分配的 Region

  1. 处理来自客户端的读写请求
  2. 负责和底层 HDFS 的交互,存储数据到 HDFS
  3. 负责 Region 变大以后的拆分
  4. 负责 Storefile 的合并工作
Region寻址方式(通过 zookeeper .META)

第 1 步:Client 请求 ZK 获取.META.所在的 RegionServer 的地址。

第 2 步:Client 请求.META.所在的 RegionServer 获取访问数据所在的 RegionServer 地址,client 会将.META.的相关信息 cache 下来,以便下一次快速访问。第 3 步:Client 请求数据所在的 RegionServer,获取所需要的数据。

HDFS

§ HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 Hbase 提供高可用(Hlog 存储在 HDFS)的支持。

 Hbase 的写逻辑

 
  
Hbase的写入流程

从上图可以看出氛围 3 步骤:

获取RegionServer 第 1 步:Client 获取数据写入的 Region 所在的 RegionServer 请求写Hlog 第 2 步:请求写 Hlog, Hlog 存储在 HDFS,当 RegionServer 出现异常,需要使用

Hlog 来恢复数据。

请求写MemStore 第 3 步:请求写 MemStore,只有当写 Hlog 和写 MemStore 都成功了才算请求写入完成。

MemStore 后续会逐渐刷到 HDFS 中。

MemStore刷盘

为了提高  Hbase  的写入性能,当写请求写入  MemStore  后,不会立即刷盘。而是会等到一定的时候进行刷盘的操作。具体是哪些场景会触发刷盘的操作呢?总结成如下的几个场景:

全局内存控制

  1. 这个全局的参数是控制内存整体的使用情况,当所有 memstore 占整个 heap 的最大比例的时候,会触发刷盘的操作。这个参数是

hbase.regionserver.global.memstore.upperLimit,默认为整个 heap 内存的 40%。

但这并不意味着全局内存触发的刷盘操作会将所有的 MemStore 都进行输盘,而是通过

另外一个参数 hbase.regionserver.global.memstore.lowerLimit 来控制,默认是整个 heap 内存的 35%。当 flush 到所有 memstore 占整个 heap 内存的比率为 35%的时候,就停止刷盘。这么做主要是为了减少刷盘对业务带来的影响,实现平滑系统负载的目的。

MemStore达到上限

  1. 当 MemStore 的大小达到 hbase.hregion.memstore.flush.size 大小的时候会触发刷盘,默认 128M 大小

RegionServer Hlog 数量达到上限

  1. 前面说到 Hlog 为了保证 Hbase 数据的一致性,那么如果 Hlog 太多的话,会导致故障恢复的时间太长,因此 Hbase 会对 Hlog 的最大个数做限制。当达到 Hlog 的最大个数的时候,会强制刷盘。这个参数是 hase.regionserver.max.logs,默认是 32 个。手工触发
  2. 可以通过 hbase shell 或者 java api 手工触发 flush 的操作。

关闭RegionServer触发

  1. 在正常关闭 RegionServer 会触发刷盘的操作,全部数据刷盘后就不需要再使用 Hlog 恢复数据。

Region 使用HLOG 恢复完数据后触发

  1. :当 RegionServer 出现故障的时候,其上面的 Region 会迁移到其他正常的 RegionServer 上,在恢复完 Region 的数据后,会触发刷盘,当刷盘完成后才会提供给业务访问。

 HBase vs Cassandra

HBase

Cassandra

语言

Java

Java

出发点

BigTable

BigTable and Dynamo

License

Apache

Apache

Protocol

HTTP/REST (also Thrift)

Custom, binary (Thrift)

数据分布

表划分为多个 region 存在不同 region server 上

改进的一致性哈希(虚拟节点)

存储目标

大文件

小文件

一致性

强一致性

最终一致性,Quorum NRW 策略

架构

master/slave

p2p

高可用性

NameNode 是 HDFS 的单点故障点

P2P 和去中心化设计,不会出现单点故障

伸缩性

Region Server 扩容,通过将自身发布到 Master, Master 均匀分布 Region

扩容需在 Hash Ring 上多个节点间调整数据分布

读写性能

数据读写定位可能要通过最多 6 次的网络 RPC,性能较低。

数据读写定位非常快

数据冲突处理

乐观并发控制(optimistic concurrency control)

向量时钟

临时故障处理

Region Server 宕机,重做 HLog

数据回传机制:某节点宕机,hash 到该节点的新数据自动路由到下一节点做 hinted handoff,源节点恢复后,推送回源节点。

永久故障恢复

Region Server 恢复,master 重新给其分配 region

Merkle 哈希树,通过 Gossip 协议同步 Merkle Tree,维 护集群节点间的数据一致性

成员通信及错误检测

Zookeeper

基于 Gossip

CAP

1,强一致性,0 数据丢失。2,可用性低。3,扩容方便。

1,弱一致性,数据可能丢失。2,可用性高。3,扩容方便。

 概念

MongoDB 是由 C++语言编写的,是一个基于分布式文件存储的开源数据库系统。在高负载的情况下,添加更多的节点,可以保证服务器性能。MongoDB  旨在为  WEB  应用提供可扩展的高性能数据存储解决方案。

 
  

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

特点

  • MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。
  • 你可以在 MongoDB 记录中设置任何属性的索引 (如:FirstName="Sameer",Address="8 Ga ndhi Road")来实现更快的排序。
  • 你可以通过本地或者网络创建数据镜像,这使得 MongoDB 有更强的扩展性。
  • 如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。
  • Mongo 支持丰富的查询表达式。查询指令使用 JSON 形式的标记,可轻易查询文档中内嵌的对象及数组。
  • MongoDb 使用 update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。
  • Mongodb 中的 Map/reduce 主要是用来对数据进行批量处理和聚合操作。
  • Map 和 Reduce。Map 函数调用 emit(key,value)遍历集合中所有的记录,将 key 与 value 传给 Reduce 函数进行处理。
  • Map 函数和 Reduce 函数是使用 Javascript 编写的,并可以通过 db.runCommand 或 mapre duce 命令来执行 MapReduce 操作。
  • GridFS 是 MongoDB 中的一个内置功能,可以用于存放大量小文件。

MongoDB 允许在服务端执行脚本,可以用 Javascript 编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。

 概念

Apache Cassandra 是高度可扩展的,高性能的分布式 NoSQL 数据库。 Cassandra 旨在处理许多商品服务器上的大量数据,提供高可用性而无需担心单点故障。

Cassandra 具有能够处理大量数据的分布式架构。 数据放置在具有多个复制因子的不同机器上,以获得高可用性,而无需担心单点故障。

 数据模型

Key Space(对应 SQL 数据库中的 database)

  1. 一个 Key Space 中可包含若干个 CF,如同 SQL 数据库中一个 database 可包含多个 table

Key(对应 SQL 数据库中的主键)

  1. 在 Cassandra 中,每一行数据记录是以 key/value 的形式存储的,其中 key 是唯一标识。

column(对应 SQL 数据库中的列)

  1. Cassandra 中每个 key/value 对中的 value 又称为 column,它是一个三元组,即:name, value 和 timestamp,其中 name 需要是唯一的。

super column(SQL 数据库不支持)

  1. cassandra 允许 key/value 中的 value 是一个 map(key/value_list),即某个 column 有多个子列。

Standard Column Family(相对应 SQL 数据库中的 table)

  1. 每个 CF 由一系列 row 组成,每个 row 包含一个 key 以及其对应的若干 column。

Super Column Family(SQL 数据库不支持)

  1. 每个 SCF 由一系列 row 组成,每个 row 包含一个 key 以及其对应的若干 super column。

 Cassandra 一致 Hash 和虚拟节点

一致性 Hash(多米诺 down 机)

为每个节点分配一个 token,根据这个 token  值来决定节点在集群中的位置以及这个节点所存储的数据范围。虚拟节点(down 机多节点托管)

 
  

由于这种方式会造成数据分布不均的问题,在  Cassandra1.2  以后采用了虚拟节点的思想:不需要为每个节点分配  token,把圆环分成更多部分,让每个节点负责多个部分的数据,这样一个节点移除后,它所负责的多个 token 会托管给多个节点处理,这种思想解决了数据分布不均的问题。

如图所示,上面部分是标准一致性哈希,每个节点负责圆环中连续的一段,如果 Node2 突然 down 掉,Node2 负责的数据托管给 Node1,即 Node1 负责 EFAB 四段,如果Node1 里面有很多热点用户产生的数据导致 Node1 已经有点撑不住了,恰巧 B 也是热点用户产生的数据,这样一来 Node1 可能会接着 down 机,Node1down 机,Node6 还hold 住吗?

下面部分是虚拟节点实现,每个节点不再负责连续部分,且圆环被分为更多的部分。如果   Node2   突然down掉,Node2负责的数据不全是托管给Node1,而是托管给多个节点。而且也保持了一致性哈希的特点。

 Gossip** 协 议

Gossip  算法如其名,灵感来自办公室八卦,只要一个人八卦一下,在有限的时间内所有的人都会知道该八卦的信息,这种方式也与病毒传播类似,因此    Gossip   有众多的别名“闲话算法”、

“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。    Gossip    的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。因为 Gossip 不要求节点知道所有其他节点,因此又具有去中心化的特点,节点之间完全对等,

不需要任何的中心节点。实际上 Gossip 可以用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡。

Gossip 节点的通信方式及收敛性

Gossip 两个节点(AB)之间存在三种通信方式(pushpullpush&pull)

  1. push: A 节点将数据(key,value,version)及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据。
  2. pull:A 仅将数据 key,version 推送给 B,B 将本地比 A 新的数据(Key,value,version)推送给 A,A 更新本地。
  3. push/pull:与 pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 更新本地。

如果把两个节点数据同步一次定义为一个周期,则在一个周期内,push 需通信 1 次,pull 需 2 次, push/pull 则需 3 次,从效果上来讲,push/pull 最好,理论上一个周期内可以使两个节点完全一致。直观上也感觉,push/pull 的收敛速度是最快的。

gossip 的协议和seed list(防止集群分列)

cassandra 使用称为  gossip 的协议来发现加入  C 集群中的其他节点的位置和状态信息。gossip 进程每秒都在进行,并与至多三个节点交换状态信息。节点交换他们自己和所知道的信息,于是所有的节点很快就能学习到整个集群中的其他节点的信息。gossip       信息有一个相关的版本号,于是在一次gossip信息交换中,旧的信息会被新的信息覆盖重写。要阻止分区进行gossip交流,那么在集群中的所有节点中使用相同的 seed list,种子节点的指定除了启动起  gossip  进程外,没有其他的目的。种子节点不是一个单点故障,他们在集群操作中也没有其他的特殊目的,除了引导节点以外

数据复制

Partitioners(计算 primary key token hash 函数)在 Cassandra 中,table 的每行由唯一的 primarykey 标识,partitioner 实际上为一 hash 函数用以计算 primary key

的 token。Cassandra 依据这个 token 值在集群中放置对应的行

两种可用的复制策略:

SimpleStrategy:仅用于单数据中心,

将第一个 replica 放在由 partitioner 确定的节点中,其余的 replicas 放在上述节点顺时针方向的后续节点中。NetworkTopologyStrategy:可用于较复杂的多数据中心。可以指定在每个数据中心分别存储多少份 replicas。复制策略在创建 keyspace 时指定,如

CREATE KEYSPACE Excelsior WITH REPLICATION = { 'class' : 'SimpleStrategy','replication_factor' : 3 }; CREATE KEYSPACE Excalibur WITH REPLICATION = {'class' :'NetworkTopologyStrategy',

'dc1' : 3, 'dc2' : 2};

数据写请求和协调者

协调者(coordinator)

协调者(coordinator)将 write 请求发送到拥有对应row 的所有 replica 节点,只要节点可用便获取并执行写请求。写一致性级别(write consistency level)确定要有多少个 replica

 
  

节点必须返回成功的确认信息。成功意味着数据被正确写入了 commit log 和 memtable。

其中 dc1、dc2 这些数据中心名称要与 snitch 中配置的名称一致.上面的拓扑策略表示在 dc1 配置3 个副本,在 dc2 配置 2 个副本

数据读请求和后台修复

  1. 协调者首先与一致性级别确定的所有 replica 联系,被联系的节点返回请求的数据。
  2. 若多个节点被联系,则来自各 replica 的 row 会在内存中作比较,若不一致,则协调者使用含最新数据的 replica 向 client 返回结果。那么比较操作过程中只需要传递时间戳就可以,因为要比较的只是哪个副本数据是最新的。
 
  

协调者在后台联系和比较来自其余拥有对应 row 的 replica 的数据,若不一致,会向过时的 replica 发写请求用最新的数据进行更新 read repair。

数据存储(CommitLogMemTableSSTable)

写请求分别到 CommitLog 和 MemTable, 并且 MemTable 的数据会刷写到磁盘 SSTable 上. 除了写数据,还有索引也会保存到磁盘上.

先将数据写到磁盘中的 commitlog,同时追加到中内存中的数据结构 memtable 。这个时候就会返 回 客 户 端 状 态 , memtable 内 容 超 出 指 定 容 量 后 会 被 放 进 将 被 刷入 磁 盘 的 队 列

(memtable_flush_queue_size 配置队列长度)。若将被刷入磁盘的数据超出了队列长度,将内存数据刷进磁盘中的 SSTable,之后 commit log 被清空。

SSTable 文件构成(BloomFilterindexdatastatic)

SSTable 文件有fileer(判断数据key是否存在,这里使用了BloomFilter提高效率),index(寻找对应 column 值所在 data 文件位置)文件,data(存储真实数据)文件,

static(存储和统计 column 和 row 大小)文件。

二级索引(对要索引的 value 摘要,生成 RowKey)

在 Cassandra 中,数据都是以 Key-value 的形式保存的。

KeysIndex 所创建的二级索引也被保存在一张 ColumnFamily 中。在插入数据时,对需要进行索引的value进行摘要,生成独一无二的key,将其作为RowKey保存在索引的

ColumnFamily中;同时在 RowKey 上添加一个 Column,将插入数据的 RowKey 作为 name 域的值,value 域则赋空值,timestamp 域则赋为插入数据的时间戳。

如果有相同的 value 被索引了,则会在索引 ColumnFamily 中相同的 RowKey 后再添加新的 Column。如果有新的 value 被索引,则会在索引 ColumnFamily 中添加新的

RowKey 以及对应新的 Column。

当对 value 进行查询时,只需计算该 value 的 RowKey,在索引 ColumnFamily 中的查找该

RowKey,对其 Columns 进行遍历就能得到该 value 所有数据的 RowKey。

 数据读写

数据写入和更新(数据追加)

Cassandra 的设计思路与这些系统不同,无论是 insert 还是 remove 操作,都是在已有的数据后面进行追加,而不修改已有的数据。这种设计称为 Log structured 存储,顾名思义就是系统中的数据是以日志的形式存在的,所以只会将新的数据追加到已有数据的后面。Log structured 存储系统有两个主要优点:

数据的写和删除效率极高

  • 传统的存储系统需要更新元信息和数据,因此磁盘的磁头需要反复移动,这是一个比较耗时

的操作,而 Log structured 的系统则是顺序写,可以充分利用文件系统的 cache,所以效率很高。错误恢复简单

  • 由于数据本身就是以日志形式保存,老的数据不会被覆盖,所以在设计 journal 的时候不需要考虑 undo,简化了错误恢复。

读的复杂度高

  • 但是,Log   structured    的存储系统也引入了一个重要的问题:读的复杂度和性能。理论上说,读操作需要从后往前扫描数据,以找到某个记录的最新版本。相比传统的存储系统,这是比较耗时的。

数据删除(column 的墓碑)

如果一次删除操作在一个节点上失败了(总共 3 个节点,副本为 3,  RF=3).整个删除操作仍然被认为成功的(因为有两个节点应答成功,使用  CL.QUORUM 一致性)。接下来如果读发生在该节点上就会变的不明确,因为结果返回是空,还是返回数据,没有办法确定哪一种是正确的。

Cassandra  总是认为返回数据是对的,那就会发生删除的数据又出现了的事情,这些数据可以叫”   僵尸”,并且他们的表现是不可预见的。墓碑

删除一个 column 其实只是插入一个关于这个 column 的墓碑(tombstone),并不直接删除原有的 column。该墓碑被作为对该 CF 的一次修改记录在 Memtable 和 SSTable 中。墓碑的内容是删除请求被执行的时间,该时间是接受客户端请求的存储节点在执行该请求时的本地时间(local   delete    time),称为本地删除时间。需要注意区分本地删除时间和时间戳,每个 CF 修改记录都有一个时间戳,这个时间戳可以理解为该 column 的修改时间,是由客户端给定的。

垃圾回收compaction

由于被删除的 column 并不会立即被从磁盘中删除,所以系统占用的磁盘空间会越来越大,这就需要有一种垃圾回收的机制,定期删除被标记了墓碑的 column。垃圾回收是在compaction 的过程中完成的。

数据读取(memtable+SStables)

为了满足读 cassandra 读取的数据是 memtable 中的数据和 SStables 中数据的合并结果。读取 SSTables 中的数据就是查找到具体的哪些的 SSTables 以及数据在这些 SSTables

中的偏移量 (SSTables 是按主键排序后的数据块)。首先如果 row cache enable 了话,会检测缓存。缓存命中

直接返回数据,没有则查找 Bloom filter,查找可能的 SSTable。然后有一层 Partition key cache,

找 partition key 的位置。如果有根据找到的 partition 去压缩偏移量映射表找具体的数据块。如果缓存没有,则要经过 Partition summary,Partition index 去找 partition key。然后经过压缩偏移量映射表找具体的数据块。

  1. 检查 memtable
  2. 如果 enabled 了,检查 row cache
  3. 检查 Bloom filter
  4. 如果 enable 了,检查 partition key 缓存
  5. 如果在 partition key 缓存中找到了 partition key,直接去 compression offset 命中,如果没有,检查 partition summary
  6. 根据 compression offset map 找到数据位置
  7. 从磁盘的 SSTable 中取出数据
 
  

行缓存和键缓存请求流程图

MemTable如果 memtable 有目标分区数据,这个数据会被读出来并且和从 SSTables 中读出来的数据进行合并。SSTable 的数据访问如下面所示的步骤。

Row Cache(SSTables中频繁被访问的数据)

在 Cassandra2.2+,它们被存储在堆外内存,使用全新的实现避免造成垃圾回收对 JVM 造成压力。

存在在 row cache 的子集数据可以在特定的一段时间内配置一定大小的内存。row cache 使用 LRU(least-recently-userd)进行回收在申请内存。存储在 row cache 中的数据是SSTables 中频繁被访问的数据。存储到row cache中后,数据就可以被后续的查询访问。row cache不是写更新。如果写某行了,这行的缓存就会失效,并且不会被继续缓存, 直到这行被读到。类似的,如果一个partition更新了,整个partition的cache都会被移除,但目标的数据在row cache中找不到,就会去检查 Bloom filter。

Bloom Filter(查找数据可能对应的SSTable)

首先,Cassandra 检查 Bloom filter 去发现哪个 SSTables 中有可能有请求的分区数据。Bloom filter 是存储在堆外内存。每个 SSTable 都有一个关联的 Bloom filter。一个Bloom filter 可以建立一个 SSTable 没有包含的特定的分区数据。同样也可以找到分区数据存在 SSTable 中的可能性。它可以加速查找 partition key 的查找过程。然而,因为Bloom filter 是一个概率函数,所以可能会得到错误的结果,并不是所有的 SSTables 都可以被 Bloom filter 识别出是否有数据。如果

Bloom filter 不能够查找到 SSTable,Cassandra 会检查 partition key cache。Bloom filter 大小增长很适宜,每 10 亿数据 1~2GB。在极端情况下,可以一个分区一行。都可以很轻松的将数十亿的 entries 存储在单个机器上。Bloom filter 是可以调节的,如果你愿意用内存来换取性能。

Partition Key Cache(查找数据可能对应的Partition key)

partition key 缓存如果开启了,将 partition index 存储在堆外内存。key cache 使用一小块可配置大小的内存。在读的过程中,每个”hit”保存一个检索。如果在 key cache 中找到了 partition key。就直接到 compression offset map 中招对应的块。partition key cache 热启动后工作的更好,相比较冷启动,有很大的性能提升。如果一个节点上的内存非常受限制,可能的话,需要限制保存在 key cache 中的 partition key 数目。如果一个在 key cache 中没有找到 partition key。就会去partition summary中去找。partition key cache 大小是可以配置的,意义就是存储在key cache 中的 partition keys 数目。

Partition Summary(内存中存储一些partition index 的样本)

partition summary 是存储在堆外内存的结构,存储一些 partition index 的样本。如果一个 partition index 包含所有的 partition keys。鉴于一个 partition summary 从每 X 个keys 中取样,然后将每X个key map到index 文件中。例如,如果一个partition summary设置了20keys 进行取样。它就会存储 SSTable file 开始的一个 key,20th 个 key,以此类推。尽管并不知道 partition key 的具体位置,partition summary 可以缩短找到 partition 数据位置。当找到了 partition key 值可能的范围后,就会去找 partition index。通过配置取样频率,你可以用内存来换取性能,当 partition summary 包含的数据越多,使用的内存越多。可以通过表定义的 index interval 属性来改变样本频率。固定大小的内存可以通过 index_summary_capacity_in_mb 属性来设置,默认是堆大小的 5%。

Partition Index(磁盘中)

partition index 驻扎在磁盘中,索引所有 partition keys 和偏移量的映射。如果 partition summary 已经查到 partition keys 的范围,现在的检索就是根据这个范围值来检索目标 partition key。需要进行单次检索和顺序读。根据找到的信息。然后去 compression offset map 中去找磁盘中有这个数据的块。如果 partition index 必须要被检索,则需要检索两次磁盘去找到目标数据。

Compression offset map(磁盘中)

compression offset map 存储磁盘数据准确位置的指针。存储在堆外内存,可以被 partition key cache 或者 partition index 访问。一旦 compression offset map 识别出来磁盘中的数据位置,就会从正确的 SStable(s)中取出数据。查询就会收到结果集。

 存储引擎

概念

数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。存储引擎主要有: 1. MyIsam , 2. InnoDB, 3. Memory,

4. Archive, 5. Federated 。

InnoDB(B+树)

InnoDB 底层存储结构为B+树, B树的每个节点对应innodb的一个page,page大小是固定的,一般设为  16k。其中非叶子节点只有键值,叶子节点包含完成数据。适用场景:

1) 经常更新的表,适合处理多重并发的更新请求。

2) 支持事务。

3) 可以从灾难中恢复(通过 bin-log 日志等)。

4) 外键约束。只有他支持外键。            5)支持自动增加列属性 auto_increment。TokuDB(Fractal Tree-节点带数据)

TokuDB 底层存储结构为 Fractal Tree,Fractal Tree 的结构与 B+树有些类似, 在 Fractal Tree

中,每一个 child 指针除了需要指向一个 child 节点外,还会带有一个 Message Buffer ,这个

Message Buffer 是一个 FIFO 的队列,用来缓存更新操作。

 
  

例如,一次插入操作只需要落在某节点的 Message Buffer 就可以马上返回了,并不需要搜索到叶子节点。这些缓存的更新会在查询时或后台异步合并应用到对应的节点中。

TokuDB 在线添加索引,不影响读写操作, 非常快的写入性能, Fractal-tree 在事务实现上有优势。 他主要适用于访问频率不高的数据或历史数据归档。

MyIASM

MyIASM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当 INSERT(插入)或 UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。

ISAM   执行读取操作的速度很快,而且不占用大量的内存和存储资源。在设计之初就预想数据组织成有固定长度的记录,按顺序存储的。---ISAM    是一种静态索引结构。缺点是它不 支持事务处理。

Memory

Memory(也叫 HEAP)堆内存:使用存在内存中的内容来创建表。每个 MEMORY 表只实际对应一个磁盘文件。MEMORY 类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用 HASH 索引。但是一旦服务关闭,表中的数据就会丢失掉。  Memory 同时支持散列索引和  B 树索引,B树索引可以使用部分查询和通配查询,也可以使用<,> 和>=等操作符方便数据挖掘,散列索引相等的比较快但是对于范围的比较慢很多。

 索引

索引(Index)是帮助 MySQL 高效获取数据的数据结构。常见的查询算法,顺序查找,二分查找,二叉排序树查找,哈希散列法,分块查找,平衡多路搜索树 B 树(B-tree)

常见索引原则有

1.选择唯一性索引

1. 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。2.为经常需要排序、分组和联合操作的字段建立索引:                             3.为常作为查询条件的字段建立索引。

4.限制索引的数目:

越多的索引,会使更新表变得很浪费时间。

尽量使用数据量少的索引

  1. 如果索引的值很长,那么查询的速度会受到影响。尽量使用前缀来索引
    1. 如果索引字段的值很长,最好使用值的前缀来索引。

7.删除不再使用或者很少使用的索引

8 . 最左前缀匹配原则,非常重要的原则。

  1. 尽量选择区分度高的列作为索引

区分度的公式是表示字段不重复的比例

  1. 索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
  2. 尽量的扩展索引,不要新建索引。

 数据库三范式

范式是具有最小冗余的表结构。3 范式具体如下:

第一范式(1st NF -列都是不可再分)
 
  

第一范式的目标是确保每列的原子性:如果每列都是不可再分的最小数据单元(也称为最小的原子单元),则满足第一范式(1NF)

第二范式(2nd NF-每个表只描述一件事情)
 
  

首先满足第一范式,并且表中非主键列不存在对主键的部分依赖。 第二范式要求每个表只描述一件事情。

第三范式(3rd NF- 不存在对非主键列的传递依赖)
 
  

第三范式定义是,满足第二范式,并且表中的列不存在对非主键列的传递依赖。除了主键订单编号外,顾客姓名依赖于非主键顾客编号。

 数据库是事务

事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行       。事务是一个不可分割的工作逻辑单元事务必须具备以下四个属性,简称 ACID 属性:

原子性(Atomicity)

  1. 事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。一致性(Consistency)
  2. 当事务完成时,数据必须处于一致状态。隔离性(Isolation)
  3. 对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。永久性(Durability)
    1. 事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。

存储过程(特定功能的 SQL 语句集)

一组为了完成特定功能的 SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。

存储过程优化思路:

  1. 尽量利用一些 sql 语句来替代一些小循环,例如聚合函数,求平均函数等。
  2. 中间结果存放于临时表,加索引。
  3. 少使用游标。sql 是个集合语言,对于集合运算具有较高性能。而 cursors 是过程运算。比如对一个 100 万行的数据进行查询。游标需要读表  100 万次,而不使用游标则只需要少量几次读取。
  4. 事务越短越好。sqlserver 支持并发操作。如果事务过多过长,或者隔离级别过高,都会造成并发操作的阻塞,死锁。导致查询极慢,cpu 占用率极地。
  5. 使用 try-catch 处理错误异常。
  6. 查找语句尽量不要放在循环内。

触发器(一段能自动执行的程序)

触发器是一段能自动执行的程序,是一种特殊的存储过程,触发器和普通的存储过程的区别是:触发器是当对某一个表进行操作时触发。诸如:update、insert、delete      这些操作的时候,系统会自动调用执行该表上对应的触发器。SQL Server 2005 中触发器可以分为两类:DML 触发器和 DDL 触发器,其中 DDL 触发器它们会影响多种数据定义语言语句而激发,这些语句有 create、 alter、drop 语句。

数据库并发策略

并发控制一般采用三种方法,分别是乐观锁和悲观锁以及时间戳。乐观锁

乐观锁认为一个用户读数据的时候,别人不会去写自己所读的数据;悲观锁就刚好相反,觉得自己读数据库的时候,别人可能刚好在写自己刚读的数据,其实就是持一种比较保守的态度;时间戳就是不加锁,通过时间戳来控制并发出现的问题。

悲观锁

悲观锁就是在读取数据的时候,为了不让别人修改自己读取的数据,就会先对自己读取的数据加锁,只有自己把数据读完了,才允许别人修改那部分数据,或者反过来说,就是自己修改某条数据的时候,不允许别人读取该数据,只有等自己的整个事务提交了,才释放自己加上的锁,才允许其他用户访问那部分数据。

时间戳

时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前        ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量,

以上悲观锁所说的加“锁”,其实分为几种锁,分别是:排它锁(写锁)和共享锁(读锁)。

数据库锁

行级锁

行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle 会自动应用行级锁:

  1. INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
  2. SELECT … FOR UPDATE 语句允许用户一次锁定多条记录进行更新
  3. 使用 COMMIT 或 ROLLBACK 语句释放锁。

表级锁

表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MYISAM 与  INNODB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

页级锁

页级锁是  MySQL   中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB   支持页级锁

SemphoreCountDownLatchCyclicBarrierPhaser

并发工具类_同步屏障CyclicBarrier

简介

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障

时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

 
  

实例代码如下:

如果把new CyclicBarrier(2)修改成new CyclicBarrier(3)则主线程和子线程会永远等待,因为没有第三个线程执行await方法,即没有第三个线程到达屏障,所以之前到达屏障的两个线程都不会继续执行。

 
  

CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int  parties,  Runnable  barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。代码如下:

CyclicBarrier的应用场景

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在     需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计     算出整个Excel的日均银行流水。

CyclicBarrierCountDownLatch的区别

CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()        方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码          执行完之后会返回true。

 
  

isBroken的使用代码如下:

并发工具类_控制并发线程数的Semaphore

简介

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。很多年以来,我都觉得从字面上很难理解Semaphore所        表达的含义,只能把它比作是控制流量的红绿灯,比如XX马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可           以开进这条马路,后面的车会看到红灯,不能驶入XX马路,但是如果前一百辆中有五辆车已经离开了XX马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,          驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

应用场景
 
  

Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个         线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用Semaphore来做流控,代码如下:

在代码中,虽然有30个线程在执行,但是只允许10个并发的执行。Semaphore的构造方法Semaphore(int permits) 接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()获取一个许可证,使用完之后调用release()归还许可证。还可以用tryAcquire()方法尝试获取许可证。

其他方法

Semaphore还提供一些其他方法:

int availablePermits() :返回此信号量中当前可用的许可证数。

int getQueueLength():返回正在等待获取许可证的线程数。

boolean hasQueuedThreads() :是否有线程正在等待获取许可证。

void reducePermits(int reduction) :减少reduction个许可证。是个protected方法。

Collection getQueuedThreads() :返回所有等待获取许可证的线程集合。是个protected方法。

 CLH队列

Java并发编程实战】—– AQS()CLH同步队列

在【Java并发编程实战】—–“J.U.C”:CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形。其主要从两方面进行了改造:节点的结构与节点等待机制。在结构上引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用;在等待机制上由原来的自旋改成阻塞唤醒。

 
  

知道其结构了,我们再看看他的实现。在线程获取锁时会调用AQS的acquire()方法,该方法第一次尝试获取锁如果失败,会将该线程加入到CLH队列中:

 
  

addWaiter:

 
  

这是addWaiter()的实现,在厘清这段代码之前我们要先看一个更重要的东东,Node,CLH队列的节点。其源码如下:

在这个源代码中有三个值(CANCELLED、SIGNAL、CONDITION)要特别注意,前面提到过CLH队列的节点都有一个状态位,该状态位与线程状态密切相关: CANCELLED = 1:因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态; SIGNAL = -1:其后继节点已经被阻塞了,到时需要进行唤醒操作;

CONDITION = -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞; 0:新建节点一般都为0。

入列

在线程尝试获取锁的时候,如果失败了需要将该线程加入到CLH队列,入列中的主要流程是:tail执行新建node,然后将node的后继节点指向旧tail值。注意在这个过程中有一个

 
  

CAS操作,采用自旋方式直到成功为止。其代码如下:

其实这段代码在enq()方法中存在。

出列
 
  

当线程是否锁时,需要进行“出列”,出列的主要工作则是唤醒其后继节点(一般来说就是head节点),让所有线程有序地进行下去:

取消
 
  

线程因为超时或者中断涉及到取消的操作,如果某个节点被取消了,那个该节点就不会参与到锁竞争当中,它会等待GC回收。取消的主要过程是将取消状态的节点移除掉,移除           的过程还是比较简单的。先将其状态设置为CANCELLED,然后将其前驱节点的pred执行其后继节点,当然这个过程仍然会是一个CAS操作:

挂起
 
  

我们了解了AQS的CLH队列相比原始的CLH队列锁,它采用了一种变形操作,将自旋机制改为阻塞机制。当前线程将首先检测是否为头结点且尝试获取锁,如果当前节点为头结点        并成功获取锁则直接返回,当前线程不进入阻塞,否则将当前线程阻塞:

JAVA并发编程学习笔记之CLH队列锁

NUMASMP

SMP(Symmetric Multi-Processor),即对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。SMP的优点是能够保证内存一致性,缺点是这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲

突,可能会导致CPU资源的浪费。常用的PC机就属于这种。

NUMA(Non-Uniform Memory Access)非一致存储访问, 将CPU分为CPU模块,每个CPU模块由多个CPU组成, 并且具有独立的本地内存、 I/O 槽口等,模块之间可以通过互联模块 相互访问 ,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。 NUMA优点是 可以较好地解决原来SMP 系统的扩展问题,缺点是由于 访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。

CLH算法实现
 
  

CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。结点之间是通过隐形的链表相连,之所以叫      隐形的链表是因为这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。CLHLock上还有一个尾指针,始终指向队列的最后一个    结点。CLHLock的类图如下所示:

当一个线程需要获取锁时,会创建一个新的QNode,将其中的locked设置为true表示需要获取锁,然后线程对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个   指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁。当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋     结点。如下图所示,线程A需要获取锁,其myNode域为true,些时tail指向线程A的结点,然后线程B也加入到线程A后面,tail指向线程B的结点。然后线程A和B都在它的

myPred域上旋转,一量它的myPred结点的locked字段变为false,它就可以获取锁扫行。明显线程A的myPred locked域为false,此时线程A获取到了锁。

 
  

整个CLH的代码如下,其中用到了ThreadLocal类,将QNode绑定到每一个线程上,同时用到了AtomicReference,对尾指针的修改正是调用它的getAndSet()操作来实现的,它能            够保证以原子方式更新对象引用。

从代码中可以看出lock方法中有一个while循环,这 是在等待前趋结点的locked域变为false,这是一个自旋等待的过程。unlock方法很简单,只需要将自己的locked域设置为

false即可。

CLH优缺点

CLH队列锁的优点是空间复杂度低(  如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH 的一种变体被应用在了JAVA并发框架中。唯一的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断        前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。一种解决NUMA系统结构的思路是MCS队列锁。

里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态

 算法(左老师-主要讲技巧)

 红黑树的了解(平衡树,二叉搜索树),使用场景

把数据结构上几种树集中的讨论一下:

1. AVLtree

定义:先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度   大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和坏情况    下都是O(log   n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这 个树。

节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有 平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为 是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或 从可能存储在节点中的子树高度计算出来。

一般我们所看见的都是排序平衡二叉树。

AVLtree使用场景:AVL树适合用于插入删除次数比较少,但查找多的情况。插 入删除导致很多的旋转,旋转是非常耗时的。AVL 在linux内核的vm area中使用。

2. 二叉搜索树

二叉搜索树也是一种树,适用与一般二叉树的全部操作,但二叉搜索树能够实现 数据的快速查找。二叉搜索树满足的条件:

  1. 非空左子树的所有键值小于其根节点的键值
  2. 非空右子树的所有键值大于其根节点的键值
  3. 左右子树都是二叉搜索树

二叉搜索树的应用场景:如果是没有退化称为链表的二叉树,查找效率就是     lgn,效率不错,但是一旦退换称为链表了,要么使用平衡二叉树,或者之后的      RB树,因为链表就是线性的查找效率。

3. 红黑树的定义

红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,     可以是RED或者BLACK。通过对任何一条从根到叶子的路径上各个着色方式的     限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。 当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时, 这些操作的性能可能不比用链表好。红黑树(red-black tree)是一种平衡的二 叉查找树,它能保证在坏情况下,基本的动态操作集合运行时间为O(lgn)。 红黑树必须要满足的五条性质:

性质一:节点是红色或者是黑色; 在树里面的节点不是红色的就是黑色的,没有其他 颜色,要不怎么叫红黑树呢,是吧。性质二:根节点是黑色; 根节点总是黑色的。它不能为红。

性质三:每个叶节点(NIL或空节点)是黑色;

性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节 点); 就是连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色。

性质五:从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点。从根节点 到每一个NIL节点的路径中,都包含了相同数量的黑色节点。

红黑树的应用场景:红黑树是一种不是非常严格的平衡二叉树,没有AVLtree那 么严格的平衡要求,所以它的平均查找,增添删除效率都还不错。广泛用在 C++的STL中。如map

和set都是用红黑树实现的。

4. B树定义

B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径 不只两个),不属于二叉搜索树的范畴,因为它不止两路,存在多路。

B树满足的条件:

(1) 树种的每个节点多拥有m个子节点且m>=2,空树除外(注:m阶代表一个树节 点多有多少个查找路径,m阶=m路,当m=2则是2叉树,m=3则是3叉);

(2) 除根节点外每个节点的关键字数量大于等于ceil(m/2)-1个小于等于m-1个,非根 节点关键字数必须>=2;(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2)

(3) 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也 有指向其子节点的指针只不过其指针地址都为null对应下图后一层节点的空格子

(4) 如果一个非叶节点有N个子节点,则该节点的关键字数等于N-1;

(5) 

 
  

所有节点关键字是按递增次序排列,并遵循左小右大原则;

B树的应用场景:构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,     保证层数尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而     且B 类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定 的。

5. B+

B+树是B树的一个升级版,B+树是B树的变种树,有n棵子树的节点中含有n个 关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文 件系统而生的。

相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度     完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们      先看看两者的区别

(1) B+跟B树不同,B+树的非叶子节点不保存关键字记录的指针,这样使得B+树每 个节点所能保存的关键字大大增加;

(2) B+树叶子节点保存了父节点的所有关键字和关键字记录的指针,每个叶子节点的 关键字从小到大链接;

(3) B+树的根节点关键字数量和其子节点个数相等;

(4) 

 
  

B+的非叶子节点只进行数据索引,不会存实际的关键字记录的指针,所有数据地 址必须要到叶子节点才能获取到,所以每次数据查询的次数都一样;

特点:

在B树的基础上每个节点存储的关键字数更多,树的层级更少所以查询数据更 快,所有指关键字指针都存在叶子节点,所以每次查找的次数都相同所以查询速 度更稳定; 应用场景: 用在磁盘文件组织 数据索引和数据库索引。

6. Trie树(字典树)

trie,又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符 串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决 定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根 节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点 和部分内部节点所对应的键才有相关的值。

在图示中,键标注在节点中,值标注在节点之下。每一个完整的英文单词对应一 个特定的整数。Trie 可以看作是一个确定有限状态自动机,尽管边上的符号一 般是隐含在分支的顺序中的。

键不需要被显式地保存在节点中。图示中标注出完整的单词,只是为了演示 trie 的原理。

trie树的优点:利用字符串的公共前缀来节约存储空间,大限度地减少无谓的 字符串比较,查询效率比哈希表高。

缺点:Trie树是一种比较简单的数据结构.理 解起来比较简单,正所谓简单的东西也得付出代价.故Trie树也有它的缺点,Trie树  的内存消耗非常大. 其基本性质可以归纳为:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。

典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经 常被搜索引擎系统用于文本词频统计。字典树与字典很相似,当你要查一个单词 是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说      明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二      个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来 储存字母,也可以储存数字等其它数据。

(2)  红黑树在STL上的应用

STL中set、multiset、map、multimap底层是红黑树实现的,而 unordered_map、unordered_set 底层是哈希表实现的。

multiset、multimap: 插入相同的key的时候,我们将后插入的key放在相等的 key的右边,之后不管怎么进行插入或删除操作,后加入的key始终被认为比之 前的大。

 了解并查集吗?(低频)

什么是合并查找问题呢?

顾名思义,就是既有合并又有查找操作的问题。举个例子,有一群人,他们之间 有若干好友关系。如果两个人有直接或者间接好友关系,那么我们就说他们在同 一个朋友圈中, 这里解释下,如果Alice是Bob好友的好友,或者好友的好友的   好友等等,即通过若干好友可以认识,那么我们说Alice和Bob是间接好友。随    着时间的变化,这群人中有可能会有新的朋友关系,这时候我们会对当中某些人 是否在同一朋友圈进行询问。这就是一个典型的合并-查找操作问题,既包含了 合并操作,又包含了查找操作。

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素 构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并, 其间要反复查找一个元素在哪个集合中。

并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合 并及查询问题。

并查集也是使用树形结构实现。不过,不是二叉树。每个元素对应一个节点,每 个组对应一棵树。在并查集中,哪个节点是哪个节点的父亲以及树的形状等信息 无需多加关注, 整体组成一个树形结构才是重要的。类似森林

(4) 贪心算法和动态规划的区别

贪心算法:局部优,划分的每个子问题都优,得到全局优,但是不能保证 是全局优解,所以对于贪心算法来说,解是从上到下的,一步一步优,直到 后。 动态规划:将问题分解成重复的子问题,每次都寻找左右子问题解中优的解, 一步步得到全局的优解.重复的子问题可以通过记录的方式,避免多次计算。 所以对于动态规划来说,解是从小到上, 从底层所有可能性中找到优解,再一 步步向上。 分治法:和动态规划类似,将大问题分解成小问题,但是这些小问题是独立的, 没有重复的问题。独立问题取得解,再合并成大问题的解。 例子:比如钱币分为1元3元4元,要拿6元钱,贪心的话,先拿4,再拿两个1, 一共3张钱;实际优却是两张3元就够了。

(5) 判断一个链表是否有环,如何找到这个环的起点

给定一个单链表,只给出头指针h:

  1. 如何判断是否存在环?
  2. 如何知道环的长度?
  3. 如何找出环的连接点在哪里?
  4. 带环链表的长度是多少?

解法:

  1. 对于问题1,使用追赶的方法,设定两个指针slow、fast,从头指针开始, 每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL 退出。
  2. 对于问题2,记录下问题1的碰撞点p,slow、fast从该点开始,再次碰撞所 走过的操作数就是环的长度s。
  3. 问题3:有定理:碰撞点p到连接点的距离=头指针到连接点的距离,因此, 分别从碰撞点、头指针开始走,相遇的那个点就是连接点。(证明在后面附注)
  4. 问题3中已经求出连接点距离头指针的长度,加上问题2中求出的环的长度, 二者之和就是带环单链表的长度

 实现一个strcpy函数(或者memcpy),如果内存可能重叠呢

 
  

——大家一般认为名不见经传strcpy函数实现不是很难,流行的strcpy函数写法是:

如果注意到:

1,检查指针有效性;

2,返回目的指针des;

3,源字符串的末尾 ‘\0’ 需要拷贝。

内存重叠

内存重叠:拷贝的目的地址在源地址范围内。所谓内存重叠就是拷贝的目的地址和源地址有重叠。

在函数strcpy和函数memcpy都没有对内存重叠做处理的,使用这两个函数的时候只有程序员自己保证源地址和目标地址不重叠,或者使用memmove函数进行内存拷贝。

 
  

memmove函数对内存重叠做了处理。strcpy的正确实现应为:

memmove函数实现时考虑到了内存重叠的情况,可以完成指定大小的内存拷贝

 快排存在的问题,如何优化

快排的时间复杂度

时间复杂度 快平均是O(nlogn), 慢的时候是O(n2);辅助空间也是

O(logn); 开始学快排时 疑惑的就是这个东西不知道怎么得来的,一种是通过数学运算可以的出来,还有一种是通过递归树来理解就容易多了

所谓时间复杂度     理想的就是取到中位数情况,那么递归树就是一个完全二叉树,那么树的深度也就是      低为Logn,这个时候每一次又需要n次比较,所以时间复杂度nlogn,当快排为顺序或者逆序

时,这个数为一个斜二叉树,深度为n,同样每次需要n次比较,那那么 坏需要n2的时间优化:

  1. 当整个序列有序时退出算法;
  2. 当序列长度很小时(根据经验是大概小于 8),应该使用常数更小的算法,比如插入排序等;
  3. 随机选取分割位置;
  4. 当分割位置不理想时,考虑是否重新选取分割位置;
  5. 分割成两个序列时,只对其中一个递归进去,另一个序列仍可以在这一函数内继续划分,可以显著减小栈的大小(尾递归):
  6. 将单向扫描改成双向扫描,可以减少划分过程中的交换次数优化1:当待排序序列的长度分割到一定大小后,使用插入排序原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继          续下次分割时,不用再对与key相等元素分割优化3:优化递归操作快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归

耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为

O(logn),将会提高性能。

 Top   K问题(可以采取的方法有哪些,各自优点?)

  1. 将输入内容(假设用数组存放)进行完全排序,从中选出排在前K的元素即为所求。有了这个思路,我们可以选择相应的排序算法进行处理,目前来看快速排序,堆排序和归并           排序都能达到O(nlogn)的时间复杂度。
  2. 对输入内容进行部分排序,即只对前K大的元素进行排序(这K个元素即为所求)。此时我们可以选择冒泡排序或选择排序进行处理,即每次冒泡(选择)都能找到所求的一个          元素。这类策略的时间复杂度是O(Kn)。
  3. 对输入内容不进行排序,显而易见,这种策略将会有更好的性能开销。我们此时可以选择两种策略进行处理:用一个桶来装前k个数,桶里面可以按照 小堆来维护 a)利用 小堆维护一个大小为K的数组,目前该小根堆中的元素是排名前K的数,其中根是           小的数。此后,每次从原数组中取一个元素与根进行比较,如大于根的元素,则将根元素替换并进行堆调整(下沉),即保证小根堆中的元素仍然是排名前K的数,且根元素仍然           小;否则不予处理,取下一个数组元素继续该过程。该算法的时间复杂度是O(nlogK),一般来说企业中都采用该策略处理 top-K问题,因为该算法不需要一次将原数组中的内容全部加载到内存中,而这正是海量数据处理必然会面临的一个关卡。

b)利用快速排序的分划函数找到分划位置K,则其前面的内容即为所求。该算法是一种非常有效的处理方式,时间复杂度是O(n)(证明可以参考算法导论书籍)。对于能一次加载          到内存中的数组,该策略非常优秀。

 Bitmap的使用,存储和插入方法

BitMap从字面的意思

很多人认为是位图,其实准确的来说,翻译成基于位的映射。

在所有具有性能优化的数据结构中,大家使用           多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美。但是数据量大了,内存就不够了。当然也可以使用类似外排序来解决问题的,由于要走IO所以时间上又不行。

所谓的Bit-map就是用一个bit位来标记某个元素对应的Value,   而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。其实如果你知道计数排序的话(算法导论中有一节讲过),你就会发现这个和计数排序很像。

 
  

bitmap应用

还可以用于爬虫系统中url去重、解决全组合问题。

BitMap应用:排序示例

假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。那么我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个

 
  

Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0(如下图:)

然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0×01<<(i%8)) 当然了这里的操作涉及到Big-ending和Littleending的情况,这里默认为Big-ending。不过计算机一般是小端存储的,如

 
  

intel。小端的话就是将倒数第5位置1),因为是从零开始的,所以要把第五位置为一(如下图):

然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下:

然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。

bitmap排序复杂度分析

Bitmap排序需要的时间复杂度和空间复杂度依赖于数据中 大的数字。

bitmap排序的时间复杂度不是O(N)的,而是取决于待排序数组中的大值

MAX,在实际应用上关系也不大,比如我开10个线程去读byte数组,那么复杂度为:O(Max/10)。也就是要是读取的,可以用多线程的方式去读取。时间复杂度方面也是O(Max/n),其中Max为byte[]数组的大小,n为线程大小。空间复杂度应该就是O(Max/8)bytes吧

BitMap算法流程

假设需要排序或者查找的 大数MAX=10000000(lz:这里MAX应该是 大的数而不是int数据的总数!),那么我们需要申请内存空间的大小为int a[1 +

MAX/32]。

其中:a[0]在内存中占32为可以对应十进制数0-31,依次类推: bitmap表为: a[0]--------->0-31 a[1]--------->32-63 a[2] >64-95

 
  

a[3] >96-127

我们要把一个整数N映射到Bit-Map中去,首先要确定把这个N    Mapping到哪一个数组元素中去,即确定映射元素的index。我们用int类型的数组作为map的元素,这样我们就知道了一个元素能够表示的数字个数(这里是32)。于是N/32 就可以知道我们需要映射的key了。所以余下来的那个N%32就是要映射到的位数。

  1. 求十进制数对应在数组a中的下标:

先由十进制数n转换为与32的余可转化为对应在数组a中的下标。

如十进制数0-31,都应该对应在a[0]中,比如n=24,那么 n/32=0,则24对应在数组a中的下标为0。又比如n=60,那么n/32=1,则60对应在数组a中的下标为 1,同理可以计算0-N

在数组a中的下标。

i = N>>K % 结果就是N/(2^K)

Note: map的范围是[0, 原数组 大的数对应的2的整次方数-1]。

  1. 求十进制数对应数组元素a[i]在0-31中的位m:

十进制数0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32求得对应0-31中的数。

m = n & ((1 << K) - 1) %结果就是n%(2^K) 3.利用移位0-31使得对应第m个bit位为1 如a[i]的第m位置1:a[i] = a[i] | (1<

如:将当前4对应的bit位置1的话,只需要1左移4位与B[0] | 即可。

Note:

1 p+(i/8)|(0×01<<(i%8))这样也可以?

2 同理将int型变量a的第k位清0,即a=a&~(1<

BitMap算法评价

优点:

  1. 运算效率高,不进行比较和移位;
  2. 占用内存少,比如 大的数MAX=10000000;只需占用内存为

MAX/8=1250000Byte=1.25M。3.

缺点:

  1. 所有的数据不能重复,即不可对重复的数据进行排序。(少量重复数据查找还是可以的,用2-bitmap)。
  2. 当数据类似(1,1000,10万)只有3个数据的时候,用bitmap时间复杂度和空间复杂度相当大,只有当数据比较密集时才有优势。

 字典树的理解以及在统计上的应用

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。Trie树也有它的缺点,Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的       方法建树的话,可能会好点. 就是在海量数据中找出某一个数,比如2亿QQ号中查找出某一个特定的QQ号

 N个骰子出现和为m的概率

典型的可以用动态规划的思想来完成

  1. 现在变量有:骰子个数,点数和。当有k个骰子,点数和为n时,出现次数记为 f(k,n)。那与k-1个骰子阶段之间的关系是怎样的?
  2. 当我有k-1个骰子时,再增加一个骰子,这个骰子的点数只可能为1、2、3、

4、5或6。那k个骰子得到点数和为n的情况有: (k-1,n-1):第k个骰子投了点数1

(k-1,n-2):第k个骰子投了点数2

(k-1,n-3):第k个骰子投了点数3

(k-1,n-6):第k个骰子投了点数6

在k-1个骰子的基础上,再增加一个骰子出现点数和为n的结果只有这6种情况!所以:f(k,n)=f(k-1,n-1)+f(k-1,n-2)+f(k-1,n-3)+f(k-1,n-4)+f(k-1,n-5)+f(k- 1,n-6)

3.有1个骰子,f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。

 
  

用递归就可以解决这个问题:

用迭代来完成

 海量数据问题(可参考左神的书)

目前关于海量数据想到的解决办法:

1.bitmap

2.桶排序,外部排序,将需要排序的放到外存上,不用全部放到内存上

 一致性哈希说明

优点

  1. 当后端是缓存服务器时,经常使用一致性哈希算法来进行负载均衡。使用一致性哈希的好处在于,增减集群的缓存服务器时,只有少量的缓存会失效,回源量较小。
  2. 尽量减少数据丢失问题,减少移动数据的风险

 如何给100亿个数字排序?

场景

之前写过一篇海量数据中统计ip出现次数最多的博客,今天再写篇类似的,当然会有不同的地方,相同的地方我快速写过

今天要给100亿个数字排序,100亿个  int  型数字放在文件里面大概有   37.2GB,非常大,内存一次装不下了。那么肯定是要拆分成小的文件一个一个来处理,最终在合并成一个排好序的大文件。

实现思路

  1. 把这个37GB的大文件,用哈希分成1000个小文件,每个小文件平均38MB左右(理想情况),把100亿个数字对1000取模,模出来的结果在0到999之间,每个结果对应一个文    件,所以我这里取的哈希函数是 h = x % 1000,哈希函数取得"好",能使冲突减小,结果分布均匀。
    1. 拆分完了之后,得到一些几十MB的小文件,那么就可以放进内存里排序了,可以用快速排序,归并排序,堆排序等等。

3.1000个小文件内部排好序之后,就要把这些内部有序的小文件,合并成一个大的文件,可以用二叉堆来做1000路合并的操作,每个小文件是一路,合并后的大文件仍然有序。

首先遍历1000个文件,每个文件里面取第一个数字,组成  (数字,  文件号)  这样的组合加入到堆里(假设是从小到大排序,用小顶堆),遍历完后堆里有1000个  (数字,文件号) 这样的元素

然后不断从堆顶拿元素出来,每拿出一个元素,把它的文件号读取出来,然后去对应的文件里,加一个元素进入堆,直到那个文件被读取完。拿出来的元素当然追加到最终结果的文件里。

按照上面的操作,直到堆被取空了,此时最终结果文件里的全部数字就是有序的了。

思维拓展

类似的100亿个数字求和,求中位数,求平均数,套路就是一样的了。    求和:统计每个小文件的和,返回给master再求和就可以了。

求平均数:上面能求和了,再除以100亿就是平均数了

求中位数:在排序的基础上,遍历到中间的那个数就是中位数了。

更正,评论中网友提出了不用对数字进行哈希,直接平均分成1000份,进行内部排序后,直接进行 k 路归并排序,也是可以的。

 哈希分治法 - 统计海量数据中出现次数最多的前10IP

场景

这是一个 ip 地址 127.0.0.1

假设有100亿个这样的 ip 地址存在文件中这个文件大小大约是 100GB

问题:要统计出100亿个 ip 中,重复出现次数最多的前10

分析

100GB 几乎不可能一次加载进内存进行操作,所以必须要拆分

那么可以利用分治的思想,把规模大的问题化小,然后解决各个小的问题,最后得出结果。

实现思路

ipv4 地址是一个 32 位的整数,可以用 uint 保存。

我先设计一个哈希函数,把100个G的文件分成10000份,每份大约是 10MB,可以加载进内存了。

例如:我设计一个简单的哈希函数是 f(ip) = ip % 10000,(ip 是个32位整数)

那么 5 % 10000 = 5,不管 5 在哪个地方 5 % 10000 的结果都是 5,这就保证了相同的 ip 会被放在同一个子文件中,方便统计,相同的元素经过同一个哈希函数,得出的哈希值是一样的。

那么我把100亿个 ip,都进行 ip % 10000 的操作,就完成了 100GB 文件分解成 10000个子文件的任务了。当然实际中哈希函数的选取很重要,尽量使得元素分布均匀, 哈希冲突少的函数才是最好的。

记住,我把上面这个分解的过程叫做 Map,由一台叫 master 的计算机完成这个工作。

10MB 的小文件加进内存,统计出出现次数最多的那个ip

10MB 的小文件里面存着很多 ip,他们虽然是乱序的,但是相同的 ip 会映射到同一个文件中来!

那么可以用二叉树统计出现次数,二叉树节点保存(ip, count)的信息,把所有 ip 插入到二叉树中,如果这个 ip 不存在,那么新建一个节点, count 标记  1,如果有,那么把 count++,最终遍历一遍树,就能找出 count 最大的 ip 了。

我把这个过程叫做 Reduce,由很多台叫 worker 的计算机来完成。

每个 worker 至少要找出最大的前10个 ip 返回给 master,master 最后会收集到 10000 * 10 个 ip,大约 400KB,然后再找出最大的前 10 个 ip 就可以了。最简单的遍历10遍,每次拿个最大值出来就可以了,或者用快速排序,堆排序,归并排序等等方法,找出最大前 k 个数也行。

MapReduce

我刚刚除了介绍了一种海量数据的哈希分治算法之外,还穿插了一个谷歌的    MapReduce    分布式并行编程模型,原理就是上面说的那些了,有兴趣的可以去详细了解。哈希函数是什么?哈希函数是把大空间的元素映射到一个小空间里。

 排序算法时间复杂度

排序方法

时间复杂度(平均)

时间复杂度(最坏)

时间复杂度(最好)

空间复杂度

稳定性

复杂性

直接插入排序

O(n2)

O(n2)

O(n)

O(1)

稳定

简单

希尔排序

O(nlog2n)

O(n2)

O(n)

O(1)

不稳定

较复杂

直接选择排序

O(n2)

O(n2)

O(n2)

O(1)

不稳定

简单

堆排序

O(nlog2n)

O(nlog2n)

O(nlog2n)

O(1)

不稳定

较复杂

冒泡排序

O(n2)

O(n2)

O(n)

O(1)

稳定

简单

快速排序

O(nlog2n)

O(n2)

O(nlog2n)

O(nlog2n)

不稳定

较复杂

归并排序

O(nlog2n)

O(nlog2n)

O(nlog2n)

O(n)

稳定

较复杂

基数排序

O(d(n+r))

O(d(n+r))

O(d(n+r))

O(n+r)

稳定

较复杂

 判断链表中是否有环有关单链表中环的问题

给定一个单链表,判断其中是否有环,已经是一个比较老同时也是比较经典的问题,在网上搜集了一些资料, 然后总结一下大概可以涉及到的问题,以及相应的解法。

首先,关于单链表中的环,一般涉及到一下问题:

  1. 给一个单链表,判断其中是否有环的存在;
  2. 如果存在环,找出环的入口点;
  3. 如果存在环,求出环上节点的个数;
  4. 如果存在环,求出链表的长度;
  5. 如果存在环,求出环上距离任意一个节点最远的点(对面节点);

6.(扩展)如何判断两个无环链表是否相交;

7.(扩展)如果相交,求出第一个相交的节点;

下面,我将针对上面这七个问题一一给出解释和相应的代码。

1.判断时候有环(链表头指针为head)

对于这个问题我们可以采用“快慢指针”的方法。就是有两个指针fast和slow,开始的时候两个指针都指向链表头head,然后在每一步     操作中slow向前走一步即:slow = slow->next,而fast每一步向前两步即:fast = fast->next->next。

由于fast要比slow移动的快,如果有环,fast一定会先进入环,而slow后进入环。当两个指针都进入环之后,经过一定步的操作之后   二者一定能够在环上相遇,并且此时slow还没有绕环一圈,也就是说一定是在slow走完第一圈之前相遇。证明可以看下图:

 
  

当slow刚进入环时每个指针可能处于上面的情况,接下来slow和fast分别向前走即:

 
  

也就是说,slow每次向前走一步,fast向前追了两步,因此每一步操作后fast到slow的距离缩短了1步,这样继续下去就会使得

两者之间的距离逐渐缩小:...、5、4、3、2、1、0 -> 相遇。又因为在同一个环中fast和slow之间的距离不会大于换的长度,因此到二者相遇的时候slow一定还没有走完一周(或者正好走完以后,这种情况出现在开始的时候fast和slow都在环的入口处)。

下面给出问题1的完整代码:

 
  

下面看问题2,找出环的入口点:

我结合着下图讲解一下:

 
  

从上面的分析知道,当fast和slow相遇时,slow还没有走完链表,假设fast已经在环内循环了n(1<= n)圈。假设slow走了s步,则fast走了2s步,又由于fast走过的步数 = s + n*r(s + 在环上多走的n圈),则有下面的等式:

2*s = s + n * r ; (1)

=> s = n*r (2)

如果假设整个链表的长度是L,入口和相遇点的距离是x(如上图所示),起点到入口点的距离是a(如上图所示),则有:

a + x = s = n * r; (3) 由(2)推出

a + x = (n - 1) * r + r = (n - 1) * r + (L - a) (4) 由环的长度 = 链表总长度 - 起点到入口点的距离求出

a = (n - 1) * r + (L -a -x) (5)

集合式子(5)以及上图我们可以看出,从链表起点head开始到入口点的距离a,与从slow和fast的相遇点(如图)到入口点的距离相等。

因此我们就可以分别用一个指针(ptr1, prt2),同时从head与slow和fast的相遇点出发,每一次操作走一步,直到ptr1 == ptr2,此时的位置也就是入口点! 到此第二个问题也已经解决。

下面给出示意性的简单代码(没有测试但是应该没有问题):

 
  

第3个问题,如果存在环,求环上节点的个数:

对于这个问题,我这里有两个思路(肯定还有其它跟好的办法):

思路1:记录下相遇节点存入临时变量tempPtr,然后让slow(或者fast,都一样)继续向前走slow = slow -> next;一直到slow == tempPtr; 此时经过的步数就是环上节点的个数; 思路2: 从相遇点开始slow和fast继续按照原来的方式向前走slow = slow -> next; fast = fast -> next -> next;直到二者再次项目,此时经过的步数就是环上节点的个数 。

第一种思路很简单,其实就是一次遍历链表的环,从而统计出点的个数,没有什么可以详细解释的了。

对于第二种思路,我们可以这样想,结合上面的分析,fast和slow没一次操作都会使得两者之间的距离较少1。我们可以把两者相遇的时候看做两者之间的距离正好是整个环的         长度r。因此,当再次相遇的时候所经过的步数正好是环上节点的数目。

由于这两种思路都比较简单,代码也很容易实现,这里就不给出了。

问题4是如果存在环,求出链表的长度:

到这里,问题已经简单的多了,因为我们在问题1、2、3中已经做得足够的”准备工作“。    我们可以这样求出整个链表的长度:

链表长度L = 起点到入口点的距离 + 环的长度r;

已经知道了起点和入口点的位置,那么两者之间的距离很好求了吧!环的长度也已经知道了,因此该问题也就迎刃而解了! 问题5是,求出环上距离任意一个节点最远的点(对面节点)

 
  

如下图所示,点1和4、点2和5、点3和6分别互为”对面节点“ ,也就是换上最远的点,我们的要求是怎么求出换上任意一个点的最远点。

对于换上任意的一个点ptr0,   我们要找到它的”对面点“,可以这样思考:同样使用上面的快慢指针的方法,让slow和fast都指向ptr0,每一步都执行与上面相同的操作(slow每次跳一步,fast每次跳两步),

当fast = ptr0或者fast = prt0->next的时候slow所指向的节点就是ptr0的”对面节点“。为什么是这样呢?我们可以这样分析:

 
  

如上图,我们想像一下,把环从ptro处展开,展开后可以是无限长的(如上在6后重复前面的内容)如上图。

现在问题就简单了,由于slow移动的距离永远是fast的一般,因此当fast遍历玩整个环长度r个节点的时候slow正好遍历了r/2个节点,   也就是说,此时正好指向距离ptr0最远的点。

对于问题6(扩展)如何判断两个无环链表是否相交,和7(扩展)如果相交,求出第一个相交的节点,其实就是做一个问题的转化:

假设有连个链表listA和listB,如果两个链表都无环,并且有交点,那么我们可以让其中一个链表(不妨设是listA)的为节点连接到其头部,这样在listB中就一定会出现一个环。    因此我们将问题6和7分别转化成了问题1和2.

看看下图就会明白了:

 
  

 常见hash算法的原理

散列表,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法。顾名思义,该数据结构可以理解为一个线性表,但是其中的元素不是紧密排列的,而是可能存在           空隙。

散列表(Hash  table,也叫哈希表),是根据关键码值(Key   value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

比如我们存储70个元素,但我们可能为这70个元素申请了100个元素的空间。70/100=0.7,这个数字称为负载因子。我们之所以这样做,也       是为了“快速存取”的目的。我们基于一种结果尽可能随机平均分布的固定函数H为每个元素安排存储位置,这样就可以避免遍历性质的线性搜索,以达到快速存 取。但是由于此随机性,也必然导致一个问题就是冲突。所谓冲突,即两个元素通过散列函数H得到的地址相同,那么这两个元素称为“同义词”。这类似于70个         人去一个有100个椅子的饭店吃饭。散列函数的计算结果是一个存储单位地址,每个存储单位称为“桶”。设一个散列表有m个桶,则散列函数的值域应为 [0,m-1]。

解决冲突是一个复杂问题。冲突主要取决于:

(1) 散列函数,一个好的散列函数的值应尽可能平均分布。

(2) 处理冲突方法。

(3) 负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。解决冲突的办法:

(1) 线性探查法:冲突后,线性向前试探,找到最近的一个空位置。缺点是会出现堆积现象。存取时,可能不是同义词的词也位于探查序列,影响效率。

(2) 双散列函数法:在位置d冲突后,再次使用另一个散列函数产生一个与散列表桶容量m互质的数c,依次试探(d+n*c)%m,使探查序列跳跃式分布。

常用的构造散列函数的方法

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:

  1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)
  2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相 同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会 明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
    1. 平方取中法:取关键字平方后的中间几位作为散列地址。
    2. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
    3. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
    4. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
查找的性能分析

散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按 处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平 均查找长度来衡量。

 
  

查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:

散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度

α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的           元素较少,产生冲突的可能性就越小。

实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。

了解了hash基本定义,就不能不提到一些著名的hash算法,MD5 和 SHA-1 可以说是目前应用最广泛的Hash算法,而它们都是以 MD4 为基础设计的。那么他们都是什么意思呢?

这里简单说一下:

(1) MD4

MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,MD 是 Message Digest 的缩写。它适用在32位字长的处理器上用高速软件实现--它是基于 32 位操作数的位操作来实现的。

(2) MD5

MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好

(3) SHA-1 及其他

SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理, 并且模仿了该算法。

哈希表不可避免冲突(collision)现象:对不同的关键字可能得到同一哈希地址 即key1≠key2,而hash(key1)=hash(key2)。因此,在建造哈希表时不仅要设定一个好的哈希函数,而且要设定一种处理冲突的方法。可如下描述哈希表:根据设定的哈希函数H(key)和所选中的处理冲突的方法,将一组关键字映象到一个有限的地址连续的地址集(区间)上         并以关键字在地址集中的作为相应记录在表中的存储位置,这种表被称为哈希表。

对于动态查找表而言,1) 表长不确定;2)在设计查找表时,只知道关键字所属范围,而不知道确切的关键字。因此,一般情况需建立一个函数关系,以f(key)作为关键字为

key的 录在表中的位置,通常称这个函数f(key)为哈希函数。(注意:这个函数并不一定是数学函数)

哈希函数是一个映象,即:将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可。现实中哈希函数是需要构造的,并且构造的好才能使用的好。

那么这些Hash算法到底有什么用呢?

Hash算法在信息安全方面的应用主要体现在以下的3个方面:

(1) 文件校验

我们比较熟悉的校验算法有奇偶校验和CRC校验,这2种校验并没有抗数据篡改的能力,它们一定程度上能检测并纠正数据传输中的信道误码,但却不能防止对数据的恶意破          坏。

MD5 Hash算法的"数字指纹"特性,使它成为目前应用最广泛的一种文件完整性校验和(Checksum)算法,不少Unix系统有提供计算md5 checksum的命令。

(2) 数字签名

Hash  算法也是现代密码体系中的一个重要组成部分。由于非对称算法的运算速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色。  对  Hash  值,又称"数字摘要"进行数字签名,在统计上可以认为与对文件本身进行数字签名是等效的。而且这样的协议还有其他的优点。

(3) 鉴权协议

如下的鉴权协议又被称作挑战--认证模式:在传输信道是可被侦听,但不可被篡改的情况下,这是一种简单而安全的方法。

文件hash

MD5-Hash-文件的数字文摘通过Hash函数计算得到。不管文件长度如何,它的Hash函数计算结果是一个固定长度的数字。与加密算法不     同,这一个Hash算法是一个不可逆的单向函数。采用安全性高的Hash算法,如MD5、SHA时,两个不同的文件几乎不可能得到相同的Hash结果。因 此,一旦文件被修改,就可检测出来。

Hash函数还有另外的含义。实际中的Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。除此以外,    Hash函数往往应用于查找上。所以,在考虑使用Hash函数之前,需要明白它的几个限制:

  1. Hash的主要原理就是把大范围映射到小范围;所以,你输入的实际值的个数必须和小范围相当或者比它更小。不然冲突就会很多。
  2. 由于Hash逼近单向函数;所以,你可以用它来对数据进行加密。
  3. 不同的应用对Hash函数有着不同的要求;比如,用于加密的Hash函数主要考虑它和单项函数的差距,而用于查找的Hash函数主要考虑它映射到小范围的冲突率。

应用于加密的Hash函数已经探讨过太多了,在作者的博客里面有更详细的介绍。所以,本文只探讨用于查找的Hash函数。

Hash函数应用的主要对象是数组(比如,字符串),而其目标一般是一个int类型。以下我们都按照这种方式来说明。        一般的说,Hash函数可以简单的划分为如下几类:

  1. 加法Hash;
  2. 位运算Hash;除法Hash;
    1. 乘法Hash;

5. 查表Hash;

  1. 混合Hash;

下面详细的介绍以上各种方式在实际中的运用。

一 加法Hash

所谓的加法Hash就是把输入元素一个一个的加起来构成最后的结果。标准的加法Hash的构造如下:

static int additiveHash(String key, int prime)

{

int hash, i;

for (hash = key.length(), i = 0; i < key.length(); i++) hash += key.charAt(i);

return (hash % prime);

}

这里的prime是任意的质数,看得出,结果的值域为[0,prime-1]。

二 位运算Hash

这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素。比如,标准的旋转Hash的构造如下:

static int rotatingHash(String key, int prime)

{

int hash, i;

for (hash=key.length(), i=0; i

hash = (hash<<4>>28)^key.charAt(i); return (hash % prime);

}

先移位,然后再进行各种位运算是这种类型Hash函数的主要特点。比如,以上的那段计算hash的代码还可以有如下几种变形:

hash = (hash<<5>>27)^key.charAt(i); hash += key.charAt(i);

hash += (hash << 10); hash ^= (hash >> 6); if((i&1) == 0)

{

hash ^= (hash<<7>>3);

}

else

{

hash ^= ~((hash<<11>>5));

}

hash += (hash<<5>

hash = key.charAt(i) + (hash<<6>>16) ? hash; hash ^= ((hash<<5>>2));

三 乘法Hash

这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好)。比如,

static int bernstein(String key)

{

int hash = 0; int i;

for (i=0; i return hash;

}

jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131,  131313等等。使用这种方式的著名Hash函数还有:

// 32位FNV算法int M_SHIFT = 0;

public int FNVHash(byte[] data)

{

int hash = (int)2166136261L; for(byte b : data)

hash = (hash * 16777619) ^ b; if (M_SHIFT == 0)

return hash;

return (hash ^ (hash >> M_SHIFT)) & M_MASK;

}

以及改进的FNV算法:

public static int FNVHash1(String data)

{

final int p = 16777619;

int hash = (int)2166136261L; for(int i=0;i

hash = (hash ^ data.charAt(i)) * p;

hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; return hash;

}

除了乘以一个固定的数,常见的还有乘以一个不断改变的数,比如:

static int RSHash(String str)

{

int b = 378551; int a = 63689; int hash = 0;

for(int i = 0; i < str.length(); i++)

{

hash = hash * a + str.charAt(i); a = a * b;

}

return (hash & 0x7FFFFFFF);

}

虽然Adler32算法的应用没有CRC32广泛,不过,它可能是乘法Hash里面最有名的一个了。关于它的介绍,大家可以去看RFC 1950规范。

四 除法Hash

除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用。需要注意的是,我们在前面看到的hash的          结果除以一个prime的目的只是为了保证结果的范围。如果你不需要它限制一个范围的话,可以使用如下的代码替代”hash%prime”: hash = hash ^ (hash>>10) ^ (hash>>20)。

**五 查表Hash

**查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快的实现方式。下面是CRC32的实现: static int crctab[256] = {

0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,

0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,

0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d

};

int crc32(String key, int hash)

{

int i;

for (hash=key.length(), i=0; i

hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ k.charAt(i)]; return hash;

}

查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。

六 混合Hash

混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、Tiger都属于这个范围。它们一般很少在面向查找的Hash函数里面使用。

 七大查找算法

查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。

查找定义:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。查找算法分类:

1) 静态查找和动态查找;

注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。

2) 无序查找和有序查找。

无序查找:被查找数列有序无序均可; 有序查找:被查找数列必须为有序数列。

平均查找长度(Average Search LengthASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。

对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。Pi:查找表中第i个数据元素的概率。

Ci:找到第i个数据元素时已经比较过的次数。

1. 顺序查找

说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。

基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成           功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。

复杂度分析:

查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;

当查找不成功时,需要n+1次比较,时间复杂度为O(n);

所以,顺序查找的时间复杂度为O(n**)。**

C++实现源码:
 
  
  1. 二分查找

说明:元素必须是有序的,如果是无序的则要先进行排序操作。

基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该           中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。

复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n)

注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要**频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》**

C++实现源码:
 
  
  1. 插值查找

在介绍插值查找之前,首先考虑一个新问题,为什么上述算法一定要是折半,而不是折四分之一或者折更多呢?

打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查       起,而是有一定目的的往前或往后翻。

同样的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 我们自然会考虑从数组下标较小的开始查找。经过以上分析,折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:

mid=(low+high)/2, 即 mid=low+1/2*(high-low); 通过类比,我们可以将查找的点改进为如下:

mid=low+(key-a[low])/(a[high]-a[low])*(high-low),

也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。

基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。

注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2n))

C++实现源码:

 
  
  1. 斐波那契查找

在介绍斐波那契查找算法之前,我们先介绍一下很它紧密相连并且大家都熟知的一个概念——黄金分割。

黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。

0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。          因此被称为黄金分割。

大家记不记得斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。

基本思想:也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况:

1)相等,mid位置的元素即为所求2)>,low=mid+1;

3) <,high=mid-1。

斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,及n=F(k)-1;     开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种

1)相等,mid位置的元素即为所求

2)>,low=mid+1,k-=2;

说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。

3)<,high=mid-1,k-=1。

说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。

复杂度分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n**)。**

C++实现源码:

// 斐波那契查找.cpp

#include "stdafx.h" #include <memory> #include <iostream> using namespace std;

const int max_size=20;//斐波那契数组的长度

/*构造一个斐波那契数组*/ void Fibonacci(int * F)

{

F[0]=0;

F[1]=1;

for(int i=2;i<max_size;++i) F[i]=F[i-1]+F[i-2];

}

/*定义斐波那契查找法*/

int FibonacciSearch(int *a, int n, int key) //a为要查找的数组,n为要查找的数组长度,key为要查找的关键字

{

int low=0; int high=n-1;

int F[max_size];

Fibonacci(F);//构造一个斐波那契数组F

int k=0;

while(n>F[k]-1)//计算n位于斐波那契数列的位置

++k;

int * temp;//将数组a扩展到F[k]-1的长度temp=new int [F[k]-1]; memcpy(temp,a,n*sizeof(int));

for(int i=n;i<F[k]-1;++i) temp[i]=a[n-1];

while(low<=high)

{

int mid=low+F[k-1]-1; if(key<temp[mid])

{

high=mid-1; k-=1;

}

else if(key>temp[mid])

{

low=mid+1; k-=2;

}

else

{

if(mid<n)

return mid; //若相等则说明mid即为查找到的位置else

return n-1; //若mid>=n则说明是扩展的数值,返回n-1

}

}

delete [] temp; return -1;

}

int main()

{

int a[] = {0,16,24,35,47,59,62,73,88,99};

int key=100;

int index=FibonacciSearch(a,sizeof(a)/sizeof(int),key); cout<<key<<" is located at:"<<index;

return 0;

}

5. 树表查找

5.1 最简单的树表查找算法——二叉树查找算法。

基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。

二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树: 1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2) 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

3) 任意节点的左、右子树也分别为二叉查找树。

二叉查找树性质对二叉查找树进行中序遍历,即可得到有序的数列。

不同形态的二叉查找树如下图所示:

有关二叉查找树的查找、插入、删除等操作的详细讲解,请移步[浅谈算法和数据结构: 七 二叉查找树]

复杂度分析:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
 
  

下图为二叉树查找和顺序查找以及二分查找性能的对比图:

基于二叉查找树进行优化,进而可以得到其他的树表查找算法,如平衡树、红黑树等高效算法。

5.2 平衡查找树之2-3查找树(2-3 Tree)

2-3查找树定义:和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个

Key,2-3查找树的定义如下:

1) 要么为空,要么:

2) 对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比   key要大。

3) 对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-

 
  

3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。

2-3查找树的性质:

1) 如果中序遍历2-3查找树,就可以得到排好序的序列;

2) 在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。(这也是平衡树中平衡一词的概念,根节点到叶节点的最长距离对应于查找算法的最坏情             况,而平衡树中根节点到叶节点的距离都一样,最坏情况也具有对数复杂度。)

性质2)如下图所示:

复杂度分析:

2-3树的查找效率与树的高度是息息相关的。

在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN

在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN

距离来说,对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。

 
  

对于插入来说,只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。下面是2-3查找树的效率:

5.3 平衡查找树之红黑树(Red-Black Tree)

2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来        比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。

 
  

基本思想:红黑树的思想就是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为两种不同类型,红色链接,他用         来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2- node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。

红黑树的定义:

红黑树是一种具有红色和黑色链接的平衡查找树,同时满足: 红色节点向左倾斜

一个节点不可能有两个红色链接

整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。

下图可以看到红黑树其实是2-3树的另外一种表现形式:如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个3-node节点了。

红黑树的性质整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同(2-3树的第2)性质,从根节点到叶子节点的距离都相等)。复杂度分析:**最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍。**

下图是一个典型的红黑树,从中可以看到最长的路径(红黑相间的路径)是最短路径的2倍:

 
  

红黑树的平均高度大约为logn

 
  

下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。

红黑树这种数据结构应用十分广泛,在多种编程语言中被用作符号表的实现,如:

Java 中 的 java.util.TreeMap,java.util.TreeSet; C++ STL中的:map,multimap,multiset;

.NET中的:SortedDictionary,SortedSet  等。

5.4 B树和B+树(B Tree/B+ Tree)

平衡查找树中的2-3树以及其实现红黑树。2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key。

维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log       n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。B-tree算法减少         定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库文件系统

B树定义:

B可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。    根节点至少有两个子节点

每个节点有M-1个key,并且以升序排列

位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间其它节点至少有M/2个子节点

 
  

下图是一个M=4 阶的B树:

可以看到B树是2-3树的一种扩展,他允许一个节点有多于2个的元素。B树的插入及平衡化操作和2-3树很相似,这里就不介绍了。下面是往B树中依次插入

6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4
 
  

的演示动画:

B+树定义:

B+树是对B树的一种变形树,它与B树的差异在于:

有k个子结点的结点必然有k个关键码;

非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。

树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

 
  

如下图,是一个B+树:

 
  

下图是B+树的插入动画:

BB+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。

B+ 树的优点在于:

由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。          数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。

B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一           层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

但是B树也有优点,其优点在于,由于B树的每一个节点都包含keyvalue,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

下面是B 树和B+树的区别图:

B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部           性从而减少IO操作。它广泛用于文件系统及数据库中,如:

Windows:HPFS文件系统; Mac:HFS,HFS+文件系统;

Linux:ResiserFS,XFS,Ext3FS,JFS文件系统; 数据库:ORACLE,MYSQL,SQLSERVER等中。

有关B/B+树在数据库索引中的应用,请看张洋的[MySQL索引背后的数据结构及算法原理]这篇文章,这篇文章对MySQL中的如何使用B+树进行索引有比较详细的介绍,推荐      阅读。

树表查找总结:

二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数            据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或          少的采用了红黑树。

除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。

6. 分块查找

分块查找又称索引顺序查找,它是顺序查找的一种改进方法。

算法思想:将n个数据元素"按块有序"划分为m块(m    ≤    n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……

算法流程:

step1 先选取各块中的最大关键字构成一个索引表;

step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。

7. 哈希查找

什么是哈希表(Hash)?

我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与           函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看           到一种解决"冲突"的简便做法。

总的来说,"直接定址""解决冲突"是哈希表的两大特点。什么是哈希函数?

哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。

算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

算法流程:

1) 用给定的哈希函数构造哈希表;

2) 根据选择的冲突处理方法解决地址冲突;

3) 在哈希表的基础上执行哈希查找。

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

复杂度分析

单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。

使用Hash,我们付出了什么?

我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV     pair,经常使用Python的博友可能更有这种体会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?

Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数        据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然       是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。

Hash算法和其他查找算法的性能对比:

 一致性算法

Paxos

Paxos 算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致。zookeeper       使用的 zab 算法是该算法的一个实现。 在 Paxos 算法中,有三种角色:Proposer,Acceptor,Learners

Paxos 三种角色:ProposerAcceptorLearners Proposer

只要 Proposer 发的提案被半数以上 Acceptor 接受,Proposer 就认为该提案里的 value 被选定了。

Acceptor

只要 Acceptor 接受了某个提案,Acceptor 就认为该提案里的 value 被选定了。

Learner

Acceptor 告诉 Learner 哪个 value 被选定,Learner 就认为那个 value 被选定。

Paxos 算法分为两个阶段。具体如下: 阶段一(准leader确定 ):

(a) Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。 (b) 如果一个 Acceptor 收到一个编号为 N 的 Prepare 请求,且 N 大于该 Acceptor 已经响应过的所有  Prepare 请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给  Proposer,同时该  Acceptor 承诺不再接受任何编号小于N 的提案。

阶段二(leader确认):

(a) 如果 Proposer 收到半数以上 Acceptor 对其发出的编号为 N 的 Prepare 请求的响应,那么它就会发送一个针对[N,V]提案的 Accept 请求给半数以上的 Acceptor。注意:V 就是收到的响应中编号最大的提案的 value,如果响应中不包含任何提案,那么 V 就由 Proposer 自己决定。

(b) 如果 Acceptor 收到一个针对编号为 N 的提案的 Accept 请求,只要该 Acceptor 没有对编号大于 N 的 Prepare 请求做出过响应,它就接受该提案。

Zab

ZAB( ZooKeeper Atomic Broadcast , ZooKeeper 原子消息广播协议)协议包括两种基本的模式:崩溃恢复和消息广播

  1. 当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断崩溃退出与重启等异常情况时,ZAB 就会进入恢复模式并选举产生新的 Leader 服务器。
  2. 当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出崩溃恢复模式,进入消息广播模式。
  3. 当有新的服务器加入到集群中去,如果此时集群中已经存在一个  Leader  服务器在负责进行消息广播,那么新加入的服务器会自动进入数据恢复模式,找到  Leader  服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

以上其实大致经历了三个步骤:

  1. 崩溃恢复:主要就是Leader 选举过程
  2. 数据同步:Leader服务器与其他服务器进行数据同步
  3. 消息广播:Leader服务器将数据发送给其他服务器说明:zookeeper 章节对该协议有详细描述。

Raft

与 Paxos 不同 Raft 强调的是易懂(Understandability),Raft 和 Paxos 一样只要保证 n/2+1 节点正常就能够提供服务;raft 把算法流程分为三个子问题:选举(Leader election)、日志复制

(Log replication)、安全性(Safety)三个子问题。

  1. 角色

Raft 把集群中的节点分为三种状态:Leader、 Follower 、Candidate,理所当然每种状态负责的任务也是不一样的,Raft 运行时提供服务的时候只存在 Leader 与 Follower 两种状态;

Leader(领导者-日志管理)

负责日志的同步管理,处理来自客户端的请求,与 Follower 保持这 heartBeat 的联系;

Follower(追随者-日志同步)

刚启动时所有节点为Follower状态,响应Leader的日志同步请求,响应Candidate的请求,把请求到 Follower 的事务转发给 Leader;

Candidate(候选者-负责选票)

负责选举投票,Raft 刚启动时由一个节点从 Follower 转为 Candidate 发起选举,选举出

Leader 后从 Candidate 转为 Leader 状态;

  1. Term(任期)

在 Raft 中使用了一个可以理解为周期(第几届、任期)的概念,用 Term 作为一个周期,每个 Term 都是一个连续递增的编号,每一轮选举都是一个 Term 周期,在一个 Term

中只能产生一个 Leader;当某节点收到的请求中 Term 比当前 Term 小时则拒绝该请求。

  1. 选举(Election) 选举定时器

Raft 的选举由定时器来触发,每个节点的选举定时器时间都是不一样的,开始时状态都为 Follower 某个节点定时器触发选举后 Term 递增,状态由 Follower 转为 Candidate, 向其他节点发起 RequestVote RPC 请求,这时候有三种可能的情况发生:

1:该RequestVote请求接收到n/2+1(过半数)个节点的投票,从Candidate 转为Leader,向其他节点发送 heartBeat 以保持 Leader 的正常运转。2:在此期间如果收到其他节点发送过来的 AppendEntries RPC 请求,如该节点的 Term 大则当前节点转为 Follower,否则保持 Candidate 拒绝该请求。3:Election timeout 发生则 Term 递增,重新发起选举

在一个 Term 期间每个节点只能投票一次,所以当有多个 Candidate 存在时就会出现每个 Candidate 发起的选举都存在接收到的投票数都不过半的问题,这时每个 Candidate

都将 Term 递增、重启定时器并重新发起选举,由于每个节点中定时器的时间都是随机的,所以就不会多次存在有多个 Candidate 同时发起投票的问题。

在 Raft 中当接收到客户端的日志(事务请求)后先把该日志追加到本地的 Log 中,然后通过 heartbeat 把该 Entry 同步给其他 Follower,Follower 接收到日志后记录日志然后向 Leader 发送 ACK,当 Leader 收到大多数(n/2+1)Follower 的 ACK 信息后将该日志设置为已提交并追加到本地磁盘中,通知客户端并在下个 heartbeat 中 Leader 将通知所有的 Follower 将该日志存储在自己的本地磁盘中。

  1. 安全性(Safety)

安全性是用于保证每个节点都执行相同序列的安全机制如当某个 Follower 在当前 Leader commit Log 时变得不可用了,稍后可能该 Follower 又会倍选举为 Leader,这时新Leader 可能会用新的 Log 覆盖先前已 committed 的 Log,这就是导致节点执行不同序列;Safety 就是用于保证选举出来的 Leader 一定包含先前 commited Log 的机制;

选举安全性(Election Safety):每个 Term 只能选举出一个 Leader

Leader 完整性(Leader Completeness):这里所说的完整性是指 Leader 日志的完整性, Raft 在选举阶段就使用 Term 的判断用于保证完整性:当请求投票的该 Candidate

的 Term 较大或 Term 相同 Index 更大则投票,该节点将容易变成 leader。

  1. raft 协议和 zab 协议区别相同点

§ 采用 quorum 来确定整个系统的一致性,这个 quorum 一般实现是集群中半数以上的服务器,

§ zookeeper 里还提供了带权重的 quorum 实现.

§ 都由 leader 来发起写操作.

§ 都采用心跳检测存活性

§ leader election 都采用先到先得的投票方式不同点

§ zab 用的是 epoch 和 count 的组合来唯一表示一个值, 而 raft 用的是 term 和 index

§ zab 的 follower 在投票给一个 leader 之前必须和 leader 的日志达成一致,而 raft 的 follower 则简单地说是谁的 term 高就投票给谁

raft 协议的心跳是从 leader 到 follower, 而 zab 协议则相反

§ raft 协议数据只有单向地从 leader 到 follower(成为 leader 的条件之一就是拥有最新的 log),

而 zab 协议在 discovery 阶段, 一个 prospective leader 需要将自己的 log 更新为 quorum 里面最新的 log,然后才好在 synchronization 阶段将 quorum 里的其他机器的 log 都同步到一致.

NWR

N:在分布式存储系统中,有多少份备份数据                   W:代表一次成功的更新操作要求至少有w份数据写入成功R: 代表一次成功的读数据操作要求至少有R份数据成功读取

NWR值的不同组合会产生不同的一致性效果,当W+R>N的时候,整个系统对于客户端来讲能保     证强一致性。而如果 R+W<=N,则无法保证数据的强一致性。以常见的 N=3、W=2、R=2 为例:

 
  

N=3,表示,任何一个对象都必须有三个副本(Replica),W=2 表示,对数据的修改操作(Write)只需要在 3 个 Replica 中的 2 个上面完成就返回,R=2 表示,从三个对象中要读取到 2 个数据对象,才能返回。

Gossip

Gossip  算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了   Gossip  的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。

一致性 Hash

一致性哈希算法(Consistent Hashing Algorithm)是一种分布式算法,常用于负载均衡。 Memcached client 也选择这种算法,解决将 key-value 均匀分配到众多 Memcached server 上的问题。它可以取代传统的取模操作,解决了取模操作无法应对增删 Memcached Server 的问题 (增删 server 会导致同一个 key,在 get 操作时分配不到数据真正存储的server,命中率会急剧下

降)。

一致性 Hash 特性

§ 平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

§       单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。容易看到,上面的简单求余算法 hash(object)%N 难以满足单调性要求。

§  平滑性(Smoothness):平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。一致性 Hash 原理

  1. 建构环形hash 空间:
    1. 考虑通常的 hash 算法都是将 value 映射到一个 32 为的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环。
    2. 把需要缓存的内容(对象)映射到hash 空间
      1. 接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash 值 key 在环上的分布
      2. 把服务器(节点)映射到hash 空间
        1. Consistent hashing 的基本思想就是将对象和 cache 都映射到同一个 hash 数值空间中,并且使用相同的 hash 算法。一般的方法可以使用 服务器(节点) 机器的 IP 地址或者机器名作为 hash 输入。
        2. 把对象映射到服务节点
 
  

现在服务节点和对象都已经通过同一个 hash 算法映射到 hash 数值空间中了,首先确定对象  hash 值在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位的服务器。

考察cache 的变动

  1. 通过 hash 然后求余的方法带来的最大问题就在于不能满足单调性,当 cache 有所变动时, cache 会失效。

5.1 移除 cache:考虑假设 cache B 挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿 cache B 逆时针遍历直到下一个 cache ( cache C )之间的对象。

5.2 添加 cache:再考虑添加一台新的 cache D 的情况,这时受影响的将仅是那些沿 cache D 逆时针遍历直到下一个 cache 之间的对象,将这些对象重新映射到 cache D 上即可。

虚拟节点

hash 算法并不是保证绝对的平衡,如果 cache 较少的话,对象并不能被均匀的映射到 cache 上,为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:虚拟节点( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在hash 空间中以 hash 值排列。

仍以仅部署 cache A 和 cache C 的情况为例。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了 cache A; cache C1, cache C2 代表了 cache C 。此时,对象到“虚拟节点”的映射关系为:

objec1->cache A2 ; objec2->cache A1 ; objec3->cache C1 ; objec4->cache C2 ;因此对象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了

cache

C 上;平衡性有了很大提高。

 
  

引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如下图 所示。

 JAVA 算法

二分查找

 
  

又叫折半查找,要求待查找的序列有序。每次取中间位置的值与待查关键字比较,如果中间位置的值比待查关键字大,则在前半部分循环这个查找的过程,如果中间位置的值比待查关键字小,则在后半部分循环这个查找的过程。直到查找到了为止,否则序列中没有待查的关键字。

冒泡排序算法

(1) 比较前后相邻的二个数据,如果前面数据大于后面的数据,就将这二个数据交换。

(2) 这样对数组的第 0 个数据到 N-1 个数据进行一次遍历后,最大的一个数据就“沉”到数组第

N-1 个位置。

(3) 

 
  

N=N-1,如果 N 不为 0 就重复前面二步,否则排序完成。

插入排序算法

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应的位置并插入。插入排序非常类似于整扑克牌。在开始摸牌时,左手是空的,牌面朝下放在桌 上。接着,一次从桌上摸起一张牌,并将它插入到左手一把牌中的正确位置上。为了找到这张牌的正确位置,要将它与手中已有的牌从右到左地进行比较。无论什么时候,左手中的牌都是排好序的。

如果输入数组已经是排好序的话,插入排序出现最佳情况,其运行时间是输入规模的一个线性函数。如果输入数组是逆序排列的,将出现最坏情况。平均情况与最坏情况一样, 其时间代价是(n2)。

 
  

快速排序算法

快速排序的原理:选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。

一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束            第一次循环,此时,对于基准值来说,左右两边就是有序的了。

 
  

希尔排序算法

基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

  1. 操作方法:

选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;

  1. 按增量序列个数 k,对序列进行 k 趟排序;
 
  

每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

归并排序算法

 
  

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序           序列。

*

* @param data

* 数组对象

* @param left

* 左数组的第一个元素的索引

* @param center

* 左数组的最后一个元素的索引,center+1 是右数组第一个元素的索引

* @param right

* 右数组最后一个元素的索引

*/

public static void merge(int[] data, int left, int center, int right) {

// 临时数组 int[] tmpArr = new int[data.length];

// 右数组第一个元素索引

int mid = center + 1;

// third 记录临时数组的索引

int third = left;

// 缓存左数组第一个元素的索引

int tmp = left; while (left <= center && mid <= right) {

// 从两个数组中取出最小的放入临时数组

if (data[left] <= data[mid]) { tmpArr[third++] = data[left++];

} else {

tmpArr[third++] = data[mid++];

}

}

// 剩余部分依次放入临时数组(实际上两个 while 只会执行其中一个)

while (mid <= right) { tmpArr[third++] = data[mid++]; public class MergeSortTest {

public static void main(String[] args) {

int[] data = new int[] { 5, 3, 6, 2, 1, 9, 4, 8, 7 }; print(data); mergeSort(data);

System.out.println("排序后的数组:");

print(data);

}

public static void mergeSort(int[] data) { sort(data, 0, data.length - 1);

}

public static void sort(int[] data, int left, int right) { if (left >= right) return;

// 找出中间索引

int center = (left + right) / 2;

// 对左边数组进行递归

sort(data, left, center);

// 对右边数组进行递归

sort(data, center + 1, right);

// 合 并

merge(data, left, center, right); print(data);

}

/**

* 将两个数组进行归并,归并前面 2 个数组已有序,归并后依然有序

*

* @param data

* 数组对象

* @param left

* 左数组的第一个元素的索引

* @param center

* 左数组的最后一个元素的索引,center+1 是右数组第一个元素的索引

* @param right

* 右数组最后一个元素的索引

*/

public static void merge(int[] data, int left, int center, int right) {

// 临时数组 int[] tmpArr = new int[data.length];

// 右数组第一个元素索引

int mid = center + 1;

// third 记录临时数组的索引

int third = left;

桶排序算法

桶排序的基本思想是: 把数组 arr 划分为 n 个大小相同子区间(桶),每个子区间各自排序,最后合并 。计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。

  1. 找出待排序数组中的最大值 max、最小值 min
  2. 我们使用 动态数组 ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(maxmin)/arr.length+1 3.遍历数组 arr,计算每个元素 arr[i] 放的桶
 
  

4.每个桶各自排序

基数排序算法

 
  

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就            变成一个有序序列。

剪枝算法

 
  

在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些“枝条”,故称剪枝。应用剪枝优化的核心问题是设计剪枝判           断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。

回溯算法

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

最短路径算法

从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。解决最短路的问题有以下算法,Dijkstra 算法,Bellman-Ford 算法,

Floyd 算法和 SPFA 算法等。

最大子数组算法

最长公共子序算法最小生成树算法

现在假设有一个很实际的问题:我们要在 n 个城市中建立一个通信网络,则连通这 n 个城市需要布置 n-1 一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?

于是我们就可以引入连通图来解决我们遇到的问题,n 个城市就是图上的 n 个顶点,然后,边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本, 所以现在我们有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个通信网,当我们构造这个连通网所花的成本最小时,搭建该连通网的生成树,就称为最小生成树。构造最小生成树有很多算法,但是他们都是利用了最小生成树的同一种性质:MST 性质(假设

N=(V,{E})是一个连通网,U 是顶点集 V 的一个非空子集,如果(u,v)是一条具有最小权值的边,其中 u 属于U,v 属于V-U,则必定存在一颗包含边(u,v)的最小生成树), 下面就介绍两种使用 MST 性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

 数据结构

栈(stack)

栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈顶(top)。它是后进先出(LIFO)的。对栈的基本操作只有   push(进栈)和    pop(出栈)两种,前者相当于插入,后者相当于删除最后的元素。

 
  

队列(queue)

 
  

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插          入操作的端称为队尾,进行删除操作的端称为队头。

链表(Link)

 
  

链表是一种数据结构,和数组同级。比如,Java 中我们使用的 ArrayList,其实现原理是数组。而LinkedList  的实现原理就是链表了。链表在进行循环遍历时效率不高,但是插入和删除时优势明显。

散列表(Hash Table)

散列表(Hash       table,也叫哈希表)是一种查找算法,与链表、树等算法不同的是,散列表算法在查找时不需要进行一系列和关键字(关键字是数据元素中某个数据项的值,用以标识一个数据元素)的比较操作。

散列表算法希望能尽量做到不经过任何比较,通过一次存取就能得到所查找的数据元素,因而必须要在数据元素的存储位置和它的关键字(可用key表示)之间建立一个确定的对           应关系,使每个关键字和散列表中一个唯一的存储位置相对应。因此在查找时,只要根据这个对应关系找到给定关键字在散列表中的位置即可。这种对应关系被称为散列函数(可用 h(key)表示)。

用的构造散列函数的方法有:

(1) 直接定址法: 取关键字或关键字的某个线性函数值为散列地址。即:h(key) = key 或 h(key) = a * key + b,其中 a 和 b 为常数。

(2) 数字分析法

(3) 平方取值法: 取关键字平方后的中间几位为散列地址。

(4) 折叠法:将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为散列地址。

(5) 除留余数法:取关键字被某个不大于散列表表长 m 的数 p 除后所得的余数为散列地址,即:h(key) = key MOD p p ≤ m

(6)随机数法:选择一个随机函数,取关键字的随机函数值为它的散列地址,即:h(key) = random(key)

排序二叉树

首先如果普通二叉树每个节点满足:左子树所有节点值小于它的根节点值,且右子树所有节点值大于它的根节点值,则这样的二叉树就是排序二叉树。

插入操作
 
  

首先要从根节点开始往下找到自己要插入的位置(即新节点的父节点);具体流程是:新节点与当前节点比较,如果相同则表示已经存在且不能再重复插入;如果小于当前节 点,则到左子树中寻找,如果左子树为空则当前节点为要找的父节点,新节点插入到当前节点的左子树即可;如果大于当前节点,则到右子树中寻找,如果右子树为空则当前节点为要找的父节点,新节点插入到当前节点的右子树即可。

删除操作

删除操作主要分为三种情况,即要删除的节点无子节点,要删除的节点只有一个子节点,要删除的节点有两个子节点。

  1. 对于要删除的节点无子节点可以直接删除,即让其父节点将该子节点置空即可。
  2. 对于要删除的节点只有一个子节点,则替换要删除的节点为其子节点。
 
  

对于要删除的节点有两个子节点,则首先找该节点的替换节点(即右子树中最小的节点),接着替换要删除的节点为替换节点,然后删除替换节点。

查询操作

查找操作的主要流程为:先和根节点比较,如果相同就返回,如果小于根节点则到左子树中递归查找,如果大于根节点则到右子树中递归查找。因此在排序二叉树中可以很容易获取最大(最右最深子节点)和最小(最左最深子节点)值。

红黑树

R-B Tree,全称是 Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性

(1) 每个节点或者是黑色,或者是红色。

(2) 根节点是黑色。

(3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

(4) 如果一个节点是红色的,则它的子节点必须是黑色的。

(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

左旋
 
  

对 x 进行左旋,意味着,将“x 的右孩子”设为“x 的父亲节点”;即,将 x 变成了一个左节点(x 成了为 z 的左孩子)。 因此,左旋中的“左”,意味着“被旋转的节点将变成一个左节点”。

 
  
右旋
 
  
 
  

对 x 进行右旋,意味着,将“x 的左孩子”设为“x 的父亲节点”;即,将 x 变成了一个右节点(x 成了为 y 的右孩子)! 因此,右旋中的“右”,意味着“被旋转的节点将变成一个右节点”。

添加

第一步: 将红黑树当作一颗二叉查找树,将节点插入。第二步:将插入的节点着色为"红色"。

根据被插入节点的父节点的情况,可以将"当节点 z 被着色为红色节点,并插入二叉树"划分为三种情况来处理。

① 情况说明:被插入的节点是根节点。处理方法:直接把此节点涂为黑色。

② 情况说明:被插入的节点的父节点是黑色。

处理方法:什么也不需要做。节点被插入后,仍然是红黑树。

 
  

③ 情况说明:被插入的节点的父节点是红色。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为 3 种情况(Case)。

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。

删除

第一步:将红黑树当作一颗二叉查找树,将节点删除。

这和"删除常规二叉查找树中删除节点的方法是一样的"。分 3 种情况:

① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就 OK 了。

② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。

③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给

“该节点的内容”;之后,删除“它的后继节点”。

第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。

因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。           选择重着色 3 种情况。

① 情况说明:x 是“红+黑”节点。

处理方法:直接把 x 设为黑色,结束。此时红黑树性质全部恢复。

② 情况说明:x 是“黑+黑”节点,且 x 是根。

处理方法:什么都不做,结束。此时红黑树性质全部恢复。

③ 情况说明:x 是“黑+黑”节点,且 x 不是根。

处理方法:这种情况又可以划分为 4 种子情况。这 4 种子情况如下表所示:

B-TREE

B-tree 又叫平衡多路查找树。一棵 m 阶的 B-tree (m 叉树)的特性如下(其中 ceil(x)是一个取上限的函数):

  1. 树中每个结点至多有 m 个孩子;
  2. 除根结点和叶子结点外,其它每个结点至少有有 ceil(m / 2)个孩子;
  3. 若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
  4. 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部结点或查询失败的结点,实际上这些结点不存在,指向这些结点的指针都为 null);
  5. 每个非终端结点中包含有 n 个关键字信息: (n,P0,K1,P1,K2,P2, ,Kn,Pn)。其中:

a) Ki (i=1. n)为关键字,且关键字按顺序排序 K(i-1)< Ki。

b) Pi 为指向子树根的接点,且指针 P(i-1)指向子树种所有结点的关键字均小于 Ki,但都大于 K(i-1)。

c) 关键字的个数 n 必须满足: ceil(m / 2)-1 <= n <= m-1。

 
  

一棵 m 阶的 B+tree 和 m 阶的 B-tree 的差异在于:

  1. 有 n 棵子树的结点中含有 n 个关键字; (B-tree 是 n 棵子树有 n-1 个关键字)
  2. 所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。     (B-tree     的叶子节点并没有包括全部需要查找的信息)
  3. 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。(B-tree 的非终节点也包含需要查找的有效信息)

位图

位图的原理就是用一个 bit 来标识一个数字是否存在,采用一个 bit 来存储一个数据,所以这样可以大大的节省空间。 bitmap 是很常用的数据结构,比如用于 Bloom Filter 中; 用于无重复整数的排序等等。bitmap 通常基于数组来实现,数组中每个元素可以看成是一系列二进制数,所有元素组成更大的二进制集合。

 加密算法

AES

高级加密标准(AES,Advanced Encryption Standard)为最常见的对称加密算法(微信小程序加密传输就是用这个加密算法的)。对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

 
  

RSA

 
  

RSA    加密算法是一种典型的非对称加密算法,它基于大数的因式分解数学难题,它也是应用最广泛的非对称加密算法。非对称加密是通过两个密钥(公钥-私钥)来实现对数据的加密和解密的。公钥用于加密,私钥用于解密。

CRC

循环冗余校验(Cyclic Redundancy Check, CRC)是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。

它是利用除法及余数的原理来作错误侦测的。

MD5

MD5 常常作为文件的签名出现,我们在下载文件的时候,常常会看到文件页面上附带一个扩展名为.MD5 的文本或者一行字符,这行字符就是就是把整个文件当作原数据通过MD5 计算后的值,我们下载文件后,可以用检查文件 MD5 信息的软件对下载到的文件在进行一次计算。两次结果对比就可以确保下载到文件的准确性。 另一种常见用途就是网站敏感信息加密,比如用户名密码,支付签名等等。随着 https 技术的普及,现在的网站广泛采用前台明文传输到后台,MD5 加密

(使用偏移量)的方式保护敏感数据保护站点和数据安全。

项目方面(晁老师-主要讲经验)

日志

Slf4j

slf4j 的全称是 Simple Loging Facade For Java,即它仅仅是一个为 Java 程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如 JDBC 一样,只是一种规则而已。所以单独的 slf4j 是不能工作的,必须搭配其他具体的日志实现方案,比如 apache 的 org.apache.log4j.Logger,jdk 自带的 java.util.logging.Logger 等。

Log4j

Log4j 是 Apache 的一个开源项目,通过使用 Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI 组件,甚至是套接口服务器、NT 的事件记录器、UNIX Syslog

守护进程等;我们也可以控

制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。Log4j由三个重要的组成构成:日志记录器(Loggers),输出端(Appenders)和日志格式化器(Layout)。1.Logger:控制要启用或禁用哪些日志记录语句,并对日志信息进行级别限制

  1. Appenders : 指定了日志将打印到控制台还是文件中
  2. Layout : 控制日志信息的显示格式

Log4j 中将要输出的 Log 信息定义了 5 种级别,依次为 DEBUG、INFO、WARN、ERROR 和 FATAL,当输出时,只有级别高过配置中规定的 级别的信息才能真正的输出,这样就很方便的来配置不同情况下要输出的内容,而不需要更改代码。

LogBack

简单地说,Logback 是一个 Java 领域的日志框架。它被认为是 Log4J 的继承人。

Logback 主要由三个模块组成:logback-core,logback-classic。logback-access

logback-core 是其它模块的基础设施,其它模块基于它构建,显然,logback-core 提供了一些关键的通用机制。

logback-classic 的地位和作用等同于 Log4J,它也被认为是 Log4J 的一个改进版,并且它实现了简单日志门面 SLF4J;

logback-access 主要作为一个与 Servlet 容器交互的模块,比如说 tomcat 或者 jetty,提供一些与HTTP 访问相关的功能。

Logback 优点

§ 同样的代码路径,Logback 执行更快

§ 更充分的测试

§ 原生实现了 SLF4J API(Log4J 还需要有一个中间转换层)

§ 内容更丰富的文档

§ 支持 XML 或者 Groovy 方式配置

§ 配置文件自动热加载

§ 从 IO 错误中优雅恢复

§ 自动删除日志归档

§ 自动压缩日志成为归档文件

§ 支持 Prudent 模式,使多个 JVM 进程能记录同一个日志文件

§ 支持配置文件中加入条件判断来适应不同的环境

§ 更强大的过滤器

§ 支持 SiftingAppender(可筛选 Appender)

§ 异常栈信息带有包信息

设计模式(马老师)

单例模式

 
  

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这 个实例。

使用场景:

要求生成唯一序列号的环境;

在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数 器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并 确保是线程安全的;

创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;

需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式 (当然,也可以直接声明为static的方式)。

工厂模式

定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂 方法使一个类的实例化延迟到其子类。

 
  

使用场景:jdbc连接数据库,硬件访问,降低对象的产生和销毁

抽象工厂模式

为创建一组相关或相互依赖的对象提供一个接口,而且无须指 定它们的具体类。

 
  

相对于工厂模式,我们可以新增产品类(只需要实现产品接口),只需要同时新 增一个工厂类,客户端就可以轻松调用新产品的代码。

使用场景:一个对象族(或是一组没有任何关系的对象)都有相同的约束。 涉及不同操作系统的时候,都可以考虑使用抽象工厂模式

建造者模式

 
  

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可 以创建不同的表示。

使用场景:

  1. 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模 式。
  2. 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同 时,则可以使用该模式。
  3. 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候 使用建造者模式非常合适。

 原型模式

 
  

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的 对象。

原型模式实际上就是实现Cloneable接口,重写clone()方法。使用原型模式的优点:

  1. 性能优良

原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是 要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。

  1. 逃避构造函数的约束

这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的(参见     13.4节)。使用场景:

资源优化场景 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。

性能和安全要求的场景 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模 式。

一个对象多个修改者的场景 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可 以考虑使用原型模式拷贝多个对象供调用者使用。浅拷贝和深拷贝:

浅拷贝:Object类提供的方法clone只是拷贝本对象,其对象内部的数组、引用    对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝,     其他的原始类型比如int、long、char、string(当做是原始类型)等都会被拷 贝。

注意: 使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:一 是类的成员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是 一个原始类型或不可变对象。

深拷贝:对私有的类变量进行独立的拷贝

如:this.arrayList = (ArrayList)this.arrayList.clone();

适配器模式

将一个类的接口变换成客户端所期待的另一种接口,从而使原本 因接口不匹配而无法在一起工作的两个类能够在一起工作。主要可分为3种:

  1. 类适配:创建新类,继承源类,并实现新接口,例如 class adapter extends oldClass implements newFunc{}
  2. 对象适配:创建新类持源类的实例,并实现新接口,例如 class adapter implements newFunc { private oldClass oldInstance ;}
  3. 接口适配:创建新的抽象类实现旧接口方法。例如 abstract class adapter implements oldClassFunc { void newFunc();}

使用场景:

你有动机修改一个已经投产中的接口时,适配器模式可能是适合你的模式。比 如系统扩展了,需要使用一个已有或新建立的类,但这个类又不符合系统的接 口,怎么办?使用适配器模式,这也是我们例子中提到的。

装饰器模式

 
  

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰 器模式相比生成子类更为灵活 。

使用场景:

  1. 需要扩展一个类的功能,或给一个类增加附加功能。
  2. 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
  3. 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。

代理模式

 
  

为其他对象提供一种代理以控制对这个对象的访问。

中介者模式

 
  

用一个中介对象封装一系列的对象交互,中介者使各对象不需要 显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

使用场景: 中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中 出现了蜘蛛网状结构,即每个类都与其他的类有直接的联系。

 命令模式

将一个请求封装成一个对象,从而让你使用不同的请求把客户端 参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

Receiver接受者角色:该角色就是干活的角色,命令传递到这里是应该被执行的Command命令角色:需要执行的所有命令都在这里声明

Invoker调用者角色:接收到命令,并执行命令

//通用Receiver类

public abstract class Receiver  { public abstract void doSomething();

}

//具体Receiver类

public class ConcreteReciver1 extends Receiver{

//每个接收者都必须处理一定的业务逻辑public void doSomething(){ }

}

public class ConcreteReciver2 extends Receiver{

//每个接收者都必须处理一定的业务逻辑public void doSomething(){ }

}

//抽象Command类 public abstract class Command { public abstract void execute();

}

//具体的Command类

public class ConcreteCommand1 extends Command {

//对哪个Receiver类进行命令处理private Receiver receiver;

//构造函数传递接收者

public ConcreteCommand1(Receiver _receiver){ this.receiver = _receiver;

}

//必须实现一个命令

public void execute() {

//业务处理

this.receiver.doSomething();

}

}

public class ConcreteCommand2 extends Command {

//哪个Receiver类进行命令处理private Receiver receiver;

//构造函数传递接收者

public ConcreteCommand2(Receiver _receiver){ this.receiver = _receiver;

}

//必须实现一个命令

public void execute() {

//业务处理this.receiver.doSomething();

}

}

//调用者Invoker类 public class Invoker { private Command command;

public void setCommand(Command _command){ this.command = _command;

}

public void action() { this.command.execute();

}

}

//场景类

public class Client {

public static void main(String[] args){ Invoker invoker = new Invoker();

Receiver receiver = new ConcreteReceiver1();

Command command = new ConcreteCommand1(receiver); invoker.setCommand(command); invoker.action();

}

}

使用场景:     认为是命令的地方就可以采用命令模式,例如,在GUI开发中,一个按钮的点击      是一个命令,可以采用命令模式;模拟DOS命令的时候,当然也要采用命令模式;触发-反馈机制的处理等。

 责任链模式

 
  

使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。

策略模式

定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。使用场景:

  1. 多个类只有在算法或行为上稍有不同的场景。
  2. 算法需要自由切换的场景。
  3. 需要屏蔽算法规则的场景。

迭代器模式

它提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。

迭代器模式已经被淘汰,java中已经把迭代器运用到各个聚集类(collection)中了,使用java自带的迭代器就已经满足我们的需求了。

组合模式

 
  

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

使用场景:

  1. 维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理。
  2. 从一个整体中能够独立出部分模块或功能的场景。

观察者模式

 
  

定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

使用场景:

  1. 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
  2. 事件多级触发场景。
  3. 跨系统的消息交换场景,如消息队列的处理机制

门面模式

 
  

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。

使用场景:

  1. 为一个复杂的模块或子系统提供一个供外界访问的接口
  2. 子系统相对独立——外界对子系统的访问只要黑箱操作即可
  3. 预防低水平人员带来的风险扩散

备忘录模式

 
  

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

使用场景:

  1. 需要保存和恢复数据的相关状态场景。
  2. 提供一个可回滚(rollback)的操作。
  3. 需要监控的副本场景中。
  4. 数据库连接的事务管理就是用的备忘录模式。

访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。使用场景:

  1. 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
  2. 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。

状态模式

当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。使用场景:

  1. 行为随状态改变而改变的场景这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式。
  2. 条件、分支判断语句的替代者

解释器模式

给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。使用场景:

  1. 重复发生的问题可以使用解释器模式
  2. 一个简单语法需要解释的场景

享元模式

 
  

使用共享对象的方法,用来尽可能减少内存使用量以及分享资讯。

使用场景:

  1. 系统中存在大量的相似对象。
  2. 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
  3. 需要缓冲池的场景。

桥梁模式

将抽象和实现解耦,使得两者可以独立地变化。

 
  

Circle类将DrwaApi与Shape类进行了桥接,

使用场景:

  1. 不希望或不适用使用继承的场景
  2. 接口或抽象类不稳定的场景
  3. 重用性要求较高的场景

 模板方法模式

定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。使用场景:

  1. 多个子类有公有的方法,并且逻辑基本相同时。
  2. 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
  3. 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为。

 一千万的用户实时排名如何实现;

海量积分数据实时排名处理

需求概述

积分排名在很多项目都会出现,大家都不会陌生,需求也很简单,积分排名主要满足以下需求:

  1. 查询用户名次。
  2. 查询TopN(即查询前N名的用户)
  3. 实时排名(很多项目是可选的)

当排序的数据量不大的时候,这个需求很容易满足,但是如果数据量很大的时候比如百万级、千万级甚至上亿的时候,或者有实时排名需求;这个时候要满足性能、低成本等需求,在设计上就变得复杂起来了。

常规积分排名处理

这里列举下日常对于排名的常规做法和缺陷。

  1. 数据库解决方案

这是最简单的做法,数据存储在数据库里面,然后利用数据库做排序处理。这里分两种情况:

1.1 单库/单表

参与排名的数据量小的时候的做法,所有数据存储在一张表上。查询操作示例:

查询用户名次:

SELECT count(*) as rank FROM 积分表 WHERE 积分 > (SELECT 积分 FROM 积分表 WHERE uid=’用户ID’)

查询前N名:

SELECT uid, 积分 FROM 积分表 ORDER BY 积分 DESC LIMIT 0,N

1.2 分库/分表

对于这种情况数据不在一块,在查询操作上跟上面单表情况的区别就是,分库/分表需要做,查询任务切割和查询结果合并处理。缺陷:

查询排名效率低,会造成扫描大量的记录,甚至全表扫描,性能低,在数据量大、高并发的情况下这种方案是不可用的。

  1. 采用常规排序算法

思路上就是把积分排序处理从数据库转移出来,自己实现排序和查询处理。实际排名业务的特点:

每次用户的积分更新都会在一个小的积分范围内波动。已有的积分数据都是已排序的。

常见的几种排序算法大家都熟知这里就不列举了。缺陷:

对于海量数据排序处理,简单的使用常规排序算法并不合适,要么就是排序造成大量的数据移动、要么就是对已排序的数据查询名次效率不高。

 高效的排名算法

前面的排名算法都是针对积分进行排序,然后通过统计积分高于自己的人数获得排名。

要想知道某个用户的名次,只需要知道比这个用户高分的人数,不一定需要对积分做排序。

在这里换个思路不对积分进行排序,仅仅是统计每个积分区间的人数,用积分区间的形式去统计相应的人数,下面是算法描述。

  1. 根据*积分范围*创建平衡二叉树。
 
  

设[0, N]为积分范围, 构造的平衡二叉树如下图。

每个节点包含两个数据字段(除了指针):

Range: 表示积分范围。

Counts: 表示当前积分区间包含多少人。

积分的区间的划分是根据平分的方式,把当前积分范围一分为二生成两个子节点,然后递归的重复该步骤,直到积分区间无法划分为止(即区间[x, y], x  ==  y) 例子:

假设积分范围为: [0, 5], 构造的平衡二叉树如下图: 节点内的数据表示当前积分区间的人数。

 
  

从上图可以看出来,所有积分都在叶子节点,叶子节点即最小粒度的积分区间。

2. 统计相应积分区间的人数。

这里主要有两种操作: 假设积分为i

添加积分

添加积分的过程就是查找积分i, 同时累加查找过程经过的节点计数。下面给出操作例子,注意观察操作路径。

例: 需要添加积分3, 结果如下图

 
  

接着在添加积分4,结果如下图

 
  

接着再添加积分4,结果如下图

 
  

接着添加积分2,结果如下图

删除积分

删除积分的过程也是查找积分i, 区别是查找过程经过的节点计数全部减1。

Ps:    只有积分是存在的情况下,才能做删除操作,另外用一组标记,标识积分是否存在,这里就不列举了。例子: 删除积分4, 结果如下图

 
  
3. 查询名次操作。

查询某个积分的排名的过程也是查找积分i的过程,下面是查找过程统计节点计数的算法: 对于查找路径上的任意节点,如果积分在左节点区间,则累加右节点区间的计数。

最终累加计数的结果加1即是积分的名次例子: 查找积分3的名次

 
  

蓝色节点是查找积分3经过的路径,红色节点是需要累加的计数值。

最终结果是:0 + 1 + 1, 积分3的名次是第2名

从上面的算法可以看出,对平衡二叉树的操作,算法复杂度是O(log N), N是最大积分。

在积分范围不变的情况下,算法复杂度是稳定的,跟用户量无关,因此可以实现海量用户积分排名、实时排名算法需要。

对于海量积分数据实时排名、这里给出的是核心算法,实际业务的时候还需要增加一些额外的处理,比如uid于积分的映射表用于记录用户历史积分、积分与uid的映射表用于

TopN这种查询前N名的需求、数据持久化、高可用等需求。

如何高效地获得玩家排名?

这种题目一般很难答,因为具体得看需求,但是暂且抛开需求,我想了一种做法,并没有实践过。

由于排名是动态变化的,因此玩家不需要记录当前是第几名,玩家身上只保存有积分即可。然后可以构建一个以积分为下标的int数组,这个数组多长就看你积分最多会有多少

了。(少说1000万也够用了吧?也就4G的内存,这个按具体的需求,如果每周清一次积分?)数组元素表示当前以此下标为积分数对应的玩家数,即:当前有多少个玩家拥有这          么多积分。我们把这个数组叫作rank,rank[100]的值就是当前积分为100的玩家的数量。

这个数组在服务器启动的时候根据玩家数重建一份置于内存中(或者共享内存,对于启动速度有要求的话),后续的操作都是增量式操作这个数组,具体的步骤是:当A与B打架            完后,取出两者老的排名,在对应的数组元素中减去1,再往新的排名下标元素加上1。这样,它的排序复杂度是O(1),但是取玩家排名的时候就蛋疼了,因为我要从头到当前的           位置扫一遍,把前面数量加一下才可以知道当前是第几名,针对这种情况我觉得可以优化一下,把数组元素扩展多一个变量(above_num),保存前面的排名数量,这样当A跟B打        架的时候,只需要把A跟B之间变化的above_num更新一下即可(一般来讲A跟B变化的区间总不会很大吧)。

如果你一定要立刻获得排名的话,你可以贡献一台服务器不断的做排序,一旦有分数update就立刻排序,这样延迟最多也就几秒钟。你战斗结束之后放个动画,就有准确排名          了。

 基于rediszset实现排行榜功能

临近中秋,公司需要开发一款微信小游戏,里面有一个排行榜的功能主要需求包括:

  1. 用户可以上传每次游戏的分数,系统返回该用户的最高分和最高分排名(分数相同时,时间优先);
  2. 用户可以查询排行榜,返回top50,和自己所在的排名

最开始是想使用数据库来实现,保存每个用户最高分的记录,主要字段【name, score, createTime】

针对需求1,用户有新的高分产生的话,就更新用户的最高分,否则返回当前的最高分,获取排名时,需要查询两次数据库(1:查询分数大于自己的记录          数;2:查询分数相同,时间小于自己的记录数)

针对需求2,按照分数倒序,时间正序查询top50,判断自己如果不是前50,则查询自己的记录,放到列表末尾      这样对于数据库的查询压力会比较大,而且只是一个临时活动,也没必要专门创建一张表来实现

然后和同事讨论可不可以参考HashMap的原理,使用数组+链表的形式的来实现,将分数作为数组下标,每个数组元素上是链表,按照达到该分数的先后        顺序保存用户

针对需求1: 用户上传新的分数时,在对应索引位置末尾增加用户,将原先的用户节点删除(需要另外维护用户原先的最大分数),从末尾循环每个节点的链表,获取排名和最高分

针对需求2:类似于需求1,从末尾循环每个节点的链表,获取top50

这样每次都需要循环列表,如果用户分数很低的话,循环会比较消耗性能,而且系统重启也会丢失数据。

最后都懂得,搜索引擎,看到redis 的zset原来是解决排行榜的标配,天生就是来做排行榜的,参考了一些网上的文章

redis的zset可以给每个object标记一个分数,然后可以针对这个分数,为object排名,基于hashtable和skiplist执行insert和remove操作,可以通过range方法获取top50,通过    rank方法获取排名,完美解决排行榜问题,直接上代码

获取top50的逻辑

 
  

提交新分数的逻辑

为了排错,最后用户的每次提交都会记录到mongo

 五万人并发抢票怎么实现;

Web大规模高并发请求和抢购的解决方案

电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多           请求时,系统的优化和稳定至关重要。这次我们会关注秒杀和抢购的技术实现和优化,同时,从技术层面揭开,为什么我们总是不容易抢到火车票的原因?

一、大规模并发带来的挑战

在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻而易举地陷入到异常          状态。我们现在一起来讨论下,优化的思路和方法哈。

1. 请求接口的合理设计

一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口。

通常静态HTML等内容,是通过CDN的部署,一般压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须能够支持高并发请求,同时,非常重要的一点,必须尽可          能“快”,在最短的时间里返回用户的请求结果。为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。仍然直接面向MySQL之类的存储是不合适的,如果         有这种复杂业务的需求,都建议采用异步写入。

当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也          不好,容易被用户认为是“暗箱操作”。

2. 高并发的挑战:一定要

我们通常衡量一个Web系统的吞吐率的指标是QPS(Query  Per   Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,         在这个时候平均响应时间会被大大增加。

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数    目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,    在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

然后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定           出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数           占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导          致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是

redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

二、作弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。不少用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可能多的请求到服务器。还有         一部分高级用户,制作强大的自动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,自己的请求数目占比越多,成功的概率越高。

这些都是属于“作弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。

1. 同一个账号,一次性发出多个请求

部分用户通过浏览器的插件或者其他工具,在秒杀开始的时间里,以自己的账号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这种请求在某些没有做数据安全处理的系统里,也可能造成另外一种破坏,导致某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。这是个非常简单的逻辑,但是,在高并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内网的多台Web服务器,它们           首先向存储发送查询请求,然后,在某个请求成功写入参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。

应对方案:

在程序入口处,一个账号只允许接受1个请求,其他请求过滤。不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,可以通过Redis这种          内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加。

或者,自己实现一个服务,将同一个账号的请求放入一个队列中,处理完一个,再处理下一个。

2. 多个账号,一次性发送多个请求

很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵            尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵          尸号”去混进去转发,这样就可以大大提升我们中奖的概率。

这种账号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。

应对方案:

这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求:

  1. 弹出验证码,最核心的追求,就是分辨出真实用户。因此,大家可能经常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让我们根本无法看清。他们这样做的原           因,其实也是为了让验证码的图片不被轻易识别,因为强大的“自动脚本”可以通过图片识别里面的字符,然后让脚本自动填写验证码。实际上,有一些非常创新的验证码,           效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。
  2. 直接禁止IP,实际上是有些粗暴的,因为有些真实用户的网络场景恰好是同一出口IP的,可能会有“误伤“。但是这一个做法简单高效,根据实际场景使用可以获得很好的效         果。
3. 多个账号,不同IP发送不同请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断         改变IP。

有同学会好奇,这些随机IP服务怎么来的。有一些是某些机构自己占据一批独立IP,然后做成一个随机代理IP的服务,有偿提供给这些“工作室”使用。还有一些更为黑暗一点的,           就是通过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运作,只做一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。通过这种做法,黑客就拿到了           大量的独立IP,然后搭建为随机IP服务,就是为了挣钱。

应对方案:

说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想做分辨很困难。再做进一步的限制很容易“误伤“真实用户,这个时候,通常只能通过设置业务门槛高来限制           这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们。

僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号。

4. 火车票的抢购

看到这里,同学们是否明白你为什么抢不到火车票?如果你只是老老实实地去抢票,真的很难。通过多账号的方式,火车票的黄牛将很多车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。

高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展示验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式, 验证码的保护限制作用被废除了,目前也没有很好的解决方案。

因为火车票是根据身份证实名制的,这里还有一个火车票的转让操作方式。大致的操作方式,是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛账号选择退票,然后黄牛买家成功通过自己的身份证购票成功。当一列车厢没有票了的时候,是没有很多人盯着看的,况且黄牛们的抢票工具也很强大,即使让我们看见有退票,我们也不一定能抢得过他们哈。

最终,黄牛顺利将火车票转移到买家的身份证下。

解决方案:

并没有很好的解决方案,唯一可以动心思的也许是对账号数据进行“数据挖掘”,这些黄牛账号也是有一些共同特征的,例如经常抢票和退票,节假日异常活跃等等。将它们分析出           来,再做进一步处理和甄别。

三、高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线            程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问         题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是

99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

2. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机          会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First  Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统          又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

4. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求        都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计           算开销。但是,综合来说,这是一个比较好的解决方案。

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

四、小结

互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得越来越多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然我们解决问题的具体技术方案可能千差万别,但是遇到的挑战却是相似的,因此解决问题的思路也异曲同工。

 集群服务器Session同步

事实上,网站总是有状态的。每一个登录信息、用户信息常常被存储在session内部。而当一个网站被部署在不止一台服务器的时候,就会遇到session同步的问题。事实上即使       一个很小的网站,也要至少有两台服务器互为备份,分单流量是必须得,更重要的是无缝切流量升级。为了保证服务的不间断又要进行网站的维护升级,切流量是最简单的。那么如何保证切流量的时候session也会跟着同步过去呢?在集群环境下,大致有以下几种手段:

一、Session复制

这是一种在早期应用系统中使用较多的服务器session管理方式。应用服务器开启Web容器的session的复制功能,在集群中的几台服务器之间同步session对象,这样一台服务   器宕机不会导致session数据丢失。即每一台服务器都持有集群中所有的session,每次访问仅从本机获取就可以了。其工作形式如下所示:

从session复制的几条线就可以看出,这种方式仅适用用小型集群。当服务集群规模很大时,集群服务器间的复制就需要大量的通讯,占用大量网络资源,甚至会出现内存不够          的情况。

二、Session绑定
 
  

Session绑定可以利用负载均衡的源地址Hash算法实现,负载均衡服务器总是将来自同一个IP地址的访问分发到同一台服务器上。这样整个会话期间,用户所有的请求都来自一       台服务器,保证了Session总是从这台服务器获取。其工作形式如下图所示:

但是这样的系统显然不符合我们对系统的需求。如果一台服务器宕机,那么其处理的所有请求Session会话全部丢失,用户因为切换服务器后没有Session而导致无法完成业务。

三、利用Cookie记录Session
 
  

这种管理方式将Session记录在客户端,每次请求服务器的时候,将Session放在请求中发送给服务器,服务器处理完成后再将修改后的Session响应给客户端。

利用Cookie记录当然也有缺点,比如Cookie大小限制,能记录的信息也有限,因为很多时候我们在Session中储存的也并非String类型的记录。每次请求都需要传输Cookie,影 响性能;另外如果用户关闭Cookie功能就不能用了。但是这种方式因此高可用性、支持服务器的线性伸缩,许多网站都在使用这种方式。我的学校网站也应用了这种技术。

四、Session服务器

如果有这样一个服务器,可用性高、伸缩性好、性能也不错,对信息大小又没有限制,那它就是Session服务器。利用独立部署的Session服务器统一管理Session,应用服务器    每次读写Session时,都访问Session服务器。其工作形式如下所示:

这种方式实际上是将应用服务器的状态分离,分为无状态的应用服务器和有状态的Session服务器,然后针对这两种服务器的不同特性分别设计其架构。       对于有状态的Session服务器,一种比较简单的方式是利用分布式缓存、数据库等。

 大数据量高并发的数据库优化

一、数据库结构的设计

如果不能设计一个合理的数据库模型,不仅会增加客户端和服务器段程序的编程和维护的难度,而且将会影响系统实际运行的性能。所以,在一个系统开始实施之前,完备的数据库模型的设计是必须的。

在一个系统分析、设计阶段,因为数据量较小,负荷较低。我们往往只注意到功能的实现,而很难注意到性能的薄弱之处,等到系统投入实际行一段时间后,才发现系统的性能在降低,这时再来考虑提高系统性能则要花费更多的人力物力,而整个系统也不可避免的形成了一个打补丁工程。

所以在考虑整个系统的流程的时候,我们必须要考虑,在高并发大数据量的访问情况下,我们的系统会不会出现极端的情况。(例如:对外统计系统在7月16日出现的数据异常的            情况,并发大数据量的的访问造成,数据库的响应时间不能跟上数据刷新的速度造成。具体情况是:在日期临界时(00:00:00),判断数据库中是否有当前日期的记录,没有           则插入一条当前日期的记录。在低并发访问的情况下,不会发生问题,但是当日期临界时的访问量相当大的时候,在做这一判断的时候,会出现多次条件成立,则数据库里会被插入多条当前日期的记录,从而造成数据错误。),数据库的模型确定下来之后,我们有必要做一个系统内数据流向图,分析可能出现的瓶颈。

为了保证数据库的一致性和完整性,在逻辑设计的时候往往会设计过多的表间关联,尽可能的降低数据的冗余。(例如用户表的地区,我们可以把地区另外存放到一个地区表 中)如果数据冗余低,数据的完整性容易得到保证,提高了数据吞吐速度,保证了数据的完整性,清楚地表达数据元素之间的关系。而对于多表之间的关联查询(尤其是大数据表)时,其性能将会降低,同时也提高了客户端程序的编程难度,因此,物理设计需折衷考虑,根据业务规则,确定对关联表的数据量大小、数据项的访问频度,对此类数据表频繁的关联查询应适当提高数据冗余设计但增加了表间连接查询的操作,也使得程序的变得复杂,为了提高系统的响应时间,合理的数据冗余也是必要的。设计人员在设计阶段应根据系统操作的类型、频度加以均衡考虑。

另外,最好不要用自增属性字段作为主键与子表关联。不便于系统的迁移和数据恢复。对外统计系统映射关系丢失(******)。

原来的表格必须可以通过由它分离出去的表格重新构建。使用这个规定的好处是,你可以确保不会在分离的表格中引入多余的列,所有你创建的表格结构都与它们的实际需要一样大。应用这条规定是一个好习惯,不过除非你要处理一个非常大型的数据,否则你将不需要用到它。(例如一个通行证系统,我可以将USERID,USERNAME, USERPASSWORD,单独出来作个表,再把USERID作为其他表的外键)

表的设计具体注意的问题:

1、数据行的长度不要超过8020字节,如果超过这个长度的话在物理页中这条数据会占用两行从而造成存储碎片,低查询效率。

2、能够用数字类型的字段尽量选择数字类型而不用字符串类型的(电话号码),这会降低查询和连接的性能,并增加存储开销。这是因为引擎在处理查询和连接回逐个比较字符            串中每一个字符,而对于数字型而言只需要比较一次就够了。

3、对于不可变字符类型char和可变字符类型varchar  都是8000字节,char查询快,但是耗存储空间,varchar查询相对慢一些但是节省存储空间。在设计字段的时候可以灵活选择,例如用户名、密码等长度变化不大的字段可以选择CHAR,对于评论等长度变化大的字段可以选择VARCHAR。

4、字段的长度在最大限度的满足可能的需要的前提下,应该尽可能的设得短一些,这样可以提高查询的效率,而且在建立索引的时候也可以减少资源的消耗。          二、查询的优化

保证在实现功能的基础上,尽量减少对数据库的访问次数;通过搜索参数,尽量减少对表的 访问行数,最小化结果集,从而减轻网络负担;能够分开的操作尽量分开处理,提高每次的 响应速度;在数据窗口使用 SQL 时,尽量把使用的索引放在选择的首列;算法的结构尽量 简单;在查询时,不要过多地使用通配符如 SELECT * FROM T1 语句,要用到几 列就选择 几列如:SELECT COL1,COL2 FROM T1;在可能的情况下尽量限制尽量结果集行数如: SELECT TOP 300 COL1,COL2,COL3 FROM T1,因为某些情况下用户是不需要那么多的数据 的。

在没有建索引的情况下,数据库查找某一条数据,就必须进行全表扫描了,对所有数据进行 一次遍历,查找出符合条件的记录。在数据量比较小的情况下,也许看不出明显的差别,但 是当数据量大的情况下,这种情况就是极为糟糕的了。

SQL 语句在 SQL SERVER 中是如何执行的,他们担心自己所写的 SQL 语句会被 SQL SERVER 误解。比如:

select * from table1 where name='zhangsan' and tID > 10000

和执行:

select * from table1 where tID > 10000 and name='zhangsan'

一些人不知道以上两条语句的执行效率是否一样,因为如果简单的从语句先后上看,这两个 语句的确是不一样,如果 tID 是一个聚合索引,那么后一句仅仅从表的 10000 条以后的记录 中查找就行了;而前一句则要先从全表中查找看有几个 name='zhangsan'的,而后再根据限 制条件条件 tID>10000 来提出查询结果。

事实上,这样的担心是不必要的。SQL SERVER 中有一个“查询分析优化器”,它可以计算 出 where 子句中的搜索条件并确定哪个索引能缩小表扫描的搜索空间,也就是说,它能实现 自动优化。虽然查询优化器可以根据 where 子句自动的进行查询优化,但有时查询优化器就 会不按照您的本意进行快速查询。

在查询分析阶段,查询优化器查看查询的每个阶段并决定限制需要扫描的数据量是否有用。          如果一个阶段可以被用作一个扫描参数(SARG),那么就称之为可优化的,并且可以利用 索引快速获得所需数据。

SARG 的定义:用于限制搜索的一个操作,因为它通常是指一个特定的匹配,一个值的范围  内的匹配或者两个以上条件的  AND  连接。形式如下: 列名 操作符 <常数 或 变量> 或 <常数 或 变量> 操作符 列名

列名可以出现在操作符的一边,而常数或变量出现在操作符的另一边。如:

Name=’张三’ 价格>5000 5000<价格

Name=’张三’ and 价格>5000

 
  

如果一个表达式不能满足 SARG 的形式,那它就无法限制搜索的范围了,也就是 SQL SERVER 必须对每一行都判断它是否满足 WHERE 子句中的所有条件。所以一个索引对于 不满足 SARG 形式的表达式来说是无用的。

具体要注意的:

  1. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进 行全表扫描,如:

select id from t where num is null

可以在 num上设置默认值 0,确保表中 num列没有 null 值,然后这样查询:

select id from t where num=0

  1. 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫 描。优化器将无法通过索引来确定将要命中的行数,因此需要搜索该表的所有行。
  2. 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全 表扫描,如:

select id from t where num=10 or num=20 可以这样查询:

select id from t where num=10 union all

select id from t where num=20

  1. in 和 not in 也要慎用,因为 IN 会使系统无法使用索引,而只能直接搜索表中的数据。如:

select id from t where num in(1,2,3)

对于连续的数值,能用 between 就不要用 in 了:

select id from t where num between 1 and 3

  1. 尽量避免在索引过的字符数据中,使用非打头字母搜索。这也使得引擎无法利用索引。见如下例子:

SELECT * FROM T1 WHERE NAME LIKE ‘%L%’ SELECT * FROM T1 WHERE SUBSTING(NAME,2,1)=’L’ SELECT * FROM T1 WHERE NAME LIKE ‘L%’

即使 NAME 字段建有索引,前两个查询依然无法利用索引完成加快操作,引擎不得不对全 表所有数据逐条操作来完成任务。而第三个查询能够使用索引来加快操作。

  1. 必要时强制查询优化器使用某个索引,如在 where 子句中使用参数,也会导致全表扫描。

因为 SQL 只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行 时;

它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的, 因而无法作为索引选择的输入项。如下面语句将进行全表扫描:

select id from t where num=@num

可以改为强制查询使用索引:

select id from t with(index(索引名)) where num=@num

  1. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行 全表扫描。如: SELECT * FROM T1 WHERE F1/2=100 应改为: SELECT * FROM T1 WHERE F1=100*2

SELECT * FROM RECORD WHERE SUBSTRING(CARD_NO,1,4)=’5378’

应改为: SELECT * FROM RECORD WHERE CARD_NO LIKE ‘5378%’

SELECT member_number, first_name, last_name FROM members WHERE DATEDIFF(yy,datofbirth,GETDATE()) > 21

应改为:

SELECT member_number, first_name, last_name FROM members WHERE dateofbirth < DATEADD(yy,-21,GETDATE())

即:任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可 能将操作移至等号右边。

  1. 应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表 扫描。如:

select id from t where substring(name,1,3)='abc'--name 以 abc 开头的 id

select id from t where datediff(day,createdate,'2005-11-30')=0--‘2005-11-30’

生成的 id 应改为:

select id from t where name like 'abc%'

select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'

  1. 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可 能无法正确使用索引
  2. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一 个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让 字段顺序与索引顺序相一致
    1. 很多时候用 exists 是一个好的选择:

elect num from a where num in(select num from b)

用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num)

SELECT SUM(T1.C1)FROM T1 WHERE(

(SELECT COUNT(*)FROM T2 WHERE T2.C2=T1.C2>0) SELECT SUM(T1.C1) FROM T1WHERE EXISTS( SELECT * FROM T2 WHERE T2.C2=T1.C2)

两者产生相同的结果,但是后者的效率显然要高于前者。因为后者不会产生大量锁定的表扫 描或是索引扫描。

如果你想校验表里是否存在某条纪录,不要用 count()那样效率很低,而且浪费服务器资源。 可以用 EXISTS 代替。如:

IF (SELECT COUNT() FROM table_name WHERE column_name = 'xxx')

可以写成:

IF EXISTS (SELECT * FROM table_name WHERE column_name = 'xxx')

经常需要写一个 T_SQL 语句比较一个父结果集和子结果集,从而找到是否存在在父结果集 中有而在子结果集中没有的记录,如:

SELECT a.hdr_key FROM hdr_tbl a tbl a 表示 tbl 用别名 a 代替

WHERE NOT EXISTS (SELECT * FROM dtl_tbl b WHERE a.hdr_key = b.hdr_key) SELECT a.hdr_key FROM hdr_tbl a LEFT JOIN dtl_tbl b ON a.hdr_key = b.hdr_key

WHERE b.hdr_key IS NULL SELECT hdr_key FROM hdr_tbl WHERE hdr_key NOT IN (SELECT hdr_key FROM dtl_tbl)

三种写法都可以得到同样正确的结果,但是效率依次降低。

  1. 尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有 主键索引)。
  2. 避免频繁创建和删除临时表,以减少系统表资源的消耗。
  3. 临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引 用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
  4. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先 create table,然后 insert。
    1. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
    2. 在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。 无 需 在 执 行 存 储 过 程 和 触 发 器 的 每 个 语 句 后 向 客 户 端 发 送

DONE_IN_PROC 消息。

  1. 尽量避免大事务操作,提高系统并发能力。
  2. 尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
    1. 避免使用不兼容的数据类型。例如 float 和 int、char 和 varchar、binary和 varbinary是不 兼容的。数据类型的不兼容可能使优化器无法执行一些本来可以进行的优化操作。例如: SELECT name FROM employee WHERE salary > 60000

在这条语句中,如salary字段是money型的,则优化器很难对其进行优化,因为60000是个整型 数。我们应当在编程时将整型转化成为钱币型,而不要等到运行时转化。

  1. 充分利用连接条件,在某种情况下,两个表之间可能不只一个的连接条件,这时在    WHERE    子句中将连接条件完整的写上,有可能大大提高查询速度。例:

SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO

SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO AND A.ACCOUNT_NO=B.ACCOUNT_NO

第二句将比第一句执行快得多。

22、使用视图加速查询

把表的一个子集进行排序并创建视图,有时能加速查询。它有助于避免多重排序 操作,而 且在其他方面还能简化优化器的工作。例如:

SELECT cust.name,rcvbles.balance,……other columns FROM cust,rcvbles

WHERE cust.customer_id = rcvlbes.customer_id AND rcvblls.balance>0

AND cust.postcode>“98000”

ORDER BY cust.name

如果这个查询要被执行多次而不止一次,可以把所有未付款的客户找出来放在一个视图中, 并按客户的名字进行排序:

CREATE VIEW DBO.V_CUST_RCVLBES AS

SELECT cust.name,rcvbles.balance,……other columns FROM cust,rcvbles

WHERE cust.customer_id = rcvlbes.customer_id AND rcvblls.balance>0

ORDER BY cust.name

然后以下面的方式在视图中查询:

SELECT * FROM V_CUST_RCVLBES

WHERE postcode>“98000”

视图中的行要比主表中的行少,而且物理顺序就是所要求的顺序,减少了磁盘 I/O,所以查 询工作量可以得到大幅减少。

23、能用DISTINCT的就不用 GROUP BY

SELECT OrderID FROM Details WHERE UnitPrice > 10 GROUP BY OrderID

可改为:

SELECT DISTINCT OrderID FROM Details WHERE UnitPrice > 10

24.能用 UNION ALL就不要用 UNION

UNION ALL不执行 SELECT DISTINCT函数,这样就会减少很多不必要的资源

35.尽量不要用 SELECT INTO 语句。

SELECT INOT 语句会导致表锁定,阻止其他用户访问该表。

上面我们提到的是一些基本的提高查询速度的注意事项,但是在更多的情况下,往往需要 反复试验比较不同的语句以得到最佳方案。最好的方法当然是测试,看实现相同功能的SQL 语句哪个执行时间最少,但是数据库中如果数据量很少,是比较不出来的,这时可以用查看 执行计划,即:把实现相同功能的多条  SQL语句考到查询分析器,按  CTRL+L  看查所利用 的索引,表扫描次数(这两个对性能影响最大),总体上看询成本百分比即可

三、算法的优化

尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过 1 万行,那么就应该考 虑改写。.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,    基于集的方法通常更有效。与临时表一样,游标并不是不可使用。对小型数据集使AST_FORWARD   游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发 时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更

好。

游标提供了对特定集合中逐行扫描的手段,一般使用游标逐行遍历数据,根据取出的数 据不同条件进行不同的操作。尤其对多表和大表定义的游标(大的数据集合)循环很容易使 程序进入一个漫长的等特甚至死机。

在有些场合,有时也非得使用游标,此时也可考虑将符合条件的数据行转入临时表中, 再对临时表定义游标进行操作,可时性能得到明显提高。 (例如:对内统计第一版) 封装存储过程

四、建立高效的索引

创建索引一般有以下两个目的:维护被索引列的唯一性和提供快速访问表中数据的策略。 大型数据库有两种索引即簇索引和非簇索引,一个没有簇索引的表是按堆结构存储数据,所 有的数据均添加在表的尾部,而建立了簇索引的表,其数据在物理上会按照簇索引键的顺序 存储,一个表只允许有一个簇索引,因此,根据 B 树结构,可以理解添加任何一种索引均    能提高按索引列查询的速度,但会降低插入、更新、删除操作的性能,尤其是当填充因子(Fill    Factor)较大时。所以对索引较多的表进行频繁的插入、更新、删除操作,建表和索引时因 设置较小的填充因子,以便在各数据页中留下较多的自由空间,减少页分割及重新组织的工 作。

索引是从数据库中获取数据的最高效方式之一。95% 的数据库性能问题都可以采用索引技 术得到解决。作为一条规则,我通常对逻辑主键使用唯一的成组索引,对系统键(作为存储

过程)采用唯一的非成组索引,对任何外键列[字段]采用非成组索引。不过,索引就象是盐, 太多了菜就咸了。你得考虑数据库的空间有多大,表如何进行访问,还有这些访问是否主要 用作读写。

实际上,您可以把索引理解为一种特殊的目录。微软的 SQL SERVER 提供了两种索引:聚 集索引(clustered index,也称聚类索引、簇集索引)和非聚集索引(nonclustered index,也 称非聚类索引、非簇集索引)。下面,我们举例来说明一下聚集索引和非聚集索引的区别: 其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自 然地翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文 字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果

您翻完了所   有以“a”开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字;同样的,如    果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是 说,字典的正文部分本身就是一个目录,您不需要再去查其他目录来找到您需要找的内容。 我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。

如果您认识某个字,您可以快速地从自动中查到这个字。但您也可能会遇到您不认识的字, 不知道它的发音,这时候,您就不能按照刚才的方法找到您要查的字,而需要去根据“偏旁     部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结     合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张” 字,我们可以看到在查部首之后的检字表中“张”的页码是 672页,检字表中“张”的上面 是“驰”字,但页码却是 63 页,“张”的下面是“弩”字,页面是 390 页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰、张、弩”三字实际上          就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所 需要的页码。

我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。 进一步引申一下,我们可以很容易的理解:每个表只能有一个聚集索引,因为目录只能按照 一种方法进行排序。

(一)何时使用聚集索引或非聚集索引

下面的表总结了何时使用聚集索引或非聚集索引(很重要)。动作描述 使用聚集索引 使用非聚集索引

列经常被分组排序 应 应

返回某范围内的数据 应 不应一个或极少不同值 不应 不应小数目的不同值 应 不应

大数目的不同值 不应 应频繁更新的列 不应 应外键列 应 应

主键列 应 应

频繁修改索引列 不应 应

事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。如:返回某范 围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列, 这时您 查询 2004 年 1 月 1 日至2004 年 10 月 1 日之间的全部数据时,这个速度就将是很快的,因

为您的这本字典正文是按日期进行排序的,聚类索引只需要找到要检索的所有数据中的开头 和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码,然后 再根据页码查到具体内容。

(二)结合实际,谈索引使用的误区

理论的目的是应用。虽然我们刚才列出了何时应使用聚集索引或非聚集索引,但在实践中以 上规则却很容易被忽视或不能根据实际情况进行综合分析。下面我们将根据在实践中遇到的 实际问题来谈一下索引使用的误区,以便于大家掌握索引建立的方法。

1、主键就是聚集索引

这种想法笔者认为是极端错误的,是对聚集索引的一种浪费。虽然 SQL SERVER 默认是在 主键上建立聚集索引的。 通常,我们会在每个表中都建立一个 ID 列,以区分每条数据,并且这个 ID 列是自动增大 的,步长一般为 1。我们的这个办公自动化的实例中的列 Gid就是如此。此时,如果我们将 这个列设为主键,SQL SERVER 会将此列默认为聚集索引。这样做有好处,就是可以让您 的数据在数据库中按照 ID 进行物理排序,但笔者认为这样做意义不大。

显而易见,聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这使得聚 集索引变得更加珍贵。

从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够根据查 询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为 ID 号是自动生成的,我 们并不知道每条记录的 ID 号,所以我们很难在实践中用 ID 号来进行查询。这就使让 ID 号 这个主键作为聚集索引成为一种资源浪费。其次,让每个 ID 号都不同的字段作为聚集索引 也不符合“大数目的不同值情况下不应建立聚合索引”规则;当然,这种情况只是针对用户 经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。

在办公自动化系统中,无论是系统首页显示的需要用户签收的文件、会议还是用户进行文件 查询等任何情况下进行数据查询都离不开字段的是“日期”还有用户本身的“用户名”。通常,办公自动化的首页会显示每个用户尚未签收的文件或会议。虽然我们的 where 语句可 以仅仅限制当前用户尚未签收的情况,但如果您的系统已建立了很长时间,并且数据量很大, 那么,每次每个用户打开首页的时候都进行一次全表扫描,这样做意义是不大的,绝大多数 的用户 1 个月前的文件都已经浏览过了,这样做只能徒增数据库的开销而已。事实上,我们 完全可以让用户打开系统首页时,数据库仅仅查询这个用户近 3个月来未阅览的文件,通过 “日期”这个字段来限制表扫描,提高查询速度。如果您的办公自动化系统已经建立的 2 年,那么您的首页显示速度理论上将是原来速度8 倍,甚至更快。

2、只要建立索引就能显著提高查询速度

事实上,我们可以发现上面的例子中,第2、3 条语句完全相同,且建立索引的字段也相同; 不同的仅是前者在 fariqi 字段上建立的是非聚合索引,后者在此字段上建立的是聚合索引, 但查询速度却有着天壤之别。所以,并非是在任何字段上简单地建立索引就能提高查询速度。

从建表的语句中,我们可以看到这个有着1000万数据的表中fariqi字段有5003个不同记录。        在此字段上建立聚合索引是再合适不过了。在现实中,我们每天都会发几个文件,这几个文 件的发文日期就相同,这完全符合建立聚集索引要求的:“既不能绝大多数都相同,又不能 只有极少数相同”的规则。由此看来,我们建立“适当”的聚合索引对于我们提高查询速度 是非常重要的。

3、把所有需要提高查询速度的字段都加进聚集索引,以提高查询速度

上面已经谈到:在进行数据查询时都离不开字段的是“日期”还有用户本身的“用户名”。 既然这两个字段都是如此的重要,我们可以把他们合并起来,建立一个复合索引

(compound index)。

很多人认为只要把任何字段加进聚集索引,就能提高查询速度,也有人感到迷惑:如果把复 合的聚集索引字段分开查询,那么查询速度会减慢吗?带着这个问题,我们来看一下以下的 查询速度(结果集都是 25 万条数据):(日期列 fariqi 首先排在复合聚集索引的起始列,用 户名 neibuyonghu排在后列)

我们可以看到如果仅用聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列 的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(在查询结果集数目一样 的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的话,这个索引是不起任何 作用的。当然,语句 1、2 的查询速度一样是因为查询的条目数一样,如果复合索引的所有 列都用上,而且查询结果少的话,这样就会形成“索引覆盖”,因而性能可以达到最优。同 时,请记住:无论您是否经常使用聚合索引的其他列,但其前导列一定要是使用最频繁的列。

(三)其他注意事项 “水可载舟,亦可覆舟”,索引也一样。索引有助于提高检索性能,但过多或不当的索引也 会导致系统低效。因为用户在表中每加进一个索引,数据库就要做更多的工作。过多的索引 甚至会导致索引碎片。 所以说,我们要建立一个“适当”的索引体系,特别是对聚合索引的创建,更应精益求精, 以使您的数据库能得到高性能的发挥

 如何提高服务器并发处理能力

说明

以下内容为入门级介绍,意在对老技术作较全的总结而不是较深的研究。主要参考《构建高性能Web站点》一书。

什么是服务器并发处理能力

一台服务器在单位时间里能处理的请求越多,服务器的能力越高,也就是服务器并发处理能力越强

有什么方法衡量服务器并发处理能力

1. 吞吐率

吞吐率,单位时间里服务器处理的最大请求数,单位req/s

从服务器角度,实际并发用户数的可以理解为服务器当前维护的代表不同用户的文件描述符总数,也就是并发连接数。服务器一般会限制同时服务的最多用户数,比如apache的

MaxClents参数。

这里再深入一下,对于服务器来说,服务器希望支持高吞吐率,对于用户来说,用户只希望等待最少的时间,显然,双方不能满足,所以双方利益的平衡点,就是我们希望的最大并发用户数。

2. 压力测试

有一个原理一定要先搞清楚,假如100个用户同时向服务器分别进行10个请求,与1个用户向服务器连续进行1000次请求,对服务器的压力是一样吗?实际上是不一样的,因对每         一个用户,连续发送请求实际上是指发送一个请求并接收到响应数据后再发送下一个请求。这样对于1个用户向服务器连续进行1000次请求,         任何时刻服务器的网卡接收缓冲区中只有1个请求,而对于100个用户同时向服务器分别进行10个请求,服务器的网卡接收缓冲区最多有100个等待处理的请求,显然这时的服务器压力更大。

压力测试前提考虑的条件

并发用户数: 指在某一时刻同时向服务器发送请求的用户总数(HttpWatch) 总请求数

请求资源描述

请求等待时间(用户等待时间) 用户平均请求的等待时间

服务器平均请求处理的时间硬件环境

压力测试中关心的时间又细分以下2种:

  1. 用户平均请求等待时间(这里暂不把数据在网络的传输时间,还有用户PC本地的计算时间计算入内)
  2. 服务器平均请求处理时间

用户平均请求等待时间主要用于衡量服务器在一定并发用户数下,单个用户的服务质量;而服务器平均请求处理时间就是吞吐率的倒数,一般来说,用户平均请求等待时间 = 服务器平均请求处理时间 * 并发用户数

怎么提高服务器的并发处理能力

1. 提高CPU并发计算能力

服务器之所以可以同时处理多个请求,在于操作系统通过多执行流体系设计使得多个任务可以轮流使用系统资源,这些资源包括CPU,内存以及I/O.   这里的I/O主要指磁盘I/O,    和网络I/O。

多进程 & 多线程

多执行流的一般实现便是进程,多进程的好处可以对CPU时间的轮流使用,对CPU计算和IO操作重叠利用。这里的IO主要是指磁盘IO和网络IO,相对CPU而言,它们慢的可怜。

而实际上,大多数进程的时间主要消耗在I/O操作上。现代计算机的DMA技术可以让CPU不参与I/O操作的全过程,比如进程通过系统调用,使得CPU向网卡或者磁盘等I/O设备发        出指令,然后进程被挂起,释放出CPU资源,等待I/O设备完成工作后通过中断来通知进程重新就绪。对于单任务而言,CPU大部分时间空闲,这时候多进程的作用尤为重要。

多进程不仅能够提高CPU的并发度。其优越性还体现在独立的内存地址空间和生命周期所带来的稳定性和健壮性,其中一个进程崩溃不会影响到另一个进程。         但是进程也有如下缺点:

  1. fork()系统调用开销很大: prefork
  2. 进程间调度和上下文切换成本: 减少进程数量
  3. 庞大的内存重复:共享内存
  4. IPC编程相对比较麻烦
减少进程切换

当硬件上下文频繁装入和移出时,所消耗的时间是非常可观的。可用Nmon工具监视服务器每秒的上下文切换次数。          为了尽量减少上下文切换次数,最简单的做法就是减少进程数,尽量使用线程并配合其它I/O模型来设计并发策略。

还可以考虑使用进程绑定CPU技术,增加CPU缓存的命中率。若进程不断在各CPU上切换,这样旧的CPU缓存就会失效。

减少使用不必要的锁

服务器处理大量并发请求时,多个请求处理任务时存在一些资源抢占竞争,这时一般采用“锁”机制来控制资源的占用,当一个任务占用资源时,我们锁住资源,这时其它任务都在           等待锁的释放,这个现象称为锁竞争。

通过锁竞争的本质,我们要意识到尽量减少并发请求对于共享资源的竞争。比如在允许情况下关闭服务器访问日志,这可以大大减少在锁等待时的延迟时间。要最大程度减少无辜的等待时间。

这里说下无锁编程,就是由内核完成这个锁机制,主要是使用原子操作替代锁来实现对共享资源的访问保护          ,使用原子操作时,在进行实际的写操作时,使用了lock指令,这样就可以阻止其他任务写这块内存,避免出现数据竞争现象。原子操作速度比锁快,一般要快一倍以上。

例如fwrite(), fopen(),其是使用append方式写文件,其原理就是使用了无锁编程,无锁编程的复杂度高,但是效率快,而且发生死锁概率低。

考虑进程优先级

进程调度器会动态调整运行队列中进程的优先级,通过top观察进程的PR值

考虑系统负载

可在任何时刻查看/proc/loadavg, top中的load average也可看出

考虑CPU使用率

除了用户空间和内核空间的CPU使用率以外,还要关注I/O wait,它是指CPU空闲并且等待I/O操作完成的时间比例(top中查看wa的值)。

2. 考虑减少内存分配和释放

服务器的工作过程中,需要大量的内存,使得内存的分配和释放工作尤为重要。

可以通过改善数据结构和算法复制度来适当减少中间临时变量的内存分配及数据复制时间,而服务器本身也使用了各自的策略来提高效率。

例如Apache,在运行开始时一次申请大片的内存作为内存池,若随后需要时就在内存池中直接获取,不需要再次分配,避免了频繁的内存分配和释放引起的内存整理时间。

再如Nginx使用多线程来处理请求,使得多个线程之间可以共享内存资源,从而令它的内存总体使用量大大减少,另外,nginx分阶段的内存分配策略,按需分配,及时释放,使         得内存使用量保持在很小的数量范围。

另外,还可以考虑共享内存。

共享内存指在多处理器的计算机系统中,可以被不同中央处理器(CPU)访问的大容量内存,也可以由不同进程共享,是非常快的进程通信方式。        但是使用共享内存也有不好的地方,就是对于多机器时数据不好统一。

shell命令ipcs可用来显示系统下共享内存的状态,函数shmget可以创建或打开一块共享内存区,函数shmat将一个存在的共享内存段连接到本进程空间,  函数shmctl可以对共享内存段进行多种操作,函数shmdt函数分离该共享内存。

3. 考虑使用持久连接

持久连接也为长连接,它本身是TCP通信的一种普通方式,即在一次TCP连接中持续发送多分数据而不断开连接,与它相反的方式称为短连接,也就是建立连接后发送一份数据就         断开,然后再次建立连接发送下一份数据, 周而复始。是否采用持久连接,完全取决于应用特点。从性能角度看,建立TCP连接的操作本身是一项不小的开销,在允许的情况

下,连接次数越少,越有利于性能的提升; 尤其对于密集型的图片或网页等小数据请求处理有明显的加速所用。

HTTP长连接需要浏览器和web服务器的共同协作,目前浏览器普遍支持长连接,表现在其发出的HTTP请求数据头中包含关于长连接的声明,如下: Connection: Keep-Alive

主流的web服务器都支持长连接,比如apache中,可以用KeepAlive off关闭长连接。

对于长连接的有效使用,还有关键一点在于长连接超时时间的设置,即长连接在什么时候关闭吗?    Apache的默认设置为5s,    若这个时间设置过长,则可能导致资源无效占有,维持大量空闲进程,影响服务器性能。

4. 改进I/O 模型

I/O操作根据设备的不同分为很多类型,比如内存I/O,  网络I/O,  磁盘I/O.  对于网络I/O和磁盘I/O,  它们的速度要慢很多,尽管使用RAID磁盘阵列可通过并行磁盘磁盘来加快磁盘I/O 速度,购买大连独享网络带宽以及使用高带宽网络适配器可以提高网络i/O的速度。但这些I/O操作需要内核系统调用来完成,这些需要CPU来调度,这使得CPU不得不浪费宝贵的         时间来等待慢速I/O操作。我们希望让CPU足够少的时间在i/O操作的调度上,如何让高速的CPU和慢速的I/O设备更好地协调工作,是现代计算机一直探讨的话题。各种I/O模型的         本质区别在于CPU的参与方式。

1. DMA技术

I/O设备和内存之间的数据传输方式由DMA控制器完成。在DMA模式下,CPU只需向DMA下达命令,让DMA控制器来处理数据的传送,这样可以大大节省系统资源。

2. 异步I/O

异步I/O指主动请求数据后便可以继续处理其它任务,随后等待I/O操作的通知,这样进程在数据读写时不发生阻塞。           异步I/O是非阻塞的,当函数返回时,真正的I/O传输已经完成,这让CPU处理和I/O操作达到很好的重叠。

3. I/O多路复用

epoll服务器同时处理大量的文件描述符是必不可少的,若采用同步非阻塞I/O模型,若同时接收TCP连接的数据,就必须轮流对每个socket调用接收数据的方法,不管这些socket   有没有可接收的数据,都要询问一次。假如大部分socket并没有数据可以接收,那么进程便会浪费很多CPU时间用于检查这些socket有没有可以接收的数据。多路I/O就绪通知的      出现,提供了对大量文件描述符就绪检查的高性能方案,它允许进程通过一种方法同时监视所有文件描述符,并可以快速获得所有就绪的文件描述符,然后只针对这些文件描述符进行数据访问。

epoll可以同时支持水平触发和边缘触发,理论上边缘触发性能更高,但是代码实现复杂,因为任何意外的丢失事件都会造成请求处理错误。   epoll主要有2大改进:

  1. epoll只告知就绪的文件描述符,而且当调用epoll_wait()获得文件描述符时,返回并不是实际的描述符,而是一个代表就绪描述符数量的值,然后只需去epoll指定的一个数     组中依次取得相应数量的文件描述符即可,这里使用了内存映射(mmap)技术,这样彻底省掉了这些文件描述符在系统调用时复制的开销。
  2. epoll采用基于事件的就绪通知方式。其事先通过epoll_ctrl()注册每一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,当进程调用epoll_wait()时得到通知
4. Sendfile

大多数时候,我们都向服务器请求静态文件,比如图片,样式表等,在处理这些请求时,磁盘文件的数据先经过内核缓冲区,然后到用户内存空间,不需经过任何处理,其又被送到网卡对应的内核缓冲区,接着再被送入网卡进行发送。

Linux提供sendfile()系统调用,可以讲磁盘文件的特定部分直接传送到代表客户端的socket描述符,加快了静态文件的请求速度,同时减少CPU和内存的开销。  适用场景: 对于请求较小的静态文件,sendfile发挥的作用不那么明显,因发送数据的环节在整个过程中所占时间的比例相比于大文件请求时小很多。

5. 内存映射

Linux内核提供一种访问磁盘文件的特殊方式,它可以将内存中某块地址空间和我们指定的磁盘文件相关联,从而对这块内存的访问转换为对磁盘文件的访问。这种技术称为内存           映射。

多数情况下,内存映射可以提高磁盘I/O的性能,无须使用read()或write()等系统调用来访问文件,而是通过mmap()系统调用来建立内存和磁盘文件的关联,然后像访问内存一样      自由访问文件。

缺点:在处理较大文件时,内存映射会导致较大的内存开销,得不偿失。

6. 直接I/O

在linux 2.6中,内存映射和直接访问文件没有本质差异,因为数据需要经过2次复制,即在磁盘与内核缓冲区之间以及在内核缓冲区与用户态内存空间。

引入内核缓冲区的目的在于提高磁盘文件的访问性能,然而对于一些复杂的应用,比如数据库服务器,它们为了进一步提高性能,希望绕过内核缓冲区,由自己在用户态空间实现并管理I/O缓冲区,比如数据库可根据更加合理的策略来提高查询缓存命中率。另一方面,绕过内核缓冲区也可以减少系统内存的开销,因内核缓冲区本身就在使用系统内存。

Linux在open()系统调用中增加参数选项O_DIRECT,即可绕过内核缓冲区直接访问文件,实现直接I/O。

在Mysql中,对于Innodb存储引擎,自身进行数据和索引的缓存管理,可在my.cnf配置中分配raw分区跳过内核缓冲区,实现直接I/O。

5. 改进服务器并发策略

服务器并发策略的目的,是让I/O操作和CPU计算尽量重叠进行,一方面让CPU在I/O等待时不要空闲,另一方面让CPU在I/O调度上尽量花最少的时间。

一个进程处理一个连接,非阻塞I/O

这样会存在多个并发请求同时到达时,服务器必然要准备多个进程来处理请求。其进程的开销限制了它的并发连接数。但从稳定性和兼容性的角度,则其相对安全,任何一个子进程的崩溃不会影响服务器本身,父进程可以创建新的子进程;这种策略典型的例子就是Apache的fork和prefork模式。对于并发数不高(如150以内)的站点同时依赖Apache   其它功能时的应用选择Apache还是可以的。

一个线程处理一个连接,非阻塞IO

这种方式允许在一个进程中通过多个线程来处理多个连接,一个线程处理一个连接。Apache的worker模式就是这种典型例子,使其可支持更多的并发连接。不过这种模式的总体        性能还不如prefork,所以一般不选用worker模式。

一个进程处理多个连接,异步I/O

一个线程同时处理多个连接,潜在的前提条件就是使用IO多路复用就绪通知。

这种情况下,将处理多个连接的进程叫做worker进程或服务进程。worker的数量可以配置,如Nginx中的worker_processes 4。

一个线程处理多个连接,异步IO

即使有高性能的IO多路复用就绪通知,但磁盘IO的等待还是无法避免的。更加高效的方法是对磁盘文件使用异步IO,目前很少有Web服务器真正意义上支持这种异步IO。

6. 改进硬件环境

还有一点要提及的是硬件环境,服务器的硬件配置对应用程序的性能提升往往是最直接,也是最简单的方式,这就是所谓的scale up。这里不做论述。

 假如你的项目出现性能瓶颈了,你觉得可能会是哪些方面,怎么解决问题。

三个方面解决性能问题的基本思路和方法

先来个大图(电脑端观看,否则显示不清)

发现问题、解决问题和预防问题三个方面

首先,根据个人的开发经验,我不得不承认,当应用发展到一定程度后,性能问题就不可能完全避免。

以往我们总是希望能寻找一种解决性能问题的一劳永逸的方法,其实是不太现实的。所以我们换个思路,如何尽早的发现性能问题,然后解决问题。在发现问题方面,我们项目也并没有什么高招,主要有两个方面

1. 用户反馈(包括测试人员)

受限于测试时间和用户反馈的积极性,性能问题往往到了比较严重的程度,开发人员才真正发现问题。

2. 在线监控

在线监控主要有业务性能监控和卡顿监控

卡顿监控,是用了RDM的工具,然后通过动态下发开关,用抽样的方法进行上报     还有一些反馈卡顿的用户,我们也会通过这个方法来查找问题

然后,在解决性能问题方法,相信大家都累积了很多经验。

产生性能问题的原因多种多样,所以解决的办法也不尽相同,各种奇技淫巧都有可能派上用场,这里我大概介绍一下我们项目中用到的一些方面:

(1)优化业务流程 (2)合理的线程分配 (3)预处理和延时加载 (4)缓存 (5)使用正确的API (1). 优化业务流程

性能优化看似高深,真正落到实处才会发现,最大的坑往往都隐藏在于业务不断累积和频繁变更之处。优化业务流程就是在满足需求的同时,提出更加高效优雅的解决方案,从根本上解决问题。从实践来看,这种方法解决问题是最彻底的,但通常也是难度最大的。

这是我们其中一个业务优化的案例,看似挺简单的优化,但真正落到实处,才会出现其中的坑有多大,所以重构优化的时候,还得有颗坚强的心!

(2). 合理的线程分配

由于GCD实在太方便了,如果不加控制,大部分需要抛到子线程操作都会被直接加到global队列,这样会导致两个问题,1.开的子线程越来越多,线程的开销逐渐明显,因为开启         线程需要占用一定的内存空间(默认的情况下,主线程占1M,子线程占用512KB)。2.多线程情况下,网络回调的时序问题,导致数据处理错乱,而且不容易发现。为此,我们项         目定了一些基本原则。

UI操作和DataSource的操作一定在主线程。    DB操作、日志记录、网络回调都在各自的固定线程。     不同业务,可以通过创建队列保证数据一致性。例如,想法列表的数据加载、书籍章节下载、书架加载等。

合理的线程分配,最终目的就是保证主线程尽量少的处理非UI操作,同时控制整个App的子线程数量在合理的范围内。

(3). 预处理和延时加载。

预处理,是将初次显示需要耗费大量线程时间的操作,提前放到后台线程进行计算,再将结果数据拿来显示。

延时加载,是指首先加载当前必须的可视内容,在稍后一段时间内或特定事件时,再触发其他内容的加载。这种方式可以很有效的提升界面绘制速度,使体验更加流畅。

(UITableView就是最典型的例子)

这两种方法都是在资源比较紧张的情况下,优先处理马上要用到的数据,同时尽可能提前加载即将要用到的数据。在微信读书中阅读的排版是优先级最高的,所在在阅读过程中会预处理下一页、下一章的排版,同时可能会延时加载阅读相关的其它数据(如想法、划线、书签等)。

(4). 缓存

cache可能是所有性能优化中最常用的手段,但也是我们极不推荐的手段。cache建立的成本低,见效快,但是带来维护的成本却很高。如果一定要用,也请谨慎使用,并注意以       下几点:

并发访问cache时,数据一致性问题。 cache线程安全问题,防止一边修改一边遍历的crash。 cache查找时性能问题。 cache的释放与重建,避免占用空间无限扩大,同时释放的粒度也要依实际需求而定。

(5). 使用正确的API

选择合适的容器; 了解imageNamedimageWithContentsOfFile的差异(imageNamed适用于会重复加载的小图片,因为系统会自动缓存加载的图片;imageWithContentsOfFile 仅加载图片) 缓存NSDateFormatter的结果。 寻找(NSDate *)dateFromString:(NSString )string的替换品 不要随意使用NSLog();

这方面主要还是靠经验的累积

上面只是列举了几种常规手段,相信大家在实践过程中,肯定还有很多的高招。

经过一段时间的性能优化工作,我们团队达成了一项共识,与其花那么时间去发现问题,查问题,还不如多开发一些工具,让问题尽量暴露在开发阶段,最好达到避免共性问题。

所以,我们总是想开发一些有意思小工具来做这种事情。下面列举几个我们认识还挺有帮忙的工具

(1).FPS/SQL性能监测工具条

工具条是在DEBUG模式下,以浮窗的形式,实时展示当前可能存在问题的FPS次数和执行时间较长的SQL语句个数,是团队成员tower开发的。

FPS监测的原理并不复杂,虽然不是百分百准确,但非常实用,因为可以随时查看FPS低于某个阈值时的堆栈信息,再结合当时的使用场景,开发人员使用起来非常便利,可以很          快定位到引起卡顿的场景和原因。SQL语句的监测也非常实用,对于微信读书,DB的读写速度是影响性能的瓶颈之一。因此在DEBUG阶段,我们监测了每一条SQL语句的执行速       度,一旦执行时间超出某个阈值,就会表现在工具条的数字上,点击后可以进一步查询到具体的SQL操作以及实际耗时。

 
  

顶部工具条点击后,就可以查到具体是哪条sql语句慢       这个工具帮助我们在开发阶段发现了很多卡顿问题,尤其是一些不合理的SQL语句,例如: 在想法圏的优化过程中,利用这个工具,我们就发现想法圈第一次加载更多,执行的SQL语句耗时竟然达到了1000多毫秒。

通过explain,可以发现这条SQL效率之低:

 
  

没有建立合适的索引,导致WRReview全表扫描。 排序字段没有索引,导致SQLite需要再一次B-TREE排序。 两字段排序,性能更低。优化:给WRReview的 fromId createTime 两个字段增加了索引,并去掉一个排序字段:

Explain的结果:

SQL执行时间直接降了一个数量级,到100毫秒左右。

(2). UI/DataSource主线程检测工具。

该工具是为了保证所有的UI的操作和DataSource操作一定是在主线程进行。实现原理是通过hook UIView的setNeedsLayoutsetNeedsDisplaysetNeedsDisplayInRect

三个方法,确保它们都是在主线程执行。子线程操作UI可能会引起什么问题,苹果说得并不清楚,实际开发中我们遇到几种神奇的问题似乎都是跟这个有关。

app突然丢动画,似乎iOS系统也有这个bug。虽然没有确切的证据,但使用这个工具,改完所有的问题后,bug也好了(不止一次是这样)。       UI操作偶尔响应特别慢,从代码看没有任何耗时操作,只是简单的push某个controller。

莫名的crash,这当然是因为UI操作非线程安全引起的。

更多时候,子线程操作UI也并不一定会发生什么问题,也正因为不知道会发生什么,所以更需要我们警惕,这个工具替我们扫除了这些隐患。虽然,苹果表示,现在部分的UI操           作也已经是线程安全了,但毕竟大部分还不是。DataSource的监测是因为我们业务定下的原则,保证列表DataSource的线程安全。

(3) 排版引擎自动化检测工具

排版引擎是微信读书最核心的功能,排版引擎检测工具原本是为了检验排版引擎改进过程中准确性,防止因为业务变更,而影响原来的排版特性。实现原理是结合自动化脚本和App本身的排版引擎,给书库中的每一本书建立一个镜像,镜像的内容包括书籍的每一章每一页的截图。然后分析同一页码的两个不同版本的图片差异,就可以知道不同版本的            排版引擎渲染效果。但是我发现,只要稍加改进,排版后记录每个章节排版耗时,就可以知道每个版本变化后同一个章节的耗时变化,以此作为排版引擎的性能指标。这个工具保证了微信读书,即使在快速迭代过程中也不会丢失阅读的核心体验。虽然这个工具无法在其它项目中复用,但是提醒了我们,可以通过自动化工具来保证产品最核心功能的体验。

这个虽然业务相关性比较强,但是对于某些应用的自动化测试也是有效的

(4) 书源检测工具

微信读书为了支持正版版权,目前书源完全依赖于后台,不允许本地导入。书源的优劣的直接影响排版的效果和性能。为了解决了部分书籍无法打开或者乱码的问题,我们借助了后台同学的书源检测工具。对线上所有epub书籍(大概13,000本)进行扫描,按照章节大小进行排序。对于章节内容特别大的书籍重点检测,重新排版,解决了一批epub书         籍无法打开的问题。同时针对章节内容乱码的问题,对所有txt的书籍进行了一次全量扫描,发现了一些问题,但还无法准确找出所有乱码的章节,这一点还在努力改善中。

优化成果

整体使用感受上,已经可以明显区分两个版本的性能差异,这一点也可以通过每天的用户反馈数据中得到验证。

1.3.0和1.3.1分别发布一周后反馈的卡顿数从10个降到了3个,从总体反馈比例的2.8%降到0.8%。  某些关键业务,耗时也有明显改善。  极端案例的修复。超大的epub书籍已通过后台进行拆分,解决了无法打开书籍的情况。 针对低端机型,去掉了某些动画,交互更加流畅。

总结

通过上述介绍,我们可以看出,性能问题普遍存在,无可避免,与其花费大量时间,查找线上版本的性能问题,

不如提高整体团队成员性能优化意识,借助性能查找工具,将性能问题尽早暴露在开发阶段,达到预防为主的效果。

问题
(0) 想问下你们 DB 操作这部分涉及到多线程读写是怎么处理的?

我们用了FMDB,它已经处理了这种情况

MDB      的解决方案,我理解是放到一个队列里,虽然可以解决多线程读写的问题,但是队列的处理还是会阻塞住来自不同线程的请求,对么? 是的。我们一直也是读写都在同一条队列,其实并没有太明显的性能瓶颈,

因为在sqlite之上我们还有一层基于model的cache

(1) 除了 sqlite 语句的优化之外,db 这部分还有没有其他方面的优化工作?

除了 sqlite 语句的优化之外,db 这部分还有没有其他方面的优化工作

我们有一个自己的DB框架,是ORM的,做了很多优化的工作,最近刚开源,大家可以看看

(2) 请问你们选择用sqlite的考量是什么, 有没有考虑过使用其他的dbrealm?

选择sqlite是历史原因,因为我们已经基于sqlite做了一个高性能的DB框架,而且也是经过QQMail App验证的。realm有考虑过,但是因为不是开源,所以估计不用采用

(3) 合理的使用线程,多线程之间的同步这块儿有什么方案或建议?

这里我们也并没有什么通用的方案,原则是尽量避免使用多线程。一定要用的时候,也是根据业务谨慎选择

(4) 我把我的问题编号为4吧,方便对应。业务场景里会不会涉及到有 读操作 依赖写操作完成的情况,否则会出现读操作的数据不准确的情况。FMDB 感觉不能很好的解决这个问题。

读操作 依赖写操作完成,这种场景一定会有的。但是这种问题应该是业务流程自己控制,而不是DB应该考虑的事情,DB性一能保证的就是按照业务提交的顺序,顺序执行

数据库的记录-一般是在什么级别百千有没有尝试去做过一些压测数据量达到多少的时候会遇到瓶颈">(5)能不能问下 微信读书的数据库的记录 一般是在什么级别,百、千?有没有尝试去做过一些压测,数据量达到多少的时候会遇到瓶颈?

微信读书的数据库记录并不是很大,单表记录最多可能也就10w的数据级别。QQ邮箱的mailApp跟我们是用的同一套,但是数量级别远大于微信读书。目前发现的瓶颈是DB文件        达到200M以上时,sqlite的性能会明显受到影响,不过具体原因还在调查中。做过一些压力测试,用来对比CoreData,但是具体数据我这里暂时没有

(6) 微信读书这么成功,方便说下她的架构吗?我觉得架构好才是她可优化的第一步

哈哈,现在还远谈不上成功啦。架构要用图来画才方便看,我暂时还没总结整个app的架构, 可以看看关于阅读器epub渲染的一个架构

(7) 你们的 db 是只有一个文件,还是尝试分文件存储的?

看业务需求,目前是多个DB文件

 造成性能瓶颈。

五步定位性能瓶颈

1、着手在*测试*前:理清数据流向,数据流程分解

通过绘制数据流向图,以便清晰的列出所有可能出现瓶颈的位置,避免在分析过程中遗漏可能的瓶颈点。系统架构分解——水池模型

要查找瓶颈,首先要对系统的架构有详细的了解,清楚知道所有可能成为瓶颈的位置。只有这样才能在遇到问题是合理的设计测试用例,对流程的各个步骤进行逐一排查。

举个例子,家里厨房的水池下水堵了,我们要找原因,首先得知道水池的下水道都有哪些部分:

简单的看,可以把下水道分解为水漏、上连接管、回水弯、下连接管最后接入地漏。再查找堵塞位置时,我们就可以将水直接导入回水弯,排除水漏和上连接管道堵塞的可能。

应用在测试中,我们也可以利用直接向应用中间件发请求,来排除Web代理层的瓶颈。

通过绘制流向图,可以更清晰的展现系统中数据流向,帮助我们在定位瓶颈的过程始终能迅速的分析预计到下一个可能的瓶颈位置。

如上图,就是在play一个Mobile game时的数据流向图。

2、最直观的指征:检索日志中的异常

日志是系统异常的最直接反映,通过客户端(负载工具端)、服务器端的日志,可以迅速确定瓶颈可能存在的方向。一些在大用户量大并发情况下的功能问题,也会在错误日志           中体现。

性能测试过程中,一般情况是不把全部日志打开的,而是尽量保持与生产环境的设置相同,生产环境开启什么样的日志级别,在性能测试环境中也应该开启同样的级别。但是往往在生产环境出于性能考虑,并不会把日志级别开的非常高,所以在发现系统存在性能问题时,我们可以适当调高日志级别,以便获得更多的信息。

在日志中,我们可以由一些关键字直接推断出系统的问题所在,比如:

  • Too many open files

Linux下存在句柄数限制,系统的默认值较小,在测试前应该优化,另外还要怀疑是否程序存在打开句柄却在某些情况下没有关闭。

  • OutOfMemoryError/Cannot allocate memory

Java环境的虚拟内存异常,往往需要关注是否有溢出。

  • SQLException

数据库语句执行异常,一般日志中还会有数据库返回的信息。

  • Connection closed/connection refused

连接被关闭被拒绝,一般是连接数限制不能承担当前的压力。

3、最底层的反映:分析硬件资源占用

硬件资源也是系统性能达到瓶颈点的重要指征,如果没有在日志中找到异常,那么通过监控硬件资源消耗,往往可以发现系统的资源瓶颈。

3.1 CPU占用率

CPU的高占用,并不一定表示有问题,因为实现最优性能的一方面就是充分发挥当前的硬件资源能力。

但是如果CPU长期出于满负荷,就很值得我们关注,至少说明在大多数情况下,系统已经是在耗用最大的计算能力进行计算,运算能力已经成为瓶颈。         另外还要注意CPU是消耗在User还是Sys还是Wait, 如果是Wait,还要观察其他硬件资源,查看CPU是在等待什么。

3.2 内存占用

内存在性能测试中是被重点关注的指标,因为它是反映重大缺陷——内存泄露的最直接指标,但是我们应该注意到,在JAVA框架中的内存泄漏是发生在虚拟内存中的。        观察内存/虚拟内存的占用情况,尤其是在压力消失后的内存占用恢复情况,是比较直接的判断内存泄漏的依据。

如果观察到内存的使用情况,在每次Full       GC后,占用的内存都没能恢复到原来的水平,如果在压力撤除一段时间后,内存依旧不能恢复,那么十有八九当前系统存在内存泄漏。

3.3 磁盘I/O

通常情况下,磁盘是计算机中速度最慢的一个子系统,因此很多情况中,磁盘I/O会成为系统的瓶颈。实际上在设计高性能系统的时候,会把避免磁盘I/O作为一个首要准           则。

虽然当前的技术发展让存储系统的读写速度不断提升,但高昂的成本使得大多数情况下,高速存储会使用在数据库或文件服务器上,而不会使用在应用服务器中。所以在我们进行性能测试时,要更多的注意应用服务器的磁盘使用情况。

3.4 网络I/O

很多时候大家都容易忽略网络对系统的影响,实际上网络带宽在一些情况下也会成为系统的瓶颈。一旦在业务的请求和响应中包含较大的数据传输时,往往会遇到网络瓶 颈。因为更多的时候服务器采用的还是以太网卡,1000M网卡在全双工模式下传输速率也只有80M/s,如果响应中包含报表、图片之类的大尺寸数据,很有可能在性能测试中出          现网络瓶颈。

还有一点就是不要忽略回环地址传输的影响,比如一些应用访问本地监听的其他服务,都会受到网卡的传输速率限制的影响。

4、软件性能软肋:数据库的监控分析

对于Web系统,超过七成的瓶颈都出现在数据库子系统,因此在进行之前几步不能明确瓶颈位置的时候,应优先进行数据库的监控分析。

Oracle数据库监控工具

Oracle本身提供了ASH,AWR等Report来帮助进行性能分析,但是对于测试人员来说,掌握这些需要较深入的数据库知识学习,不是一朝一夕可以达成的。而一些第三方提     供的工具,通过图形界面,可以更加直观的帮助我们进行Oracle数据库的监控和分析。

Lab128就是国内开发的一块很不错的共享软件,而且它还提供无限期的试用key,可以免费试用。

Oracle中的等待事件

判断Oracle中的瓶颈,了解Oracle中的等待事件Wait event,对于查找瓶颈有很大的帮助。在Oracle中,处理SQL的过程,会产生一系列的等待事件。

 
  

有等待事件并不代表数据库存在瓶颈,正常的处理也会有等待事件,但如果发现等待事件激增,或者SQL执行缓慢,这时候等待事件中排名靠前的事件将会直接反映出瓶颈          所在。

上图是在测试中的某一时刻,log sync的等待事件突然增高,同时数据库的吞吐率大幅下降,原本正常的SQL执行速度也突然变长。

因为压力并没有突然改变,很有可能是写log的过程出现了问题,或者是在传输过程,或者是在存储子系统。后来经过排查,发现是存储集群的一个存储单元出现故障导致写           入速度变慢致使出现大量等待。

5、最后的大杀器:应用服务器监控及代码分析

如果没能在其他位置发现瓶颈,那么软件程序所运行的平台——应用服务器很可能是最大的潜在瓶颈点,进行应用服务器的监控与分析将是我们最后的大杀器。

5.1 常见的软件资源种类

相对于硬件资源,软件资源往往容易被忽略,它不像CPU占用率那么让人更直观的和性能联系起来,但是实际上,软件资源同样限制着软件系统能达到什么样的性能。

软件资源不论是在Web层,应用层还是在数据库层,都可以按“入口”、“内部”、“出口”来划分。对于常见的原因中间件,“入口”就是如HTTP连接池之类,是数据来源方向的相      关设置,比如连接数限制,超时时间,连接回收策略等等;“内部”就是处理请求的各项资源,不如线程数,线程调度策略,虚拟内存设置,GC策略等等;“出口”则是向后端交互的         各项资源,如数据库连接池的配置。

5.2 应用中间件监控

 
  

要了解软件资源是否成为瓶颈,我们就需要监控这些软件资源指标。以JAVA环境为例,Weblogic 本身就有控制台,提供了各种计数器。

上图显示的是Execute Threads的计数器,对请求的处理就在这些Thread中进行。

Tomcat也有开源的控制台,常用的像PSI-Probe,提供了Tomcat服务器各项资源的图形化监控。

5.3 应用中间件剖析

仅仅监控只能初步判断问题的方向,例如发现ExecuteThreads持续的增加,我们虽然知道这个现象不正常,但是想要确定是程序中的哪个方法导致了当前问题,我还需要其      他的工具进行深入剖析。

对于Java程序,最常用的工具有JProfiler,YourKit,他们的原理类似,都是要把一个小插件挂在到应用服务器上,以获取需要的程序运行信息。而Sun在JDK1.7后版本整合了继承自JRockit的MissionControl,也提供了很强大的分析监控功能,而且开销较小,确实是个不错的选择。

 
  

它提供的Mem leak detector可以对对象的创建进行趋势分析,帮你找到最有可能出现泄漏的对象,

再通过展开剖析工具中的invoke  tree,找出创建该对象的方法,可以更细致的定位问题的原因。同时,Call tree 也可以依据CPU时间进行分析,找到在虚拟机中消耗最高的方法。

性能测试如何定位瓶颈

性能测试这种测试方式在发生过程中,其中一个过渡性的工作

1、网络瓶颈,如带宽,流量等形成的网络环境

2、应用服务瓶颈,如中间件的基本配置,CACHE等

3、系统瓶颈,这个比较常用:应用服务器,数据库服务器以及客户机的CPU,内存,硬盘等配置

4、数据库瓶颈,以ORACLE为例,SYS中默认的一些参数设置

5、应用程序本身瓶颈,这个是测试过程中最需要去关注的,需要测试人员和开发人员配合执行,然后定位

逐步细化分析,先可以监控一些常见衡量CPU,内存,磁盘的性能指标,进行综合分析,然后根据所测系统具体情况,进行初步问题定位,然后确定更详细的监控指标来分析。           怀疑内存不足时:

方法1:

【监控指标】:Memory Available MBytes ,Memory的Pages/sec, page read/sec, Page Faults/sec

【参考值】:

如果 Page Reads/Sec 比率持续保持为 5,表示可能内存不足。

 
  

Page/sec 推荐00-20(如果服务器没有足够的内存处理其工作负荷,此数值将一直很高。如果大于80,表示有问题)。

【监控指标】:Memory Available MBytes ,Pages read/sec,%Disk Time 和 Avg.Disk Queue Length

【参考值】:%Disk Time建议阈值90%

当内存不足时,有点进程会转移到硬盘上去运行,造成性能急剧下降,而且一个缺少内存的系统常常表现出很高的CPU利用率,因为它需要不断的扫描内存,将内存中的页           面移到硬盘上。

怀疑内存泄漏时

【监控指标】:Memory Available MBytes ,Process\Private Bytes和Process\Working Set,PhysicalDisk/%Disk Time

【说明】:

Windows资源监控中,如果Process\Private Bytes计数器和Process\Working Set计数器的值在长时间内持续升高,同时Memory\Available  bytes计数器的值持续降低,则很可能存在内存泄漏。内存泄漏应该通过一个长时间的,用来研究分析当所有内存都耗尽时,应用程序反应情况的测试来检验。

CPU分析

【监控指标】:

System %Processor Time CPU,Processor %Processor Time CPU Processor%user time 和Processor%Privileged Time

system\Processor Queue Length

Context Switches/sec 和%Privileged Time

【参考值】:

System\%Total processor time不持续超过90%,如果服务器专用于SQL Server,可接受的最大上限是80-85% ,合理使用的范围在60%至70%。

Processor %Processor Time小于75%

system\Processor Queue Length值,小于CPU数量的总数+1

CPU瓶颈问题

1、System\%Total processor time如果该值持续超过90%,且伴随处理器阻塞,则说明整个系统面临着处理器方面的瓶颈.

注:在某些多CPU系统中,该数据虽然本身并不大,但CPU之间的负载状况极不均衡,此时也应该视作系统产生了处理器方面的瓶颈.

 
  

2、排除内存因素,如果Processor %Processor Time计数器的值比较大,而同时网卡和硬盘的值比较低,那么可以确定CPU 瓶颈。(内存不足时,有点进程会转移到硬盘上去运行,造成性能急剧下降,而且一个缺少内存的系统常常表现出很高的CPU利用率,因为它需要不断的扫描内存,将内存中的页面移到硬盘上。)

频繁执行程序,复杂运算操作,消耗CPU严重

数据库查询语句复杂,大量的 where 子句,order by, group by 排序等,CPU容易出现瓶颈内存不足,IO磁盘问题使得CPU的开销增加

磁盘I/O分析

【监控指标】:PhysicalDisk/%Disk time,PhysicalDisk/%Idle Time,Physical Disk\ Avg.Disk Queue Length, Disk sec/Transfer

【参考值】:%Disk Time建议阈值90%

Windows资源监控中,如果% Disk Time和Avg.Disk Queue Length的值很高,而Page Reads/sec页面读取操作速率很低,则可能存在磁盘瓶径。

Processor%Privileged Time该参数值一直很高,且如果在 Physical Disk 计数器中,只有%Disk time 比较大,其他值都比较适中,硬盘可能会是瓶颈。若几个值都比较大, 那么硬盘不是瓶颈。若数值持续超过80%,则可能是内存泄露。如果 Physical Disk 计数器的值很高时该计数器的值(Processor%Privileged Time)也一直很高, 则考虑使用速度更快或效率更高的磁盘子系统。

Disk sec/Transfer 一般来说,该数值小于15ms为最好,介于15-30ms之间为良好,30-60ms之间为可以接受,超过60ms则需要考虑更换硬盘或是硬盘的RAID方式了.

 
  

Average Transaciton Response Time(事务平均响应时间)随着测试时间的变化,系统处理事务的速度开始逐渐变慢,这说明应用系统随着投产时间的变化,整体性能将会有下降的趋势

Transactions per Second(每秒通过事务数/TPS)当压力加大时,点击率/TPS曲线如果变化缓慢或者有平坦的趋势,很有可能是服务器开始出现瓶颈

Hits  per   Second(每秒点击次数)通过对查看“每秒点击次数”,可以判断系统是否稳定。系统点击率下降通常表明服务器的响应速度在变慢,需进一步分析,发现系统瓶颈所在。

Throughput(吞吐率)可以依据服务器的吞吐量来评估虚拟用户产生的负载量,以及看出服务器在流量方面的处理能力以及是否存在瓶颈。Connections(连接数)当连接数到达稳定状态而事务响应时间迅速增大时,添加连接可以使性能得到极大提高(事务响应时间将降低)

 
  

Time to First Buffer Breakdown(Over Time)(第一次缓冲时间细分(随时间变化))可以使用该图确定场景或会话步骤运行期间服务器或网络出现问题的时间。

如何定位这些性能问题:
 
  

比如,系统宕机时,系统日志打印了某方法执行时抛出out of memory的错误,我们就可以顺藤摸瓜,很快定位到导致内存溢出的问题在哪里。

利用Spotlight可以监控数据库使用情况。

 
  

我们需要关注的性能点有:CPU负载,内存使用率,网络I/O等

具体场景有:性能测试,负载测试,压力测试,稳定性测试,浪涌测试等好的测试场景,能更加快速的发现瓶颈,定位瓶颈

性能测试如何定位瓶颈

性能测试这种测试方式在发生过程中,其中一个过渡性的工作

1、网络瓶颈,如带宽,流量等形成的网络环境

2、应用服务瓶颈,如中间件的基本配置,CACHE等

3、系统瓶颈,这个比较常用:应用服务器,数据库服务器以及客户机的CPU,内存,硬盘等配置

4、数据库瓶颈,以ORACLE为例,SYS中默认的一些参数设置

5、应用程序本身瓶颈,这个是测试过程中最需要去关注的,需要测试人员和开发人员配合执行,然后定位

逐步细化分析,先可以监控一些常见衡量CPU,内存,磁盘的性能指标,进行综合分析,然后根据所测系统具体情况,进行初步问题定位,然后确定更详细的监控指标来分析。           怀疑内存不足时:

方法1:

【监控指标】:Memory Available MBytes ,Memory的Pages/sec, page read/sec, Page Faults/sec

【参考值】:

如果 Page Reads/Sec 比率持续保持为 5,表示可能内存不足。

 
  

Page/sec 推荐00-20(如果服务器没有足够的内存处理其工作负荷,此数值将一直很高。如果大于80,表示有问题)。

【监控指标】:Memory Available MBytes ,Pages read/sec,%Disk Time 和 Avg.Disk Queue Length

【参考值】:%Disk Time建议阈值90%

当内存不足时,有点进程会转移到硬盘上去运行,造成性能急剧下降,而且一个缺少内存的系统常常表现出很高的CPU利用率,因为它需要不断的扫描内存,将内存中的页           面移到硬盘上。

怀疑内存泄漏时

【监控指标】:Memory Available MBytes ,Process\Private Bytes和Process\Working Set,PhysicalDisk/%Disk Time

【说明】:

Windows资源监控中,如果Process\Private Bytes计数器和Process\Working Set计数器的值在长时间内持续升高,同时Memory\Available  bytes计数器的值持续降低,则很可能存在内存泄漏。内存泄漏应该通过一个长时间的,用来研究分析当所有内存都耗尽时,应用程序反应情况的测试来检验。

CPU分析

【监控指标】:

System %Processor Time CPU,Processor %Processor Time CPU Processor%user time 和Processor%Privileged Time

system\Processor Queue Length Context Switches/sec 和%Privileged Time

【参考值】:

System\%Total processor time不持续超过90%,如果服务器专用于SQL Server,可接受的最大上限是80-85% ,合理使用的范围在60%至70%。

Processor %Processor Time小于75%

system\Processor Queue Length值,小于CPU数量的总数+1

CPU瓶颈问题

1、System\%Total processor time如果该值持续超过90%,且伴随处理器阻塞,则说明整个系统面临着处理器方面的瓶颈.

注:在某些多CPU系统中,该数据虽然本身并不大,但CPU之间的负载状况极不均衡,此时也应该视作系统产生了处理器方面的瓶颈.

2、排除内存因素,如果Processor %Processor Time计数器的值比较大,而同时网卡和硬盘的值比较低,那么可以确定CPU 瓶颈。(内存不足时,有点进程会转移到硬盘上去运行,造成性能急剧下降,而且一个缺少内存的系统常常表现出很高的CPU利用率,因为它需要不断的扫描内存,将内存中的页面移到硬盘上。)

 
  

频繁执行程序,复杂运算操作,消耗CPU严重

数据库查询语句复杂,大量的 where 子句,order by, group by 排序等,CPU容易出现瓶颈内存不足,IO磁盘问题使得CPU的开销增加

磁盘I/O分析

【监控指标】:PhysicalDisk/%Disk time,PhysicalDisk/%Idle Time,Physical Disk\ Avg.Disk Queue Length, Disk sec/Transfer

【参考值】:%Disk Time建议阈值90%

Windows资源监控中,如果% Disk Time和Avg.Disk Queue Length的值很高,而Page Reads/sec页面读取操作速率很低,则可能存在磁盘瓶径。

Processor%Privileged Time该参数值一直很高,且如果在 Physical Disk 计数器中,只有%Disk time 比较大,其他值都比较适中,硬盘可能会是瓶颈。若几个值都比较大, 那么硬盘不是瓶颈。若数值持续超过80%,则可能是内存泄露。如果 Physical Disk 计数器的值很高时该计数器的值(Processor%Privileged Time)也一直很高, 则考虑使用速度更快或效率更高的磁盘子系统。

Disk sec/Transfer 一般来说,该数值小于15ms为最好,介于15-30ms之间为良好,30-60ms之间为可以接受,超过60ms则需要考虑更换硬盘或是硬盘的RAID方式了.

 
  

Average Transaciton Response Time(事务平均响应时间)随着测试时间的变化,系统处理事务的速度开始逐渐变慢,这说明应用系统随着投产时间的变化,整体性能将会有下降的趋势

Transactions per Second(每秒通过事务数/TPS)当压力加大时,点击率/TPS曲线如果变化缓慢或者有平坦的趋势,很有可能是服务器开始出现瓶颈

Hits  per   Second(每秒点击次数)通过对查看“每秒点击次数”,可以判断系统是否稳定。系统点击率下降通常表明服务器的响应速度在变慢,需进一步分析,发现系统瓶颈所在。

Throughput(吞吐率)可以依据服务器的吞吐量来评估虚拟用户产生的负载量,以及看出服务器在流量方面的处理能力以及是否存在瓶颈。Connections(连接数)当连接数到达稳定状态而事务响应时间迅速增大时,添加连接可以使性能得到极大提高(事务响应时间将降低)

 
  

Time to First Buffer Breakdown(Over Time)(第一次缓冲时间细分(随时间变化))可以使用该图确定场景或会话步骤运行期间服务器或网络出现问题的时间。

如何定位这些性能问题:

比如,系统宕机时,系统日志打印了某方法执行时抛出out of memory的错误,我们就可以顺藤摸瓜,很快定位到导致内存溢出的问题在哪里。

利用Spotlight可以监控数据库使用情况。

 
  

我们需要关注的性能点有:CPU负载,内存使用率,网络I/O等

具体场景有:性能测试,负载测试,压力测试,稳定性测试,浪涌测试等好的测试场景,能更加快速的发现瓶颈,定位瓶颈

 你的项目中使用过缓存机制吗?有没用用户非本地缓存

使用缓存的9大误区(上)

如果说要对一个站点或者应用程序经常优化,可以说缓存的使用是最快也是效果最明显的方式。一般而言,我们会把一些常用的,或者需要花费大量的资源或时间而产生的数据缓存起来,使得后续的使用更加快速。

如果真要细说缓存的好处,还真是不少,但是在实际的应用中,很多时候使用缓存的时候,总是那么的不尽人意。换句话说,假设本来采用缓存,可以使得性能提升为100(这里的数字只是一个计量符号而已,只是为了给大家一个“量”的体会),但是很多时候,提升的效果只有80,70,或者更少,甚至还会导致性能严重的下降,这个现象在使        用分布式缓存的时候尤为突出。

在本篇文章中,我们将为大家讲述导致以上问题的9大症结,并且给出相对应的解决方案。文章以.NET为例子进行代码的演示,对于来及其他技术平台的朋友也是有参考价值          的,只要替换相对应的代码就行了!

 
  

为了使得后文的阐述更加的方便,也使得文章更为的完整,我们首先来看看缓存的两种形式:本地内存缓存,分布式缓存。首先对于本地内存缓存,就是把数据缓存在本机的内存中,如下图1所示:

从上图中可以很清楚的看出:

应用程序把数据缓存在本机的内存,需要的时候直接去本机内存进行获取。

对于.NET的应用而言,在获取缓存中的数据的时候,是通过对象的引用去内存中查找数据对象的,也就说,如果我们通过引用获取了数据对象之后,我们直接修改这个对          象,其实我们真正的是在修改处于内存中的那个缓存对象。

 
  

对于分布式的缓存,此时因为缓存的数据是放在缓存服务器中的,或者说,此时应用程序需要跨进程的去访问分布式缓存服务器,如图2:

不管缓存服务器在哪里,因为涉及到了跨进程,甚至是跨域访问缓存数据,那么缓存数据在发送到缓存服务器之前就要先被序列化,当要用缓存数据的时候,应用程序服务器接收到了序列化的数据之后,会将之反序列化。序列化与反序列化的过程是非常消耗CPU的操作,很多问题就出现在这上面。

另外,如果我们把获取到的数据,在应用程序中进行了修改,此时缓存服务器中的原先的数据是没有修改的,除非我们再次将数据保存到缓存服务器。请注意:这一点和之前的本地内存缓存是不一样的。

对于缓存中的每一份数据,为了后文的讲述方面,我们称之为“缓存项“。

普及完了这两个概念之后,我们就进入今天的主题:使用缓存常见的9大误区:

  1. 太过于依赖.NET**默认的序列化机制**
  2. 使用缓存机制在线程间进行数据的共享
  3. 认为调用缓存API**之后,数据会被立刻缓存起来**
  4. 缓存大量具有图结构的对象导致内存浪费
  5. 缓存应用程序的配置信息
  6. 使用很多不同的键指向相同的缓存项
  7. 没有及时的更新或者删除再缓存中已经过期或者失效的数据
2. 缓存大对象
5. 缓存大量的数据集合,而读取其中一部分

下面,我们就每一点来具体的看看!

太过于依赖.NET默认的序列化机制

当我们在应用中使用跨进程的缓存机制,例如分布式缓存memcached或者微软的AppFabric,此时数据被缓存在应用程序之外的进程中。每次,当我们要把一些数据缓存起      来的时候,缓存的API就会把数据首先序列化为字节的形式,然后把这些字节发送给缓存服务器去保存。同理,当我们在应用中要再次使用缓存的数据的时候,缓存服务器就会将           缓存的字节发送给应用程序,而缓存的客户端类库接受到这些字节之后就要进行反序列化的操作了,将之转换为我们需要的数据对象。

另外还有三点需要注意的就是:

这个序列化与反序列化的机制都是发生在应用程序服务器上的,而缓存服务器只是负责保存而已。

.NET中的默认使用的序列化机制不是最优的,因为它要使用反射机制,而反射机制是是非常耗CPU的,特别是当我们缓存了比较复杂的数据对象的时候。

 
  

基于这个问题,我们要自己选择一个比较好的序列化方法来尽可能的减少对CPU的使用。常用的方法就是让对象自己来实现ISerializable接口。   首先我们来看看默认的序列化机制是怎么样的。如图3:

然后,我们自己来实现ISerializable接口,如下图4所示:

我们自己实现的方式与.NET默认的序列化机制的最大区别在于:没有使用反射。自己实现的这种方式速度可以是默认机制的上百倍。       可能有人认为没有什么,不就是一个小小的序列化而已,有必要小题大做么?

在开发一个高性能应用(例如网站)而言,从架构,到代码的编写,以及后面的部署,每一个地方都需要优化。一个小问题,例如这个序列化的问题,初看起来不是问题, 如果我们站点应用的访问量是百万,千万,甚至更高级别的,而这些访问需要去获取一些公共的缓存的数据,这个之前所谓的小问题就不小了!

下面,我们来看第二个误区。

缓存大对象

有时候,我们想要把一些大对象缓存起来,因为产生一次大对象的代价很大,我们需要产生一次,尽可能的多次使用,从而提升响应。

提到大对象,这里就很有必要对其进行一个比较深入的介绍了。在.NET中,所谓的大对象,就是指的其占用的内存大于了85K的对象,下面通过一个比较将问题说清楚。         如果现在有一个Person类的集合,定义为List,每个Person对象占用1K的内存,如果这个Person集合中包含了100个Person对象实例,那么这个集合是否是大对象呢? 回答是:不是!

因为集合中只是包含的Person对象实例的引用而言,即,在.NET的托管堆上面,这个Person集合分配的内存大小也就是100个引用的大小而言。   然后,对于下面的这个对象,就是大对象了: byte[] data = new byte[87040](85 * 1024 = 87040)。

说到了这里,那就就谈谈,为什么说:产生一次大对象的代价很大。

 
  

因为在.NET中,大对象是分配在大对象托管堆上面的(我们简称为“大堆”,当然,还有一个对应的小堆),而这个大堆上面的对象的分配机制和小堆不一样:大堆在分配的         时候,总是去需找合适的内存空间,结果就是导致出现内存碎片,导致内存不足!我们用一个图来描述一下,如图5所示:

上图非常明了,在图5中:

垃圾回收机制不会在回收对象之后压缩大堆(小堆是压缩的)。

分配对象的时候,需要去遍历大堆,去需找合适的空间,遍历是要花成本的。如果某些空间小于85K,那么就不能分配了,只能白白浪费,也导致内存碎片。

讲完了这些之后,我们言归正传,来看看大对象的缓存。

正如之前讲过,将对象缓存和读取的时候是要进行序列化与反序列化的,缓存的对象越大(例如,有1M等),整个过程中就消耗更多的CPU。

对于这样的大对象,要看它使用的是否很频繁,是否是公用的数据对象,还是每个用户都要产生的。因为我们一旦缓存了(特别在分布式缓存中),就需要同时消耗缓存服务器的内存与应用程序服务器的CPU。如果使用的不频繁,建议每次生成!如果是公用的数据,那么建议多多的测试:将生产大对象的成本与缓存它的时候消耗的内存和CPU的          成本进行比较,选择成本小的!如果是每个用户都要产生的,看看是否可以分解,如果实在不能分解,那么缓存,但是及时的释放!

使用缓存机制在线程间进行数据的共享

当数据放在缓存中的时候,我们程序的多个线程都可以访问这个公共的区域。多个线程在访问缓存数据的时候,会产生一些竞争,这也是多线程中常常发生的问题。下面我们分别从本地内存缓存与分布式缓存两个方面介绍竞争的带来的问题。

 
  

看下面的一段代码:

对于本地内存缓存,对于上面的代码,当这个三个线程运行起来之后,在线程1中,item的值很多时候可能为1,线程2可能是2,线程3可能是3。当然,这不一定!只是大多         数情况下的可能值!

如果是对于分布式缓存,就不好说了!因为数据的修改不是立刻发生在本机的内存中的,而是经过了一个跨进程的过程。有一些缓存模块已经实现了加锁的方式来解决这个问题,例如AppFabric。大家在修改缓存数据的时候要特别注意这一点。       认为调用缓存API*之后,数据会被立刻缓存起来*

有时候,当我们调用了缓存的API之后,我们就会认为:数据已经被换成了,之后就可以直接读取缓存中的数据。尽管情况很多时候如此,但是不是绝对的!很多的问题就是           这样产生的!

我们通过一个例子来讲解。

 
  

例如,对于一个ASP.NET 应用而言,如果我们在一个按钮的Click事件中调用了缓存API,然后在页面呈现的时候,就去读取缓存,代码如下:

上面的代码照道理来说是对的,但是会发生问题。按钮点击之后回传页面,然后呈现页面的时候显示数据,流程没有问题。但是没有考虑到这样一个问题:如果服务器的内存紧张,而导致进行服务器内存的回收,那么很有可能缓存的数据就没有了!

这里有朋友就要说了:内存回收这么快? 这主要看我们的一些设置和处理。

一般而言,缓存机制都是会设置绝对过期时间与相对过期时间,二者的区别,大家应很清楚,我这里不多说。对于上面的代码而言,如果我们设置的是绝对过期时间,假设1         分钟,如果页面处理的非常慢,时间超过了1分钟,那么等到呈现的时候,可能缓存中的数据已经没有了!

有时候,即使我们在第一行代码中缓存了数据,那么也许在第三行代码中,我们去缓存读取数据的时候,就已经没有了。这或许是因为在服务器内存压力很大的,缓存机制将最少访问的数据直接清掉。或者服务器CPU很忙,网络也不好,导致数据没有被即使的序列化保存到缓存服务器中。

另外,对于ASP.NET而言,如果使用了本地内存缓存,那么,还涉及到IIS的配置问题(对缓存内存的限制),我们有机会专门为大家分享这方面的知识。     所以,每次在使用缓存数据的时候,要判断是否存在,不然,会有很多的“找不到对象”的错误,产生一些我们认为的“奇怪而又合理的现象”。

使用缓存的9大误区(下)

本篇文章在上篇的基础上继续讨论了使用缓存的几个误区,包括:缓存大量的数据集合,而读取其中一部分;缓存大量具有图结构的对象导致内存浪费;缓存应用程序的配置信息;使用很多不同的键指向相同的缓存项;没有及时的更新或者删除再缓存中已经过期或者失效的数据。

缓存大量的数据集合,而读取其中一部分

在很多时候,我们往往会缓存一个对象的集合,但是,我们在读取的时候,只是每次读取其中一部分。 我们举个例子来说明这个问题(例子可能不是很恰当,但是足以说明问题)。

在购物站点中,常见的操作就是查询一些产品的信息,这个时候,如果用户输入了“25寸电视机”,然后查找相关的产品。这个时候,在后台,我们可以查询数据库,找到几          百条这样的数据,然后,我们将这几百条数据作为一个缓存项缓存起来,代码的代码如下:

 
  

同时,我们对找出的产品进行分页的显示,每次展示10条。其实在每次分页的时候,我们都是根据缓存的键去获取数据,然后选择下一个10条数据,然后显示。          如果是使用本地内存缓存,那么这可能不是什么问题,如果是采用分布式缓存,问题就来了。下图可以清楚的说明这个过程,如图所示:

相信大家看完这个图,然后结合之前的讲述应该很清楚了问题所在了:每次都按照缓存键获取全部数据,然后在应用服务器那里反序列化全部数据,但是只是取其中10条。           这里可以将数据集合再次拆分,分为例如25-0-10-products,25-11-20-products等的缓存项,如下图所示:

当然,查询和缓存的方式有很多,拆分的方式也有很多,这里这是给出一些常见的问题!

缓存大量具有图结构的对象导致内存浪费
 
  

为了更好的说明这个问题,我们首先看到下面的一个类结构图,如图:

如果我们要把一些Customer数据缓存起来,这里就可以可能出现两个问题:

  1. 由于使用.NET的默认序列化机制,或者没有适当的加入相应Attribute(属性),使得缓存了一些原本不需要缓存的数据。
  2. 将Customer缓存的时候,同时,为了更快的获取Customer的Order信息,将Order信息缓存在了另外一个缓存项中,导致同一份数据被缓存两次。

下面,我们就分别来看看这两个问题。

首先看到第一个。如果我们使用分布式缓存来缓存一些Customer的信息的时候,如果我们没有自己重新Customer的序列化机制,而是采用的默认的,那么序列化机制在序       列化Customer的时候,会将Customer所引用的对象也序列化,然后在序列化被序列化对象中的其他引用对象,最后的结果就是:Customer被序列化,Customer的Order信息 被序列化,Order引用的OrderItem被序列化,最后OrderItem引用的Product也会序列化。

整个对象图全部被序列化了,如果这种情况是我们想要的,那么没有问题;如果不是的,那么,我们就浪费了很多的资源了,解决的方法有两个:第一,自己实现序列化, 自己完全控制哪些对象需要序列化,我们前面已经讲过了;第二,如果使用默认的序列化机制,那么在不要需要序列化的对象上面加上[NonSerialized]标记。

 
  

下面,我们看到第二个问题。这个问题主要是由于第一个问题引起的:原本在缓存Customer的时候,已经将Customer的其他信息,例如Order,Product已经缓存了。但是    很多的技术人员不清楚这一点,然后又把Customer的Order信息去缓存在其他的缓存项,使用的使用就根据Customer的标识,例如ID去缓存中获取Order信息,如下代码所示:

解决这个问题的方法也比较明显,参看第一个问题的解决方案就可以了!

缓存应用程序的配置信息

因为缓存是有一套数据失效检测周期的(之前说过,要么是固定时间失效,要么是相对时间失效),所以,很多的技术人员喜欢把一些动态变化的信息保存在缓存中,以充分利用缓存机制的这种特性,其中,缓存程序的配置信息就是其中一个例子。

因为在应用的中的一些配置,可能会发生变化,最简单的就是数据库连接字符串了,如下代码:

当这样设置之后,每隔一段时间缓存失效之后,就去重新读取配置文件,这时候,可能此时的配置就和之前不一样了,并且其他的地方都可以读取缓存从而进行更新,特别是在多台服务器上面部署同一个站点的时候,有时候,我们没有及时的去修改每个服务器上面的站点的配置文件里面的信息,这个时候如何使用分布式缓存缓存配置信息,只要更新一个站点的配置文件,其他站点就全部修改了,技术人员皆大欢喜。OK,这确实看起来是个不错的方法(在必要的时候可以采用一下),但是,不是所有的配置信息都要保           持一样的,而且还要考虑怎样一个情况:如果缓存服务器出了问题,宕机了,那么我们所有使用这个配置信息的站点可能都会出问题。

建议对于这些配置文件的信息,采用监控的机制,例如文件监控,每次文件发生变化,就重新加载配置信息。

使用很多不同的键指向相同的缓存项

我们有时候会遇到这样的一个情况:我们把一个对象缓存起来,用一个键作为缓存键来获取这个数据,之后,我们又通过一个索引作为缓存键来获取这个数据,如下代码所示:

 
  

我们之所以这样写,主要因为我们会以多种方式来从缓存中读取数据,例如在进行循环遍历的时候,需要通过索引来获取数据,例如index++等,而有些情况,我们可能需要          通过其他的方式,例如,产品名来获取产品的信息。

如果遇到这样的情况,那么就建议将这些多个键组合起来,形成如下的形式:

 
  

另外一个常见的问题就是:相同的数据被缓存在不同的缓存项中,例如,如果用户查询尺寸为36寸的彩电,那么可能有可能一个编号为100的电视产品就在结果中,此时,             我们将结果缓存。另外,用户在查找一个生产厂家为TCL的电视,如果编号为100的电视产品又出现在结果中,我们把结果又缓存在另外一个缓存项中。这个时候,很显然,出现          了内存的浪费。

 
  

对于这样的情况,之前笔者采用的方法就是,在缓存中创建了一个索引列表,如图所示:

当然,这其中有很多的细节和问题需要解决,这里就不一一述说,要看各自的应用和情况而定! 也非常欢迎大家提供更好的方法。

没有及时的更新或者删除再缓存中已经过期或者失效的数据
 
  

这种情况应该是使用缓存最常见的问题,例如,如果我们现在获取了一个Customer的所有没有处理的订单的信息,然后缓存起来,类似的代码如下:

之后,用户的一个订单被处理了,但是缓存还没有更新,那么这个时候,缓存中的数据就已经有问题!当然,我这里只是列举的最简单的场景,大家可以联想自己应用中的其他产品,很有可能会出现缓存中的数据和实际数据库中的不一样。

现在很多的时候,我们已经容忍了这种短时间的不一致的情况。其实对于这种情况,没有非常完美的解决方案,如果要做,倒是可以实现,例如每次修改或者删除一个数据,就去遍历缓存中的所有数据,然后进行操作,但是这样往往得不偿失。另外一个折中的方法就是,判断数据的变化周期,然后尽可能的将缓存的时间变短一点。

 Tomcat 调优及 JVM 参数优化

Tomcat        的缺省配置是不能稳定长期运行的,也就是不适合生产环境,它会死机,让你不断重新启动,甚至在午夜时分唤醒你。对于操作系统优化来说,是尽可能的增大可使用的内存容量、提高CPU 的频率,保证文件系统的读写速率等。经过压力测试验证,在并发连接很多的情况下,CPU 的处理能力越强,系统运行速度越快。

Tomcat 的优化不像其它软件那样,简简单单的修改几个参数就可以了,它的优化主要有三方面,分为系统优化,Tomcat 本身的优化,Java 虚拟机(JVM)调优。系统优化就不在介绍了,接下来就详细的介绍一下 Tomcat 本身与 JVM 优化,以 Tomcat 7 为例。

一、Tomcat 本身优化

Tomcat 的自身参数的优化,这块很像 ApacheHttp Server。修改一下 xml 配置文件中的参数,调整最大连接数,超时等。此外,我们安装 Tomcat 是,优化就已经开始了。

1、工作方式选择

为了提升性能,首先就要对代码进行动静分离,让 Tomcat 只负责 jsp 文件的解析工作。如采用 Apache 和 Tomcat 的整合方式,他们之间的连接方案有三种选择,JK、http_proxy 和 ajp_proxy。相对于 JK 的连接方式,后两种在配置上比较简单的,灵活性方面也一点都不逊色。但就稳定性而言不像JK 这样久经考验,所以建议采用 JK 的连接方式。

2、Connector 连接器的配置

之前文件介绍过的 Tomcat 连接器的三种方式: bio、nio 和 apr,三种方式性能差别很大,apr 的性能最优, bio 的性能最差。而 Tomcat 7 使用的 Connector 默认就启用的

Apr 协议,但需要系统安装 Apr 库,否则就会使用 bio 方式。

3、配置文件优化

配置文件优化其实就是对 server.xml 优化,可以提大大提高 Tomcat 的处理请求的能力,下面我们来看 Tomcat 容器内的优化。

 
  

默认配置下,Tomcat 会为每个连接器创建一个绑定的线程池(最大线程数 200),服务启动时,默认创建了  5  个空闲线程随时等待用户请求。首先,打开 ${TOMCAT_HOME}/conf/server.xml,搜索【<Executor name="tomcatThreadPool"】,开启并调整为

注意, Tomcat 7 在开启线程池前,一定要安装好 Apr 库,并可以启用,否则会有错误报出,shutdown.sh 脚本无法关闭进程。然后,修改<Connector …>节点,增加 executor 属性,搜索【port="8080"】,调整为

maxThreads :Tomcat 使用线程来处理接收的每个请求,这个值表示 Tomcat 可创建的最大的线程数,默认值是 200 minSpareThreads:最小空闲线程数,Tomcat 启动时的初始化的线程数,表示即使没有人使用也开这么多空线程等待,默认值是 10。maxSpareThreads:最大备用线程数,一旦创建的线程超过这个值,Tomcat 就会关闭不再需要的 socket 线程。

上边配置的参数,最大线程  500(一般服务器足以),要根据自己的实际情况合理设置,设置越大会耗费内存和  CPU,因为   CPU  疲于线程上下文切换,没有精力提供请求服务了,最小空闲线程数 20,线程最大空闲时间 60 秒,当然允许的最大线程连接数还受制于操作系统的内核参数设置,设置多大要根据自己的需求与环境。当然线程可以配置在“tomcatThreadPool”中,也可以直接配置在“Connector”中,但不可以重复配置。

URIEncoding:指定 Tomcat 容器的 URL 编码格式,语言编码格式这块倒不如其它 WEB 服务器软件配置方便,需要分别指定。

connnectionTimeout: 网络连接超时,单位:毫秒,设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒,可根据检测实际情况,适当修改。enableLookups: 是否反查域名,以返回远程主机的主机名,取值为:true 或 false,如果设置为false,则直接返回IP地址,为了提高处理能力,应设置为 false。disableUploadTimeout:上传时是否使用超时机制。

connectionUploadTimeout:上传超时时间,毕竟文件上传可能需要消耗更多的时间,这个根据你自己的业务需要自己调,以使Servlet有较长的时间来完成它的执行,需要与上  一个参数一起配合使用才会生效。

acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可传入连接请求的最大队列长度,超过这个数的请求将不予处理,默认为100个。keepAliveTimeout:长连接最大保持时间(毫秒),表示在下次请求过来之前,Tomcat 保持该连接多久,默认是使用 connectionTimeout 时间,-1 为不限制超时。

maxKeepAliveRequests:表示在服务器关闭之前,该连接最大支持的请求数。超过该请求数的连接也将被关闭,1表示禁用,-1表示不限制个数,默认100个,一般设置在100~200之间。

compression:是否对响应的数据进行 GZIP  压缩,off:表示禁止压缩;on:表示允许压缩(文本将被压缩)、force:表示所有情况下都进行压缩,默认值为off,压缩数据后可以有效的减少页面的大小,一般可以减小1/3左右,节省带宽。

compressionMinSize:表示压缩响应的最小值,只有当响应报文大小大于这个值的时候才会对报文进行压缩,如果开启了压缩功能,默认值就是2048。compressableMimeType:压缩类型,指定对哪些类型的文件进行数据压缩。

noCompressionUserAgents="gozilla, traviata": 对于以下的浏览器,不启用压缩。

如果已经对代码进行了动静分离,静态页面和图片等数据就不需要 Tomcat 处理了,那么也就不需要配置在 Tomcat 中配置压缩了。

以上是一些常用的配置参数属性,当然还有好多其它的参数设置,还可以继续深入的优化,HTTP Connector 与 AJP Connector 的参数属性值,可以参考官方文档的详细说明:

https://tomcat.apache.org/tomcat-7.0-doc/config/http.html https://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html

二、JVM 优化

Tomcat 启动命令行中的优化参数,就是 JVM 的优化 。Tomcat 首先跑在 JVM 之上的,因为它的启动其实也只是一个 java 命令行,首先我们需要对这个 JAVA 的启动命令行进行调优。不管是 YGC 还是 Full GC,GC 过程中都会对导致程序运行中中断,正确的选择不同的 GC 策略,调整 JVM、GC 的参数,可以极大的减少由于 GC 工作,而导致的程序运行中断方面的问题,进而适当的提高  Java 程序的工作效率。但是调整  GC 是以个极为复杂的过程,由于各个程序具备不同的特点,如:web 和  GUI 程序就有很大区别(Web可 以适当的停顿,但GUI停顿是客户无法接受的),而且由于跑在各个机器上的配置不同(主要 cup 个数,内存不同),所以使用的 GC 种类也会不同。

1、JVM 参数配置方法

Tomcat 的启动参数位于安装目录 ${JAVA_HOME}/bin目录下,Linux 操作系统就是 catalina.sh 文件。JAVA_OPTS,就是用来设置 JVM 相关运行参数的变量,还可以在

CATALINA_OPTS 变量中设置。关于这 2 个变量,还是多少有些区别的:

JAVA_OPTS:用于当 Java 运行时选项“start”、“stop”或“run”命令执行。CATALINA_OPTS:用于当 Java 运行时选项“start”或“run”命令执行。为什么有两个不同的变量?它们之间都有什么区别呢?

首先,在启动 Tomcat 时,任何指定变量的传递方式都是相同的,可以传递到执行“start”或“run”命令中,但只有设定在 JAVA_OPTS 变量里的参数被传递到“stop”命令中。对于

Tomcat 运行过程,可能没什么区别,影响的是结束程序,而不是启动程序。

第二个区别是更微妙,其他应用程序也可以使用 JAVA_OPTS 变量,但只有在 Tomcat 中使用 CATALINA_OPTS 变量。如果你设置环境变量为只使用 Tomcat,最好你会建议使用CATALINA_OPTS 变量,而如果你设置环境变量使用其它的 Java 应用程序,例如 JBoss,你应该把你的设置放在JAVA_OPTS 变量中。

2、JVM 参数属性

32 位系统下 JVM 对内存的限制:不能突破 2GB ,那么这时你的 Tomcat 要优化,就要讲究点技巧了,而在 64 位操作系统上无论是系统内存还是 JVM 都没有受到 2GB 这样的限制。

 
  

针对于 JMX 远程监控也是在这里设置,以下为 64 位系统环境下的配置,内存加入的参数如下:

为了看着方便,将每个参数单独写一行。上面参数好多啊,可能有人写到现在都没见过一个在     Tomcat    的启动命令里加了这么多参数,当然,这些参数只是我机器上的,不一定适合你,尤其是参数后的 value(值)是需要根据你自己的实际情况来设置的。

上述这样的配置,基本上可以达到: 系统响应时间增快;

JVM回收速度增快同时又不影响系统的响应率; JVM内存最大化利用;

线程阻塞情况最小化。

JVM 常用参数详解:

-server:一定要作为第一个参数,在多个  CPU  时性能佳,还有一种叫  -client  的模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试,在 32 位环境下直接运行 Java 程序默认启用该模式。Server 模式的特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境,在具有  64 位能力的 JDK 环境下默认启用该模式,可以不配置该参数。

-Xms:表示 Java 初始化堆的大小,-Xms 与-Xmx 设成一样的值,避免 JVM 反复重新申请内存,导致性能大起大落,默认值为物理内存的 1/64,默认(MinHeapFreeRatio参数可以调整)空余堆内存小于 40% 时,JVM 就会增大堆直到 -Xmx 的最大限制。

-Xmx:表示最大  Java   堆大小,当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃,因此一般建议堆的最大值设置为可用内存的最大值的80%。如何知道我的 JVM 能够使用最大值,使用 java -Xmx512M -version 命令来进行测试,然后逐渐的增大 512 的值,如果执行正常就表示指定的内存大小可用,否则会打印错误信息,默认值为物理内存的 1/4,默认(MinHeapFreeRatio参数可以调整)空余堆内存大于 70% 时,JVM 会减少堆直到-Xms 的最小限制。

-Xss:表示每个 Java 线程堆栈大小,JDK 5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 256K。根据应用的线程所需内存大小进行调整,在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。一般小的应用,  如果栈不是很深,  应该是128k  够用的,大的应用建议使用 256k 或 512K,一般不易设置超过 1M,要不然容易出现out ofmemory。这个选项对性能影响比较大,需要严格的测试。

-XX:NewSize:设置新生代内存大小。

-XX:MaxNewSize:设置最大新生代新生代内存大小

-XX:PermSize:设置持久代内存大小

-XX:MaxPermSize:设置最大值持久代内存大小,永久代不属于堆内存,堆内存只包含新生代和老年代。

-XX:+AggressiveOpts:作用如其名(aggressive),启用这个参数,则每当 JDK 版本升级时,你的 JVM 都会使用最新加入的优化技术(如果有的话)。

-XX:+UseBiasedLocking:启用一个优化了的线程锁,我们知道在我们的appserver,每个http请求就是一个线程,有的请求短有的请求长,就会有请求排队的现象,甚至还会出现线程阻塞,这个优化了的线程锁使得你的appserver内对线程处理自动进行最优调配。

-XX:+DisableExplicitGC:在 程序代码中不允许有显示的调用“System.gc()”。每次在到操作结束时手动调用 System.gc() 一下,付出的代价就是系统响应时间严重降低,就和关于

Xms,Xmx 里的解释的原理一样,这样去调用 GC 导致系统的 JVM 大起大落。

-XX:+UseConcMarkSweepGC:设置年老代为并发收集,即 CMS gc,这一特性只有 jdk1.5

后续版本才具有的功能,它使用的是 gc 估算触发和 heap 占用触发。我们知道频频繁的 GC 会造面 JVM

的大起大落从而影响到系统的效率,因此使用了 CMS GC 后可以在 GC 次数增多的情况下,每次 GC 的响应时间却很短,比如说使用了 CMS GC 后经过 jprofiler 的观察,GC 被触发次数非常多,而每次 GC 耗时仅为几毫秒。

-XX:+UseParNewGC:对新生代采用多线程并行回收,这样收得快,注意最新的 JVM 版本,当使用 -XX:+UseConcMarkSweepGC 时,-XX:UseParNewGC 会自动开启。因此, 如果年轻代的并行 GC 不想开启,可以通过设置 -XX:-UseParNewGC 来关掉。

-XX:MaxTenuringThreshold:设置垃圾最大年龄。如果设置为0的话,则新生代对象不经过   Survivor  区,直接进入老年代。对于老年代比较多的应用(需要大量常驻内存的应用),可以提高效率。如果将此值设置为一   个较大值,则新生代对象会在    Survivor   区进行多次复制,这样可以增加对象在新生代的存活时间,增加在新生代即被回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。该参数只有在串行  GC  时才有效,这个值的设置是根据本地的  jprofiler 监控后得到的一个理想的值,不能一概而论原搬照抄。

-XX:+CMSParallelRemarkEnabled:在使用 UseParNewGC 的情况下,尽量减少 mark 的时间。

-XX:+UseCMSCompactAtFullCollection:在使用 concurrent gc 的情况下,防止 memoryfragmention,对 live object 进行整理,使 memory 碎片减少。

-XX:LargePageSizeInBytes:指定 Java heap 的分页页面大小,内存页的大小不可设置过大, 会影响 Perm 的大小。

-XX:+UseFastAccessorMethods:使用 get,set 方法转成本地代码,原始类型的快速优化。

-XX:+UseCMSInitiatingOccupancyOnly:只有在 oldgeneration 在使用了初始化的比例后 concurrent collector 启动收集。

-Duser.timezone=Asia/Shanghai:设置用户所在时区。

-Djava.awt.headless=true:这个参数一般我们都是放在最后使用的,这全参数的作用是这样的,有时我们会在我们的 J2EE 工程中使用一些图表工具如:jfreechart,用于在web 网页输出 GIF/JPG 等流,在 winodws 环境下,一般我们的 app server 在输出图形时不会碰到什么问题,但是在linux/unix 环境下经常会碰到一个 exception 导致你在winodws 开发环境下图片显示的好好可是在 linux/unix 下却显示不出来,因此加上这个参数以免避这样的情况出现。

-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与 jmap -heap 中显示的 New gen 是不同的。整个堆大小 = 新生代大小 + 老生代大小 + 永久代大小。在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的 3/8。

-XX:CMSInitiatingOccupancyFraction:当堆满之后,并行收集器便开始进行垃圾收集,例如,当没有足够的空间来容纳新分配或提升的对象。对于 CMS 收集器,长时间等待是不可取的,因为在并发垃圾收集期间应用持续在运行(并且分配对象)。因此,为了在应用程序使用完内存之前完成垃圾收集周期,CMS 收集器要比并行收集器更先启动。因为不同的应用会有不同对象分配模式,JVM   会收集实际的对象分配(和释放)的运行时数据,并且分析这些数据,来决定什么时候启动一次   CMS   垃圾收集周期。这个参数设置有很大技巧,基本上满足(Xmx-Xmn)(100-CMSInitiatingOccupancyFraction)/100 >= Xmn 就不会出现 promotion failed。例如在应用中 Xmx 6000Xmn 512,那么 Xmx-Xmn 5488M,也就是老年代有 5488MCMSInitiatingOccupancyFraction=90 说明老年代到 90% 满的时候开始执行对老年代的并发垃圾回收(CMS),这时还 剩 10% 的空间是

548810% = 548M,所以即使 Xmn(也就是新生代共512M)里所有对象都搬到老年代里,548M 的空间也足够了,所以只要满足上面的公式,就不会出现垃圾回收时的

promotion failed,因此这个参数的设置必须与 Xmn 关联在一起。

-XX:+CMSIncrementalMode:该标志将开启 CMS 收集器的增量模式。增量模式经常暂停 CMS 过程,以便对应用程序线程作出完全的让步。因此,收集器将花更长的时间完成 整个收集周期。因此,只有通过测试后发现正常 CMS 周期对应用程序线程干扰太大时,才应该使用增量模式。由于现代服务器有足够的处理器来适应并发的垃圾收集,所以这种情况发生得很少,用于但 CPU情况。

-XX:NewRatio:年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代),-XX:NewRatio=4 表示年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5, Xms=Xmx 并且设置了 Xmn 的情况下,该参数不需要进行设置。

-XX:SurvivorRatio:Eden 区与 Survivor 区的大小比值,设置为 8,表示 2 个 Survivor 区(JVM 堆内存年轻代中默认有 2 个大小相等的 Survivor 区)与 1 个 Eden 区的比值为

2:8,即 1 个 Survivor 区占整个年轻代大小的 1/10。

-XX:+UseSerialGC:设置串行收集器。

-XX:+UseParallelGC:设置为并行收集器。此配置仅对年轻代有效。即年轻代使用并行收集,而年老代仍使用串行收集。

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集,JDK6.0 开始支持对年老代并行收集。

-XX:ConcGCThreads:早期 JVM 版本也叫-XX:ParallelCMSThreads,定义并发 CMS 过程运行时的线程数。比如 value=4 意味着 CMS 周期的所有阶段都以 4 个线程来执行。尽管更多的线程会加快并发 CMS 过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加 CMS 线程数是否真的能够带来性能的提升。如果还标志未设置,JVM 会根据并行收集器中的 -XX:ParallelGCThreads 参数的值来计算出默认的并行 CMS 线程数。

-XX:ParallelGCThreads:配置并行收集器的线程数,即:同时有多少个线程一起进行垃圾回收,此值建议配置与 CPU 数目相等。

-XX:OldSize:设置 JVM 启动分配的老年代内存大小,类似于新生代内存的初始大小 -XX:NewSize。

以上就是一些常用的配置参数,有些参数是可以被替代的,配置思路需要考虑的是     Java    提供的垃圾回收机制。虚拟机的堆大小决定了虚拟机花费在收集垃圾上的时间和频度。收集垃圾能够接受的速度和应用有关,应该通过分析实际的垃圾收集的时间和频率来调整。假如堆的大小很大,那么完全垃圾收集就会很慢,但是频度会降低。假如您把堆的大小和内存的需要一致,完全收集就很快,但是会更加频繁。调整堆大小的的目的是最小化垃圾收集的时间,以在特定的时间内最大化处理客户的请求。在基准测试的时候,为确保最好的性能,要把堆的大小设大,确保垃圾收集不在整个基准测试的过程中出现。

假如系统花费很多的时间收集垃圾,请减小堆大小。一次完全的垃圾收集应该不超过 3-5 秒。假如垃圾收集成为瓶颈,那么需要指定代的大小,检查垃圾收集的周详输出,研究垃圾收集参数对性能的影响。当增加处理器时,记得增加内存,因为分配能够并行进行,而垃圾收集不是并行的。

3、设置系统属性

之前说过,Tomcat 的语言编码,配置起来很慢,要经过多次设置才可以了,否则中文很有可能出现乱码情况。譬如汉字“中”,以 UTF-8 编码后得到的是 3 字节的值

%E4%B8%AD,然后通过 GET 或者 POST 方式把这 3 个字节提交到 Tomcat 容器,如果你不告诉 Tomcat 我的参数是用 UTF-8编码的,那么 Tomcat 就认为你是用 ISO-8859-1 来编码的,而 ISO8859-1(兼容 URI 中的标准字符集 US-ASCII)是兼容 ASCII 的单字节编码并且使用了单字节内的所有空间,因此 Tomcat 就以为你传递的用 ISO-8859-1 字符集编码过的 3 个字符,然后它就用 ISO-8859-1 来解码。

设置起来不难使用“ -D<名称>=<值> ”来设置系统属性:

-Djavax.servlet.request.encoding=UTF-8

-Djavax.servlet.response.encoding=UTF-8

-Dfile.encoding=UTF-8

-Duser.country=CN

-Duser.language=zh

4、常见的 Java 内存溢出有以下三种

(1) java.lang.OutOfMemoryError: Java heap space —-JVM Heap(堆)溢出

JVM 在启动的时候会自动设置 JVM Heap 的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)不可超过物理内存。可以利用 JVM提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap 的大小是 Young Generation 和 Tenured Generaion 之和。在 JVM 中如果 98% 的时间是用于 GC,且可用的 Heap size 不足 2% 的时候将抛出此异常信息。

解决方法:手动设置 JVM Heap(堆)的大小。

(2) java.lang.OutOfMemoryError: PermGen space —- PermGen space溢出。

PermGen space 的全称是 Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被 JVM 存放Class 和 Meta 信息的,Class 在被 Load 的时候被放入 PermGen space 区域,它和存放 Instance 的 Heap 区域不同,sun 的 GC 不会在主程序运行期对 PermGen space 进行清理,所以如果你的 APP 会载入很多 CLASS 的话,就很可能出现 PermGen space 溢出。

解决方法: 手动设置 MaxPermSize 大小

(3) java.lang.StackOverflowError —- 栈溢出

栈溢出了,JVM 依然是采用栈式的虚拟机,这个和 C 与 Pascal 都是一样的。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的  “层”太多了,以致于把栈区溢出了。通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K 的空间(这个大约相当于在一个 C 函数内声明了 256 个 int 类型的变

量),那么栈区也不过是需要 1MB 的空间。通常栈的大小是 1-2MB 的。通常递归也不要递归的层次过多,很容易溢出。

解决方法:修改程序。

 设计模式

设计原则

工厂方法模式抽象工厂模式单例模式

建造者模式原 型 模 式 适配器模式装饰器模式代 理 模 式 外 观 模 式 桥 接 模 式 组 合 模 式 享 元 模 式 策略模式

模板方法模式观察者模式 迭代子模式 责任链模式 命令模式

备忘录模式

状 态 模 式 访问者模式中介者模式解释器模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值