DSLBQ'S_BLOG

作者: dslbq

  • VLAN与Trunk

    VLAN与Trunk

    介绍

    网络性能是影响业务效率的一个重要因素。将大型广播域分段是提高网络性能的方法之一。路由器能够将广播包阻隔在一个接口上,但是,路由器的LAN接口数量有限,它的主要功能是在网络间传输数据,而不是对终端设备提供网络接入。访问LAN的功能还是由接入层交换机来实现。与三层交换机相类似,通过在二层交换机上创建VLAN来减少广播域。现代交换机就是通过VLAN来构造的,因此在某种程度上,学习交换机就是学习VLAN。

    更多信息

    问题的产生:

    广播域即广播帧传播覆盖的范围。如下图所示,当网络上的所有设备在广播域产生大量的广播以及多播帧,就会与数据流竞争带宽。这是由网络管理数据流组成,如:ARP,DHCP,STP等。如下图所示,假设PC 1产生ARP,Windows登录,DHCP等请求:

    这些广播帧到达交换机1之后,遍历整个网络并到达所有节点直至路由器。随着网络节点增加,开销的总数也在增长,直至影响交换机性能。通过实施VLAN断开广播域将数据流隔离开来,能够解决这一问题。

    什么是VLAN:

    VLAN(virtual local area network)是一组与位置无关的逻辑端口。VLAN就相当于一个独立的三层网络。VLAN的成员无需局限于同一交换机的顺序或偶数端口。下图显示了一个常规的部署,左边这张图节点连接到交换机,交换机连接到路由器。所有的节点都位于同一IP网络,因为他们都连接到路由器同一接口。

    图中没有显示的是,缺省情况下,所有节点实际上都是同一VLAN。因此,这种拓扑接口可看作是基于同一VLAN的,如上面右图所示。例如,Cisco设备默认VLAN是VLAN 1,也称为管理VLAN。默认配置下包含所有的端口,体现在源地址表(source address table,SAT)中。该表用于交换机按照目的MAC地址将帧转发至合适的二层端口。引入VLAN之后,源地址表按照VLAN将端口与MAC地址相对应起来,从而使得交换机能够做出更多高级转发决策。下图显示了show mac address table和show vlan命令的显示输出。所有端口(FA0/1 – FA0/24)都在VLAN 1。

    另一种常用的拓扑结构是两个交换机被一个路由器分离开来,如下图所示。这种情况下,每台交换机各连接一组节点。每个交换机上的各节点共享一个IP地址域,这里有两个网段:192.168.1.0和192.168.2.0。

    注意到两台交换机的VLAN相同。非本地网络数据流必须经过路由器转发。路由器不会转发二层单播,多播以及广播帧。这种拓扑逻辑在两个地方类似于多VLAN:同一VLAN下的节点共享一个通用地址域,非本地数据流(对应多VLAN情况不同VLAN的节点)需通过路由器转发。在一台交换机上添加一个VLAN,去掉另一台交换机的话,结构如下所示:

    每一个VLAN相当于一个独立的三层IP网络,因此,192.168.1.0上的节点试图与192.168.2.0上的节点通信时,不同VLAN通信必须通过路由器,即使所有设备都连接到同一交换机。二层单播,多播和广播数据只会在同一VLAN内转发及泛洪,因此VLAN 1产生的数据不会为VLAN2节点所见。只有交换机能看得到VLAN,节点和路由器都感觉不到VLAN的存在。添加了路由决策之后,可以利用3层的功能来实现更多的安全设定,更多流量以及负载均衡。

    VLAN的作用:

    安全性:每一个分组的敏感数据需要与网络其他部分隔离开,减少保密信息遭到破坏的可能性。如下图所示,VLAN 10上的教职工主机完全与学生和访客数据隔离。

    节约成本:无需昂贵的网络升级,并且带宽及上行链路利用率更加有效。
    性能提高:将二层网络划分成多个逻辑工作组(广播域)减少网络间不必要的数据流并提升性能。
    缩小广播域:减少一个广播域上的设备数量。如上图所示:网络上有六台主机但有三个广播域:教职工,学生,访客。
    提升IT管理效率:网络需求相似的用户共享同一VLAN,从而网络管理更为简单。当添加一个新的交换机,在指定端口VLAN时,所有策略和步骤已配置好。
    简化项目和应用管理:VLAN将用户和网络设备汇集起来,以支持不同的业务或地理位置需求。

    每一个VLAN对应于一个IP网络,因此,部署VLAN的时候必须结合考虑网络地址层级的实现情况。

    交换机间VLAN:

    多交换机的情况下,VLAN是怎么工作的呢?下图所示的这种情况,两个交换机VLAN相同,都是默认VLAN 1,即两个交换机之间的联系同在VLAN 1之内。路由器是所有节点的出口。

    这时单播,多播和广播数据自由传输,所有节点属于同一IP地址。这时节点之间的通信不会有问题,因为交换机的SAT显示它们在同一VLAN。

    而下面这种连接方式就会有问题。由于VLAN在连接端口的主机之间创建了三层边界,它们将无法通信。

    仔细看上图,这里有很多问题。第一,所有主机都在同一IP网,尽管连接到不同的VLAN。第二,路由器在VLAN 1,因此与所有节点隔离。最后,两台交换机通过不同的VLAN互连。每一点都会造成通信阻碍,合在一起,网络各元素之间会完全无法通信。

    交换机用满或同一管理单元物理上彼此分离的情形是很常见的。这种情况下,VLAN需要通过trunk延伸至相邻交换机。trunk能够连接交换机,在网络间传载VLAN信息。如下图所示:

    对之前的拓扑的改进包括:

    • PC1和PC2分配到192.168.1.0网段以及VLAN2。
    • PC3和PC4分配到192.168.2.0网段以及VLAN3。
    • 路由器接口连接到VLAN2和VLAN3。
    • 交换机间通过trunk线互连。

    注意到trunk端口出现在VLAN 1,他们没有用字母T来标识。trunk在任何VLAN都没有成员。现在VLAN跨越多交换机,同一VLAN下的节点可以物理上位于任何地方。

    什么是Trunk:

    Trunk是在两个网络设备之间承载多于一种VLAN的端到端的连接,将VLAN延伸至整个网络。没有VLAN Trunk,VLAN也不会非常有用。VLAN Trunk允许VLAN数据流在交换机间传输,所以设备在同一VLAN,但连接到不同交换机,能够不通过路由器来进行通信。

    一个VLAN trunk不属于某一特定VLAN,而是交换机和路由器间多个VLAN的通道。如下图所示,交换机S1和S2,以及S1和S3之间的链路,配置为传输从VLAN10,20,30以及90的数据流。该网络没有VLAN trunk就无法工作。

    当安装好trunk线之后,帧在trunk线传输是就可以使用trunk协议来修改以太网帧。这也意味着交换机端口有不止一种操作模式。缺省情况下,所有端口都称为接入端口。当一个端口用于交换机间互连传输VLAN信息时,这种端口模式改变为trunk,节点也路由器通常不知道VLAN的存在并使用标准以太网帧或“untagged”帧。trunk线能够使用“tagged”帧来标记VLAN或优先级。

    因此,在trunk端口,运行trunk协议来允许帧中包含trunk信息。如下图所示:

    PC 1在经过路由表处理后向PC 2发送数据流。这两个节点在同一VLAN但不同交换机。步骤如下:

    • 以太网帧离开PC 1到达Switch 1。
    • Switch 1的SAT表明目的地是trunk线的另一端。
    • Switch 1使用trunk协议在以太网帧中添加VLAN id。
    • 新帧离开Switch 1的trunk端口被Switch 2接收。
    • Switch 2读取trunk id并解析trunk协议。
    • 源帧按照Switch 2的SAT转发至目的地(端口4)。

    VLAN tag如下图所示,包含类型域,优先级域,CFI(Canonical Format Indicator)指示MAC数据域,VLAN ID。

  • 交换机

    交换机

    介绍

    本节介绍交换机的帧转发技术,MAC地址表的维护方式,三种帧转发模式,以及冲突域和广播域。

    更多信息

    帧转发:

    网络及电信中的交换概念

    以太网上的帧包含源MAC地址与目的MAC地址。交换机从源设备接收到帧并快速发往目的地址。交换的基本概念指基于以下两条准则做出决策的设备:

    • 进入(ingress)端口
    • 目的地址

    术语ingress用于描述帧通过特定端口进入设备,egress用于描述设备通过特定端口离开设备。交换机做出转发决定的时候,是基于进入端口以及消息的目的地址的。

    LAN交换机维护一张表,通过这张表决定如何转发数据流。LAN交换机唯一智能部分是利用这张表基于消息的进入端口和目的地址来转发。一个LAN交换机中只有一张定义了地址和端口的主交换表;因此,无论进入端口如何,同一目的地址的消息永远从同一出口离开。

    MAC地址表的动态更新

    一个交换机要知道使用哪一个端口传送帧,首先必须学习各端口有哪些设备。随着交换机学习到端口与设备的关系,它建立起一张MAC地址表,或内容可寻址寄存表(CAM)。CAM是一种应用于高速查找应用的特定类型的memory。交换机将连接到它的端口的设备的MAC地址记录到MAC表中,然后利用表中信息将帧发送至输出端口设备,该端口已指定给该设备。

    记住交换机操作模式的一句简单的话是:交换机学习“源地址”,基于“目的地址”转发。帧进入交换机时,交换机“学习”接收帧的源MAC地址,并将此地址添加到MAC地址表中,或刷新已存在的MAC地址表项的老化寄存器;后续报文如果去往该MAC地址,则可以根据此表项转发。帧转发时,交换机检查目的MAC地址并与MAC地址表中地址进行比较。如果地址在表中,则转发至表中与MAC地址相对应的端口。如果没有在表中找到目的MAC地址,交换机会转发到除了进入端口以外的所有端口泛洪(flooding)。有多个互连交换机的网络中,MAC地址表对于一个连接至其他交换机的端口记录多个MAC地址。

    以下步骤描述了更新MAC地址表的方法:

    1. 交换机在port 1接收到来自PC 1的帧。

    2. 交换机检查源MAC地址并与MAC地址表相比较。

    • 如果地址不在表中,则交换机在MAC地址表中将PC 1的源MAC地址关联到进入端口(port 1)。
    • 如果已经存在该源地址的MAC地址表项,则交换机重置老化计时器。通常一个表项会保持5分钟。

    3. 交换机记录源地址信息之后,检查目的地址

    • 如果目的MAC地址不在表项中或如果它是一个广播MAC地址,则交换机把该帧泛洪(flood)至除了进入端口以外的所有端口。

    4. 目标设备(PC 3)返回目的地址为PC 1的单播帧。

    5. 交换机地址表中输入PC 3的源MAC地址以及进入端口的端口号。在表项中找到该帧的目的地址及关联的输出端口。

    6. 交换机现在可以在源和目标设备之间传送帧而无需泛洪,因为地址表中已有指定关联端口的表项。

    交换机转发方式:

    存储转发交换(Store-and-Forward)

    运行在存储转发模式下的交换机在发送信息前要把整帧数据读入内存并检查其正确性。尽管采用这种方式比采用直通方式更花时间,但采用这种方式可以存储转发数据,从而保证其准确性。由于运行在存储转发模式下的交换机不传播错误数据,因而更适合大型局域网。存储转发模式有两大主要特征区别于直通转发模式:

    差错控制:

    使用存储转发技术的交换机对进入帧进行差错控制。在进入端口接收完整一帧之后,交换机将数据报最后一个字段的帧校验序列(frame check sequence, FCS)与自己的FCS进行比较。FCS校验过程用以帮助确保帧没有物理及数据链路错误,如果该帧校验正确,则交换机转发。否则,丢弃。

    自动缓存:

    存储转发交换机通过进入端口缓存,支持不同速率以太网的混合连接。例如,接收到一个以1Gb/s速率发出的帧,转发至百兆以太网端口,就需要使用存储转发方式。当进入与输出端口速率不匹配时,交换机将整帧内容放入缓存中,计算FCS校验,转发至输出缓存之后将帧发出。

    Cisco的主要交换方式是存储转发交换。

    直通交换(Cut-Through)

    直通交换的一个优势是比存储转发技术更为快速。采用直通模式的交换机会在接收完整个数据包之前就读取帧头,并决定把数据发往哪个端口。不用缓存数据也不用检查数据的完整性。这种交换方式有两大特点:快速帧转发以及无效帧处理。

    快速帧转发:

    如下图所示,一旦交换机在MAC地址表中查找到目的MAC地址,就立刻做出转发决定。而无需等待帧的剩余部分进入端口再做出转发决定。

    使用直通方式的交换机能够快速决定是否有必要检查帧头的更多部分,以针对额外的过滤目的。例如,交换机可以检查前14个字节(源MAC地址,目的MAC,以太网类型字段),以及对之后的40字节进行检查,以实现IPv4三层和四层相关功能。

    无效帧处理:

    对于大多数无效帧,直通方式交换机并不将其丢弃。错误帧被转发至其他网段。如果网络中出现高差错率(无效帧),直通交换可能会对带宽造成不利影响,损坏以及无效帧会造成带宽拥塞。在拥塞情况下,这种交换机必须像存储转发交换机那样缓存。

    无碎片转发(Fragment Free)

    无碎片转发是直通方式的一种改进模式。交换机转发之前检查帧是否大于64字节(小于则丢弃),以保证没有碎片帧。无碎片方式比直通方式拥有更好的差错检测,而实际上没有增加延时。它比较适合于高性能计算应用,即进程到进程延时小于10毫秒的应用场景。

    交换机域:

    交换机比较容易混淆的两个术语是冲突域和广播域。这一段讲述这两个影响LAN性能的重要概念。

    冲突域

    设备间共享同一网段称为冲突域。因为该网段内两个以上设备同时尝试通讯时,可能发生冲突。使用工作在数据链路层的交换机可将各个网段的冲突域隔离,并减少竞争带宽的设备数量。交换机的每一个端口就是一个新的网段,因为插入端口的设备之间无需竞争。结果是每一个端口都代表一个新的冲突域。网段上的设备可以使用更多带宽,冲突域内的冲突不会影响到其他网段,这也成为微网段。

    如下图所示,每一个交换机端口连接到一台主机,每一个交换机端口代表一个隔离的冲突域。

    广播域

    尽管交换机按照MAC地址过滤大多数帧,它们并不能过滤广播帧。LAN上的交换机接收到广播包后,必须对所有端口泛洪。互连的交换机集合形成了一个广播域。网络层设备如路由器,可隔离二层广播域。路由器可同时隔离冲突和广播域。

    当设备发出二层广播包,帧中的目的MAC地址被设置为全二进制数,广播域中的所有设备都会接收到该帧。二层广播域也称为MAC广播域。MAC广播域包含LAN上所有接收到广播帧的设备。广播通信比较多时,可能会带来广播风暴。特别是在包含不同速率的网段,高速网段产生的广播流量可能导致低速网段严重拥挤,乃至崩溃。

  • 网络传输

    网络传输

    首先来看一个例子:

    示例:网络服务器向客户端传送数据的过程:

    在详细阐述网络传输过程之前,先来看一个最常见的例子,下图显示了一个网络服务器向客户端传送数据的完整过程:

    数据封装

    消息要在网络中传输,必须对它进行编码,以特定的格式进行封装,同时需要适当地封装以足够的控制和地址信息,以使它能够从发送方移动到接收方。

    消息大小

    理论上,视频或邮件信息是能够以大块非中断型流从网络源地址传送到目的地址,但这也意味着同一时刻同一网络其他设备就无法收发消息。这种大型数据流会造成显著延时。并且,如果传输过程中连接断开,整个数据流都会丢失需要全部重传。因此更好的方法是将数据流分割(segmentation)为较小的,便于管理的片段,能够带来两点好处:

    • 发送较小片段,网络上同时可有多个会话交错进行。这种在网络上将不同会话片段交错进行的过程称为多路传输(multiplexing)。
    • 分割可提高网络通讯的可靠性。各消息片段从源地址到目的地址无需经过相同路径,如果一条路径被堵塞或断开,其余消息可从替换路径到达目的地址。如果部分消息到不了目的地址,那只需重传丢失部分。

    通过对片段打上标签的方式来保证顺序以及在接收时重组。

    协议数据单元(Protocol Data Unit, PDU)

    应用层数据在传输过程中沿着协议栈传递,每一层协议都会向其中添加信息。这就是封装的过程。

    数据片段在各层网络结构中采用的形式就称为协议数据单元(PDU)。封装过程中,下一层对从上一层收到的PDU进行封装。在处理的每一个阶段PDU都有不同的名字来反应它的功能。

    PDU按照TCP/IP协议的命名规范:

    • 数据(Data):应用层PDU的常用术语
    • 分段(Segment):传输层PDU
    • 帧(Frame):网络层PDU
    • 比特(Bits):在介质上物理传输数据所使用的PDU。

    封装

    封装是指在传输之前为数据添加额外的协议头信息的过程。在绝大多数数据通信过程中,源数据在传输前都会封装以数层协议。在网络上发送消息时,主机上的协议栈从上至下进行操作。

    以网络服务器为例,HTTP应用层协议发送HTML格式网页数据到传输层,应用层数据被分成TCP分段。各TCP分段被打上标签,称为头(header),表明接收方哪一个进程应当接收此消息。同时也包含使得接收方能够按照原有的格式来重组数据的信息。

    传输层将网页HTML数据封装成分段并发送至网络层,执行IP层协议。整个TCP分段封装成IP报文,也就是再添上IP头标签。IP头包括源和目的IP地址,以及发送报文到目的地址所必须的信息。

    之后,IP报文发送到接入层,封装以帧头和帧尾。每个帧头都包含源和目的物理地址。物理地址唯一指定了本地网络上的设备。帧尾包含差错校正信息。最后,由服务器网卡将比特编码传输给介质。

    解封装

    接收主机以相反的方式进行操作称为解封装。解封装是接收设备移除一层或多层协议头的过程。数据在协议栈中向上移动直到终端应用层伴随着解封装。

    访问本地资源:

    访问本地网络资源需要两种类型的地址:网络层地址和数据链路层地址。网络层和数据链路层负责将数据从发送设备传输至接收设备。两层协议都有源和目的地址,但两种地址的目的不同。

    示例:客户端PC1与FTP在同一IP网络的通信

    网络地址

    网络层地址或IP地址包含两个部分:网络前缀和主机。路由器使用网络前缀部分将报文转发给适当的网络。最后一个路由器使用主机部分将报文发送给目标设备。同一本地网络中,网络前缀部分是相同的,只有主机设备地址部分不同。

    源IP地址:发送设备,即客户端PC1的IP地址:192.168.1.110
    目的IP地址:接收设备,即FTP服务器:192.168.1.9

    数据链路地址

    数据链路地址的目的是在同一网络中将数据链路帧从一个网络接口发送至另一个网络接口。以太网LAN和无线网LAN是两种不同物理介质的网络示例,分别有自己的数据链路协议。

    当IP报文的发送方和接收方位于同一网络,数据链路帧直接发送到接收设备。以太网上数据链路地址就是以太网MAC地址。MAC地址是物理植入网卡的48比特地址。

    源MAC地址:发送IP报文的PC1以太网卡MAC地址,AA-AA-AA-AA-AA-AA。
    目的MAC地址:当发送设备与接收设备位于同一网络,即为接收设备的数据链路地址。本例中,FTP MAC地址:CC-CC-CC-CC-CC-CC。

    源和目的MAC地址添加到以太网帧中。

    MAC与IP地址

    发送方必须知道接收方的物理和逻辑地址。发送方主机能够以多种方式学习到接收方的IP地址:比如域名系统(Domain Name System, DNS),或通过应用手动输入,如用户指定FTP地址。

    以太网MAC地址是怎么识别的呢?发送方主机使用地址解析协议(Address Resolution Protocol, ARP)以检测本地网络的所有MAC地址。如下图所示,发送主机在整个LAN发送ARP请求消息,这是一条广播消息。ARP请求包含目标设备的IP地址,LAN上的每一个设备都会检查该ARP请求,看看是否包含它自身的IP地址。只有符合该IP地址的设备才会发送ARP响应。ARP响应包含ARP请求中IP地址相对应的MAC地址。

    访问远程资源:

    默认网关

    当主机发送消息到远端网络,必须使用路由器,也称为默认网关。默认网关就是位于发送主机同一网络上的路由器的接口IP地址。有一点很重要:本地网络上的所有主机都能够配置自己的默认网关地址。如果该主机的TCP/IP设置中没有配置默认网关地址,或指定了错误的默认网关地址,则远端网络消息无法被送达。

    如下图所示,LAN上的主机PC 1使用IP地址为192.168.1.1的R1作为默认网关,如果PDU的目的地址位于另一个网络,则主机将PDU发送至路由器上的默认网关。

    与远端网络设备通讯

    下图显示了客户端主机PC 1与远端IP网络服务器进行通讯的网络层地址与数据链路层地址:

    网络地址

    当报文的发送方与接收方位于不同网络,源和目的IP地址将会代表不同网络上的主机。

    源IP地址:发送设备即客户端主机PC 1的IP地址:192.168.1.110。
    目的IP地址:接收设备即网络服务器的IP地址:172.16.1.99。

    数据链路地址

    当报文的发送方与接收方位于不同网络,以太网数据链路帧无法直接被发送到目的主机。以太网帧必须先发送给路由器或默认网关。本例中,默认网关是R1,R1的接口IP地址与PC 1属于同一网络,因此PC 1能够直接达到路由器。

    源MAC地址:发送设备即PC 1的MAC地址,PC1的以太网接口MAC地址为:AA-AA-AA-AA-AA-AA。
    目的MAC地址:当报文的发送方与接收方位于不同网络,这一值为路由器或默认网关的以太网MAC地址。本例中,即R1的以太网接口MAC地址,即:11-11-11-11-11-11。

    IP报文封装成的以太网帧先被传输至R1,R1再转发给目的地址即网络服务器。R1可以转发给另一个路由器,如果目的服务器所在网路连接至R1,则直接发送给服务器。

    发送设备如何确定路由器的MAC地址?每一个设备通过自己的TCP/IP设置中的默认网关地址得知路由器的IP地址。之后,它通过ARP来得知默认网关的MAC地址,该MAC地址随后添加到帧中。

  • TCP窗口调整与流控

    TCP窗口调整与流控

    介绍

    在客户端与服务器的连接中,客户端告知服务器它一次希望从服务器接收多少字节数据,这是客户端的接收窗口,即服务器的发送窗口。类似地,服务器告知客户端一次希望从客户端接收多少字节数据,也就是服务器的接收窗口和客户端的发送窗口。

    要理解为什么窗口大小会产生波动,首先需要理解它的含义。最简单的方式是它代表了设备对于特定连接的接收缓存大小。即,窗口大小代表一个设备一次能够从对端处理多少数据,之后再传递给应用层处理。

    更多信息

    当服务器从客户端接收数据,它就将数据放在缓存中,服务器必须对数据做以下两步操作:

    确认:服务器必须将确认信息发回客户端以表明数据接收。
    传输:服务器必须处理数据,将它传递给目标应用程序处理

    区分开这两件事情是非常重要的。关键在于基本的滑动窗口机制中,数据于接收时确认,但并不一定立即从缓存中传输出去。也就意味着当接收数据速度快于接收TCP处理速度时,缓存有可能被填满。当这一情况发生时,接收设备需要调整窗口大小已防止缓存过载。

    由于窗口大小能够以这种方式管理连接两端设备数据流的速率,TCP就是以这种方式实现流控这一传输层非常典型的任务。流控对于TCP来说是很重要的,因为它是设备间互通状态的方式。通过增加或缩小窗口大小,服务器和客户端能够确保对端发送数据的速度等同于处理速度。

    减小窗口大小以降低发送速率:

    首先看一下客户端到服务器的数据传输,如下图所示:

    客户端传输140字节数据至服务器。之后,客户端的可用窗口还剩下220字节:发送窗口的360字节减去发送的140字节。

    一段时间过后,服务器接收到140字节并将它们放在缓存中。现在,理想的情况下,140字节进入缓存,确认之后立刻从缓存移出。也就是说,缓存有足够的大小来容纳客户端发送的所有数据。缓存的空闲空间维持在360字节,因此告知客户端窗口大小保持不变。

    只要服务器处理速度和数据进入速度相同,窗口大小就会保持在360字节。客户端在接收到140字节的确认信息以及窗口大小保持不变的信息之后,将360字节窗口向右移动140字节。由于现在未确认字节数为0,因此客户端又可以发送360字节数据。对应于之前可用窗口的220字节,加上刚刚确认的140字节数据。

    然而,现实中服务器可能需要处理数十,数百乃至数千个TCP连接。TCP可能无法立刻处理数据,或应用应用程序本身无法接收140字节数据。任何一种情况下,服务器TCP都无法立刻将140字节从缓存中移出。这时,除了发回确认信息给客户端以外,服务器会想要告知客户端更改窗口大小,以表示缓存已经被部分写入了。

    假设我们接收到140字节,但只能发送40字节给应用程序,缓存中剩下100字节。当发送140字节的确认信息,服务器将发送窗口缩小100字节,至260字节。当客户端从服务器接收到这一片段,它将会看到140字节的确认信息并将窗口向右滑动140字节。在滑动过程中,将大小缩减至260字节。可以认为将窗口左端滑动140字节,但右端仅滑动40字节。新的稍小一些的窗口保证服务器从客户端接收最多260字节数据,以适应接收缓存中的剩余空间,如下图的1-3步所示:

    缩减发送窗口以停止发送新数据:

    如果服务器无法接收任何新数据会怎么样呢?假设客户端下一次传输180字节,但是服务器太忙碌而无法对其进行处理。这种情况下,服务器将这180字节缓存下来,并且在确认信息中,将窗口大小从260字节缩减为80字节。当客户端接收到180字节的确认信息,它也会看到窗口缩减了180字节,它会滑动与缩减同样的大小,告知服务器:我确认接收180字节数据,但不允许你再发送新的数据。也可以看作窗口左端滑动180字节,但右端维持不动。只要右端不移动,客户端就无法发送更多数据。这一过程显示在上图的4-6中。

    关闭发送窗口:

    窗口调整可以通过双方设备来完成。如果服务器从客户端接收的数据持续快于推送给应用的速率,则服务器将会继续减小接收窗口。假设发送窗口减小至80字节,客户端发送第三个请求,长度为80字节,但服务器仍处于繁忙状态。之后服务器将窗口减小为0,也称为关闭窗口。这一信息告知客户端服务器已经过载,它需要彻底停止发送数据,如上图最后一步所示。之后,当服务器负载减轻时,可以再次增加这一连接的窗口,允许更多数据传输。

  • TCP确认机制

    TCP确认机制

    介绍

    在TCP确认机制中,无法有效处理非连续TCP片段。确认号表明所有低于该编号的sequence number已经被发送该编号的设备接收。如果我们收到的字节数落在两个非连续的范围内,则无法只通过一个编号来确认。这可能导致潜在严重的性能问题,特别是高速或可靠性较差的网络。

    更多信息

    还是以下图为例,服务器发送了4个片段并收到1条回复,确认号为201。因此,片段1和片段2被当成已确认。它们从重传队列中移出,同时允许服务器发送窗口向右移动200字节,从而发送数据增加200个字节。

    然而,再次假设片段3,从sequence number201开始,在发送过程中丢失了。由于客户端从没有收到这一片段,所以它也无法发送确认号高于201的确认信息,从而导致滑动窗口停滞。服务器可以继续发送其他片段直到填满客户端的接收窗口,但是直到客户端发送另一条确认信息,服务器的发送窗口都不会滑动。

    另一个问题是如果片段3丢失了,客户端将无法告知服务器是否收到后续的片段。在客户端接收窗口填满之前,很有可能客户端已经接收到片段4以及之后的片段。但是客户端无法发送值为501的确认信息以表明接收到片段4,因为这意味着片段3也接收到了。

    这里我们看到了TCP单编号,累积确认机制的缺点。我们可以想象一个最差的情况,服务器被告知它有一个10,000字节窗口,20个片段每个片段500字节。第一个片段丢失了,其他19个被接收到了。但是由于第一个片段从没有接收到,其他19个也无法确认。

    未确认片段处理策略:

    我们怎样处理丢失片段之后的片段呢?本例中,当服务器片段3重传超时,它必须决定怎样处理片段4,它不知道客户端是否已经接收到。在上述最差情况下,第一个片段丢失后,其余19个可能或可能无法被客户端接收到。

    处理这种情况有两种可能的方式:

    仅重传超时片段:这是一种更加保守的方式,仅重传超时的片段,希望其他片段都能够成功接收。如果该片段之后的其他片段实际上接收到了,这一方式是最佳的,如果没接收到,就无法正常执行。后者的情况每一个片段需要单独计时并重传。假设上述最坏情况下,所有20个500字节片段都丢失了。我们需要等片段1超时并重传。这一片段也许会得到确认,但之后我们需要等待片段2超时并重传。这一过程会重复多次。

    重传所有片段:这是一种更激进或者说更悲观的方式。无论何时一个片段超时了,不仅重传该片段,还有所有其他尚未确认的片段。这一方式确保了任何时间都有一个等待确认的停顿时间,在所有未确认片段丢失的情况下,会刷新全部未确认片段,以使对端设备多一次接收机会。在所有20个片段都丢失的情况下,相对于第一种方式节省了大量时间。这种方式的问题在于可能这些重传是不必要的。如果第一个片段丢失而其他19个实际上接收到了,也得重传那9500字节数据。

    由于TCP不知道其他片段是否接收到,所以它也无法确认哪种方法更好,但只能选择一种方式。上图示例了保守的方式,而下图显示的是激进的方式:

    问题的关键在于无法确认非连续片段。解决方式是对TCP滑动窗口算法进行扩展,添加允许设备分别确认非连续片段的功能。这一功能称为选择确认(selective acknowledgment,SACK)。

    选择确认:

    通过SACK,连接的两方设备必须同时支持这一功能,通过连接时使用的SYN片段来协商是否允许SACK。这一过程完成之后,任一设备都可以在常规TCP片段中使用SACK选项。这一选项包含一个关于已接收但未确认片段数据sequence number范围的列表,由于它们是非连续的。

    各设备对重传队列进行修改,如果该片段已被选择确认过,则该片段中的SACK比特位置为1。该设备使用图2中激进方式的改进版本,一个片段重传之后,之后所有片段也会重传,除非SACK比特位为1。

    例如,在4个片段的情况下,如果客户端接收到片段4而没有接收到片段3,当它发回确认号为201(片段1和片段2)的确认信息,其中包含一个SACK选项指明:“已接收到字节361至500,但尚未确认”。如果片段4在片段1和2之后到达,上述信息也可以通过第二个确认片段来完成。服务器确认片段4的字节范围,并为片段4打开SACK位。当片段3重传时,服务器看到片段4的SACK位为1,就不会对其重传。如下图所示。

    在片段3重传之后,片段4的SACK位被清除。这是为了防止客户端出于某种原因改变片段4已接收的想法。客户端应当发送确认号为501或更高的确认信息,正式确认片段3和4接收到。如果这一情况没有发生,服务器必须接收到片段4的另一条选择确认信息才能将它的SACK位打开,否则,在片段3重传时或计时器超时的情况下会对其自动重传。

  • TCP重传

    TCP重传

    介绍

    TCP的主要任务是很简单:打包和发送数据。TCP与其他协议的不同之处在于使用滑动窗口来管理基本数据收发过程,同时确保数据流的有效及可靠传输,从而不致发送速率明显快于接收速率。本文将描述TCP是如何确保设备可靠、有效地进行传输的。首先阐述TCP检测丢失片段以及重传的基本方法,之后介绍TCP如何判断一个片段为丢失片段。

    更多信息

    TCP片段重传计时器以及重传队列:

    检测丢失片段并对之重传的方法概念上是很简单的。每一次发送一个片段,就开启一个重传计时器。计时器有一个初始值并随时间递减。如果在片段接收到确认之前计时器超时,就重传片段。TCP使用了这一基本技术,但实现方式稍有不同。原因在于为了提高效率需要一次处理多个未被确认的片段,以保证每一个在恰当的时间重传。TCP按照以下特定顺序工作:

    放置于重传队列中,计时器开始 包含数据的片段一经发送,片段的一份复制就放在名为重传队列的数据结构中,此时启动重传计时器。因此,在某些时间点,每一个片段都会放在队列里。队列按照重传计时器的剩余时间来排列,因此TCP软件可追踪那几个计时器在最短时间内超时。

    确认处理 如果在计时器超时之前收到了确认信息,则该片段从重传队列中移除。

    重传超时 如果在计时器超时之前没有收到确认信息,则发生重传超时,片段自动重传。当然,相比于原片段,对于重传片段并没有更多的保障机制。因此,重传之后该片段还是保留在重传队列里。重传计时器被重启,重新开始倒计时。如果重传之后没有收到确认,则片段会再次重传并重复这一过程。在某些情况下重传也会失败。我们不想要TCP永远重传下去,因此TCP只会重传一定数量的次数,并判断出现故障终止连接。

    但是我们怎样知道一个片段被完全确认呢?重传是基于片段的,而TCP确认信息是基于序列号累积的。每次当设备A发送片段给设备B,设备B查看该片段的确认号字段。所有低于该字段的序列号都已经被设备A接收了。因此,当片段中所发送的所有字节的序列号都比设备A到设备B的最后一个确认号小的时候,一个从设备B发到设备A的片段被认为是确认了。这是通过计算片段中最后一个序列号结合片段的数据字段来实现的。

    让我们以下图为例来说明一下确认和重传是怎样工作的。假设连接中的服务器发出了四个连续片段(号码从1开始)

    片段1 序列号字段是1片段长度80。所以片段1中最后一个序列号是80。
    片段2 序列号是81片段长度是120。片段2中最后一个序列号是200。
    片段3 序列号是201片段长度是160。片段3中最后一个序列号是360。
    片段4 序列号是361片段长度是140。片段3中最后一个序列号是500。

    这些片段是一个接一个发送的,而无需等待前一个发送得到确认。这是TCP滑动窗口的一个主要优势

    假设客户端接收到前两个传输,它会发回一条确认消息确认号为201。从而告知服务器前两个片段已经被客户端成功接收了,它们从重传队列中移除(并且服务器发送窗口右移200字节)。在接收到确认号361或更高的片段之前,片段3会保留在重传队列中;片段4需要确认号501或更高。

    现在,让我们进一步假设传输过程中片段3丢失了,但片段4被接收到了。客户端将片段4保存在接收buffer中,但是不需要确认,因为TCP是累积确认机制——确认片段4表示片段3也接收到了,但实际上并没有。因此,客户端需要等待片段3。实际上,服务器端片段3的重传计时器会超时,服务器之后重传片段3。之后客户端收到,然后发送片段3和4的确认信息给服务器。

    还有一个重要的问题,服务器将如何处理片段4呢?虽然客户端在等待片段3,服务器没有收到反馈,所以它并不知道片段3丢失了,同样它也不知道片段4发生了什么(以及接下来传输的数据)。很有可能客户端已经接收到了片段4但是不能确认,也有可能片段4也丢失了。一些实现中会选择仅仅重传片段3,也有些会把3和4都重传。

    最后一个问题是重传队列中所使用片段重传计时器的值。如果设置过低,会发生过量重传,如果设置过高,重传丢失片段会减弱性能。必须通过一个称为自适应重传的过程来动态调整这个值。

  • TCP滑动窗口

    TCP滑动窗口

    介绍

    将TCP与UDP这样的简单传输协议区分开来的是它传输数据的质量。TCP对于发送数据进行跟踪,这种数据管理需要协议有以下两大关键功能:
    可靠性:保证数据确实到达目的地。如果未到达,能够发现并重传。
    数据流控:管理数据的发送速率,以使接收设备不致于过载。
    要完成这些任务,整个协议操作是围绕滑动窗口确认机制来进行的。因此,理解了滑动窗口,也就是理解了TCP。

    更多信息

    TCP面向流的滑动窗口确认机制:

    TCP将独立的字节数据当作流来处理。一次发送一个字节并接收一次确认显然是不可行的。即使重叠传输(即不等待确认就发送下一个数据),速度也还是会非常缓慢。

    TCP消息确认机制如上图所示,首先,每一条消息都有一个识别编号,每一条消息都能够被独立地确认,因此同一时刻可以发送多条信息。设备B定期发送给A一条发送限制参数,制约设备A一次能发送的消息最大数量。设备B可以对该参数进行调整,以控制设备A的数据流。为了提高速度,TCP并没有按照字节单个发送而是将数据流划分为片段。片段内所有字节都是一起发送和接收的,因此也是一起确认的。确认机制没有采用message ID字段,而是使用的片段内最后一个字节的sequence number。因此一次可以处理不同的字节数,这一数量即为片段内的sequence number。

    TCP数据流的概念划分类别

    假设A和B之间新建立了一条TCP连接。设备A需要传送一长串数据流,但设备B无法一次全部接收,所以它限制设备A每次发送分段指定数量的字节数,直到分段中已发送的字节数得到确认。之后,设备A可以继续发送更多字节。每一个设备都对发送,接收及确认数据进行追踪。

    如果我们在任一时间点对于这一过程做一个“快照”,那么我们可以将TCP buffer中的数据分为以下四类,并把它们看作一个时间轴:

    1. 已发送已确认 数据流中最早的字节已经发送并得到确认。这些数据是站在发送设备的角度来看的。如下图所示,31个字节已经发送并确认。
    2. 已发送但尚未确认 已发送但尚未得到确认的字节。发送方在确认之前,不认为这些数据已经被处理。下图所示14字节为第2类。
    3. 未发送而接收方已Ready 设备尚未将数据发出,但接收方根据最近一次关于发送方一次要发送多少字节确认自己有足够空间。发送方会立即尝试发送。如图,第3类有6字节。
    4. 未发送而接收方Not Ready 由于接收方not ready,还不允许将这部分数据发出。

    接收方采用类似的机制来区分已接收并已确认,尚未接受但准备好接收,以及尚未接收并尚未准备好接收的数据。实际上,收发双方各自维护一套独立的变量,来监控发送和接收的数据流落在哪一类。

    Sequence Number设定与同步:

    发送方和接收方必须就它们将要为数据流中的字节指定的sequence number达成一致。这一过程称为同步,在TCP连接建立时完成。为了简化假设第一个字节sequence number是1,按照上图示例,四类字节如下:

    1. 已发送已确认字节1至31。
    2. 已发送但尚未确认字节32至45。
    3. 未发送而接收方已Ready字节46至51。
    4. 未发送而接收方Not Ready字节52至95。

    发送窗口与可用窗口:

    整个过程关键的操作在于接收方允许发送方一次能容纳的未确认的字节数。这称为发送窗口,有时也称为窗口。该窗口决定了发送方允许传送的字节数,也是2类和3类的字节数之和。因此,最后两类(接收方准备好而尚未发送,接收方未准备好)的分界线在于添加了从第一个未确认字节开始的窗口。本例中,第一个未确认字节是32,整个窗口大小是20。

    可用窗口的定义是:考虑到正在传输的数据量,发送方仍被允许发送的数据量。实际上等于第3类的大小。左边界就是窗口中的第一个字节(字节32),右边界是窗口中最后一个字节(字节51)。概念的详细解释看下图。

    可用窗口字节发送后TCP类目与窗口大小的改变:
    当上图中第三类的6字节立即发送之后,这6字节从第3类转移到第2类。字节变为如下:

    1. 已发送已确认字节1至31。
    2. 已发送但尚未确认字节32至51。
    3. 未发送而接收方已Ready字节为0。
    4. 未发送而接收方Not Ready字节52至95。

    确认处理以及窗口缩放:

    过了一段时间,目标设备向发送方传回确认信息。目标设备不会特别列出它已经确认的字节,因为这会导致效率低下。目标设备会发送自上一次成功接收后的最长字节数。

    例如,假设已发送未确认字节(32至45)分为4段传输:32-34,35-36,37-41,42-45。第1,2,4已经到达,而3段没有收到。接收方只会发回32-36的确认信息。接收方会保留42-45但不会确认,因为这会表示接收方已经收到了37-41。这是很必要的,因为TCP的确认机制是累计的,只使用一个数字来确认数据。这一数字是自上一次成功接收后的最长字节数。假设目标设备同样将窗口设为20字节。

    当发送设备接收到确认信息,则会将一部分第2类字节转移到第1类,因为它们已经得到了确认。由于5个字节已被确认,窗口大小没有改变,允许发送方多发5个字节。结果,窗口向右滑动5个字节。同时5个字节从第二类移动到第1类,5个字节从第4类移动至第3类,为接下来的传输创建了新的可用窗口。因此,在接收到确认信息以后,看起来如下图所示。字节变为如下:

    1. 已发送已确认字节1至36。
    2. 已发送但尚未确认字节37至51。
    3. 未发送而接收方已Ready字节为52至56。
    4. 未发送而接收方Not Ready字节57至95。

    每一次确认接收以后,这一过程都会发生,从而让窗口滑动过整个数据流以供传输。

    处理丢失确认信息:

    但是丢失的42-45如何处理呢?在接收到第3段(37-41)之前,接收设备不会发送确认信息,也不会发送这一段之后字节的确认信息。发送设备可以将新的字节添加到第3类之后,即52-56。发送设备之后会停止发送,窗口停留在37-41。

    TCP包括一个传输及重传的计时机制。TCP会重传丢失的片段。但有一个缺陷是:因为它不会对每一个片段分别进行确认,这可能会导致其他实际上已经接收到的片段被重传(比如42至45)。

  • MongoDB

    MongoDB

    简介

    说明

    • 官方MongoDB是一个文档数据库,旨在方便应用开发和扩展。
    • 百度百科

    MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展高性能数据存储解决方案

    MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型 。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引

    总结: mongoDB 是一个非关系型文档数据库

    历史

    • 2009年2月,MongoDB数据库首次在数据库领域亮相,打破了关系型数据库一统天下的局面;
    • 2010年8月, MongoDB 1.6发布。这个版本最大的一个功能就是Sharding,自动分片
    • 2014年12月, MongoDB3.0发布。由于收购了WiredTiger 存储引擎,大幅提升了MongoDB的写入性能;
    • 2015年12月,3.2版本发布,开始支持了关系型数据库的核心功能:关联。你可以一次同时查询多个MongoDB的集合。
    • 2016年, MongoDB推出Atlas,在AWS、 Azure 和GCP上的MongoDB托管服务;
    • 2017年10月,MongoDB成功在纳斯达克敲钟,成为26年来第一家以数据库产品为主要业务的上市公司。
    • 2018年6月, MongoDB4.0 发布推出ACID事务支持,成为第一个支持强事务的NoSQL数据库;
    • 2018年–至今,MongoDB已经从一个在数据库领域籍籍无名的“小透明”,变成了话题度和热度都很高的“流量”数据库。

    特点

    特点

    • 面向集合存储,易存储对象类型的数据
    • 支持查询,以及动态查询
    • 支持RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言
    • 文件存储格式为BSON(一种JSON的扩展)
    • 支持复制和故障恢复和分片
    • 支持事务支持
    • 索引 聚合 关联….

    应用场景

    • 游戏应用:使用云数据库MongoDB作为游戏服务器的数据库存储用户信息。用户的游戏装备、积分等直接以内嵌文档的形式存储,方便进行查询与更新。
    • 物流应用:使用云数据库MongoDB存储订单信息,订单状态在运送过程中会不断更新,以云数据库MongoDB内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来,方便快捷且一目了然。
    • 社交应用:使用云数据库MongoDB存储用户信息以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。并且,云数据库MongoDB非常适合用来存储聊天记录,因为它提供了非常丰富的查询,并在写入和读取方面都相对较快。
    • 视频直播:使用云数据库MongoDB存储用户信息、礼物信息等。
    • 大数据应用:使用云数据库MongoDB作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。

    安装

    传统方式

    # 1.下载 MongoDB - https://www.mongodb.com/try/download/community

    # 2.将下载安装包上传到 linux 系统 - tar -zxf mongodb-linux-aarch64-ubuntu2004-5.0.5.tgz

    # 3.查看安装目录 - ls `bin`目录   用来存放启动mongoDB的服务以及客户端链接的脚本文件等

    # 4.启动 MongoDB 服务 - ./mongod --port=27017 --dbpath=../data --logpath=../logs/mongo.log `--port`   指定服务监听端口号 默认为 27017 `--dbpath` 指定 mongodb 数据存放目录 启动要求目录必须存在 `--logpath` 指定 mongodb 日志文件存放位置

    注意: 由于指定日志文件因此启动时日志输出到日志中终端不显示任何日志


    # 5.客户端连接 - ./mongo --port=27017

    Docker方式

    # 1.拉取 mongodb 镜像 - docker pull mongo:5.0.5

    # 2.运行 mongo 镜像 - docker run -d --name mongo --p 27017:27017 mongo:5.0.5

    # 3.进入 mongo 容器 - docker exec -it bc6c bash

    核心概念

    库<DataBase>

    mongodb中的库就类似于传统关系型数据库中库的概念,用来通过不同库隔离不同应用数据。mongodb中可以建立多个数据库。每一个库都有自己的集合和权限,不同的数据库也放置在不同的文件中。默认的数据库为”test”,数据库存储在启动指定的data目录中。

    集合<Collection>

    集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表的概念。

    集合存在于数据库中,一个库中可以创建多个集合。每个集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。

    文档<Document>

    文档集合中一条条记录,是一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

    一个简单的文档例子如下:

    {"site":"www.baizhiedu.xin", "name":"编程不良人"}

    关系总结

    RDBMSMongoDB
    数据库<database>数据库<database>
    表<table>集合<collection>
    行<row>文档<document>
    列<colume>字段<field>

    基本操作

    库<database>

    • 查看所有库
      > show databases; | show dbs;

      注意

      • admin: 从权限的角度来看,这是”root”数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
      • local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
      • config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
    • 创建数据库
      >use 库名

      注意: use 代表创建并使用,当库中没有数据时默认不显示这个库

    • 删除数据库
      • 默认删除当前选中的库
      > db.dropDatabase()
    • 查看当前所在库
      > db;

    集合<Collection>

    • 查看库中所有集合
      > show collections; | show tables;
    • 创建集合
      > db.createCollection('集合名称', [options])
    • options可以是如下参数:

    字段类型描述
    capped布尔(可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 当该值为 true 时,必须指定 size 参数。
    size数值(可选)为固定集合指定一个最大值,即字节数。 如果 capped 为 true,也需要指定该字段。
    max数值(可选)指定固定集合中包含文档的最大数量。

    注意:当集合不存在时,向集合中插入文档也会自动创建该集合。

    • 删除集合
    > db.集合名称.drop();

    文档<document>

    参考文档: https://docs.mongodb.com/manual/reference/method/

    • 插入文档
      • 单条文档
        > db.集合名称.insert({"name":"编程不良人","age":23,"bir":"2012-12-12"});
      • 多条文档
        > db.集合名称.insertMany(
           [ <document 1> , <document 2>, ... ],
           {
         			writeConcern: 1,//写入策略,默认为 1,即要求确认写操作,0 是不要求。
              ordered: true //指定是否按顺序写入,默认 true,按顺序写入。
           }
        )
        > db.集合名称.insert([
          	{"name":"不良人","age":23,"bir":"2012-12-12"},
          	{"name":"小黑","age":25,"bir":"2012-12-12"}
        ]);
        
      • 脚本方式
        for(let i=0;i<100;i++){     
            db.users.insert(
                {"_id":i,"name":"编程不良人_"+i,"age":23}
            ); 
        }
        
      注意:在 mongodb 中每个文档都会有一个_id作为唯一标识,_id默认会自动生成如果手动指定将使用手动指定的值作为_id 的值。
    • 查询所有
      > db.集合名称.find();
    • 删除文档
      db.集合名称.remove(
         <query>,
         {
           justOne: <boolean>,
           writeConcern: <document>
         }
      )
      参数说明:
      • query :可选删除的文档的条件。
      • justOne : 可选如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
      • writeConcern :可选抛出异常的级别。
    • 更新文档
      db.集合名称.update(
         <query>,
         <update>,
         {
           upsert: <boolean>,
           multi: <boolean>,
           writeConcern: <document>
         }
      );
      参数说明:
      • query : update的查询条件,类似sql update查询内where后面的。
      • update : update的对象和一些更新的操作符(如$,$inc…)等,也可以理解为sql update查询内set后面的
      • upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
      • multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
      • writeConcern :可选,抛出异常的级别。
      - db.集合名称.update({"name":"zhangsan"},{name:"11",bir:new date()}) 
      	`这个更新是将符合条件的全部更新成后面的文档,相当于先删除在更新`
      - db.集合名称.update({"name":"xiaohei"},{$set:{name:"mingming"}})
      	`保留原来数据更新,但是只更新符合条件的第一条数据`
      - db.集合名称.update({name:”小黑”},{$set:{name:”小明”}},{multi:true})		
      	`保留原来数据更新,更新符合条件的所有数据`
      - db.集合名称.update({name:”小黑”},{$set:{name:”小明”}},{multi:true,upsert:true})
      	`保留原来数据更新,更新符合条件的所有数据 没有条件符合时插入数据
      );

    文档查询

    MongoDB 查询文档使用 find() 方法。find() 方法以非结构化的方式来显示所有文档。

    语法

    > db.集合名称.find(query, projection)
    • query可选,使用查询操作符指定查询条件
    • projection可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。

    如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:

    > db.集合名称.find().pretty()

    注意: pretty() 方法以格式化的方式来显示所有文档。

    对比语法

    如果你熟悉常规的 SQL 数据,通过下表可以更好的理解 MongoDB 的条件语句查询:

    AND

    > db.集合名称.find({key1:value1, key2:value2,...}).pretty()

    类似于 WHERE 语句:WHERE key1=value1 AND key2=value2

    OR

    MongoDB OR 条件语句使用了关键字 $or,语法格式如下:

    > db.集合名称.find(
       {
          $or: [
             {key1: value1}, {key2:value2}
          ]
       }
    ).pretty()

    类似于 WHERE 语句:WHERE key1=value1 or key2=value2

    AND 和 OR 联合

    类似SQL语句为:’where age >50 AND (name = ‘编程不良人’ OR name = ‘MongoDB’)’

    > db.集合名称.find({"age": {$gt:50}, $or: [{"name": "编程不良人"},{"name": "MongoDB"}]}).pretty();

    数组中查询

    -- 测试数据
    > db.集合名称.insert({ "_id" : 11, "age" : 29, "likes" : [ "看电视", "读书xx", "美女" ], "name" : "不良人_xx_11" })
    -- 执行数组查询
    > db.users.find({likes:"看电视"})
    -- $size 按照数组长度查询
    > db.users.find({likes:{$size:3}});

    模糊查询

    类似 SQL 中为 ‘where name like ‘%name%”

    > db.users.find({likes:/良/});

    注意:在 mongoDB 中使用正则表达式可以是实现近似模糊查询功能

    排序

    > db.集合名称.find().sort({name:1,age:1}),
    - 1 升序  -1 降序

    类似 SQL 语句为: ‘order by name,age’

    分页

    > db.集合名称.find().sort({条件}).skip(start).limit(rows);

    类似于 SQL 语句为: ‘limit start,rows’

    总条数

    > db.集合名称.count();
    > db.集合名称.find({"name":"编程不良人"}).count();

    类似于 SQL 语句为: ‘select count(id) from ….’

    去重

    > db.集合名称.distinct('字段')

    类似于 SQL 语句为: ‘select distinct name from ….’

    指定返回字段

    > db.集合名称.find({条件},{name:1,age:1}) 
    - 参数2: 1 返回  0 不返回    `注意:1和0不能同时使用`

    $type

    说明

    $type操作符是基于BSON类型来检索集合中匹配的数据类型,并返回结果。

    MongoDB 中可以使用的类型如下表所示:

    使用

    > db.col.insert({
        title: 'PHP 教程', 
        description: 'PHP 是一种创建动态交互性站点的强有力的服务器端脚本语言。',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['php'],
        likes: 200
    });
    ​
    > db.col.insert({
        title: 'Java 教程', 
        description: 'Java 是由Sun Microsystems公司于1995年5月推出的高级程序设计语言。',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['java'],
        likes: 550
    });
    ​
    > db.col.insert({
        title: 'MongoDB 教程', 
        description: 'MongoDB 是一个 Nosql 数据库',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['mongodb'],
        likes: 100
    });
    ​
    > db.col.insert({
        title: 2233, 
        description: '2233 是一个 B站的',
        by: '编程不良人',
        url: 'http://www.baizhiedu.xin',
        tags: ['2233'],
        likes: 100
    });
    • 如果想获取 “col” 集合中 title 为 String 的数据,你可以使用以下命令:
    db.col.find({"title" : {$type : 2}}).pretty();
    或
    db.col.find({"title" : {$type : 'string'}}).pretty();
    • 如果想获取 “col” 集合中 tags 为 Array 的数据,你可以使用以下命令:
    dge
    或
    db.col.find({"tags" : {$type : 'array'}}).pretty();

    索引<index>

    https://docs.mongodb.com/manual/indexes/

    说明

    索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。

    原理

    从根本上说,MongoDB中的索引与其他数据库系统中的索引类似。MongoDB在集合层面上定义了索引,并支持对MongoDB集合中的任何字段或文档的子字段进行索引。

    操作

    0、创建索引

    > db.集合名称.createIndex(keys, options)
    > db.集合名称.createIndex({"title":1,"description":-1})

    说明: 语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。

    createIndex() 接收可选参数,可选参数列表如下:

    ParameterTypeDescription
    backgroundBoolean建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false
    uniqueBoolean建立的索引是否唯一。指定为true创建唯一索引。默认值为false.
    namestring索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。
    sparseBoolean对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.
    expireAfterSecondsinteger指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。
    vindex version索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。
    weightsdocument索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。
    default_languagestring对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语
    language_overridestring对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.

    1、查看集合索引

    > db.集合名称.getIndexes()

    2、查看集合索引大小

    > db.集合名称.totalIndexSize()

    3、删除集合所有索引

    > db.集合名称.dropIndexes()

    4、删除集合指定索引

    > db.集合名称.dropIndex("索引名称")

    复合索引

    说明: 一个索引的值是由多个 key 进行维护的索引的称之为复合索引

    > db.集合名称.createIndex({"title":1,"description":-1})

    注意: mongoDB 中复合索引和传统关系型数据库一致都是左前缀原则

    聚合<aggregate>

    说明

    MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似 SQL 语句中的 count(*)

    使用

    {
       title: 'MongoDB Overview', 
       description: 'MongoDB is no sql database',
       by_user: 'runoob.com',
       url: 'http://www.runoob.com',
       tags: ['mongodb', 'database', 'NoSQL'],
       likes: 100
    },
    {
       title: 'NoSQL Overview', 
       description: 'No sql database is very fast',
       by_user: 'runoob.com',
       url: 'http://www.runoob.com',
       tags: ['mongodb', 'database', 'NoSQL'],
       likes: 10
    },
    {
       title: 'Neo4j Overview', 
       description: 'Neo4j is no sql database',
       by_user: 'Neo4j',
       url: 'http://www.neo4j.com',
       tags: ['neo4j', 'database', 'NoSQL'],
       likes: 750
    }

    现在我们通过以上集合计算每个作者所写的文章数,使用aggregate()计算结果如下:

    > db.集合名称.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])

    常见聚合表达式

    整合应用

    说明: 这里主要以 springboot 应用为基础应用进行整合开发。

    Spring Data : Spring 数据框架 JPA 、Redis、Elasticsearch、AMQP、MongoDBJdbcTemplateRedisTemplateElasticTempalteAmqpTemplateMongoTemplate

    环境搭建

    # 引入依赖
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    # 编写配置
    # mongodb 没有开启任何安全协议
    # mongodb(协议)://121.5.167.13(主机):27017(端口)/baizhi(库名)
    spring.data.mongodb.uri=mongodb://121.5.167.13:27017/baizhi
    ​
    ​
    # mongodb 存在密码
    #spring.data.mongodb.host=tx.chenyn.cn
    #spring.data.mongodb.port=27017
    #spring.data.mongodb.database=baizhi
    #spring.data.mongodb.username=root
    #spring.data.mongodb.password=root

    集合操作

    创建集合

    @Test
    public void testCreateCollection(){
      mongoTemplate.createCollection("users");//参数: 创建集合名称
    }
    // 注意:创建集合不能存在,存在报错

    删除集合

    @Test
    public void testDeleteCollection(){
      mongoTemplate.dropCollection("users");
    }

    相关注解

    • @Document
      • 修饰范围: 用在类上
      • 作用: 用来映射这个类的一个对象为 mongo 中一条文档数据
      • 属性:(value 、collection )用来指定操作的集合名称
    • @Id
      • 修饰范围: 用在成员变量、方法上
      • 作用: 用来将成员变量的值映射为文档的_id 的值
    • @Field
      • 修饰范围: 用在成员变量、方法上
      • 作用: 用来将成员变量以及值映射为文档中一个key、value对
      • 属性: ( name,value)用来指定在文档中 key 的名称,默认为成员变量名
    • @Transient
      • 修饰范围: 用在成员变量、方法上
      • 作用 : 用来指定改成员变量,不参与文档的序列化

    文档操作

    查询

    Criteria

    常见查询

    @Test
    public void testQuery(){
      //基于 id 查询
      template.findById("1",User.class);
    
      //查询所有
      template.findAll(User.class);
      template.find(new Query(),User.class);
    
      //等值查询
      template.find(Query.query(Criteria.where("name").is("编程不良人")), 
                   User.class);
    
      // > gt  < lt  >= gte  <= lte
      template.find(Query.query(Criteria.where("age").lt(25)),
                    User.class);
      template.find(Query.query(Criteria.where("age").gt(25)),
                    User.class);
      template.find(Query.query(Criteria.where("age").lte(25)),
                    User.class);
      template.find(Query.query(Criteria.where("age").gte(25)),
                    User.class);
    
      //and
      template.find(Query.query(Criteria.where("name").is("编程不良人")
                                .and("age").is(23)),User.class);
    
      //or
      Criteria criteria = new Criteria()
        .orOperator(Criteria.where("name").is("编程不良人_1"),
         Criteria.where("name").is("编程不良人_2"));
      template.find(Query.query(criteria), User.class);
    
      //and or
      Criteria criteria1 = new Criteria()
        .and("age").is(23)
        .orOperator(
        Criteria.where("name").is("编程不良人_1"),
        Criteria.where("name").is("编程不良人_2"));
      template.find(Query.query(criteria1), User.class);
    
      //sort 排序
      Query query = new Query();
      query.with(Sort.by(Sort.Order.desc("age")));//desc 降序  asc 升序
      template.find(query, User.class);
    
    
      //skip limit 分页
      Query queryPage = new Query();
      queryPage.with(Sort.by(Sort.Order.desc("age")))//desc 降序  asc 升序
        .skip(0) //起始条数
        .limit(4); //每页显示记录数
      template.find(queryPage, User.class);
    
    
      //count 总条数
      template.count(new Query(), User.class);
    
      //distinct 去重
      //参数 1:查询条件 参数 2: 去重字段  参数 3: 操作集合  参数 4: 返回类型
      template.findDistinct(new Query(), "name", 
                            User.class, String.class);
      
      //使用 json 字符串方式查询 
            Query query = new BasicQuery(
              "{$or:[{name:'编程不良人'},{name:'徐凤年'}]}", 
              "{name:0}");
    
      template.find(query, User.class);
    }

    添加

    @Test
    public void testSaveOrUpdate(){
      User user = new User();
      user.setId("1");
      user.setAge(23);
      user.setName("编程不良人_1");
      user.setBir(new Date());
      User userDB = mongoTemplate.insert(user);//返回保存的对象 insert or save
      System.out.println(userDB);
    }

    insert: 插入重复数据时:insert报DuplicateKeyException提示主键重复;save对已存在的数据进行更新。
    save: 批处理操作时:insert可以一次性插入整个数据,效率较高;save需遍历整个数据,一次插入或更新,效率较低。

    更新

    @Test
    public void  testUpdate(){
      //1.更新条件
      Query query = Query.query(Criteria.where("age").is(23));
      //2.更新内容
      Update update = new Update();
      update.set("name","编程小陈陈");
    
      //单条更新
      mongoTemplate.updateFirst(query, update, User.class);
      //多条更新
      mongoTemplate.updateMulti(query, update, User.class);
      //更新插入
      mongoTemplate.upsert(query,update,User.class);
    
      //返回值均为 updateResult
      //System.out.println("匹配条数:" + updateResult.getMatchedCount());
      //System.out.println("修改条数:" + updateResult.getModifiedCount());
      //System.out.println("插入id_:" + updateResult.getUpsertedId());
    }

    删除

    @Test
    public void testDelete(){
      //删除所有
      mongoTemplate.remove(new Query(),User.class);
      //条件删除
      mongoTemplate.remove(
        Query.query(Criteria.where("name").is("编程不良人")),
        User.class
      );
    }

    副本集

    <Replica Set>

    说明

    https://docs.mongodb.com/manual/replication/

    MongoDB 副本集(Replica Set)是有自动故障恢复功能的主从集群,有一个Primary节点和一个或多个Secondary节点组成。副本集没有固定的主节点,当主节点发生故障时整个集群会选举一个主节点为系统提供服务以保证系统的高可用。

    Automatic Failover

    自动故障转移机制: 当主节点未与集合的其他成员通信超过配置的选举超时时间(默认为 10 秒)时,合格的辅助节点将调用选举以将自己提名为新的主节点。集群尝试完成新主节点的选举并恢复正常操作。

    搭建副本集

    • 创建数据目录
      # 在安装目录中创建 - mkdir -p ../repl/data1 - mkdir -p ../repl/data2 - mkdir -p ../repl/data3
    • 搭建副本集 $ mongod --port 27017  --dbpath ../repl/data1 --bind_ip 0.0.0.0 --replSet myreplace/[121.5.167.13:27018,121.5.167.13:27019] $ mongod --port 27018  --dbpath ../repl/data2 --bind_ip 0.0.0.0 --replSet myreplace/[121.5.167.13:27019,121.5.167.13:27017] $ mongod --port 27019  --dbpath ../repl/data3 --bind_ip 0.0.0.0 --replSet myreplace/[121.5.167.13:27017,121.5.167.13:27018] # 注意: --replSet 副本集 myreplace 副本集名称/集群中其他节点的主机和端口
    • 配置副本集,连接任意节点
      • use admin
      • 初始化副本集
        > var config = { _id:"myreplace", members:[ {_id:0,host:"121.5.167.13:27017"}, {_id:1,host:"121.5.167.13:27018"}, {_id:2,host:"121.5.167.13:27019"} ] } > rs.initiate(config);//初始化配置
      • 设置客户端临时可以访问
        > rs.slaveOk(); > rs.secondaryOk();

    分片集群<Sharding Cluster>

    说明

    https://docs.mongodb.com/manual/sharding/

    分片(sharding)是指将数据拆分,将其分散存在不同机器的过程,有时也用分区(partitioning)来表示这个概念,将数据分散在不同的机器上,不需要功能强大的大型计算机就能存储更多的数据,处理更大的负载。

    分片目的是通过分片能够增加更多机器来应对不断的增加负载和数据,还不影响应用运行。

    MongoDB支持自动分片,可以摆脱手动分片的管理困扰,集群自动切分数据做负载均衡。MongoDB分片的基本思想就是将集合拆分成多个块,这些快分散在若干个片里,每个片只负责总数据的一部分,应用程序不必知道哪些片对应哪些数据,甚至不需要知道数据拆分了,所以在分片之前会运行一个路由进程,mongos进程,这个路由器知道所有的数据存放位置,应用只需要直接与mongos交互即可。mongos自动将请求转到相应的片上获取数据,从应用角度看分不分片没有什么区别。

    架构

    • Shard: 用于存储实际的数据块,实际生产环境中一个shard server角色可由几台机器组个一个replica set承担,防止主机单点故障
    • Config Server:mongod实例,存储了整个 ClusterMetadata。
    • Query Routers: 前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用可以透明使用。
    • Shard Key: 片键,设置分片时需要在集合中选一个键,用该键的值作为拆分数据的依据,这个片键称之为(shard key),片键的选取很重要,片键的选取决定了数据散列是否均匀。

    搭建

    # 1.集群规划
    - Shard Server 1:27017
    - Shard Repl   1:27018
    
    - Shard Server 2:27019
    - Shard Repl   2:27020
    
    - Shard Server 3:27021
    - Shard Repl   3:27022
    
    - Config Server :27023
    - Config Server :27024
    - Config Server :27025
    
    - Route Process :27026
    
    # 2.进入安装的 bin 目录创建数据目录
    - mkdir -p ../cluster/shard/s0
    - mkdir -p ../cluster/shard/s0-repl
    
    - mkdir -p ../cluster/shard/s1
    - mkdir -p ../cluster/shard/s1-repl
    
    - mkdir -p ../cluster/shard/s2
    - mkdir -p ../cluster/shard/s2-repl
    
    - mkdir -p ../cluster/shard/config1
    - mkdir -p ../cluster/shard/config2
    - mkdir -p ../cluster/shard/config3
    
    # 3.启动4个 shard服务
    
    # 启动 s0、r0
    > ./mongod --port 27017 --dbpath ../cluster/shard/s0 --bind_ip 0.0.0.0 --shardsvr --replSet r0/121.5.167.13:27018
    > ./mongod --port 27018 --dbpath ../cluster/shard/s0-repl --bind_ip 0.0.0.0 --shardsvr --replSet r0/121.5.167.13:27017
    -- 1.登录任意节点
    -- 2. use admin
    -- 3. 执行
    		config = { _id:"r0", members:[
          {_id:0,host:"121.5.167.13:27017"},
          {_id:1,host:"121.5.167.13:27018"},
        	]
        }
    		rs.initiate(config);//初始化
    
    # 启动 s1、r1
    > ./mongod --port 27019 --dbpath ../cluster/shard/s1 --bind_ip 0.0.0.0 --shardsvr  --replSet r1/121.5.167.13:27020
    > ./mongod --port 27020 --dbpath ../cluster/shard/s1-repl --bind_ip 0.0.0.0 --shardsvr --replSet r1/121.5.167.13:27019
    -- 1.登录任意节点
    -- 2. use admin
    -- 3. 执行
    		config = { _id:"r1", members:[
          {_id:0,host:"121.5.167.13:27019"},
          {_id:1,host:"121.5.167.13:27020"},
        	]
        }
    		rs.initiate(config);//初始化
    
    # 启动 s2、r2
    > ./mongod --port 27021 --dbpath ../cluster/shard/s2 --bind_ip 0.0.0.0 --shardsvr --replSet r2/121.5.167.13:27022
    > ./mongod --port 27022 --dbpath ../cluster/shard/s2-repl --bind_ip 0.0.0.0 --shardsvr --replSet r2/121.5.167.13:27021
    -- 1.登录任意节点
    -- 2. use admin
    -- 3. 执行
    		config = { _id:"r2", members:[
          {_id:0,host:"121.5.167.13:27021"},
          {_id:1,host:"121.5.167.13:27022"},
        	]
        }
    		rs.initiate(config);//初始化
    
    # 4.启动3个config服务
    
    > ./mongod --port 27023 --dbpath ../cluster/shard/config1 --bind_ip 0.0.0.0 --replSet  config/[121.5.167.13:27024,121.5.167.13:27025] --configsvr
    
    > ./mongod --port 27024 --dbpath ../cluster/shard/config2 --bind_ip 0.0.0.0 --replSet  config/[121.5.167.13:27023,121.5.167.13:27025] --configsvr
    
    > ./mongod --port 27025 --dbpath ../cluster/shard/config3 --bind_ip 0.0.0.0 --replSet  config/[121.5.167.13:27023,121.5.167.13:27024] --configsvr
    
    # 5.初始化 config server 副本集
    - `登录任意节点 congfig server`
    > 1.use admin 
    > 2.在admin中执行
      config = { 
          _id:"config", 
          configsvr: true,
          members:[
              {_id:0,host:"121.5.167.13:27023"},
              {_id:1,host:"121.5.167.13:27024"},
              {_id:2,host:"121.5.167.13:27025"}
            ]
      }
    > 3.rs.initiate(config); //初始化副本集配置 
    
    # 6.启动 mongos 路由服务
    
    > ./mongos --port 27026 --configdb config/121.5.167.13:27023,121.5.167.13:27024,121.5.167.13:27025 --bind_ip 0.0.0.0 
    
    # 7.登录 mongos 服务
    > 1.登录 mongo --port 27026
    > 2.use admin
    > 3.添加分片信息
    	db.runCommand({ addshard:"r0/121.5.167.13:27017,121.5.167.13:27018",
    	"allowLocal":true });
    	db.runCommand({ addshard:"r1/121.5.167.13:27019,121.5.167.13:27020",
    	"allowLocal":true });
    	db.runCommand({ addshard:"r2/121.5.167.13:27021,121.5.167.13:27022",
    	"allowLocal":true });
    > 4.指定分片的数据库
    	db.runCommand({ enablesharding:"baizhi" });
    
    > 5.设置库的片键信息
    	db.runCommand({ shardcollection: "baizhi.users", key: { _id:1}});
    	db.runCommand({ shardcollection: "baizhi.emps", key: { _id: "hashed"}})
  • Redis

    Redis

    1. NoSQL的引言

    NoSQL(Not Only SQL ),意即不仅仅是SQL, 泛指非关系型的数据库。Nosql这个技术门类,早期就有人提出,发展至2009年趋势越发高涨。

    2. 为什么是NoSQL

    随着互联网网站的兴起,传统的关系数据库在应付动态网站,特别是超大规模和高并发的纯动态网站已经显得力不从心,暴露了很多难以克服的问题。如商城网站中对商品数据频繁查询、对热搜商品的排行统计、订单超时问题、以及微信朋友圈(音频,视频)存储等相关使用传统的关系型数据库实现就显得非常复杂,虽然能实现相应功能但是在性能上却不是那么乐观。nosql这个技术门类的出现,更好的解决了这些问题,它告诉了世界不仅仅是sql。

    3. NoSQL的四大分类

    3.1 键值(Key-Value)存储数据库

    # 1.说明: 
    - 这一类数据库主要会使用到一个哈希表,这个表中有一个特定的键和一个指针指向特定的数据。
    ​
    # 2.特点
    - Key/value模型对于IT系统来说的优势在于简单、易部署。  
    - 但是如果DBA只对部分值进行查询或更新的时候,Key/value就显得效率低下了。
    ​
    # 3.相关产品
    - Tokyo Cabinet/Tyrant,
    - Redis
    - SSDB
    - Voldemort 
    - Oracle BDB

    3.2 列存储数据库

    # 1.说明
    - 这部分数据库通常是用来应对分布式存储的海量数据。
    ​
    # 2.特点
    - 键仍然存在,但是它们的特点是指向了多个列。这些列是由列家族来安排的。
    ​
    # 3.相关产品
    - Cassandra、HBase、Riak.

    3.3 文档型数据库

    # 1.说明
    - 文档型数据库的灵感是来自于Lotus Notes办公软件的,而且它同第一种键值存储相类似该类型的数据模型是版本化的文档,半结构化的文档以特定的格式存储,比如JSON。文档型数据库可 以看作是键值数据库的升级版,允许之间嵌套键值。而且文档型数据库比键值数据库的查询效率更高
    ​
    # 2.特点
    - 以文档形式存储
    ​
    # 3.相关产品
    - MongoDB、CouchDB、 MongoDb(4.x). 国内也有文档型数据库SequoiaDB,已经开源。

    3.4 图形(Graph)数据库

    # 1.说明
    - 图形结构的数据库同其他行列以及刚性结构的SQL数据库不同,它是使用灵活的图形模型,并且能够扩展到多个服务器上。
    - NoSQL数据库没有标准的查询语言(SQL),因此进行数据库查询需要制定数据模型。许多NoSQL数据库都有REST式的数据接口或者查询API。
    ​
    # 2.特点
    ​
    # 3.相关产品
    - Neo4J、InfoGrid、 Infinite Graph、

    4. NoSQL应用场景

    • 数据模型比较简单
    • 需要灵活性更强的IT系统
    • 对数据库性能要求较高
    • 不需要高度的数据一致性

    5. 什么是Redis

    Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.

    Redis 开源 遵循BSD 基于内存数据存储 被用于作为 数据库 缓存 消息中间件

    • 总结: redis是一个内存型的数据库

    6. Redis特点

    • Redis是一个高性能key/value内存型数据库
    • Redis支持丰富的数据类型
    • Redis支持持久化
    • Redis单线程,单进程

    7. Redis安装

    # 0.准备环境
    - vmware15.x+
    - centos7.x+
    ​
    # 1.下载redis源码包
    - https://redis.io/

    # 2.下载完整源码包
    - redis-4.0.10.tar.gz
    # 3.将下载redis资料包上传到Linux中

    # 4.解压缩文件
    [root@localhost ~]# tar -zxvf redis-4.0.10.tar.gz
    [root@localhost ~]# ll

    # 5.安装gcc  
    - yum install -y gcc
    ​
    # 6.进入解压缩目录执行如下命令
    - make MALLOC=libc
    ​
    # 7.编译完成后执行如下命令
    - make install PREFIX=/usr/redis
    ​
    # 8.进入/usr/redis目录启动redis服务 
    - ./redis-server
    # 9.Redis服务端口默认是 6379
    ​
    # 10.进入bin目录执行客户端连接操作
    - ./redis-cli –p 6379

    # 11.连接成功出现上面界面连接成功

    8. Redis数据库相关指令

    8.1 数据库操作指令

    # 1.Redis中库说明
    - 使用redis的默认配置器动redis服务后,默认会存在16个库,编号从0-15
    - 可以使用select 库的编号 来选择一个redis的库
    ​
    # 2.Redis中操作库的指令
    - 清空当前的库  FLUSHDB
    - 清空全部的库  FLUSHALL
    ​
    # 3.redis客户端显示中文
    - ./redis-cli  -p 7000 --raw

    8.2 操作key相关指令

    # 1.DEL指令
    - 语法 :  DEL key [key ...] 
    - 作用 :  删除给定的一个或多个key 。不存在的key 会被忽略。
    - 可用版本: >= 1.0.0
    - 返回值: 被删除key 的数量。 
    ​
    # 2.EXISTS指令
    - 语法:  EXISTS key
    - 作用:  检查给定key 是否存在。
    - 可用版本: >= 1.0.0
    - 返回值: 若key 存在,返回1 ,否则返回0。
    ​
    # 3.EXPIRE
    - 语法:  EXPIRE key seconds
    - 作用:  为给定key 设置生存时间,当key 过期时(生存时间为0 ),它会被自动删除。
    - 可用版本: >= 1.0.0
    - 时间复杂度: O(1)
    - 返回值:设置成功返回1 。
    ​
    # 4.KEYS
    - 语法 :  KEYS pattern
    - 作用 :  查找所有符合给定模式pattern 的key 。
    - 语法:
      KEYS * 匹配数据库中所有key 。
      KEYS h?llo 匹配hello ,hallo 和hxllo 等。
      KEYS h*llo 匹配hllo 和heeeeello 等。
      KEYS h[ae]llo 匹配hello 和hallo ,但不匹配hillo 。特殊符号用 "\" 隔开
    - 可用版本: >= 1.0.0
    - 返回值: 符合给定模式的key 列表。
    ​
    # 5.MOVE
    - 语法 :  MOVE key db
    - 作用 :  将当前数据库的key 移动到给定的数据库db 当中。
    - 可用版本: >= 1.0.0
    - 返回值: 移动成功返回1 ,失败则返回0 。
    ​
    # 6.PEXPIRE
    - 语法 :  PEXPIRE key milliseconds
    - 作用 :  这个命令和EXPIRE 命令的作用类似,但是它以毫秒为单位设置key 的生存时间,而不像EXPIRE 命令那样,以秒为单位。
    - 可用版本: >= 2.6.0
    - 时间复杂度: O(1)
    - 返回值:设置成功,返回1  key 不存在或设置失败,返回0
    ​
    # 7.PEXPIREAT
    - 语法 :  PEXPIREAT key milliseconds-timestamp
    - 作用 :  这个命令和EXPIREAT 命令类似,但它以毫秒为单位设置key 的过期unix 时间戳,而不是像EXPIREAT那样,以秒为单位。
    - 可用版本: >= 2.6.0
    - 返回值:如果生存时间设置成功,返回1 。当key 不存在或没办法设置生存时间时,返回0 。(查看EXPIRE 命令获取更多信息)
    ​
    # 8.TTL
    - 语法 :   TTL key
    - 作用 :   以秒为单位,返回给定key 的剩余生存时间(TTL, time to live)。
    - 可用版本: >= 1.0.0
    - 返回值:
      当key 不存在时,返回-2 。
      当key 存在但没有设置剩余生存时间时,返回-1 。
      否则,以秒为单位,返回key 的剩余生存时间。
    - Note : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。
    ​
    # 9.PTTL
    - 语法 :  PTTL key
    - 作用 :  这个命令类似于TTL 命令,但它以毫秒为单位返回key 的剩余生存时间,而不是像TTL 命令那样,以秒为单位。
    - 可用版本: >= 2.6.0
    - 返回值: 当key 不存在时,返回-2 。当key 存在但没有设置剩余生存时间时,返回-1 。
    - 否则,以毫秒为单位,返回key 的剩余生存时间。
    - 注意 : 在Redis 2.8 以前,当key 不存在,或者key 没有设置剩余生存时间时,命令都返回-1 。
    ​
    # 10.RANDOMKEY
    - 语法 :  RANDOMKEY
    - 作用 :  从当前数据库中随机返回(不删除) 一个key 。
    - 可用版本: >= 1.0.0
    - 返回值:当数据库不为空时,返回一个key 。当数据库为空时,返回nil 。
    ​
    # 11.RENAME
    - 语法 :  RENAME key newkey
    - 作用 :  将key 改名为newkey 。当key 和newkey 相同,或者key 不存在时,返回一个错误。当newkey 已经存在时,RENAME 命令将覆盖旧值。
    - 可用版本: >= 1.0.0
    - 返回值: 改名成功时提示OK ,失败时候返回一个错误。
    ​
    # 12.TYPE
    - 语法 :  TYPE key
    - 作用 :  返回key 所储存的值的类型。
    - 可用版本: >= 1.0.0
    - 返回值:
      none (key 不存在)
      string (字符串)
      list (列表)
      set (集合)
      zset (有序集)
      hash (哈希表)

    8.3 String类型

    1. 内存存储模型

    2. 常用操作命令

    命令说明
    set设置一个key/value
    get根据key获得对应的value
    mset一次设置多个key value
    mget一次获得多个key的value
    getset获得原始key的值,同时设置新值
    strlen获得对应key存储value的长度
    append为对应key的value追加内容
    getrange 索引0开始截取value的内容
    setex设置一个key存活的有效期(秒)
    psetex设置一个key存活的有效期(毫秒)
    setnx存在不做任何操作,不存在添加
    msetnx原子操作(只要有一个存在不做任何操作)可以同时设置多个key,只有有一个存在都不保存
    decr进行数值类型的-1操作
    decrby根据提供的数据进行减法操作
    Incr进行数值类型的+1操作
    incrby根据提供的数据进行加法操作
    Incrbyfloat根据提供的数据加入浮点数

    8.4 List类型

    list 列表 相当于java中list 集合 特点 元素有序 且 可以重复

    1.内存存储模型

    2.常用操作指令

    命令说明
    lpush将某个值加入到一个key列表头部
    lpushx同lpush,但是必须要保证这个key存在
    rpush将某个值加入到一个key列表末尾
    rpushx同rpush,但是必须要保证这个key存在
    lpop返回和移除列表左边的第一个元素
    rpop返回和移除列表右边的第一个元素
    lrange获取某一个下标区间内的元素
    llen获取列表元素个数
    lset设置某一个指定索引的值(索引必须存在)
    lindex获取某一个指定索引位置的元素
    lrem删除重复元素
    ltrim保留列表中特定区间内的元素
    linsert在某一个元素之前,之后插入新元素

    8.5 Set类型

    特点: Set类型 Set集合 元素无序 不可以重复

    1.内存存储模型

    2.常用命令

    命令说明
    sadd为集合添加元素
    smembers显示集合中所有元素 无序
    scard返回集合中元素的个数
    spop随机返回一个元素 并将元素在集合中删除
    smove从一个集合中向另一个集合移动元素 必须是同一种类型
    srem从集合中删除一个元素
    sismember判断一个集合中是否含有这个元素
    srandmember随机返回元素
    sdiff去掉第一个集合中其它集合含有的相同元素
    sinter求交集
    sunion求和集

    8.6 ZSet类型

    特点: 可排序的set集合 排序 不可重复

    ZSET 官方 可排序SET sortSet

    1.内存模型

    2.常用命令

    命令说明
    zadd添加一个有序集合元素
    zcard返回集合的元素个数
    zrange 升序 zrevrange 降序返回一个范围内的元素
    zrangebyscore按照分数查找一个范围内的元素
    zrank返回排名
    zrevrank倒序排名
    zscore显示某一个元素的分数
    zrem移除某一个元素
    zincrby给某个特定元素加分

    8.7 hash类型

    特点: value 是一个map结构 存在key value key 无序的

    1.内存模型

    2.常用命令

    命令说明
    hset设置一个key/value对
    hget获得一个key对应的value
    hgetall获得所有的key/value对
    hdel删除某一个key/value对
    hexists判断一个key是否存在
    hkeys获得所有的key
    hvals获得所有的value
    hmset设置多个key/value
    hmget获得多个key的value
    hsetnx设置一个不存在的key的值
    hincrby为value进行加法运算
    hincrbyfloat为value加入浮点值

    9. 持久化机制

    client redis[内存] —–> 内存数据- 数据持久化–>磁盘

    Redis官方提供了两种不同的持久化方法来将数据存储到硬盘里面分别是:

    • 快照(Snapshot)
    • AOF (Append Only File) 只追加日志文件

    9.1 快照(Snapshot)

    1. 特点

    这种方式可以将某一时刻的所有数据都写入硬盘中,当然这也是redis的默认开启持久化方式,保存的文件是以.rdb形式结尾的文件因此这种方式也称之为RDB方式。

    2.快照生成方式

    • 客户端方式: BGSAVE 和 SAVE指令
    • 服务器配置自动触发
    # 1.客户端方式之BGSAVE
    - a.客户端可以使用BGSAVE命令来创建一个快照,当接收到客户端的BGSAVE命令时,redis会调用fork¹来创建一个子进程,然后子进程负责将快照写入磁盘中,而父进程则继续处理命令请求。
      
      `名词解释: fork当一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会进行优化:在刚开始的时候,父子进程共享相同内存,直到父进程或子进程对内存进行了写之后,对被写入的内存的共享才会结束服务`

    # 2.客户端方式之SAVE
    - b.客户端还可以使用SAVE命令来创建一个快照,接收到SAVE命令的redis服务器在快照创建完毕之前将不再响应任何其他的命令
    • 注意: SAVE命令并不常用,使用SAVE命令在快照创建完毕之前,redis处于阻塞状态,无法对外服务
    # 3.服务器配置方式之满足配置自动触发
    - 如果用户在redis.conf中设置了save配置选项,redis会在save选项条件满足之后自动触发一次BGSAVE命令,如果设置多个save配置选项,当任意一个save配置选项条件满足,redis也会触发一次BGSAVE命令
    # 4.服务器接收客户端shutdown指令
    - 当redis通过shutdown指令接收到关闭服务器的请求时,会执行一个save命令,阻塞所有的客户端,不再执行客户端执行发送的任何命令,并且在save命令执行完毕之后关闭服务器

    3.配置生成快照名称和位置

    #1.修改生成快照名称
    - dbfilename dump.rdb
    ​
    # 2.修改生成位置
    - dir ./

    9.2 AOF 只追加日志文件

    1.特点

    这种方式可以将所有客户端执行的写命令记录到日志文件中,AOF持久化会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化,因此只要redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录的数据集.

    2.开启AOF持久化

    在redis的默认配置中AOF持久化机制是没有开启的,需要在配置中开启

    # 1.开启AOF持久化
    - a.修改 appendonly yes 开启持久化
    - b.修改 appendfilename "appendonly.aof" 指定生成文件名称

    3.日志追加频率

    # 1.always 【谨慎使用】
    - 说明: 每个redis写命令都要同步写入硬盘,严重降低redis速度
    - 解释: 如果用户使用了always选项,那么每个redis写命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少;遗憾的是,因为这种同步策略需要对硬盘进行大量的写入操作,所以redis处理命令的速度会受到硬盘性能的限制;
    - 注意: 转盘式硬盘在这种频率下200左右个命令/s ; 固态硬盘(SSD) 几百万个命令/s;
    - 警告: 使用SSD用户请谨慎使用always选项,这种模式不断写入少量数据的做法有可能会引发严重的写入放大问题,导致将固态硬盘的寿命从原来的几年降低为几个月。
    
    # 2.everysec 【推荐】
    - 说明: 每秒执行一次同步显式的将多个写命令同步到磁盘
    - 解释: 为了兼顾数据安全和写入性能,用户可以考虑使用everysec选项,让redis每秒一次的频率对AOF文件进行同步;redis每秒同步一次AOF文件时性能和不使用任何持久化特性时的性能相差无几,而通过每秒同步一次AOF文件,redis可以保证,即使系统崩溃,用户最多丢失一秒之内产生的数据。
    
    # 3.no	【不推荐】
    - 说明: 由操作系统决定何时同步 
    - 解释:最后使用no选项,将完全有操作系统决定什么时候同步AOF日志文件,这个选项不会对redis性能带来影响但是系统崩溃时,会丢失不定数量的数据,另外如果用户硬盘处理写入操作不够快的话,当缓冲区被等待写入硬盘数据填满时,redis会处于阻塞状态,并导致redis的处理命令请求的速度变慢。

    4.修改同步频率

    # 1.修改日志同步频率
    - 修改appendfsync everysec|always|no 指定

    9.3 AOF文件的重写

    1. AOF带来的问题

    AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。为了压缩aof的持久化文件Redis提供了AOF重写(ReWriter)机制。

    2. AOF重写

    用来在一定程度上减小AOF文件的体积

    3. 触发重写方式

    # 1.客户端方式触发重写
    - 执行BGREWRITEAOF命令  不会阻塞redis的服务
    
    # 2.服务器配置方式自动触发
    - 配置redis.conf中的auto-aof-rewrite-percentage选项 参加下图↓↓↓
    - 如果设置auto-aof-rewrite-percentage值为100和auto-aof-rewrite-min-size 64mb,并且启用的AOF持久化时,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(100%)时,会自动触发,如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大

    4. 重写原理

    注意:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,替换原有的文件这点和快照有点类似。

    # 重写流程
    - 1. redis调用fork ,现在有父子两个进程 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
    - 2. 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
    - 3. 当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
    - 4. 现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

    9.4 持久化总结

    两种持久化方案既可以同时使用(aof),又可以单独使用,在某种情况下也可以都不使用,具体使用那种持久化方案取决于用户的数据和应用决定。

    无论使用AOF还是快照机制持久化,将数据持久化到硬盘都是有必要的,除了持久化外,用户还应该对持久化的文件进行备份(最好备份在多个不同地方)。

    10. java操作Redis

    10.1 环境准备

    1. 引入依赖

    <!--引入jedis连接依赖-->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
    </dependency>

    2.创建jedis对象

     public static void main(String[] args) {
       //1.创建jedis对象
       Jedis jedis = new Jedis("192.168.40.4", 6379);//1.redis服务必须关闭防火墙  2.redis服务必须开启远程连接
       jedis.select(0);//选择操作的库默认0号库
       //2.执行相关操作
       //....
       //3.释放资源
       jedis.close();
     }

    10.2 操作key相关API

    private Jedis jedis;
        @Before
        public void before(){
            this.jedis = new Jedis("192.168.202.205", 7000);
        }
        @After
        public void after(){
            jedis.close();
        }
    
        //测试key相关
        @Test
        public void testKeys(){
            //删除一个key
            jedis.del("name");
            //删除多个key
            jedis.del("name","age");
    
            //判断一个key是否存在exits
            Boolean name = jedis.exists("name");
            System.out.println(name);
    
            //设置一个key超时时间 expire pexpire
            Long age = jedis.expire("age", 100);
            System.out.println(age);
    
            //获取一个key超时时间 ttl
            Long age1 = jedis.ttl("newage");
            System.out.println(age1);
    
            //随机获取一个key
            String s = jedis.randomKey();
    
            //修改key名称
            jedis.rename("age","newage");
    
            //查看可以对应值的类型
            String name1 = jedis.type("name");
            System.out.println(name1);
            String maps = jedis.type("maps");
            System.out.println(maps);
        }

    10.3操作String相关API

    //测试String相关
        @Test
        public void testString(){
            //set
            jedis.set("name","小陈");
            //get
            String s = jedis.get("name");
            System.out.println(s);
            //mset
            jedis.mset("content","好人","address","海淀区");
            //mget
            List<String> mget = jedis.mget("name", "content", "address");
            mget.forEach(v-> System.out.println("v = " + v));
            //getset
            String set = jedis.getSet("name", "小明");
            System.out.println(set);
    
            //............
        }

    10.4操作List相关API

    //测试List相关
        @Test
        public void testList(){
    
            //lpush
            jedis.lpush("names1","张三","王五","赵柳","win7");
    
            //rpush
            jedis.rpush("names1","xiaomingming");
    
            //lrange
    
            List<String> names1 = jedis.lrange("names1", 0, -1);
            names1.forEach(name-> System.out.println("name = " + name));
    
            //lpop rpop
            String names11 = jedis.lpop("names1");
            System.out.println(names11);
    
            //llen
            jedis.linsert("lists", BinaryClient.LIST_POSITION.BEFORE,"xiaohei","xiaobai");
    
          	//........
    
        }

    10.5操作Set的相关API

    //测试SET相关
    @Test
    public void testSet(){
    
      //sadd
      jedis.sadd("names","zhangsan","lisi");
    
      //smembers
      jedis.smembers("names");
    
      //sismember
      jedis.sismember("names","xiaochen");
    
      //...
    }

    10.6 操作ZSet相关API

    //测试ZSET相关
    @Test
    public void testZset(){
    
      //zadd
      jedis.zadd("names",10,"张三");
    
      //zrange
      jedis.zrange("names",0,-1);
    
      //zcard
      jedis.zcard("names");
    
      //zrangeByScore
      jedis.zrangeByScore("names","0","100",0,5);
    
      //..
    
    }

    10.7 操作Hash相关API

    //测试HASH相关
    @Test
    public void testHash(){
      //hset
      jedis.hset("maps","name","zhangsan");
      //hget
      jedis.hget("maps","name");
      //hgetall
      jedis.hgetAll("mps");
      //hkeys
      jedis.hkeys("maps");
      //hvals
      jedis.hvals("maps");
      //....
    }

    11.SpringBoot整合Redis

    Spring Boot Data(数据) Redis 中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate中的两个泛型都是Object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着StringRedisTemplate的key和value都只能是字符串。

    注意: 使用RedisTemplate默认是将对象序列化到Redis中,所以放入的对象必须实现对象序列化接口

    11.1 环境准备

    1.引入依赖

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    2.配置application.propertie

    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.database=0

    11.2 使用StringRedisTemplate和RedisTemplate

    @Autowired
        private StringRedisTemplate stringRedisTemplate;  //对字符串支持比较友好,不能存储对象
        @Autowired
        private RedisTemplate redisTemplate;  //存储对象
    
        @Test
        public void testRedisTemplate(){
            System.out.println(redisTemplate);
            //设置redistemplate值使用对象序列化策略
            redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());//指定值使用对象序列化
            //redisTemplate.opsForValue().set("user",new User("21","小黑",23,new Date()));
            User user = (User) redisTemplate.opsForValue().get("user");
            System.out.println(user);
    //      Set keys = redisTemplate.keys("*");
    //      keys.forEach(key -> System.out.println(key));
            /*Object name = redisTemplate.opsForValue().get("name");
            System.out.println(name);*/
    
            //Object xiaohei = redisTemplate.opsForValue().get("xiaohei");
            //System.out.println(xiaohei);
            /*redisTemplate.opsForValue().set("name","xxxx");
            Object name = redisTemplate.opsForValue().get("name");
            System.out.println(name);*/
            /*redisTemplate.opsForList().leftPushAll("lists","xxxx","1111");
            List lists = redisTemplate.opsForList().range("lists", 0, -1);
            lists.forEach(list-> System.out.println(list));*/
        }
    
    
        //key的绑定操作 如果日后对某一个key的操作及其频繁,可以将这个key绑定到对应redistemplate中,日后基于绑定操作都是操作这个key
        //boundValueOps 用来对String值绑定key
        //boundListOps 用来对List值绑定key
        //boundSetOps 用来对Set值绑定key
        //boundZsetOps 用来对Zset值绑定key
        //boundHashOps 用来对Hash值绑定key
    
        @Test
        public void testBoundKey(){
            BoundValueOperations<String, String> nameValueOperations = stringRedisTemplate.boundValueOps("name");
            nameValueOperations.set("1");
            //yuew
            nameValueOperations.set("2");
            String s = nameValueOperations.get();
            System.out.println(s);
    
        }
    
    
        //hash相关操作 opsForHash
        @Test
        public void testHash(){
            stringRedisTemplate.opsForHash().put("maps","name","小黑");
            Object o = stringRedisTemplate.opsForHash().get("maps", "name");
            System.out.println(o);
        }
    
        //zset相关操作 opsForZSet
        @Test
        public void testZSet(){
            stringRedisTemplate.opsForZSet().add("zsets","小黑",10);
            Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);
            zsets.forEach(value-> System.out.println(value));
        }
    
        //set相关操作 opsForSet
        @Test
        public void testSet(){
            stringRedisTemplate.opsForSet().add("sets","xiaosan","xiaosi","xiaowu");
            Set<String> sets = stringRedisTemplate.opsForSet().members("sets");
            sets.forEach(value-> System.out.println(value));
        }
    
        //list相关的操作opsForList
        @Test
        public void testList(){
            // stringRedisTemplate.opsForList().leftPushAll("lists","张三","李四","王五");
            List<String> lists = stringRedisTemplate.opsForList().range("lists", 0, -1);
            lists.forEach(key -> System.out.println(key));
        }
    
    
        //String相关的操作 opsForValue
        @Test
        public void testString(){
            //stringRedisTemplate.opsForValue().set("166","好同学");
            String s = stringRedisTemplate.opsForValue().get("166");
            System.out.println(s);
            Long size = stringRedisTemplate.opsForValue().size("166");
            System.out.println(size);
        }
    
    
        //key相关的操作
        @Test
        public void test(){
            Set<String> keys = stringRedisTemplate.keys("*");//查看所有key
            Boolean name = stringRedisTemplate.hasKey("name");//判断某个key是否存在
            stringRedisTemplate.delete("age");//根据指定key删除
            stringRedisTemplate.rename("","");//修改key的名称
            stringRedisTemplate.expire("key",10, TimeUnit.HOURS);
          	//设置key超时时间 参数1:设置key名 参数2:时间 参数3:时间的单位
            stringRedisTemplate.move("",1);//移动key
        }

    12. Redis 主从复制

    12.1 主从复制

    主从复制架构仅仅用来解决数据的冗余备份,从节点仅仅用来同步数据

    无法解决: 1.master节点出现故障的自动故障转移

    12.2 主从复制架构图

    12.3 搭建主从复制

    # 1.准备3台机器并修改配置
    - master
    	port 6379
    	bind 0.0.0.0
    	
    - slave1
    	port 6380
    	bind 0.0.0.0
    	slaveof masterip masterport
    
    - slave2
    	port 6381
    	bind 0.0.0.0
    	slaveof masterip masterport
    # 2.启动3台机器进行测试
    - cd /usr/redis/bin
    - ./redis-server /root/master/redis.conf
    - ./redis-server /root/slave1/redis.conf
    - ./redis-server /root/slave2/redis.conf

    13. Redis哨兵机制

    13.1 哨兵Sentinel机制

    Sentinel(哨兵)是Redis 的高可用性解决方案:由一个或多个Sentinel 实例 组成的Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。简单的说哨兵就是带有自动故障转移功能的主从架构

    无法解决: 1.单节点并发压力问题 2.单节点内存和磁盘物理上限

    13.2 哨兵架构原理

    13.3 搭建哨兵架构

    # 1.在主节点上创建哨兵配置
    - 在Master对应redis.conf同目录下新建sentinel.conf文件,名字绝对不能错;
    
    # 2.配置哨兵,在sentinel.conf文件中填入内容:
    - sentinel monitor 被监控数据库名字(自己起名字) ip port 1
    
    # 3.启动哨兵模式进行测试
    - redis-sentinel  /root/sentinel/sentinel.conf
    	说明:这个后面的数字2,是指当有两个及以上的sentinel服务检测到master宕机,才会去执行主从切换的功能。

    13.4 通过springboot操作哨兵

    # redis sentinel 配置
    # master书写是使用哨兵监听的那个名称
    spring.redis.sentinel.master=mymaster
    # 连接的不再是一个具体redis主机,书写的是多个哨兵节点
    spring.redis.sentinel.nodes=192.168.202.206:26379
    • 注意:如果连接过程中出现如下错误:RedisConnectionException: DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command ‘CONFIG SET protected-mode no’ from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2)
    • 解决方案:在哨兵的配置文件中加入bind 0.0.0.0 开启远程连接权限

    14. Redis集群

    14.1 集群

    Redis在3.0后开始支持Cluster(模式)模式,目前redis的集群支持节点的自动发现,支持slave-master选举和容错,支持在线分片(sharding shard )等特性。reshard

    14.2 集群架构图

    14.3 集群细节

    - 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
    - 节点的fail是通过集群中超过半数的节点检测失效时才生效. 
    - 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
    - redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value

    14.4 集群搭建

    判断一个是集群中的节点是否可用,是集群中的所用主节点选举过程,如果半数以上的节点认为当前节点挂掉,那么当前节点就是挂掉了,所以搭建redis集群时建议节点数最好为奇数,搭建集群至少需要三个主节点,三个从节点,至少需要6个节点

    # 1.准备环境安装ruby以及redis集群依赖
    - yum install -y ruby rubygems
    - gem install redis-xxx.gem
    # 2.在一台机器创建7个目录
    # 3.每个目录复制一份配置文件
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7000/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7001/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7002/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7003/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7004/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7005/
    [root@localhost ~]# cp redis-4.0.10/redis.conf 7006/
    # 4.修改不同目录配置文件
    - port 	6379 .....                		 //修改端口
    - bind  0.0.0.0                   		 //开启远程连接
    - cluster-enabled  yes 	        			 //开启集群模式
    - cluster-config-file  nodes-port.conf //集群节点配置文件
    - cluster-node-timeout  5000      	   //集群节点超时时间
    - appendonly  yes   		               //开启AOF持久化
    
    # 5.指定不同目录配置文件启动七个节点
    - [root@localhost bin]# ./redis-server  /root/7000/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7001/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7002/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7003/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7004/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7005/redis.conf
    - [root@localhost bin]# ./redis-server  /root/7006/redis.conf
    # 6.查看进程
    - [root@localhost bin]# ps aux|grep redis

    1.创建集群

    # 1.复制集群操作脚本到bin目录中
    - [root@localhost bin]# cp /root/redis-4.0.10/src/redis-trib.rb .
    
    # 2.创建集群
    - ./redis-trib.rb create --replicas 1 192.168.202.205:7000 192.168.202.205:7001 192.168.202.205:7002 192.168.202.205:7003 192.168.202.205:7004 192.168.202.205:7005
    # 3.集群创建成功出现如下提示

    2.查看集群状态

    # 1.查看集群状态 check [原始集群中任意节点] [无]
    - ./redis-trib.rb check 192.168.202.205:7000
    
    # 2.集群节点状态说明
    - 主节点 
    	主节点存在hash slots,且主节点的hash slots 没有交叉
    	主节点不能删除
    	一个主节点可以有多个从节点
    	主节点宕机时多个副本之间自动选举主节点
    
    - 从节点
    	从节点没有hash slots
    	从节点可以删除
    	从节点不负责数据的写,只负责数据的同步

    3.添加主节点

    # 1.添加主节点 add-node [新加入节点] [原始集群中任意节点]
    - ./redis-trib.rb  add-node 192.168.1.158:7006  192.168.1.158:7005
    - 注意:
      1.该节点必须以集群模式启动
      2.默认情况下该节点就是以master节点形式添加

    4.添加从节点

    # 1.添加从节点 add-node --slave [新加入节点] [集群中任意节点]
    - ./redis-trib.rb  add-node --slave 192.168.1.158:7006 192.168.1.158:7000
    - 注意:
    	当添加副本节点时没有指定主节点,redis会随机给副本节点较少的主节点添加当前副本节点
    	
    # 2.为确定的master节点添加主节点 add-node --slave --master-id master节点id [新加入节点] [集群任意节点]
    - ./redis-trib.rb  add-node --slave --master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e 127.0.0.1:7006  127.0.0.1:7000

    5.删除副本节点

    # 1.删除节点 del-node [集群中任意节点] [删除节点id]
    - ./redis-trib.rb  del-node 127.0.0.1:7002 0ca3f102ecf0c888fc7a7ce43a13e9be9f6d3dd1
    - 注意:
     1.被删除的节点必须是从节点或没有被分配hash slots的节点

    6.集群在线分片

    # 1.在线分片 reshard [集群中任意节点] [无]
    - ./redis-trib.rb  reshard  192.168.1.158:7000

    15.Redis实现分布式Session管理

    15.1 管理机制

    redis的session管理是利用spring提供的session管理解决方案,将一个应用session交给Redis存储,整个应用中所有session的请求都会去redis中获取对应的session数据。

    15.2 开发Session管理

    1. 引入依赖

    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
    </dependency>

    2. 开发Session管理配置类

    @Configuration
    @EnableRedisHttpSession
    public class RedisSessionManager {
       
    }

    3.打包测试即可

    16.分布式缓存

    1. 什么是缓存(Cache)

    定义:就是计算机中内存的一段数据

    2. 内存中数据特点

    • 读写快
    • 断电立即消失

    3. 缓存解决了什么问题?

    • 提高网站吞吐量,提高网站运行效率
    • 核心解决问题:缓存的存在是用来减轻数据库访问压力

    4.既然缓存能提高效率,那项目中所有数据加入缓存岂不是更好?

    注意:使用缓存时一定是数据库中极少发生修改,更多用于查询这种情况,如省市县

    5. 本地缓存和分布式缓存区别?

    • 本地缓存:存储在应用服务器内存中数据称之为本地缓存(local cache)
    • 分布式缓存:存储在当前应用服务器内存之外数据称之为分布式缓存(Distribute cache)
    • 集群:将同一种服务的多个节点放在一起共同对系统提供服务过程称之为集群
    • 分布式:有多个不同服务集群共同对系统提供服务,这个系统称之为分布式系统(Distribute System)

    6. 利用mybatis自身本地缓存结合redis实现分布式缓存

    1. mybatis中应用级缓存(二级缓存)SqlSessionFactory级别缓存,所有会话共享
    2. 如何开启二级缓存
      <!-- mapper.xml--><cache />
    3. 查看cache标签缓存实现:mybatis底层默认使用的是org.apache.ibatis.cache.impl.PerpetualCache实现
    4. 自定义RedisCache实现
      1. 通过mybatis默认cache源码得知,可以使用自定义cache类 implements Cache接口,并对里面的方法进行实现
        public class RedisCache implements Cache {…}
      2. mybatis配置使用redisCache实现
        <cache type="com.xxx.xxx.RedisCache" />

    7. 缓存在项目中的应用

    • 如果项目中表查询之间没有任何关联,查询使用现在的这种缓存方式没有任何问题
    • 现在缓存方式在表连接查询过程中一定存在问题:
      当A表与B表同时开启了二级缓存并且做了关联查询并缓存到redis对应的数据中,另一请求删除A表数据并且删除redis中对应的A表缓存,此时B表由于使用关联查询,在redis中还缓存着A表的数据。解决方案:在xml使用<cache-ref /> 将多个具有关联关系的查谒缓存放在一起处理

    17.经典场景

    缓存穿透

    什么叫缓存穿透:去缓存层中没有命中数据,进而去数据库查询数据。不能避免低频缓存穿透,可以避免高频的缓存穿透

    场景1:黑客可以通过一个固定的请求去攻击数据库

    解决方案:可以查询数据库,然后缓存NULL值到redis中,这样下一次查询就直接中缓存层返回

    场景2:黑客用一个随机条件的请求去攻击数据库

    解决方案:黑客如果用随机条件请求去攻击数据库,场景1的解决方案不能解决问题,反而会事得其反,在redis中占用更多的无效的数据,此时应该使用布隆过滤器来解决此问题

    • 布隆算法:通过一定的错误率来换取空间,假设传值10,经过hash函数计算(hash值范围为[0,lenth-1])为1,那么在bitset的1下标位标记为true,客户端再传998,经过hash函数计算为5,那么在bitset的5下标位标记为true
    • 布隆算法特性:标记为true时,可能不存在,标记为false时,绝对不存在
    • 布隆算法由于存在hash碰撞所以导致会有错误率的产生,那么如何降低错误率?
      • 加大数组的长度:范围越大,错误率产生的机率就越低
      • 增加hash函数的个数:假如使用3个hash函数,值10经过计算后分别为2,6,8,存储到bitset,值78经过计算2,9,11存储到bitset,如果只使用一个hash函数,那么这两个值可能出现重复,使用3个hash函数后,就降低了值重复的错误率
      • hash函数并不是越多越好,需要参数数组的长度,假设数据长度为10,hash函数为9个,那么经过多次标记后,所有数组都被置为true,此时错误率就增加
    • 布隆算法弊端:因为错误率导致下标位可能不止标记一条数据的存在,所以,删除数据的同时不能直接把下标位标为false,因为会影响到其他数据的使用,此时应该使用记数器记录此下标的出现的个数,再根据个数进行操作
    • guava有提供对布隆算法的使用封装

    缓存雪崩

    什么叫缓存雪崩:缓存层中的数据,在某一时刻突然失效(无法访问)导致大量的请求打向数据库

    导致雪崩的原因:

    1. redis中缓存的数据有效期是一致的:给每一条数据加上一个随机有效期,不要同时失效
    2. redis数据库挂掉了:使用分布式部署,hotdata使用切片集群部署,当一台redis挂掉,只是丢失一部份数据,如果hotdata数据量较少,则使用副本集群部署

    一致性hash算法

    1. 为什么需要一致性Hash环?

    假设我们有N个Cache服务器的Node,我们的要求就是将key均分的存储到Cache节点上,经典的做饭就是如下:

    position = hash(key) mod N

    那么这样在如下两种情况下会出现问题:

    • 有一台机器挂了
    • 新增一台机器

    这时候公式变成

    position = hash(key) mod (N+1) position = hash(key) mod (N-1)

    之前所有的求得结果都不对了,导致全部cache miss,只能回源到mysql,最终可想而知整个系统崩溃,如果这样设计分布式缓存系统,那这个分布式缓存系统是失败的。

    2. 一致性hash算法的特性

    • 平衡性:尽可能让数据尽可能分散到所有节点上,避免造成极其不均匀
    • 单调性:要求在新增或者减少节点的时候,原有的结果绝大部分不受影响,而新增的数据尽可能分配到新加的节点
    • 分散性:好的算法在不同终端,针对相同的数据的计算,得到的结果应该是一样的,一致性要很强
    • 负载:针对相同的节点,避免被不同终端映射不同的内容
    • 平滑性:对于增加节点或者减少节点,应该能够平滑过渡

    3. 一致性Hash算法原理介绍

    • 背景:一致性哈希算法在1997年由MIT的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题。
    • 算法原理
      1. 虚拟一个环的概念,在环上构造一个0~232
      1. 将N台服务器节点计算Hash值,映射到这个环上(可以用节点的IP或者主机名来计算hash值)
    1. 将数据用相同的Hash算法计算的值,映射到这个环上
    1. 然后顺时针寻找,找到的第一个服务器节点就是目标要保存的节点,如果超过232,就放到第一个节点
    2. d1,d2就存储到Node B
    3. d3, d4, d5存储到Node C
    4. d6, d7, d8存储到Node A
    1. 假如Node B宕机了,看看是什么情形:Node B宕机后,其他都不受印象,d1,d2会发生一次cache miss,然后再会存储Node C,影响比较小
    1. 如果新增加一台机器Node D,又会发生什么呢?d6,d7就会存储到了Node D,不再存储到Node A

    4. 一致性Hash节点太少改良版本

    如果出现极限情况下,我只有两台机器,然后大量的数据都集中某一台机器,例如下面情况

    这个根本原因就是节点太少,hash算法不均衡造成的,那么怎么解决

    • 将一个物理节点虚拟出N个虚拟节点来用,在环上体现虚拟节点。 Node A可以虚拟出Node A1,Node A2,Node A3 Node B可以虚拟出Node B1,Node B2,Node B3

    这样环上其实就有6个节点了。

    5. 一致性hash算法的应用场景

    • 场景1:经典CDN
      当你请求一个静态资源的时候,你需要请求到最近的节点
    • 场景2:session的问题

    缓存击穿

    什么是缓存击穿:对一个设置过期时间的hotdata,可能在被超高并发访问时过期,导致对这个key的请求大量落在了数据库层,瞬间可能把后端的DB压垮。

    一般公司不需要解决,因为很少这么大的请求数出现

    缓存击穿和缓存雪崩本质都是缓存穿透,缓存击穿与缓存雪崩是缓存穿透的特殊表现

    解决方案:采用分布式锁

    分布式锁

    Redis分布式锁会遇到的问题:

    1. 程序运行时可能抛异常
    2. 程序运行时可能宕机
    3. 删除锁时可能会误删其他线程创建的锁

    解决方案:使用redisson的分布式锁创建,采用锁续命的机制判断当前线程是否还持有锁,如果还持有锁,则会延长锁持有时间

    分布式锁主从架构锁失效:

    1. 使用Redlock对所有redis中间件进行加锁,只有过半redis加锁成功后才会返回客户端加锁成功(有争议,性能问题)
    2. 换成使用zookeeper进行加锁,原理跟redlock类似

    分布式锁的性能优化:

    1. 使用分段锁,如库存为200个,分为10笔20个,再对这10笔数据进行加锁,性能则能提升

    redis缓存与数据库双写不一致的问题:

    有两个线程1,2,当线程1更新数据库后,由于未知原因出现卡顿还未更新到redis,此时线程2更新数据库,也同时更新redis,线程1恢复执行,再更新redis,这就导致redis缓存与数据库双写不一致

    解决方案:

    1. 延时双删:不一定能解决此问题,而且容易造成写入吞吐量降低(不推荐)
    2. 内存队列:能解决此问题,但由于串行执行,所以性能不高,而且实现不方便(不推荐)
    3. 读写锁:redisson.getReadWriteLock(),读锁可以共享,读写锁互斥,在redisson底层会判断mode是read模式,则直接继续加read锁,如果是write锁,read锁则会等待(读多写少的场景)
    4. 设置超时时间:能够容忍时间差的场景

    内存淘汰策略

    Redis作为当前最常用的开源内存数据库,性能十分高,据官方数据表示Redis读的速度是110000次/s,写的速度是81000次/s 。而且Redis支持数据持久化,众多数据结构存储,master-slave模式数据备份等多种功能。

    但是长期将Redis作为缓存使用,难免会遇到内存空间存储瓶颈,当Redis内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,使Redis性能急剧下降。此时如何淘汰无用数据释放空间,存储新数据就变得尤为重要了。

    淘汰原理

    Redis在生产环境中,采用配置参数maxmemory 的方式来限制内存大小。当实际存储内存超出maxmemory 参数值时,可以通过这几种方法——Redis内存淘汰策略,来决定如何腾出新空间继续支持读写工作。

    那么Redis内存淘汰策略是如何工作的呢? 首先,客户端会发起需要更多内存的申请;其次,Redis检查内存使用情况,如果实际使用内存已经超出maxmemory,Redis就会根据用户配置的淘汰策略选出无用的key;

    确认选中数据没有问题,成功执行淘汰任务。

    淘汰策略

    1. volatile-lru:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
    2. volatile-ttl:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。
    3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
    4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
    5. allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰。
    6. no-enviction:禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略。

    区分不同的淘汰策略选择不同的key,Redis淘汰策略主要分为LRU淘汰TTL淘汰随机淘汰三种机制。

    LRU淘汰

    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来进行排序的,然后选择最近使用时间最久的数据进行删除。另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。每一次访问数据,会更新对应redisObject.lru。在Redis中,LRU算法是一个近似算法,默认情况下,Redis会随机挑选5个键,并从中选择一个最久未使用的key进行淘汰。在配置文件中,按maxmemory-samples选项进行配置,选项配置越大,消耗时间就越长,但结构也就越精准。

    TTL淘汰

    Redis 数据集数据结构中保存了键值对过期时间的表,即 redisDb.expires。与 LRU 数据淘汰机制类似,TTL 数据淘汰机制中会先从过期时间的表中随机挑选几个键值对,取出其中 ttl ***的键值对淘汰。同样,TTL淘汰策略并不是面向所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对。

    随机淘汰

    在随机淘汰的场景下获取待删除的键值对,随机找hash桶再次hash指定位置的dictEntry即可。

    Redis中的淘汰机制都是几近于算法实现的,主要从性能和可靠性上做平衡,所以并不是完全可靠,所以开发者们在充分了解Redis淘汰策略之后还应在平时多主动设置或更新key的expire时间,主动删除没有价值的数据,提升Redis整体性能和空间。

    Redis缓存功能,是由edis.c文件中的freeMemoryIfNeeded函数实现的。如果maxmemory被设置,那么每次在执行命令钱,该函数都会被调用来判断内存是否够用、释放内存、返回错误。如果没有足够的内存程序主逻辑将会阻止设置了REDIS_COM_DENYOOM flag的命令执行,对其返回command not allowed when used memory > ‘maxmemory’的错误消息。

    使用建议

    关于使用这6种策略,还需要根据自身系统特征,正确选择或修改驱逐:

    • 在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。
    • 如果所有数据访问概率大致相等时,可以选择allkeys-random。
    • 如果需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。
    • 如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的。
    • 由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru 策略,这样就可以不再设置过期时间,高效利用内存了。
  • JWT

    JWT

    1. 什么是JWT

    JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

    翻译
    JSON Web Token(JWT)是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全的传输信息。此信息可以验证和信任,因为它是数字签名的,jwt可以使用秘密(使用HMAC算法)或使用RSA或者ECDSA的公钥/私钥对进行签名 ​

    通俗解释
    JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全的将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

    2.JWT能做什么

    授权
    这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

    信息交换
    JSON Web Token是在各方之间安全的传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以可以确保发件人是他们所说的人,此外,由于签名是使用标头和有效负载计算的,因此还可以验证内容是否遭到篡改。

    3.为什么是JWT

    基于传统的Session认证

    认证方式
    我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再发一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

    认证流程

    暴露问题
    1. 每个用户经过我们的应用认证后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
    2. 用户认证后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能务。
    3. 因为是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
    4. 在前后端分离系统中就更加痛苦,如下图所示: 也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果session每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,会给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富,不容易扩展。而且如果后端应用是多节点部署,那么就需要实现session共享机制,不方便集群应用。

    基于JWT认证

    1.认证流程
    首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这一过程一般是一个http post请求,建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
    后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
    后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
    前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
    后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Tokey的接收方是否是自己(可选)。
    验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

    2. JWT优势
    简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
    自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
    因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
    不需要在服务端保存会话信息,特别适用于分布式微服务。

    4. JWT的结构

    令牌组成
    1.标头(Header)
    2. 有效载荷(Payload)
    3. 签名(Signature)
    因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature

    Header
    标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或者RSA。它会使用Base64编码组成JWT结构的第一部份。
    注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子,它并不是一种加密过程。

    {
        "alg": "HS256",
        "typ":"JWT"
    }

    Payload
    令牌的第二部份是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分(不要放敏感信息)

    {
        "sub":"123456",
        "name":"John Doe",
        "admin":true
    }

    Signature
    前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过,如:

    HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret); 

    签名目的
    最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改。如果有人对头部以及负载的内空解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。 ​

    信息安全问题
    在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
    是的,所在在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID,这个值实际上不是什么敏感内容,一般情况下被知道也是安全的,但是像密码这样的内容就不能被放在JWT中了,如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快的知道你的密码了,因此JWT适合用于向Web应用传弟一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

    放在一起
    输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。

    5.使用JWT

    1. 引入依赖

    <!--引入JWT-->
    <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.4.0</version>
    </dependency>

    2. 生成token

    Calendar instance = Calendar.getInstance();
    instance.add(Calendar.SECOND, 90);
    // 生成令牌
    String token = JWT.create()
                    .withClaim("username", "张三") // 设置自定义用户名
                    .withExpiresAt(instance.getTime()) // 设置过期时间
                    .sign(Algorithm.HMAC256("token!#@$#@")); // 设置签名 保密 复杂
    // 输出令牌
    System.out.println(token);
    
    - 生成结果
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTY3MjM5MzQsInVzZXJuYW1lIjoi5byg5LiJIn0.vQwD5Lij6a0HlsOsVla_xUB2kOcJD9MgEDhL1MZJMYE

    3. 根据令牌和签名解析数据

    String token = "";
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!#@$#@")).build();
    DecodedJWT decodedJWT = jwtVerifier.verify(token);
    System.out.println("用户名:"  + decodedJWT.getClaim("username").asString());
    System.out.println("用户名:"  + decodedJWT.getClaim("userid").asInt());

    4. 常见异常信息

    - TokenExpiredException (com.auth0.jwt.exceptions)       令牌过期异常
    - AlgorithmMismatchException (com.auth0.jwt.exceptions)   算法不匹配异常
    - InvalidClaimException (com.auth0.jwt.exceptions)          失效的payload异常
    - SignatureVerificationException (com.auth0.jwt.exceptions)   签名不一致异常
    - JWTDecodeException (com.auth0.jwt.exceptions)           令牌解密异常

    6. JWT验证

    1. 使用拦截器对请求头进行验证

    public class JWTInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            Map<String, Object> map = new HashMap<>();
            String token = request.getHeader("token");
            try {
                JWTUtils.verify(token); // 验证令牌
                return true;
            } catch (Exception e ) { // 各个验证异常
                map.put("msg","无效签名");
                map.put("msg","token过期");
            }
            map.put("state", false); //设置状态
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            return false;
        }
    }

    2. 配置拦截器

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new JWTInterceptor())
                    .addPathPatterns("/**")                 // 其它接口的验证
                    .excludePathPatterns("/user/login");    // 所有用户都放行
        }
    }