当年,兔子学姐靠这个面试小抄拿了个22k

本文顺序是操作系统(jvm)、网络、数据库(mysql/redis),都是当时兔子的学姐准备面试的时候总结的,学生面试基本不会跑出这个范围,懂行的应该能看出来。

学姐原话:因为我本身的知识是A集合,我觉得一次记不住,需要反复看的,我就写了下来B集合。这些并不一定是所有,特别简单的或者特别生僻的A-B都没写。

所以按我的理解,那些,特别特别基础的,不用学姐说,自己也应该会,然后再看这个文章。

目录

生产消费读者写者

进程vs线程

线程生命周期

进程间通信方式

线程间通信方式

进程调度算法

死锁

页面置换算法

磁盘调度算法

jvmjvmjvmjvm区域

判断对象死亡?引用计数 vs 可达性分析

如何回收对象?垃圾收集算法

垃圾收集器

分配回收策略(老年代新生代)

GC调优

JVM常用参数

Cookie session

网络分层

握手挥手

UDP想可靠

网络访问过程

为什么tcp可靠?

UDP可靠

HTTP and HTTPS

HTTP方法及相互区别

状态码

http结构

Tcp结构

Socket

redisredisredis数据结构

对象

跳表

HyperLogLog

单线程

持久化

Redis和数据库双写一致性

缓存击穿雪崩穿透

解决Redis并发竞争Key问题

Redis缓存策略

哨兵

解决会话

sqlsqlsqlsqllsqlslqslqslq数据类型

mysql特点

存储引擎

增删改

范式

索引优缺点和使用原则

索引分类

索引区别

事务特性:ACID

并发错误

隔离级别

并发控制技术(锁)

死锁

分布式事务

SQL语句性能优化

索引的优化

分库分表

Kafka

Es


生产消费读者写者

读者写者:

多读者/多写者互斥/多读者互斥

消费者生产者:全互斥

生产者:p(empty),p(mutex),v(mutex),v(full)

消费者:p(full),p(mutex)v(mutex)v(empty)

进程vs线程

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。

线程是程序执行时的最小单位,是CPU调度和分派的基本单位。

线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。

线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。

线程生命周期

(五个状态):新建、就绪、运行、阻塞、死亡

新建状态:线程对象已经创建,还没有在其上调用start()方法

就绪状态:当线程调用start方法,但调度程序还没有把它选定为运行线程时线程

运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。(是线程进入运行状态的唯一方式)

阻塞(等待/睡眠)状态:线程仍旧是活的,但是当前没有条件运行。当某件事件出现,他可能返回到可运行状态

死亡状态:当线程的run()方法完成时就认为它死去。线程一旦死亡,就不能复生。 一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常

进程间通信方式

  1. 匿名管道:管道的实质是一个缓冲区,该缓冲区是一个循环队列,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。管道的局限性体现在:只支持单向数据流、由于管道没有名字,只能用于有亲缘关系(共同祖先)的进程间通信、缓冲区有限,效率不高
  2. 有名管道:有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信。
  3. 消息队列:消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
  4. 信号量:信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
  1. 创建信号量:指定初始值,对于二值信号量通常是1。(2)等待信号量:测试信号量的值,如果小于0就阻塞。也称P操作。(3)挂出信号量:信号量值加1,称V操作。

 

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。

 

  1. 套接字:是一种通信机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

(套接字位于传输层与应用层之间)

套接字是支持TCP/IP网络通信的基本操作单元,可以看做主机间进程进行双向通信的端点

 套节字通信基本过程

线程间通信方式

1.锁机制:包括互斥锁(对应于JAVA中的Synchronized)、条件变量(对应于JAVA中的volatile)

*互斥锁提供了以排他方式防止数据结构被并发修改的方法。

*条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

2.信号机制:包括无名线程信号量和命名线程信号量

JAVA中线程通信的具体方式有:1.Volatile和Synchronized 2.wait()和notify()机制 3.管道输入/输出流:PipedReader, PipedWriter 4.Thread.join() 5.TheadLocal类:线程变量,用于线程内部共享数据。以ThreadLocal对象为键,任意对象为值的存储结构 ThreadLocal<String> tl = new ThreadLocal<>();

进程调度算法

先来先服务(FCFS)最简单调度算法。按照进入后备队列的先后次序来加入就绪队列等待执行。是非抢占式,易于实现,效率不高,利于长作业(CPU繁忙)不利于短作业(I/O繁忙)

短作业优先(Short Job First)是非抢占式的,具有很好性能,降低平均等待时间,提高吞吐量。不利于长作业,可能一直处于等待出现饥饿;未考虑作业紧迫程度,不能用于实时系统。

高响应比优先调度算法(Highest Reponse Ratio First, HRRF)(响应比高意味着等待时间长而服务时间短,优先处理)响应比 = (等待 + 服务) / 服务时间 = 等待 / 服务时间 + 1

时间片轮转:用于分时系统的进程调度,抢占式。

基本思想:将CPU时间分为若干时间片(q),进程按到达顺序排列。每次调度队首,执行1个时间片后,该进程移至队尾。能在给定时间响应所有用户请求,达到分时系统的目的。

其性能主要取决于时间片q的大小,q太大,则所有的进程在1个时间片完成;太小则进程频繁切换,系统开销大。

多级反馈队列调度算法:将时间片轮转与优先级调度相结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的,按时间片轮转。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。

死锁

什么是死锁?

当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。

必要条件

禁止抢占 - 资源不能被强制从一个进程中退出

持有和等待 - 一个进程可以在等待时持有系统资源

互斥 – 某个资源在一段时间内只能由一个进程占有

循环等待 - 一系列进程互相持有其他进程所需要的资源,银行家算法-破环循环等待条件,合理分配系统资源

死锁的应对方法

1. 最简单、最常用方法是重新启动,不过代价很大,意味着之前所有进程计算都将付之东流

2. 撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。

分两种情况:一次性剥夺全部资源;或逐步撤消参与死锁的进程,选择逐步撤消目的是撤消代价最小的进程,比如按进程优先级确定代价;考虑进程运行代价和此进程相关作业代价等;

3. 进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处。操作起来系统开销大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。

页面置换算法

最佳置换算法(OPT)

最佳(OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。

先进先出置换算法(FIFO)

最简单的页面置换算法是先入先出(FIFO)法。这种算法的实质是,总是选择在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。

最近最久未使用(LRU)算法

它的实质是,当需要置换一页时,选择在最近一段时间里最久没有使用过的页面予以置换。

 

 

磁盘调度算法

1.FIFO:先来先服务算法;(依次处理)

2.SSTF: 最短寻道时间算法;(距离当前磁道最近的有限处理)

3.SCAN:电梯调度算法;(这样命名很形象,先按一个方向,扫描的过程依次访问要求服务的队列,当扫描到最里层的一个服务序列时就反向扫描)

4.CSCAN: 循环扫描算法(从最里面一个磁道访问完之后,立即返回最外层,也称单向扫描调度算法)

5.FSCAN:分步电梯调度算法(分两个队列)

 

jvmjvmjvmjvm区域

线程私有的:程序计数器、(虚拟机栈、本地方法栈)

线程共享的:堆、方法区、直接内存 (非运行时数据区的一部分)

程序计数器是一块较小的内存空间,是当前线程执行字节码的行号指示。

工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。

作用:

通过改变程序计数器来依次读取指令实现流程控制,如:顺序执行、选择、循环、异常处理。

多线程:记录当前线程执行的位置,线程被切换回来时能够知道该线程上次运行到哪儿了。

虚拟机栈(Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack))

存放基本数据类型(boolean、byte、char、short、int、float、long、double)和对象引用

会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

StackOverFlowError:不动态拓展, 线程请求栈的深度超过当前虚拟机栈的最大深度

OutOfMemoryError: 动态扩展,且内存用完了,无法扩展

堆:唯一目的就是存放对象实例,几乎所有实例和数组都在这里。

方法区(永久代):存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等。

《Java 虚拟机规范》规定了有方法区和作用,并没实现。(像接口),在不同的 JVM 上方法区的实现肯定是不同的了。 永久代是 HotSpot 虚拟机对方法区的一种实现方式。(像类)

判断对象死亡?引用计数 vs 可达性分析

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用到他,就加1;引用失效就减1。有问题,有可能存在循环引用,导致对象无法被回收。

可达性分析算法:以GC Roots对象为起始点,从这些节点向下搜索

可以作为GC ROOT对象的有:Java虚拟机栈中的引用对象。本地方法栈中JNI(既一般说的Native方法)引用的对象。方法区中类静态属性所引用的对象。方法区中常量所引用的对象。

如何回收对象?垃圾收集算法

标记清除算法:分为标记和清除两个阶段。

首先从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收(老年代)

主要不足有两个:效率问题:标记和清除两个过程的效率都不高;

空间问题:不进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。

标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)

垃圾收集器

Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

ParNew收集器 (复制算法): 新生代并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

收集过程分为:初始标记,并发标记,重新标记,垃圾回收

G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

 

分配回收策略(老年代新生代)

自动内存管理可以归结为自动化地解决两个问题:

分配内存、回收内存。

一般而言,对象主要分配在新生代的Eden区上,少数情况下也可能直接分配在老年代中。

1)    对象优先在Eden分配,当Eden区没有足够空间时,虚拟机将发起一次MinorGC。

2)    大对象直接进入老年代。所谓的大对象是指,很长的字符串以及数组。

3)    长期存活对象进入老年代。在新生代中经历n次(默认15)Minor GC后,晋升老年代。

4)    动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

 

GC调优

各分区的大小对GC的性能影响很大。

如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。

活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小

通过收集GC信息,结合系统需求,确定优化方案,例如选用合适的GC回收器、重新设置内存比例、调整JVM参数等。

根据对象生命周期的分布情况:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。

 

JVM常用参数

-Xms: 初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3表示年轻代和年老代比值为1:3

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3表示Eden:3 Survivor:2(3:1:1),一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置永久代大小

 

Cookie session

Cookie与Session都是用来跟踪浏览器用户身份的会话方式。

Cookie放客户浏览器,Session放服务器。

Cookie不安全,别人可以分析本地Cookie进行Cookie欺骗,用加密的Cookie或Session。

Session存服务器。占服务器性能,如果减轻负担方面,用Cookie。

单个Cookie在客户端的限制是4K,很多浏览器都限制一个站点最多保存20个Cookie。

网络分层

物理层:在传输媒体上传输数据比特流。屏蔽介质和通信手段差异,使数据链路层感觉不到

数据链路层:主机间有很多链路,为相邻结点间服务。把网络层传来的分组封装成帧。

网络层:为主机间提供传输服务,把运输层传递下来的报文段或数据报封装成分组。

运输层:进程间的通用数据传输服务。

         传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;

         用户数据报协议 UDP,提供无连接数据传输服务,数据单位为用户数据报。

         TCP 主要提供完整性服务,UDP 主要提供及时性服务。

应用层:为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。

握手挥手

序号seq :对字节流编号,序号为 301,编号为 301,如携带数据 100 字节,下一个报文段的序号应为 401。用来标记顺序

确认号 ack:期望收到的下一个报文段序号。例如 B 收到 A报文段序号为 501,长度 200 ,因此 B 期望701,B 发送给 A 的确认报文段中确认号就为 701。

确认 ACK :当 ACK=1 时确认号ack字段有效,否则无效。

同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。

终止 FIN :用来释放一个连接,当 FIN=1 时,表示发送方已发送完毕,要求释放连接。

窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。这个限制是因为接收方的数据缓存空间有限

 

1、第一次握手:客户端给服务器发送一个 SYN 报文。

2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

4、服务器收到 ACK 报文之后,三次握手建立完成。

第一次:seq随机x,ACK=0,syn=1,ack=0,

第二次:seq随机y,ACK=X+1,syn=1,ack=1

第三次:seq=x+1,ACK=Y+1,syn=0,ack=1

第一次握手:客户端发,服务端收到。服务端就能得出结论:客户端的发送能力、服务端接收能力是正常

第二次握手:服务端发,客户端收到。客户端就能得出结论:服务端的接收发送能力,客户端的接收发送能力是正常的。服务器不能确认客户端的接收能力是否正常。

第三次握手:客户端发,服务端收到了。这样服务端就能得出结论:客户端的接收发送能力正常,服务器自己的发送、接收能力也正常。

第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。

客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。

为什么三次四次?

三次:Server在收到建立连接请求后,可以直接把ACK和SYN放在一个报文发送给Client。

四次:关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,因此,己方ACK和FIN一般都会分开发送。

 

四次挥手:

客户机:我想和你断开连接,你同意吗?(FIN=1)             服务器:我同意(ACK=1)

(期间服务器可能还会向客户机发送数据,但是反之不可以)

服务器:客户机,我想要和你断开连接,你同意吗?(FIN=1) 客户机:我同意。(ACK=1)

 

短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。

 

UDP想可靠

就要接收方收到UDP之后回复个确认包,发送方有个机制,收不到确认包就要重新发送,每个包有递增的序号,接收方发现中间丢了包就要发重传请求,当网络太差时候频繁丢包,防止越丢包越重传的恶性循环,要有个发送窗口的限制,发送窗口的大小根据网络传输情况调整,调整算法要有一定自适应性。

网络访问过程

域名解析

--> 发起TCP的3次握手 拿到域名对应的IP地址之后,浏览器向服务器的WEB程序(tomcat,nginx)80端口发起TCP连接请求。

--> 建立TCP连接后发http请求

--> 服务器响应http请求,浏览器得到html代码

--> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片)

--> 浏览器对页面渲染呈现

 

域名解析:搜索浏览器的DNS缓存 操作系统的DNS缓存  hosts文件C:\Windows 迭代DNS解析请求(根名称/顶级名称/二级名称/权威名称服务器)

 

为什么tcp可靠?

流量控制(滑动窗口)

发送方通过维持一个发送窗口确保不会发生发送方发太快接收方无法及时处理。

TCP如何保证可靠传输?

1、确认和重传:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。

2、数据校验

1)ack:数据丢失或延迟。发送时会起定时器,如果指定时间内没接收到ACK seq + 1,就再发一次。

2)数据乱序:接收方上一个收到的正确数据是seq + 4,它返回seq + 5作为ACK。这时候它收到了seq + 7,因为顺序错了,所以接收方会再次返回seq + 5给发送方。

3)数据错误:每一个TCP数据都会带着数据的校验和。接收方收到数据seq + 3以后会先对校验和进行验证。如果结果不对,则发送ACK seq + 3,让发送方重新发送数据。

4)数据重复:接收方直接丢弃重复数据。

3、流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。

4、拥塞控制:当网络拥塞时,减少数据的发送。

流量控制与滑动窗口

流量控制是为了控制发送方发送速率,保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为0,则发送方不能发送数据。

发送方和接收方各有一个窗口,接收方通过窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

TCP使用累计确认(发送方一次发送多个连续包,接收方只需要确认最后一个包),快速重传(收到3个冗余的ACK包立马重传,不用等待超时)以及选择重传(只对丢失的包进行重传)提高效率

TCP的拥塞控制与拥塞窗口

拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

发送方维持一个拥塞窗口 cwnd的状态变量。动态地在变化。发送方让自己的发送窗口等于拥塞。

原则:没拥塞,窗口就增大一些,以便把更多的分组发送出去。

出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。

几种拥塞控制方法:慢开始、拥塞避免、快重传和快恢复。

慢开始:不清楚网络的负荷情况。因此先探测一下,由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。

通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。

慢开始门限ssthresh的用法如下:

      当 cwnd < ssthresh 时,使用慢开始算法

当 cwnd > ssthresh 时,改用拥塞避免算法。

    当 cwnd = ssthresh 时,都可以

拥塞避免算法:经过一个往返时间RTT就把拥塞窗口cwnd加1,不是加倍。按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

快速恢复算法:发送方认为网络很可能没有发生拥塞,因此而是把cwnd值设置为门限减半后的数值,然后使拥塞窗口线性增大。

UDP可靠

最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。

 

1、添加seq/ack机制,确保数据发送到对端

2、添加发送和接收缓冲区,主要是用户超时重传。

3、添加超时重传机制。

详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。

 

目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。

HTTP and HTTPS

通信使用明文,内容可能被窃听(重要密码泄露)

不验证通信方身份,有可能遭遇伪装(跨站点请求伪造)

无法证明报文的完整性,有可能已遭篡改(运营商劫持)

http+加密+认证+完整性保护=https

1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

2、https则是通过TLS加密后传输。SSL 是“Secure Sockets Layer”的缩写,中文叫做“安全套接层”

3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

对称密钥加密,又称私钥加密,即发送方和接收方用同一个密钥去加密解密。优势是速度快,适合于对大数据量进行加密,但密钥管理困难。

非对称密钥加密,又称公钥加密,需要使用一对密钥分别完成加密和解密,一个公开发布,即公开密钥,另一个由用户自己保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密。

从功能角度而言非对称加密比对称加密功能强大,但加密和解密速度却比对称密钥加密慢得多。

SSL/TLS协议,公钥加密法,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密,如何保证公钥不被篡改?

解决方法:将公钥放在数字证书中,只要证书是可信的,公钥就是可信的。

HTTP方法及相互区别

GET 用于获取,而 POST 用于传输实体主体。

GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看

因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如中文会转换为%E4%B8%AD%E6%96%87 ,而空格会转换为%20。POST参数支持标准字符集。

GET请求在URL中传送的参数是有长度限制的,而POST么有

HEAD获取报文首部,和 GET 方法类似,但是不返回报文实体主体部分

状态码

1正常2成功3重定向4客户端错误5服务端错误

100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。

200 OK

204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。

206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。

301 Moved Permanently :永久性重定向

302 Found :临时性重定向

303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。

304 Not Modified :表示资源在由请求头中的If-Modified-Since或If-None-Match参数指定的这一版本之后,未曾被修改。在这种情况下,由于客户端仍然具有以前下载的副本,因此不需要重新传输资源。

307 Temporary Redirect :临时重定向,与302相反,当重新发出原始请求时,不允许更改请求方法。

400 Bad Request :请求报文中存在语法错误

401 Unauthorized :该状态码表示发送的请求需要有认证信息。如果之前已进行过一次请求,则表示用户认证失败

403 Forbidden :请求被拒绝

404 Not Found :请求失败,请求所希望得到的资源未被在服务器上发现,但允许用户的后续请求。

500 Internal Server Error :服务器正在执行请求时发生错误

503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

504 Gateway Timeout作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器

 

http结构

一个HTTP请求报文由四个部分组成:请求行、请求头部、空行、请求数据。

1.请求行:由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。比如 GET /data/info.html HTTP/1.1

方法字段就是HTTP使用的请求方法,比如常见的GET/POST

其中HTTP协议版本有两种:HTTP1.0/HTTP1.1 可以这样区别:

HTTP1.0对于每个连接都只能传送一个请求和响应,请求就会关闭,HTTP1.0没有Host字段;而HTTP1.1在同一个连接中可以传送多个请求和响应,多个请求可以重叠和同时进行,HTTP1.1必须有Host字段。

2.请求头部

指明请求类型(一般是GET或者 POST)。大多数请求头并不是必需的,但post的Content-Length除外。

Accept: 浏览器可接受的MIME类型。

Accept-Charset:浏览器可接受的字符集。

Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip。

Accept-Language:浏览器所希望的语言种类

Authorization:授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。

Content-Length:表示请求消息正文的长度。

Host: 客户机通过这个头告诉服务器,想访问的主机名。Host头域指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域,否则系统会以400状态码返回。

Referer:客户机通过这个头告诉服务器,它是从哪个资源来访问服务器的(防盗链)。包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。

User-Agent:User-Agent头域的内容包含发出请求的用户信息。浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。

Cookie:客户机通过这个头可以向服务器带数据,这是最重要的请求头信息之一。

From:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它。

Connection:处理完这次请求后是否断开连接还是继续保持连接。

同时指定几个范围:bytes=500-600,601-999

3.空行

它的作用是通过一个空行,告诉服务器请求头部到此为止。

4.请求数据

若方法字段是GET,则此项为空,没有数据

若方法字段是POST,则通常来说此处放置的就是要提交的数据

 

HTTP响应报文由三部分组成:响应行、响应头、响应体

1.响应行

响应行一般由协议版本、状态码及其描述组成 比如 HTTP/1.1 200 OK

其中协议版本HTTP/1.1或者HTTP/1.0,200就是它的状态码,OK则为它的描述。

//常见状态码:

100~199:表示成功接收请求,要求客户端继续提交下一次请求才能完成整个处理过程。

200~299:表示成功接收请求并已完成整个处理过程。常用200

300~399:为完成请求,客户需进一步细化请求。例如:请求的资源已经移动一个新地址、常用302(意味着你请求我,我让你去找别人),307和304(我不给你这个资源,自己拿缓存)

400~499:客户端的请求有错误,常用404(意味着你请求的资源在web服务器中没有)403(服务器拒绝访问,权限不够)

500~599:服务器端出现错误,常用500

2.响应头

响应头用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。

设置Cookie,指定修改日期,指示浏览器按照指定的间隔刷新页面,声明文档的长度以便利用持久HTTP连接,……等等许多其他任务。

 

常见的响应头字段含义:

Allow:服务器支持哪些请求方法(如GET、POST等)。

Content-Encoding:文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。

Content-Length:表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。

Content- Type:表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置 Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。

 

Date:当前的GMT时间,例如,Date:Mon,31Dec200104:25:57GMT。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。

Location:这个头配合302状态码使用,用于重定向接收者到一个新URI地址。表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。

Refresh:告诉浏览器隔多久刷新一次,以秒计。

 

Set-Cookie:设置和页面关联的Cookie。Servlet不应使用response.setHeader(“Set-Cookie”, …),而是应使用HttpServletResponse提供的专用方法addCookie。

 

Transfer-Encoding:告诉浏览器数据的传送格式。

 

 

注:设置应答头最常用是HttpServletResponse的setHeader,方法有两个参数:应答头的名字和值。和设置状态代码相似,设置应答头应该在发送任何文档内容之前进行。

setDateHeader方法和setIntHeadr方法专门用来设置包含日期和整数值的应答头,前者避免了把Java时间转换为GMT时间字符串的麻烦,后者则避免了把整数转换为字符串的麻烦。

HttpServletResponse设置

setContentType:设置Content-Type头。大多数Servlet都要用到这个方法。

addCookie:设置一个Cookie(Servlet API中没有setCookie方法,因为应答往往包含多个Set-Cookie头)。

3.响应体

响应体就是响应的消息体,如果是纯数据就是返回纯数据,如果请求的是HTML页面,那么返回的就是HTML代码,如果是JS就是JS代码,如此之类。

 

Tcp结构

六位标志位包含以下几项:

URG:表示紧急指针是否有效;

ACK:表示确认号是否有效,携带ACK标志的数据报文段为确认报文段;

PSH:提示接收端的应用程序应该立即从TCP接受缓冲区中读走数据,为接受后续数据腾出空间;

RST:表示要求对方重新建立连接,携带RST标志位的TCP报文段成为复位报文段;

SYN:表示请求建立一个连接,携带SYN标志的TCP报文段为同步报文段;

FIN:通知对方本端要关闭了,带FIN标志的TCP报文段为结束报文段。

确认序号:

Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。

TCP头部选项:

TCP头部选项是一个可变长的信息,这部分最多包含40字节,因为TCP头部最长60字节,(其中还包含前面20字节的固定部分)。

为什么在TCP首部的开始便是首部长度字段而UDP首部却没有?

因为UDP提供无连接服务,它的数据包包头,是固定长度的8字节,不存在可选字段,可以减少很多传输开销,所以它无需使用首部字段长,因为它的首部就是固定的。

而TCP提供连接服务,它的数据包包头,除了固定的20字节之外,还存在一个可选项,这个可选项字段,是根据TCP连接的要求而变动。这一字段最常见到的就是最大报文大小MSS,它指明发送端所能接收的最大长度的报文段。因为这个字段的存在,所以TCP包头使用了首部长字段。它占4位,以四字节为单位表示TCP包头长度,也就是说,TCP的首部最大长度可以是15x4=60字节,而可选项长可以为60-20=40字节

Socket

ServerSocket serverSocket = new ServerSocket(12016);//监听

                     do {

                            Socket socket = serverSocket.accept();//获取IO流

                            InputStream is = socket.getInputStream();

                            OutputStream os = socket.getOutputStream();



                            byte[] cache = new byte[1024];

                            is.read(cache);//读

                            cmd = new String(cache);

                            os.write(returnText.getBytes());//写

                            os.flush();



                            is.close();

                            os.close();

                     } while (!returnText.equals("end"));

redisredisredis数据结构

字符串

相对于c的改进:

获取长度:c遍历字符串,redis直接读len

缓冲区安全:c字符串容易缓冲区溢出,比如:没有分配足够空间就执行拼接操作。

redis会先检查空间是否满足要求,如果不满足会自动扩充。

内存分配:c长度变化总是内存重新分配。

redis:1、预分配:如修改后大于1MB,分配  1MBfree空间。修改长度时检查,够的话就用free空间2、惰性空间释放:缩短时不需要释放空间,用free记录即可,留作以后使用。

二进制安全

c字符串程序读到空字符会误以为是结尾,所以 c字符串不能保存二进制文件。

而redis字符串二进制安全,因为有len记录长度。

链表:双端、无环、带长度记录、多态:用 void* 指针来保存节点值。

字典:数组、大小、掩码(等于size-1),已有节点数量

整数集合:数组、数量、编码方式

升级:计算新空间、转换并放入原来的数、放入新数

降级:无

压缩列表

连锁更新:在一个压缩列表中有多个连续的、长度介于 250 字节到 253 字节之间的节点 ,这时, 如果我们将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的表头节点

对象

对象组成:// 类型// 编码// 指向底层实现数据结构的指针

不为对象关联固定编码,提升灵活性和效率,可以根据使用场景为对象设置不同编码。

字符串对象:long、字符串、embstr

列表对象:所有字符串元素长度小于 64 字节;元素数量小于 512 个:ziplist否则链表

哈希对象:所有键值对的字符串长度都小于 64 字节;数量小于 512 :ziplist 否则hashtable

集合对象:所有元素都是整数值;数量不超过 512 个:intset,否则hashtable

有序集合:所有元素的长度都小于 64 字节;元素数量小于 128 个;ziplist否则跳表

场景:

string:常规计数:微博数,粉丝数等。

hash 特别适合用于存储对象,如用户,商品

list查找遍历等,可以lrange命令实现不断下拉分页

set去重,点赞关注

sortedset 有序,做实时排行榜,礼物榜弹幕榜。

跳表

比较

哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。

做范围查找,平衡树比skiplist操作要复杂。

在平衡树上,我们找到指定范围的小值之后,需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。

平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

从算法实现难度上来比较,skiplist比平衡树要简单得多。

Redis中的skiplist和经典有何不同

skiplist的key允许重复。这在最开始介绍的经典skiplist中是不允许的。

在比较时,不仅比较key,还比较数据本身。在Redis的skiplist实现中,数据本身的内容唯一标识这份数据,而不是由key来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。

第1层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。

在skiplist中可以很方便地计算出每个元素的排名(rank)。

作者原话

有几个原因:

1)它们的记忆力不是很强。基本上由你决定。更改有关节点具有给定数量级别的概率的参数将使内存密集度低于btree。

2)排序集通常是许多Zrange或Zrevrange操作的目标,即作为链表遍历跳过列表。通过此操作,跳过列表的缓存区域性至少与其他类型的平衡树一样好。

3)它们易于实现、调试等。例如,由于跳过列表的简单性,我收到了一个补丁(已经在redis master中),其中包含在o(log(n))中实现zrank的扩展跳过列表。它只需要对代码稍作修改。

HyperLogLog

是一种概率数据结构,用来估算数据的基数。数据集可以是访客 IP 地址,E-mail 或用户 ID。

基数就是指一个集合中不同值的数目set

使用 Redis 统计集合的基数有三种方法:用 Redis 的 HashMap,BitMap 和 HyperLogLog。前两个在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 不会。

HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要不不高的场景。

这是一个很惊人的结果,以如此小的内存来记录如此大数量级的数据基数。

命令

> PFADD visitors a b c//插入

(integer) 1//数量变化返回1

> PFCOUNT visitors//获取数量

(integer) 3

> PFADD customers alice dan->(integer) 1

> PFMERGE a b d//合并

OK

> PFCOUNT everyone->(integer) 4

通过Hash函数,将元素转为64位比特串,0 代表反面,1 代表正面,从左往右第1个1

HyperLogLog: 一共分了 2^14=16384 个桶,。每个桶中是一个 6 bit 的数组。将上文所说的 64 位比特串的低 14 位单独拿出,值对应桶号,然后将第一次出现 1 的位置值设置到桶中。50位中出现1的位置值最大为50,所以每个桶中的 6 位数组正好可以表示该值。

HyperLogLog 对象中的 registers 数组就是桶,它有两种存储结构,分别为密集存储结构和稀疏存储结构,从中我们可以看到 Redis 对节省内存极致地追求。

真使用足够多的 uint8_t 字节去表示,只是此时会涉及到字节位置和桶的转换,因为字节有 8 位,而桶只需要 6 位。所以我们需要将桶的序号转换成对应的字节偏移量 offsetbytes 和其内部的位数偏移量 offsetbits。

当 offset_bits 小于等于2时,说明一个桶就在该字节内,只需要进行倒转就能得到桶的值。

 offset_bits 大于 2 ,则说明一个桶分布在两个字节内,此时需要将两个字节的内容都进行倒置,然后再进行拼接得到桶的值。

Redis 为了方便表达稀疏存储,

单线程

1、完全基于内存

2、数据结构简单, Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用考虑锁的问题,没有因为可能出现死锁。

4、使用多路I/O复用模型,非阻塞IO;

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,空闲时,当前线程阻塞,当有流有 I/O 事件时,就唤醒,于是程序就会轮询一遍所有的流(epoll 只轮询那些发出了事件的流),并且只依次顺序的处理就绪的流,避免了大量无用操作。

持久化

RDB持久化-获得内存里的数据在某个时间点上的副本。 (这称为“半持久化模式”,RDB,redis默认持久化方式);实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前文件,用二进制压缩存储。

优势:1,数据库将只包含一个文件,这对于文件备份来说非常完美;

2, 恢复容易。我们可以轻松的将一个文件压缩后转移到其它存储介质上;

3, 性能最大化;对于服务进程而言,开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

劣势:

1, 存在数据丢失的可能。此前没有来得及写入磁盘的数据都将丢失。

2, 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

 

AOF持久化-把每一次数据变化都写入到一个AOF文件中(“全持久化模式”)

优点:

1, 该机制可以带来更高的数据安全性。AOF有3种同步策略,即每秒同步、每修改同步和不同步。

缺点:平时运行效率低;AOF文件通常大于RDB文件;恢复速度慢。

 

Redis和数据库双写一致性

读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据放入缓存,同时返回响应。

更新的时候,先删除缓存,然后更新数据库。

(1)先淘汰缓存

(2)再写数据库(这两步和原来一样)

(3)休眠1秒,再次淘汰缓存

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

 

缓存击穿雪崩穿透

缓存击穿是针对缓存中没有但数据库有的数据。场景是,当某个Key失效后,瞬间涌入大量的请求同一个Key,这些请求不会命中Redis,都会请求到DB,导致数据库压力过大

解决办法

1、设置热点Key,自动检测热点Key,将热点Key的过期时间加大或者永不过期。

2、加互斥锁。当发现没有命中Redis,去查数据库的时候,在执行更新缓存的操作上加锁,当一个线程访问时,其它线程等待,这个线程访问过后,缓存中的数据会被重建,这样其他线程就可以从缓存中取值。

缓存雪崩是指大量Key同时失效,对这些Key的请求又会打到DB上,同样会导致数据库压力过大甚至挂掉。

解决办法

1)让Key的失效时间分散开,可以在统一的失效时间上再加一个随机值,或者使用更高级的算法分散失效时间。

2)构建多个redis实例,个别节点挂了还有别的可以用。

3)多级缓存:比如增加本地缓存,减小redis压力。

4)对存储层增加限流措施,当请求超出限制,提供降级服务(一般就是返回错误即可)

缓存穿透: 一些恶意的请求会故意查询redis不存在的key,请求量很大,就会对后端系统造成很大的压力。

1:对查询结果为空的情况也进行缓存,这样,再次访问时,缓存层会直接返回空值。缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2:对一定不存在的key进行过滤。具体请看布隆过滤器

 

解决Redis并发竞争Key问题

多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

Redis缓存策略

1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

当需要淘汰一个key时,随机选择3个key,淘汰其中间隔时间最长的key。**基本上淘汰key效果很好。后来随机3个key改成一个配置项"N随机key"。但把默认值提高改成5个后效果大大提高。考虑到它的效果,你根本不用修改他。

版权声明:本文为CSDN博主「一只大白兔兔兔兔兔」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://fantianzuo.blog.csdn.net/article/details/102580321

2. volatile-ttl:挑选将要过期的数据淘汰

3. volatile-random:任意选择数据淘汰

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

5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。

哨兵

1)Sentinel(哨兵) 进程用于监控集群中 Master 主服务器工作的状态

2)在主服务器故障时,可以实现 Master 和 Slave 服务器切换,保证系统的高可用

工作方式

1)每个 Sentinel(哨兵)进程每秒钟一次向所有服务器和哨兵进程发送一个 PING 命令。

2. 如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 这个实例会被 Sentinel(哨兵)进程标记为主观下线。

3. 如果一个 Master 主服务器被标记为主观下线,则正在监视这个 Master 主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认 Master 主服务器的确进入了主观下线状态。

4. 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认 Master 主服务器进入了主观下线状态, 则Master 主服务器会被标记为客观下线(ODOWN)。

5. 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master 主服务器、Slave 从服务器发送 INFO 命令。

6. 当 Master 主服务器被 Sentinel(哨兵)进程标记为客观下线时,Sentinel(哨兵)进程向下线的 Master 主服务器的所有 Slave 从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

7. 若没有足够数量的 Sentinel(哨兵)进程同意 Master 主服务器下线, Master 主服务器的客观下线状态就会被移除。若 Master 主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master 主服务器的主观下线状态就会被移除。

解决会话

(粘性session)

你可以设置nginx的分配策略,下次同一个还让同一个服务器来处理

但是很显然,这就和分布式和nginx初衷违背了:负载很难保证均衡。

(同步session)

一台服务器的session给所有服务器复制一份

第一,性能不好。第二,产生了一定的耦合

(专门session)

专门一台服务器来解决,存session,其它服务器来这个服务器取session再用。

但是也有问题:你这个服务器挂了怎么办?别的服务器都是依赖这个服务器工作的。我们分布式部署本来就是为了解决性能的瓶颈啊。

很容易想到,我们把那个处理session的服务器搞个集群:

更不行,想想就知道,本来就是为了解决分布式部署的问题,你把单独解决session的服务器又搞集群,和之前有什么区别呢?还不如一个服务器存一份简单呢。

可以,但是传统的关系数据库是存到硬盘里,速度太慢。

(nosql)

最终,我们的主流办法使用nosql数据库,比如redis,来解决这个问题的。

sqlsqlsqlsqllsqlslqslqslq数据类型

整数:1byte是tinyint 2 smallint 3mediumint 4int 8bigint

浮点数:float/double/real。定义方式为:FLOAT(M,D)一共显示M位其中D位小数点后面

Bit:MySQL把BIT当做字符串类型,而非数字类型。

字符串:CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET。

Char固定长度,varchar随便,BINARY、VARBINARY是二进制,BLOB、TEXT是大的

mysql特点

1.数据以表格的形式出现

2.每行为各种记录名称

3.每列为记录名称所对应的数据域

4.许多的行和列组成一张表单

5.若干的表单组成database

注:保证数据一致性。

注:关系型数据库,表与表之间存在对应关系。

注:非关系行数据库,表之间不存在关系,数据独立,随便存。

存储引擎

如果要提供提交、回滚和恢复的事务安全(ACID 兼容)能力,并要求实现并发控制,InnoDB

如果数据表主要用来插入和查询记录,则 MyISAM 引擎提供较高的处理效率。

如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,在内存的 MEMORY ,MySQL 中使用该引擎作为临时表,存放查询的中间结果。

增删改

--1. 表名后没有指定属性列:插入一条完整的

insert into student values ('201215128', '陈东', '18', '男', 'IS');

--2. 在表明后指定要插入数据的表名及属性列,属性列的顺序可与表定义中的顺序不一致

insert into student(sno, sname, sage, ssex, sdept)

values ('201215138', '陈东栋', '18', '男', 'CS');

--3. 插入部分列,未显示给出的列按空值计算,当然前提条件是那些列可以为空值

insert into student(sno, sname) values ('201215148', '陈栋');

--1. 修改某些符合where子句中的条件的元组的值

update student set sage = 92 where sno = '200215121';

--2. where子句缺省,默认修改所有元组的该属性的值

--3. 带子查询的修改

update sc set grade = 100

where 'CS' in (select sdeptfrom studentwhere sc.sno = student.sno);

Delete from student where sno = '201215148';

范式

最重要的好处归结为三点:

1.减少数据冗余(这是最主要的好处,其他好处都是由此而附带的)

2.消除异常(插入异常,更新异常,删除异常)

3.让数据组织的更加和谐

第一范式 – 每表中的字段不可再分割

第二范式 – 要有主键,并且其他字段依赖于主键(方便唯一确定数据行,并定位其他字段)

第三范式 – 在二范式的基础上,消除传递依赖,各种信息只在一个地方存储,不出现在多张表中。比如学生信息表的学号为主键,此时要查看他所在的系信息,那么不能直接将系信息存储在学生信息表中,因为系信息已经在系别管理表中,此时只能在学生信息中添加系别管理表中的系编号字段(主键),这样既能根据系编号找到系别信息,又避免了冗余存储的问题。

BC范式 – 每个表中只有一个候选键(唯一标识表中每一行的键,候选键可以包含多个属性)

 

优化:

1)1对N关系中复制非键属性以减少连接

例:查询学生以及所在学院名,可以在学生表中不仅存储学院id,并且存储学院名

维护时:

如果在UI中,只允许用户进行选择,不能自行输入,保证输入一致性

如果是程序员,对于类似学院名这种一般不变的代码表,在修改时直接对两张表都进行修改;如果经常变化,则可以加一个触发器。

2)N对N关系中复制属性,把两张表中经常需要的内容复制到中间关系表中以减少连接

索引优缺点和使用原则

优点:

   1、所有的MySql列类型(字段类型)都可以被索引,也就是可以给任意字段设置索引

   2、大大加快数据的查询速度

缺点:

   1、创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加

   2、占空间,数据也会有最大上线设置的,大量索引文件可能比数据文件更快到上线

   3、对表中的数据进行增加删改时,索引也需要动态的维护,降低了数据的维护速度。

使用原则:

   1、对经常更新的表避免过多索引,对经常用于查询的字段应建索引,

   2、数据量小的表最好不要使用索引,可能查询全部数据比遍历索引的时间还要短,

   3、在值少的列上不要建立索引,比如"性别"字段上只有男女。

3-5、尽量选择区分度高的列,公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少

唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0

一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录

4、最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配

5、索引列不能参与计算,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因:存的都是数据表中的字段值,需要所有元素都应用函数才能比较

索引分类

普通索引

(1)直接创建索引CREATE INDEX index_name ON table(column(length)) 

(3)创建表的时候同时创建索引

CREATE TABLE `table` (

    `id` int(11) NOT NULL AUTO_INCREMENT ,

    `title` char(255) CHARACTER NOT NULL ,

    PRIMARY KEY (`id`),

    INDEX index_name (title(length))

)

唯一索引

列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。

(1)创建唯一索引CREATE UNIQUE INDEX indexName ON table(column(length))

(3)创建表的时候直接指定

主键索引

特殊的唯一索引,一个表只能有一个主键,不允许有空值。建表同时创建主键索引:

CREATE TABLE `table` (

    `id` int(11) NOT NULL AUTO_INCREMENT ,

    `title` char(255) NOT NULL ,

    PRIMARY KEY (`id`)

);

组合索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合

ALTER TABLE `table` ADD INDEX name_city_age (name,city,age);

全文索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。

fulltext索引配合match against操作使用,而不是一般的where语句加like。

它可以在create table,alter table ,create index使用,不过目前只有char、varchar,text 列上可以创建全文索引。

在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。

CREATE TABLE `table` (

    `id` int(11) NOT NULL AUTO_INCREMENT ,

    `content` text CHARACTER NULL ,

    PRIMARY KEY (`id`),

    FULLTEXT (content)

);

聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:

(1)如果表定义了PK,则PK就是聚集索引;

(2)如果表没有定义PK,则第一个not NULL unique列是聚集索引;

(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;

所以PK查询非常快,直接定位行记录。

回表:查到一个字段,再去找对应行

5.6优化,索引下推:a=1 and b like….and c like…..,之前按查出第一个条件,返回数据,再筛,现在按索引筛完了like再返回查。

索引区别

哈希索引适合等值查询,但是无法范围查询

哈希索引没办法利用索引完成排序

哈希索引不支持多列联合索引的最左匹配规则

如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题

 

局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用——程序运行期间所需要的数据通常比较集中。磁盘顺序读取的效率很高(不需寻道时间,只需很少的旋转时间)

预读的长度一般为页的整倍数。

在经典B+Tree的基础上进行了优化,增加了顺序访问指针。

平衡树缺点:深、离得远、

 

B和b+:只存索引,值全在叶子节点。

默认16KB一页,1行数据1K,一页16条数据。假设 bigint=8字节,指针在innodb是6字节,8+6=14,16KB/14=1170,三层可满足千万记录。

事务特性:ACID

原子性(Atomicity):事务作为一个整体执行,对数据的操作要么全被执行,要么都不执行。

一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束(约定好的约束,如:AB相互转钱,A有500,B也有500,那么无论他们怎么转,最后的数额总数都会是1000)

隔离性(Isolation):多个事务并发执行时,不互相影响。

持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。

并发错误

第一类丢失更新:某一个事务的回滚,导致另外一个事务已更新的数据丢失了。

第二类丢失更新:某一个事务的提交,导致另外一个事务已更新的数据丢失了。

脏读:另一个事务修改了,但尚未提交,本事务读到未被提交的数据。

不可重复读:一个执行中,另一个更新,本事务先后读到的数据不一致。

幻读:某事务对同一个表前后查询到的行数不一致(中间有人插入)

幻读解决办法:使用串行化读的隔离级别

隔离级别

- Read Uncommitted:读取未提交的数据。事务可以读取到另一个事务未提交的修改。

- Read Committed:(有的库默认)读取已提交的数据。事务只能读取另个事务已提交修改。

- Repeatable Read:(mysql默认)可重复读。同一个事务多次读取相同数据返回结果一样。

- Serializable:串行化 事务串行执行。读写数据锁表

1、读提交时,写数据只会锁住相应的行

2、可重复读时,如果检索条件有索引(包括主键索引),默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。

是否会发生

并发控制技术(锁)

封锁(locking)(主要使用的)

基本的封锁类型有两种:排它锁(X锁,exclusive locks)、共享锁(S 锁,share locks)

排它锁又称写锁,对A加排它锁之后,其他事务不能对A加 任何类型的锁(排斥读和写)

共享锁又称读锁,对A加共享锁之后,其他事务只能对A加S锁,不能加X锁(只排斥写)

表级锁:开销小,加锁快、锁冲突概率高、不死锁。

行级锁:mysql默认,开销大、加锁慢、锁冲突概率高、可能死锁

间隙锁(NK):行级,使用范围条件时,对范围内不存在的记录加锁。一是为了防止幻读,二是为了满足恢复和复制的需要

增加行级锁之前,InnoDB会自动给表加意向锁;

• 执行增删改语句时,InnoDB会自动给数据加排他锁;

• 执行查询语句时

共享锁(S):SELECT … FROM … WHERE … LOCK IN SHARE MODE;

排他锁(X):SELECT … FROM … WHERE … FOR UPDATE;

间隙锁(NK):上述SQL采用范围条件时,InnoDB对不存在的记录自动增加间隙锁。

 

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据是一致的。根据事务开始的时间不同,每个事物对同一张表,同一时刻看到的数据可能是不一样的。

否则就更新数据(版本号+1)。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

使用数据版本(Version)记录机制实现

乐观锁(自定义)

- 版本号、时间戳等。。。在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,

1. 版本号机制

UPDATE … SET …,VERSION=#{version+1} WHERE … AND VERSION=${version}

2. CAS算法(Compare and swap)

是一种无锁的算法,该算法涉及3个操作数(内存值V、旧值A、新值B),当V等于A时,

采用原子方式用B的值更新V的值。该算法通常采用自旋操作,也叫自旋锁。它的缺点是:

• ABA问题:某线程将A该为B,再改回A,则CAS会误认为A没被修改过。

• 自旋操作采用循环的方式实现,若加锁时间长,则会给CPU带来巨大的开销。

• CAS只能保证一个共享变量的原子操作。

 

死锁

• 场景

事务1: UPDATE T SET … WHERE ID = 1; UPDATE T SET … WHERE ID = 2;

事务2: UPDATE T SET … WHERE ID = 2; UPDATE T SET … WHERE ID = 1;

• 解决方案

1. 一般InnoDB会自动检测到,并使一个事务回滚,另一个事务继续;

2. 设置超时等待参数 innodb_lock_wait_timeout;

• 避免死锁

1. 不同的业务并发访问多个表时,应约定以相同的顺序来访问这些表;

2. 以批量的方式处理数据时,应事先对数据排序,保证线程按固定的顺序来处理数据;

3. 在事务中,如果要更新记录,应直接申请足够级别的锁,即排他锁;

分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

分布式事务产生的原因总的来说有两个:1.service产生多个节点2.resource产生多个节点

CAP理论

- C (一致性):对某个指定的客户端来说,读操作能返回最新的写操作。

对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。

A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。

可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。

P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

熟悉CAP的人都知道,三者不能共有,如果感兴趣可以搜索CAP的证明,在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

分布式事务常见的方案:2PC(两阶段提交)协调者(都成功才执行)

运行过程:

准备阶段(协调者询问参与者事务是否执行成功,参与者发回事务执行结果)

提交阶段(如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务)

需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

存在问题:

1.同步阻塞:所有事务参与者在等待其它参与者时都阻塞。

2.单点问题:协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。

3.数据不一致:网络异常,只有部分参与者接收到 Commit 消息,只有部分参与者提交了事务,使得系统数据不一致。

 

SQL语句性能优化

1.    对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2.    应尽量避免在 where 子句中使用!=或<>操作符, MySQL只有对以下操作符才使用索引:<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE。

3.    应尽量避免在 where 子句中使用 or 来连接条件, 否则将导致引擎放弃使用索引而进行全表扫描,可以使用UNION合并查询

4.    尽可能的使用 varchar代替 char, 因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

索引的优化

1.避免有索引但未被用到的情况:(1) Like的参数以通配符开头时(2)where条件不符合最左前缀原则时(3)使用!= 或 <> 操作符时(4)使用or来连接条件

2.避免使用 Select *

3.对order by语句进行优化:1.重写order by语句以使用索引 2.避免在order by子句中使用表达式。

4.对group by语句进行优化:将不需要的记录在group by之前过滤掉

5.使用exists代替in

分库分表

垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。

水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。

Kafka

引入依赖- spring-kafka

• 配置Kafka- 配置server、consumer

• 访问Kafka

- 生产者kafkaTemplate.send(topic, data);

- 消费者@KafkaListener(topics = {"test"})

public void handleMessage(ConsumerRecord record) {}

一、Kafka的优势如下:

       高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒;

       持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失;

       容错性:允许集群中节点故障(若副本数量为n,则允许n-1个节点故障);

       高并发:支持数千个客户端同时读写。

二、Kafka适合以下应用场景:

       日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer;

       消息系统:解耦生产者和消费者、缓存消息等;

        用户活动跟踪:kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后消费者通过订阅这些topic来做实时的监控分析,亦可保存到数据库;

       运营指标:kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;

       流式处理:比如spark streaming和storm。

 

Broker:Kafka节点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。

Topic:一类消息,消息存放的目录即主题,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发。

Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列

Segment:partition物理上由多个segment组成,每个Segment存着message信息

Producer : 生产message发送到topic

Consumer : 订阅topic消费message, consumer作为一个线程来消费

Consumer Group:一个Consumer Group包含多个consumer, 这个是预先在配置文件中配置好的。各个consumer可以组成一个组(group ),partition中的每个message只能被组中的一个consumer消费,如果message可以被多个consumer消费的话,那么这些consumer必须在不同的组。

Kafka不支持一个partition中的message由两个或两个以上的consumer thread来处理,即便是来自不同的consumer group的也不行。它不能像AMQ那样可以多个BET作为consumer去处理message,这是因为多个BET去消费一个Queue中的数据的时候,由于要保证不能多个线程拿同一条message,所以就需要行级别悲观所(for update),这就导致了consume的性能下降,吞吐量不够。而kafka为了保证吞吐量,只允许一个consumer线程去访问一个partition。如果觉得效率不高的时候,可以加partition的数量来横向扩展,那么再加新的consumer thread去消费。这样没有锁竞争,充分发挥了横向的扩展性,吞吐量极高。这也就形成了分布式消费的概念。

import java.util.*;

class BoundedBlockingQueue {

    private int size;

    private int capacity;

    private int[] queue;

    // 指向队头的指针

    private int last;  

    // 指向队列尾

    private int first;

    // JDK 的通知模式

    private final ReentrantLock lock;

    private final Condition notFull;

    private final Condition notEmpty;



    public BoundedBlockingQueue(int capacity) {

        this.size = 0;

        this.last = 0;

        this.first = 0;

        this.capacity = capacity;

        this.queue = new int[capacity+1];

        this.lock = new ReentrantLock();

        notFull = this.lock.newCondition();

        notEmpty = this.lock.newCondition();

    }



    // 进队列

    public void enqueue(int element) throws InterruptedException {

        // 队列不满的情况下可以入队尾,在队尾添加元素

        final ReentrantLock lock = this.lock;

        lock.lockInterruptibly();

        try{

            while(size == capacity)

                notFull.await();

            // 队列不满的情况下可以入队尾,在队尾添加元素

            queue[last] = element;

            last = (last + 1)% capacity;

            size ++;

            notEmpty.signal();

        }finally{

            lock.unlock();

        }

    }



    // 出队列

    public int dequeue() throws InterruptedException {

        // 有元素的情况下才可以出队列,删掉队头的元素

        final ReentrantLock lock = this.lock;

        lock.lockInterruptibly();

        try{

            while(size == 0)

                notEmpty.await();

            int e = queue[first];

            first = (first + 1) % capacity;

            size --;

            notFull.signal();

            return e;

        }finally{

            lock.unlock();

        }

    }

   

    public int size() {

        return size;

    }

}

Es

• 搜索服务

- 将帖子保存至Elasticsearch服务器。

- 从Elasticsearch服务器删除帖子。

- 从Elasticsearch服务器搜索帖子。

• 发布事件

- 发布帖子时,将帖子异步的提交到Elasticsearch服务器。

- 增加评论时,将帖子异步的提交到Elasticsearch服务器。

- 在消费组件中增加一个方法,消费帖子发布事件。

• 显示结果

- 在控制器中处理搜索请求,在HTML上显示搜索结果。

 

ES:       index(索引)-->type(类型)-->document(文档)-->field(字段)

mysql:   database(数据库)-->table(表)-->row(行)-->line(列)

ES是一个开源分布式搜索引擎。

同时ES是分布式文档数据库,每个字段均可被索引,而且每个字段的数据均可被搜索,能够横向扩展至数以百计的服务器存储以及处理PB级的数据。

可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下的核心发动机。

 

分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。

 

内存字典树+硬盘倒排索引。

public class Trie {

    private boolean is_string=false;

    private Trie next[]=new Trie[26];

    public Trie(){}

    public void insert(String word){//插入单词

        Trie root=this;

        char w[]=word.toCharArray();

        for(int i=0;i<w.length;++i){

            if(root.next[w[i]-'a']==null)root.next[w[i]-'a']=new Trie();

            root=root.next[w[i]-'a'];

        }

        root.is_string=true;

    }

    public boolean search(String word){//查找单词

        Trie root=this;

        char w[]=word.toCharArray();

        for(int i=0;i<w.length;++i){

            if(root.next[w[i]-'a']==null)return false;

            root=root.next[w[i]-'a'];

        }

        return root.is_string;

    }

    public boolean startsWith(String prefix){//查找前缀

        Trie root=this;

        char p[]=prefix.toCharArray();

        for(int i=0;i<p.length;++i){

            if(root.next[p[i]-'a']==null)return false;

            root=root.next[p[i]-'a'];

        }

        return true;

    }

}
打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页

打赏

兔老大RabbitMQ

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者