基础语法
-
switch支持的类型:
byte
,short
,int
,char
,enum
,String(jdk7之后支持)
-
关键字:
final
- 可以修饰类、函数、变量
- 被修饰的类不可以被继承;被修饰的方法不可以被复写;被修饰的变量是一个常量,只能赋值一次
- 内部类定义在类中的局部位置上时,只能访问该局部被final修饰的局部变量
synchronized
volatile
- `保证了不同线程对这个变量进行读取时的可见性,即一个线程修改 了某个变量的值 , 这新值对其他线程来说是立即可见的 (解决了线程间共享变量)
- 禁止进行指令重排序 ,阻止编译器对代码的优化
- 程序并发正确地执行,必须要保证原子性、可见性以及有序性,锁保证了原子性,而volatile保证可见性和有序性
-
在catch中return,finally会不会执行?
- 会,会在return之前执行
-
Student s = new Student();在内存中做了哪些事情
- 加载Student.class文件进内存;
- 在栈内存为s开辟空间
- 在堆内存为学生对象开辟空间
- 对学生对象的成员变量进行默认初始化
- 对学生对象的成员变量进行显示初始化
- 通过构造方法对学生对象的成员变量赋值
- 学生对象初始化完毕,把对象地址赋值给s变量
-
Java异常有哪几种?Throwble的子类有哪些
- Throwble子类有Error,Exception
- Error代表了JVM本身的错误,不能通过代码来处理。比如StackOverFlowError
- Exception代表程序运行时发送的各种不期望发生的事件,可以被Java异常处理机制使用。比如IOException,RuntimeException
- 常见的IOException,比如EOFException,FileNotFoundException;常见的RuntimeException,比如NullPointException,ArrayIndexOutOfBoundsExceptions等
-
序列化serialVersionUID的作用:
-
&和&&的区别:
- &&:短路与,当符号左边表达式的值为false时,则不再计算右边的表单是
- &:不管左边表达式的结果为false或true,都会计算右边表达式。同时改符号可以作为位运算符
-
hashCode和equals方法的关系
设计一个类的时候为需要重写equals方法,在重写equals方法的同时,必须重写hashCode方法。
- 调用equals方法得到的结果为true,则两个对象的hashcode值必定相等
- 调用equals方法得到的结果为false,则两个对象的hashcode值不一定不同
- 两个对象的hashcode值不等,则equals方法得到的结果必定为false
- 两个对象的hashcode值相等,则equals方法得到的结果未知
-
什么是值传递和引用传递
- 值传递:传递的是值的拷贝,传递后就互不相关了
- 引用传递:传递的是变量所对应的内存空间的地址
-
强引用,软引用和弱引用的区别
- 强引用:垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
- 软引用:内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存
- 弱引用:有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
-
数组在内存中如何分配
-
深入理解Java中的String
-
String类被final修饰,不能被继承,并且它的成员方法都默认为final方法
-
调用
sub,concat,replace
方法,会重新生成了一个新的字符串对象,因此原字符串并没有被改变 -
字符串创建方式:(1)使用""引号创建字符串;(2)使用new关键字创建字符串.
- 单独使用""引号创建的字符串都是常量,编译期就已经确定存储到JVM字符串常量池
- 使用new String("")创建的对象会存储到heap中,是运行期新创建的
- new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中)
- 关于equals和==
- ==作用于基本数据类型的变量,则直接比较其存储的"值"是否相等,==作用于引用类型的变量,则比较的是所指向的对象的地址
- equals是基类Object中的方法,用来比较两个对象的引用是否相等,即是否指向同一个对象。Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等
- 字符串拼接符号“+”
- String s = "a" + "b",因为两个都是编译期常量,编译器在编译期进行了常量折叠,即变成String s = "ab"
- String s = "a" + 变量,相当于运行时new StringBuilder().append(a).append(变量);
- String str = new String("abc")创建了2个对象解释:在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象(不理解)
代码示例:
String s0 = "helloworld";// 编译期确定 String s1 = "hello"+"world";// 编译期确定,即变成“helloworld” String s2 = new String("helloworld");// 运行期确定 String s3 = s0 + "";// 运行期确定,相当于new StringBuilder().append(s0).append(""); String s4 = "hello" + 1;// 编译期确定 结果: s0 == s1 -> true s0 == s2 -> false s0 == s3 -> false s2 == s3 -> false 复制代码
-
-
List集合
-
Arraylist
- 数据结构:Object数组
- 增删查时间复杂度:插入和删除元素的时间复杂度受元素位置的影响。
add(E element)
执行的时间复杂度为O(1)
add(int index, E element)
执行的时间复杂度为O(n-i)
E remove(int index)
执行的时间复杂度为O(n-i)
E get(int index)
执行的时间复杂度为O(1)
,性能远高于LinkedList
-
LinkedList
:- 数据结构:双向循环链表
- 增删查时间复杂度:查元素的时间复杂度受元素位置的影响
add(E element)
执行的时间复杂度为O(1)
,性能略高于Arraylist,因为Arraylist需要扩容addFirst(E e)
执行的时间复杂度为O(1)
,性能远高于Arraylistadd(int index, E element)
视具体位置而定,index靠前则LinkedList性能会好一点,因为ArrayList性能主要损耗在元素的后移以及扩容上E remove(int index)
同add(int index, E element)
E get(int index)
执行的时间复杂度为O(n/2)
-
Vector
:所有的方法都是同步的,相对于Arraylist
是线程安全的,
-
-
Map集合
-
HashMap
- 数据结构:JDK1.8之前由数组+链表组成, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(阅读:(1) java8系列之重新认识HashMap (2) 红黑树详细分析
- 其它细节
- 允许null作为键,允许一个或多个键所对应的值为 null
- 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。如果给了容量的初始值不是2的幂次方大小,会将其扩充为2的幂次方大小
-
Hashtable
- 数据结构:数组+链表
- 其它细节
- 相对于HashMap是线程安全的,使用 synchronized 来保证线程安全
- 不允许存在null的键值
- 容量初始值默认大小为11,之后每次扩充,容量变为原来的2n+1。如果给了容量的初始值,会直接使用该默认值
-
ConcurrentHashMap
- JDK1.7用分段的数组+链表实现;JDK1.8采用数组+链表/红黑二叉树
- 实现线程安全的方式:
- 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率(默认分配16个Segment)
- JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作
-
LinkedHashMap
:继承自HashMap,在此基础上增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑 -
TreeMap
:基于红黑树实现,利用红黑树的性质,实现了键值对排序功能(阅读:TreeMap源码分析)
-
-
Set集合
LinkedHashSet
:继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的HashSet(无序,唯一)
: 基于 HashMap 实现的,底层采用 HashMap 来保存元素TreeSet(有序,唯一)
: 红黑树(自平衡的排序二叉树)
-
线程基础
-
生命周期:创建(new),就绪(runnable),运行(running),阻塞(blocked、time waiting、waiting),消亡(dead)
-
有关线程运行状态的方法
- start方法:启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务
- run方法:通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务
- sleep方法:线程睡眠(线程进入阻塞状态),交出CPU,让CPU去执行其他的任务。(注:不会释放锁)
- yield方法:与sleep方法类似,不同之处:a. 不能控制具体的交出CPU的时间;b. 只能让拥有相同优先级的线程有获取CPU执行时间的机会;c. 不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间
- join方法:假如在main主线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。调用join方法是调用了Object的wait方法,wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限
- interrupt方法:中断一个正处于阻塞状态的线程(如果线程不是处于阻塞状态,则无法中断)
-
如何唤醒处于睡眠状体的线程
-
如何释放处于等待中的线程
-
什么叫守护线程,用什么方法实现守护线程(Thread.setDeamon()的含义)
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的,主要的作用是提供计数等等辅助的功能
-
如何停止一个线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止(标志用volatile修饰)
- 使用stop方法可以强行终止正在运行或挂起的线程(不推荐)
- 使用interrupt方法中断线程,使用interrupt方法来终端线程可分为两种情况:
- 线程处于阻塞状态,如使用了sleep方法,将抛出一个InterruptedException
- 使用
while(!isInterrupted()){}
来判断线程是否被中断,线程将直接退出
-
-
ThreadLocal详解
- 作用:每个线程维护一个本地变量
- 概述:采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突
- 原理:ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本
- 应用:ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了
-
JDK中的动态代理和CGLIB
- JDK动态代理
- 概述:通过反射类Proxy以及InvocationHandler回调接口实现的
- 缺点: JDK中所要进行动态代理的类必须要实现一个接口,也就是说只能对该类所实现接口中定义的方法进行代理,这在实际编程中具有一定的局限性
- CGLIB动态代理
- 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑
- CGLIB底层:使用字节码处理框架ASM,来转换字节码并生成新的类
- CGLIB优点:它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择
- CGLIB缺点:对于final方法,无法进行代理
- JDK动态代理
网络通信
-
五层体系结构(TCP/IP协议簇)
物理层->数据链路层->网络层->传输层->应用层
- 应用层:应用层的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间的通信和交互的规则。互联网中的应用层协议,如域名系统DNS,HTTP协议
- 传输层:运输层的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。主要有TCP协议,UDP协议
- 网络层:负责为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送
- 数据链路层:两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。在两个相邻节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装程帧,在两个相邻节点间的链路上传送帧
- 物理层:实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异
-
TCP/IP模型
- 网络接口层(链路层)->网络层->传输层->应用层
-
TCP粘包和拆包产生的原因
- 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
- 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度
- 以太网的payload大于MTU进行IP分片。MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成若干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上
-
TCP粘包和拆包的解决策略
- 消息定长。例如100字节
- 在包尾部增加回车或者空格符等特殊字符进行分割,典型的如FTP协议
- 将消息分为消息头和消息尾
-
tcp,udp区别
TCP提供面向连接的、可靠的数据流传输;UDP提供的是非面向连接的、不可靠的数据流传输
TCP传输单位称为TCP报文段,UDP传输单位称为用户数据报
TCP注重数据安全性,UDP数据传输快,因为不需要连接等待,少了许多操作,但是其安全性却一般
-
tcp三次握手
- 第一次握手:建立连接时,客户端发送syn(同步标志)包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK(确认标志)包,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
-
tcp四次挥手
- 客户端先发送FIN(结束标志),进入FIN_WAIT1状态
- 服务端收到FIN,发送ACK,进入CLOSE_WAIT状态,客户端收到这个ACK,进入FIN_WAIT2状态
- 服务端发送FIN,进入LAST_ACK状态
- 客户端收到FIN,发送ACK,进入TIME_WAIT状态,服务端收到ACK,进入CLOSE状态
-
一次完整的HTTP请求过程
域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户
-
讲一讲长连接
- 基于http协议的长连接
在HTTP1.0和HTTP1.1协议中都有对长连接的支持。其中HTTP1.0需要在request中增加”Connection: keep-alive“ header才能够支持,而HTTP1.1默认支持
- http1.0请求与服务端的交互过程: a. 客户端发出带有包含一个header
Connection:keep-alive
的请求;b. 服务端接收到这个请求后,根据http1.0
和Connection: keep-alive
判断出这是一个长连接,就会在response的header中也增加Connection: keep-alive
,同是不会关闭已建立的tcp连接;c. 客户端收到服务端的response后,发现其中包含Connection: keep-alive
,就认为是一个长连接,不关闭这个连接。并用该连接再发送request.转到a
-
TCP如何保证可靠传输
-
详细介绍http
-
URI和URL的区别
- URI是统一资源标识符,用来唯一标识一个资源。一般由三部组成:访问资源的命名机制、存放资源的主机名、由路径表示资源自身的名称,着重强调于资源
- URL是统一资源定位器,用来标识一个资源,而且还指明了如何locate这个资源。一般由三部组成协议、存有该资源的主机IP地址(有时也包括端口号)、主机资源的具体地址。如目录和文件名等
- URL是URI的子集
-
HTTPS和HTTP的区别
- https协议需要到CA申请证书,一般免费证书很少,需要交费
- http是超文本传输协议,信息是明文传输;https 则是具有安全性的ssl加密传输协议
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443
- http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全
-
https是如何保证数据传输的安全
https实际就是在TCP层与http层之间加入了SSL/TLS来为上层的安全保驾护航,主要用到对称加密、非对称加密、证书,等技术进行客户端与服务器的数据加密传输,最终达到保证整个通信的安全性。
SSL/TLS协议作用:a. 认证用户和服务器,确保数据发送到正确的客户机和服务器;b. 加密数据以防止数据中途被窃取;c. 维护数据的完整性,确保数据在传输过程中不被改变
-
加密算法
- 对称加密
- 非对称加密
JVM虚拟机
- JVM内存区域
- 运行时数据区域组成
- 程序计数器(Program Counter Register)
- Java虚拟机栈(Java Virtual Machine Stacks)
- 本地方法栈(Native Method Stack)
- 方法区(Method Area)
- 运行时常量池(Runtime Constant Pool)
- 程序计数器
- 概述:该区域分配了一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器
- 作用:通过改变计数器的值来指定下一条需要执行的字节码指令,来恢复中断前程序运行的位置
- 特点:
- 线程私有化:每个线程都有独立的程序计数器,来恢复线程前执行的位置
- 无内存溢出
- Java虚拟机栈
- 概述:Java方法执行的内存模型,每个方法从调用直至执行的过程,对着一个栈帧,在虚拟机栈中入栈到出栈的过程
- 作用:每个方法被执行的时候,都会创建一个“栈帧”来存储局部变量表、操作数栈、动态链接、方法出口等信息
- 特点:
- 线程私有化
- 生命周期与线程执行结束相同
- 局部变量表
- 作用:存放编译期间可知的各种基本数据类型(8种)、对象引用、return Address类型(指向一条字节码指令的地址)
- 占用空间:64位长度的long和double类型占用2个局部变量空间(Slot),其余数据类型只占用1个
- 分配时机:在编译期间完成分配,当进入一个方法时,这个方法所需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
- 本地方法栈
- 概述:与虚拟机栈类似,是为虚拟机使用到的Native方法服务的内存区域
- 区别:
- 虚拟机栈:为虚拟机执行Java方法(字节码)服务
- 本地方法栈:为虚拟机使用到的Native方法服务
- Java堆
- 创建时间:JVM启动时创建该区域
- 占用空间:该区域是Java虚拟机所管理的内存中最大的一块区域
- 作用:用于存放了对象实例及数组(所有new的对象)
- 特点:
- 垃圾收集器(GC)作用于该区域,回收不使用的对象的内存空间
- 各个线程共享(读和写)的内存区域
- 该区域的大小可通过
-Xms
(最小值)和-Xmx
(最大值)参数设置,通常-Xms
与-Xmx
的值设置成一样(避免在运行时频繁调整Heap的大小)
- 方法区
- 概述:Java虚拟机规范将方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆分开。
- 占有空间:默认最小值为16MB,最大值为64MB。
- 作用:用于存储虚拟机加载的类信息、常量、静态变量,是各个线程共享的内存区域
- 特点:
- 各个线程共享(读和写)的内存区域
- 该区域的大小限制可以通过
-XX:PermSize
和-XX:MaxPernSize
参数
- 运行时常量池
- 概述:方法区的一部分。Class文件除了有类的(版本、字段、方法、接口)等描述信息外,还有一项信息就是常量池
- 作用:用于存放编译期生成的各种字面量和符号引用
- 动态性:
- Java语言并不要求常量池一定只有编译期才能产生,也就是并非预置入Class文件的常量池内的
- 容后才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中
- 这种特性用的比较广泛的便是String类的intern方法
- 运行时数据区域组成
- 类加载过程
通过类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构,在内存中生成一个代表类的数据访问入口的java.lang.Class对象
- 验证过程:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 准备过程:正式为类变量分配内存并设置类变量初始值的阶段,只包括类变量而不包括实例变量和final类变量,而且仅仅只是初始化为0值
- 解析过程:将常量池内的符号引用转换为直接引用的过程。符号引用用一组符号来描述所引用的目标。而直接引用是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄
- 初始化阶段:开始执行Java程序代码(字节码),执行类的构造器
<clinit>()
方法,<clinit>()
方法是由编译器自动收集所有类变量的赋值动作和静态语句块的语句合并而成,同一类中的静态块与类变量按顺序初始化,在同一个加载器下,一个类只会被初始化一次
- 双亲委派模型
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载
数据结构和算法
- 堆
- 二叉树
- 图
- 栈:先进后出(FILO)
- 队列:先进先出(FIFO)
- 常用排序算法
- 冒泡排序:
概述:比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
- 选择排序:
概述:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在 剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止
- 插入排序
概述:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止
- 希尔排序
概述:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序
- 归并排序
概述:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
- 快速排序
概述:通过一趟排序将待排序记录分割成独立的两部分,一部分全小于选取的参考值,另一部分全大于选取的参考值。这样分别对两部分排序之后顺序就可以排好了
- 堆排序
概述:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次最大值。如此反复执行,就能得到一个有序序列了
- 冒泡排序:
- 算法习题
- 由两个栈实现一个队列。
- 由两个队列实现一个栈。
- 判断出栈顺序是否符合要求。
- 给定一个整数,请写一个函数判断该整数的奇偶性
- 同样给定一个整数,请写一个函数判断该整数是不是2的整数次幂
- 给定一个整数,请写一个函数判断该整数的二进制表示中1的个数
- 在其他数都出现两次的数组中找到只出现一次的那个数
- 在其他数都出现两次的数组中找到只出现一次的那两个数
设计模式
- 单例模式
高并发
- CAS机制
- 原理:CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
- 缺点:
- ABA问题。 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化
- 循环时间长开销大。 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
- 只能保证一个共享变量的原子操作
- 线程池
- 执行策略ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小 int maximumPoolSize,//最大线程池大小 long keepAliveTime,//线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)成为核心线程的有效时间 TimeUnit unit,//keepAliveTime的时间单位 BlockingQueue<Runnable> workQueue,//阻塞任务队列 ThreadFactory threadFactory,//线程工厂 RejectedExecutionHandler handler) {//当提交任务数超过限制时 复制代码
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePoolSize,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常
- 常见四种线程池
Executors.newCachedThreadPool()
:可缓存线程池- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程
- 一定程序减少频繁创建/销毁线程,减少系统开销
Executors.FixedThreadPool()
:定长线程池- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
Executors.ScheduledThreadPool()
:定时线程池- 支持定时及周期性任务执行
Executors.SingleThreadExecutor()
:单线程化的线程池- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
- 四种拒绝策略
AbortPolicy
:拒绝任务,且还抛出RejectedExecutionException异常,线程池默认策略CallerRunPolicy
:拒绝新任务进入,如果该线程池还没有被关闭,那么这个新的任务在执行线程中被调用DiscardOldestPolicy
:如果执行程序尚未关闭,则位于头部的任务将会被移除,然后重试执行任务(再次失败,则重复该过程),这样将会导致新的任务将会被执行,而先前的任务将会被移除。DiscardPolicy
:没有添加进去的任务将会被抛弃,也不抛出异常。基本上为静默模式
- 执行策略ThreadPoolExecutor
数据库
-
Mysql 中 MyISAM 和 InnoDB 的区别有哪些
- InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务
- InnoDB支持外键,而MyISAM不支持
- InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的
- InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快
- Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高
-
为什么用自增列作为主键
- 表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页
- 如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面
-
Mysql中InnoDB的一级索引、二级索引
每个InnoDB表具有一个特殊的索引称为聚簇索引(也叫聚集索引,聚类索引,簇集索引)。如果表上定义有主键,该主键索引就是聚簇索引。如果未定义主键,MySQL取第一个唯一索引(unique)而且只含非空列(NOT NULL)作为主键,InnoDB使用它作为聚簇索引。如果没有这样的列,InnoDB就自己产生一个这样的ID值,它有六个字节,而且是隐藏的,使其作为聚簇索引。表中的聚簇索引(clustered index )就是一级索引,除此之外,表上的其他非聚簇索引都是二级索引,又叫辅助索引(secondary indexes)
-
B+树索引和哈希索引的区别
- B+树是一个平衡的多叉树。B+树从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动
- 哈希索引采用一定的哈希算法,把键值换成新的哈希值,检索时不需要类似B+树那样从根节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置
-
B树和B+树的区别
- B+树中只有叶子节点会带有指向记录的指针(ROWID),而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中
- B+树中所有叶子节点都是通过指针连接在一起,而B树不会
-
Mysql 索引类型有多少种
主键索引(primary key)、 唯一索引(unique index)、普通索引(index)、全文索引(fulltext key)
alert table student add primary key ('stu_id'), //主键索引 add unique index 'stu_code' ('stu_code'), //唯一索引 add index 'name_phone' ('name','phone'), //普通索引,复合索引 add fulltext index 'stu_desc' ('stu_desc'); //全文索引 复制代码
-
Mysql 索引的使用与优化
- 索引使用原则
-
列独立:保证索引包含的字段独立在查询语句中,不能是在表达式中
-
左前缀:like匹配模式左边不能以通配符开始,才能使用索引
-
复合索引由左到右生效,例如:建立索引
index(a,b,c)
,如下查询where条件分析where a = 3 # 是,只使用了a where a=3 and b=5 # 是,使用了a,b where a=3 and b=5 and c=4 # 是,使用了a,b,c where b=3 or where c=4 # 否 where a=3 and c=4 # 是,仅使用了a where a=3 and b>10 and c=7 # 是,使用了a,b where a=3 and b like '%xx%' and c=7 # 是,使用了a,b??? 复制代码
-
- 能够使用索引的典型应用
- 匹配全值
对索引中所有列都指定具体值,即是对索引中的所有列都有等值匹配的条件
- 匹配值的范围查询
对索引的值能够进行范围查找
- 匹配最左前缀
仅仅使用索引中的最左边列进行查询,比如在 col1 + col2 + col3 字段上的联合索引能够被包含 col1、(col1 + col2)、(col1 + col2 + col3)的等值查询利用到,可是不能够被 col2、(col2、col3)的等值查询利用到
- 仅仅对索引进行查询
查询的列都在索引的字段中时,查询的效率更高
- 匹配列前缀
仅仅使用索引中的第一列,并且只包含索引第一列的开头一部分进行查找
- 实现索引匹配部分精确而其他部分进行范围匹配
- 列名是索引,那么使用 column_name is null 就会使用索引
- 匹配全值
- 存在索引但不能使用索引的典型场景
- 以%开头的 like 查询不能利用 B-Tree 索引
解决方法:1. 使用全文索引;2. 利用 innodb 的表都是聚簇表的特点,采取一种轻量级别的解决方式:一般情况下,索引都会比表小,扫描索引要比扫描表更快,而Innodb 表上二级索引 idx_last_name 实际上存储字段 last_name 还有主键 actot_id,那么理想的访问应该是首先扫描二级索引 idx_last_name 获得满足条件的last_name like '%NI%' 的主键 actor_id 列表,之后根据主键回表去检索记录,这样访问避开了全表扫描演员表 actor 产生的大量 IO 请求。查询sql为
SELECT * FROM (SELECT actor_id FROM actor WHERE last_name LIKE '%NI%') a, actor b WHERE a.actor_id = b.actor_id
- 数据类型出现隐式转换的时候也不会使用索引
当列的类型是字符串,那么一定记得在 where 条件中把字符常量值用引号引起来,否则即便这个列上有索引,mysql 也不会用到,因为 MySQL 默认把输入的常量值进行转换以后才进行检索
- 复合索引的情况下,假如查询条件不包含索引列最左边部分,即不满足最左原则 leftmost,是不会使用复合索引的
- MySQL 认为使用索引比全表扫描更慢,则不使用索引
- 用 or 分割开的条件,如果 or 前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到
- 以%开头的 like 查询不能利用 B-Tree 索引
- 使用索引的小技巧
- 字符串字段权衡区分度与长度的技巧
- 左前缀不易区分的字段索引建立方法
解决办法:1. 倒过来存储并建立索引;2. 新增伪hash字段 把字符串转化为整型
- 索引覆盖
如果查询的列恰好是索引的一部分,那么查询只需要在索引文件上进行,不需要回行到磁盘,这种查询,速度极快
- 延迟关联
在根据条件查询数据时,如果查询条件不能用的索引,可以先查出数据行的id,再根据id去取数据行
- 索引排序
排序的字段上加入索引,可以提高速度
- 重复索引和冗余索引
重复索引:在同一列或者相同顺序的几个列建立了多个索引,成为重复索引,没有任何意义,删掉; 冗余索引:两个或多个索引所覆盖的列有重叠,比如对于列m,n ,加索引index m(m),indexmn(m,n),称为冗余索引
- 索引碎片与维护
在数据表长期的更改过程中,索引文件和数据文件都会产生空洞,形成碎片。修复表的过程十分耗费资源,可以用比较长的周期修复表
//清理方法 alert table xxx engine innodb; //或 optimize table xxx; 复制代码
- innodb引擎的索引注意事项
Innodb 表要尽量自己指定主键,如果有几个列都是唯一的,要选择最常作为访问条件的列作为主键,另外,Innodb 表的普通索引都会保存主键的键值,所以主键要尽可能选择较短的数据类型,可以有效的减少索引的磁盘占用,提高索引的缓存效果。
- 索引使用原则
-
为什么使用数据索引能提高效率
- 数据索引的存储是有序的
- 在有序的情况下,通过索引查询一个数据是无需遍历索引记录的
- 极端情况下,数据索引的查询效率为二分法查询效率,趋近于 log2(N)
-
mysql联合索引
- 联合索引是两个或更多个列上的索引。对于联合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a、a,b、a,b,c3种组合进行查找,但不支持b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效
- 利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处
-
什么情况下应不建或少建索引
- 表记录太少
- 经常插入、删除、修改的表
- 数据重复且分布平均的表字段
- 经常和主字段一块查询但主字段索引值比较多的表字段
-
MySQL分区
-
四种隔离级别
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生
- Repeatable read (可重复读):可避免脏读、不可重复读的发生
- Read committed (读已提交):可避免脏读的发生
- Read uncommitted (读未提交):最低级别,任何情况都无法保证
-
MySQL优化
- 开启查询缓存,优化查询
- explain你的select查询,分析你的查询语句或是表结构的性能瓶颈
- 当只要一行数据时使用limit 1,MySQL数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据
- 为搜索字段建索引
- 垂直分表,选择正确的存储引擎
-
explain性能分析
+----+-------------+-------+------------+------+---------------+-----+---------+------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+-----+---------+------+------+----------+-------+ id:select 查询的序列号,包含一组可以重复的数字,表示查询中执行sql语句的顺序。id不同时,值大的优先执行; 相同时,执行顺序由上而下 select_type:查询类型,主要是用于区别普通查询,联合查询,嵌套的复杂查询。 simple:简单的select 查询,查询中不包含子查询或者union primary:查询中若包含任何复杂的子查询,最外层查询则被标记为primary subquery:在select或where 列表中包含了子查询 derived:在from列表中包含的子查询被标记为derived(衍生)MySQL会递归执行这些子查询,把结果放在临时表里 union:若第二个select出现在union之后,则被标记为union,若union包含在from子句的子查询中,外层select将 被标记为:derived union result:从union表获取结果的select partitions:表所使用的分区,如果要统计十年公司订单的金额,可以把数据分为十个区,每一年代表一个区。 这样可以大大的提高查询效率 * type:连接类型,共有8个级别。性能从最优到最差的排序 system > const > eq_ref > ref > range > index > all all:(full table scan)全表扫描无疑是最差,若是百万千万级数据量,全表扫描会非常慢。 index:(full index scan)全索引文件扫描比all好很多,毕竟从索引树中找数据,比从全表中找数据要快。 range:只检索给定范围的行,使用索引来匹配行。范围缩小了,当然比全表扫描和全索引文件扫描要快。 sql语句中一般会有between,in,>,< 等查询。 ref:非唯一性索引扫描,本质上也是一种索引访问,返回所有匹配某个单独值的行。比如查询公司所有属于研发团队 的同事,匹配的结果是多个并非唯一值。 eq_ref:唯一性索引扫描,对于每个索引键,表中有一条记录与之匹配。比如查询公司的CEO,匹配的结果只可能是 一条记录, const:表示通过索引一次就可以找到,const用于比较primary key 或者unique索引。因为只匹配一行数据,所以很快, 若将主键至于where列表中,MySQL就能将该查询转换为一个常量。 system:表只有一条记录(等于系统表),这是const类型的特列,平时不会出现,了解即可 * possible_keys:显示查询语句可能用到的索引(一个或多个或为null),不一定被查询实际使用。仅供参考使用 * key:显示查询语句实际使用的索引。若为null,则表示没有使用索引 * key_len:显示索引中使用的字节数,可通过key_len计算查询中使用的索引长度 * ref:显示索引的哪一列或常量被用于查找索引列上的值 * rows:根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数,值越大越不好 * filtered:一个百分比的值,和rows列的值一起使用,可以估计出查询执行计划(QEP)中的前一个表的结果集,从而确定 join操作的循环次数。小表驱动大表,减轻连接的次数 * extra Using filesort: 说明MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利 用索引完成的排序操作称为“文件排序” 。出现这个就要立刻优化sql。 Using temporary: 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和 分组查询 group by。 出现这个更要立刻优化sql。 Using index: 表示相应的select 操作中使用了覆盖索引(Covering index),避免访问了表的数据行,效果不错! 如果同时出现Using where,表明索引被用来执行索引键值的查找。如果没有同时出现Using where ,表示索引用来读取数据而非执行查找动作。覆盖索引(Covering Index) :也叫索引覆盖,就是select 的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再 次读取数据文件。 Using index condition: 在5.6版本后加入的新特性,优化器会在索引存在的情况下,通过符合RANGE范围的条数 和 总数的比例来选择是使用索引还是进行全表遍历。 Using where: 表明使用了where 过滤 Using join buffer: 表明使用了连接缓存 impossible where: where 语句的值总是false,不可用,不能用来获取任何元素 distinct: 优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作 复制代码
Spring总结
-
IOC容器
-
SpringMVC处理请求的完整过程
-
源码相关
// bean创建之前修改bean的定义属性,例如修改scope值从singleton改为prototype // 注意:不要在BeanFactoryPostProcessor进行可能触发bean实例化的操作 public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory var1); } 复制代码
// bean的初始化时,设置了属性之后回调 public interface InitializingBean { void afterPropertiesSet() throws Exception; } 复制代码
// bean实例化之后,在执行bean的初始化方法前后,添加一些自己的处理逻辑 public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object var1, String var2); Object postProcessAfterInitialization(Object var1, String var2); } 复制代码
// 实现类获取Spring的应用上下文ApplicationContext public interface ApplicationContextAware extends Aware { void setApplicationContext(ApplicationContext var1); } 复制代码
// 实现类获取BeanFactory public interface BeanFactoryAware extends Aware { void setBeanFactory(BeanFactory var1) throws BeansException; } 复制代码
// 实现注册事件 public abstract class ApplicationEvent extends EventObject { private final long timestamp = System.currentTimeMillis(); public ApplicationEvent(Object source) { super(source); } public final long getTimestamp() { return this.timestamp; } } // 实现类获取ApplicationEventPublisher发布事件 public interface ApplicationEventPublisherAware extends Aware { void setApplicationEventPublisher(ApplicationEventPublisher var1); } // 实现类对发布的事件监听 public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { void onApplicationEvent(E var1); } 复制代码
参考文章:
Spring的ORM知识点
-
Mybatis 是如何执行 sql 语句的
-
JDBC 获取数据库连接的大概过程
-
PreparedStatement 和 Statement 区别
-
MyBatis的Api接口
public interface SqlSessionFactory { SqlSession openSession(); SqlSession openSession(boolean var1); SqlSession openSession(Connection var1); SqlSession openSession(TransactionIsolationLevel var1); SqlSession openSession(ExecutorType var1); SqlSession openSession(ExecutorType var1, boolean var2); SqlSession openSession(ExecutorType var1, TransactionIsolationLevel var2); SqlSession openSession(ExecutorType var1, Connection var2); Configuration getConfiguration(); } 复制代码
public interface SqlSession extends Closeable { <T> T selectOne(String var1); <T> T selectOne(String var1, Object var2); <E> List<E> selectList(String var1); <E> List<E> selectList(String var1, Object var2); <E> List<E> selectList(String var1, Object var2, RowBounds var3); <K, V> Map<K, V> selectMap(String var1, String var2); <K, V> Map<K, V> selectMap(String var1, Object var2, String var3); <K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4); <T> Cursor<T> selectCursor(String var1); <T> Cursor<T> selectCursor(String var1, Object var2); <T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3); void select(String var1, Object var2, ResultHandler var3); void select(String var1, ResultHandler var2); void select(String var1, Object var2, RowBounds var3, ResultHandler var4); int insert(String var1); int insert(String var1, Object var2); int update(String var1); int update(String var1, Object var2); int delete(String var1); int delete(String var1, Object var2); void commit(); void commit(boolean var1); void rollback(); void rollback(boolean var1); List<BatchResult> flushStatements(); void close(); void clearCache(); Configuration getConfiguration(); <T> T getMapper(Class<T> var1); Connection getConnection(); } 复制代码
netty知识点
- JAVA中的BIO、NIO和AIO
redis详解
-
单线程:指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程
-
redis能够快速执行原因
- 绝大部分请求是纯粹的内存操作
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 非阻塞IO - IO多路复用
-
Redis的 5 种基础数据结构
-
String(字符串):将用户信息结构体使用 JSON 序列化成字符串,然后将序列化后的字符串塞进 Redis 来缓存。同样,取用户信息会经过一次反序列化的过程
Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
- 常用指令
# (批量)设置值 set key value setnx key value mset key1 value1 key2 value2 # 获取值 get key mget key1 key2 # 判断值是否存在 exists key # 删除值 del key # 设置key设置过期时间,到点自动删除 expire key seconds setex key seconds value # value值是整数,可计数。自增范围是signed long的最大值和最小值 incr numberKey incrby numberKey stepLenth 复制代码
- 常用指令
-
List(列表):相当于LinkedList,底层实现的数据结构是快速列表,数据结构是链表而不是数组。插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)
底层存储快速链表: 首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是
ziplist
,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist
。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是int
类型的数据,结构上还需要两个额外的指针prev
和next
。所以 Redis 将链表和ziplist
结合起来组成了quicklist
。也就是将多个ziplist
使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余- 常用指令
# 将值value插入到列表key的表尾(最右边) rpush key value1 value2 value3 # 将值value插入到列表key的表头(最左边) lpush key value1 value2 value3 # 返回列表key的长度 llen key # 移除并返回列表 key 的头元素 lpop key # 移除并返回列表 key 的尾元素 rpop key 复制代码
- 常用指令
-
hash (字典):相当于
HashMap
,它是无序字典。内部实现是数组+链表二维结构,与HashMap
不同的是rehash
的方式不一样,因为Java
的HashMap
在字典很大时,rehash
是个耗时的操作,需要一次性全部rehash
。Redis为了高性能,不能堵塞服务,所以采用了渐进式rehash
策略。渐进式
rehash
: 在rehash
的同时,保留新旧两个hash
结构,查询时会同时查询两个hash
结构,然后在后续的定时任务中以及hash
操作指令中,循序渐进地将旧hash
的内容一点点迁移到新的hash
结构中。当搬迁完成了,就会使用新的hash
结构取而代之- 常用指令
# 将哈希表 key 中的域 field 的值设为 value hset key field value hmset key field value [field value ...] # 返回哈希表 key 中,所有的域和值 hgetall key # 返回哈希表 key 中域的数量 hlen key # 返回哈希表 key 中给定域 field 的值 hget key field hmget key field [field ...] 复制代码
-
set (集合):相当于
HashSet
,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value
都是一个值NULL
- 常用指令
# 将元素加入到集合key当中 sadd key member [member ...] # 判断 member 元素是否集合 key 的成员 sismember key member # 返回集合 key 的基数(集合中元素的数量) scard key # 移除并返回集合中的一个随机元素 spop key 复制代码
-
zset(有序列表):相当于
SortedSet
和HashMap
的结合体,一方面它是一个set
,保证了内部value
的唯一性,另一方面它可以给每个value
赋予一个score
,代表这个value
的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据跳跃列表:
- 常用指令
# 将member元素及其score值加入到有序集key当中 zadd key score1 member1 score2 member2 # 返回有序集key中(按score值从小到大),指定区间内的成员(位置数表示倒数第n位) zrange key start stop [withscores] # 返回有序集 key(按score值从大到小) 中,指定区间内的成员 zrevrange key start stop [withscores] # 返回有序集 key 的数量 zcard key # 返回有序集 key 中,成员 member 的 score 值 zscore key member # 返回有序集 key 中成员 member 的排名(按 score 值递增) zrank key member # 返回有序集key中,所有score值介于min和max之间(包括等于 min 或 max )的成员(按 score 值递增) zrangebyscore key min max [withscores] [limit offset count] # 移除有序集 key 中的一个或多个成员 zrem key member1 member2... 复制代码
- 常用指令
-
list/set/hash/zset
这四种数据结构是容器型数据结构,共享规则:a. 如果容器不存在,那就创建一个,再进行操作;b. 如果容器里元素没有了,数据结构自动删除,内存被回收
-
-
Redis分布式锁
# 只在键不存在时,才对键进行设置操作,同时设置过期时间 > set lock:lockkey true ex 5 nx ...处理逻辑... > del lock:lockkey # 结合ThreadLocal实现锁的可重入性 private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>(); private Map<String, Integer> currentLockers() { Map<String, Integer> refs = lockers.get(); if (refs != null) { return refs; } lockers.set(new HashMap<>()); return lockers.get(); } public boolean lock(String key) { Map<String, Integer> refs = currentLockers(); Integer refCnt = refs.get(key); if (refCnt != null) { refs.put(key, refCnt + 1); return true; } boolean ok = this._lock(key); if (!ok) { return false; } refs.put(key, 1); return true; } 复制代码
-
redis队列思路
- 使用rpush/lpush操作入队列;
- 使用阻塞读的指令blpop/brpop,在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零
- 空闲连接问题:如果线程一直阻塞在哪里,Redis的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候blpop/brpop会抛出异常。因此需要注意捕获异常和重试
- 延时队列:通过 Redis 的 zset(有序列表)来实现。将消息序列化成一个字符串作为 zset 的 value ,这个消息的到期处理时间作为 score ,然后用多个线程轮询 zset 获取到期的任务进行处理
-
redis位图
位数组的顺序和字符的位顺序是相反的。应用场景距离:用户签到记录
# 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。value值可以是0也可以是1 setbit key offset value # 计算给定字符串中,被设置为 1 的比特位的数量 bitcount key [start] [end] # 返回string的二进制中第一个0或1的位置 bitpos key bit [start] [end] # bitfield key [get type offset] [set type offset value] [incrby type offset increment] [overflow wrap|sat|fail] # 示例:hello的二进制表示为01101000(h) 01100101(e) 01101100(l) 01101100(l) 01101111(o),最左边的是第一位 > setbit s 1 1 (零存) > setbit s 2 1 > setbit s 4 1 > setbit s 9 1 > setbit s 10 1 > setbit s 13 1 > setbit s 15 1 > get s # 输出结果为he(整取) > get bit s 1(零取) > set w h (整存) > getbit w 2(零取) > set w hello > bitcount w # 21,返回字符串hello中1的个数 > bitcount w 0 1 # 7,返回字符串hello前两个字符中1的个数 > bitpos w 0 # 0,返回字符串hello中的第一个0位 > bitpos w 1 # 1,返回字符串hello中的第一个1位 > bitpos w 1 1 1 # 9,字符串hello从第二个字符算起,第一个1位 > bitpos w 1 2 2 # 17,字符串hello从第三个字符算起,第一个1位 复制代码
-
HyperLogLog统计
提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是0.81%。应用距离:记录用户一天之内每个网页每天的
UV
数据(用户重复访问需要去重)# 将任意数量的元素添加到指定的 HyperLogLog 里面 pfadd key element [element ...] # 返回储存在给定键的 HyperLogLog 的近似基数,多个键返回并集的近似基数 pfcount key [key ...] # 将多个 HyperLogLog 合并(merge)为一个 HyperLogLog pfmerge destkey sourcekey [sourcekey ...] > pfadd codehole user1 > pfcount codehole > pfadd codehole user1 user2 user3 > pfcount codehole # 结果 3 复制代码
-
redis布隆过滤器(4.0新特性)
布隆过滤器是一个不怎么精确的
set
结构,会有小小的误判率,例如当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。使用场景:客户端新闻推荐系统不重复推送原理:向布隆过滤器中添加key时,会使用多个hash函数对key进行hash算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个hash函数都会算得一个不同的位置。再把位数组的这几个位置都置为1就完成了add。key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都位 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在
空间占用估计:布隆过滤器有两个参数,第一个是预计元素的数量 n,第二个是错误率f。公式根据这两个输入得到两个输出,第一个输出是位数组的长度 l,也就是需要的存储空间大小 (bit),第二个输出是 hash 函数的最佳数量 k。hash 函数的数量也会直接影响到错误率,最佳的数量会有最低的错误率
k=0.7*(l/n) # 约等于 f=0.6185^(l/n) # ^ 表示次方计算,也就是 math.pow 结论: 1. 位数组相对越长 (l/n),错误率 f 越低,这个和直观上理解是一致的 2. 位数组相对越长 (l/n),hash 函数需要的最佳数量也越多,影响计算效率 3. 当一个元素平均需要 1 个字节 (8bit) 的指纹空间时 (l/n=8),错误率大约为 2% 4. 错误率为 10%,一个元素需要的平均指纹空间为 4.792 个 bit,大约为 5bit 5. 错误率为 1%,一个元素需要的平均指纹空间为 9.585 个 bit,大约为 10bit 6. 错误率为 0.1%,一个元素需要的平均指纹空间为 14.377 个 bit,大约为 15bit 复制代码
> bf.add codehole user1 # 添加单个元素 > bf.madd codehole user2 user3 user4 # 批量添加元素 > bf.exists codehole user1 # 查询元素是否存在 > f.mexists codehole user4 user5 # 一次查询多个元素是否存在 复制代码
-
简单限流
原理:zset数据结构的key记录用户的行为历史,value用毫秒时间戳,通过滑动窗口与阈值max_count进行比较就可以得出当前的行为是否允许
- 代码实现
public class SimpleRateLimiter { private Jedis jedis; public SimpleRateLimiter(Jedis jedis) { this.jedis = jedis; } public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) { String key = String.format("hist:%s:%s", userId, actionKey); long nowTs = System.currentTimeMillis(); Pipeline pipe = jedis.pipelined(); pipe.multi(); pipe.zadd(key, nowTs, "" + nowTs); pipe.zremrangeByScore(key, 0, nowTs - period * 1000); Response<Long> count = pipe.zcard(key); pipe.expire(key, period + 1); pipe.exec(); pipe.close(); return count.get() <= maxCount; } } 复制代码
- 代码实现
-
漏斗限流
- java版实现单机漏斗算法
public class FunnelRateLimiter { static class Funnel { int capacity; float leakingRate; int leftQuota; long leakingTs; public Funnel(int capacity, float leakingRate) { this.capacity = capacity; # 漏斗容量 this.leakingRate = leakingRate; # 漏嘴流水速率 this.leftQuota = capacity; # 漏斗剩余空间 this.leakingTs = System.currentTimeMillis();# 上一次漏水时间 } void makeSpace() { long nowTs = System.currentTimeMillis(); long deltaTs = nowTs - leakingTs; # 距离上一次漏水过去了多久 int deltaQuota = (int) (deltaTs * leakingRate);# 根据时间差计算出可以腾出的空间 if (deltaQuota < 0) { # 间隔时间太长,整数数字过大溢出 this.leftQuota = capacity; this.leakingTs = nowTs; return; } if (deltaQuota < 1) { # 腾出空间太小,最小单位是1 return; } this.leftQuota += deltaQuota; # 增加漏水容量 this.leakingTs = nowTs; # 重置漏水时间 if (this.leftQuota > this.capacity) { this.leftQuota = this.capacity; } } boolean watering(int quota) { makeSpace(); if (this.leftQuota >= quota) { # 判断剩余空间是否足够 this.leftQuota -= quota; return true; } return false; } } private Map<String, Funnel> funnels = new HashMap<>(); public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) { String key = String.format("%s:%s", userId, actionKey); Funnel funnel = funnels.get(key); if (funnel == null) { funnel = new Funnel(capacity, leakingRate); funnels.put(key, funnel); } return funnel.watering(1); // 需要1个quota } } 复制代码
- 漏斗限流Redis-Cell(Redis 4.0特性)
# 创建quota个容量为capacity,漏水速率leakingRate,阈值为quota的名称未key的漏斗 cl.throttle key capacity, leakingRate quota > cl.throttle laoqian:reply 15 30 60 1) (integer) 0 # 0 表示允许,1表示拒绝 2) (integer) 15 # 漏斗容量capacity 3) (integer) 14 # 漏斗剩余空间left_quota 4) (integer) -1 # 如果拒绝了,需要多长时间后再试(漏斗有空间了,单位秒) 5) (integer) 2 # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒) 复制代码
- GEOHash坐标计算
- java版实现单机漏斗算法
-
Redis主从复制
过程原理:
- 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
- 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
- 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
- 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
-
redis两种持久化方式的优缺点
- RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照
- AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集
- Redis 还可以同时使用 AOF 持久化和 RDB 持久化。当redis重启时,它会有限使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更加完整
- RDB的优点:1. RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。2. RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。3. RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。4. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快