中级Java知识点
Java 基本类型哪些,所占字节
byte :1 个字节short :2 个字节char :2 个字节int :4 个字节long :8 个字节float :4 个字节double :8 个字节
java 集合以及底层原理
Java 集合框架的根接口有 Collection 和 Map。Collection 根接口包含 List 和 Set 二个子接口。
List 接口
它的特点是:元素有序、且可重复,主要包含三个实现类:ArrayList,vector,LinkedList ArrayList 的特点:底层是数组,线程不安全,查找快,增删慢(数组的特点)。
ArrayList 的底层实现原理:通过 ArrrayList 空参构造器创建对象。
底层创建一个长度为 10 的数组,当我们向数组中添加 11 个元素时,底层会进行扩容,扩容为原来的 1.5 倍
(创建一个新的数组,长度为原数组长度的 1.5 倍,将原数组复制到新数组中)。
vector 的特点:古老的实现类,底层是数组,线程安全的,JDK1.0 就有了,Vector 总是比 ArrayList 慢,所以尽量避免使用。
LinkedList 的特点:底层是使用双向链表。增删快,查找慢。
Set 接口
它的特点:
无序性:通过 HashCode 方法算出的值来决定在数组中存放的位置;
不可重复性:进行 equals 方法比较,结果为 true 则两个数据相同,若为 false 则不同。主要包含三个实现类:HashSet,LinkedHashSet,TreeSet
HashSet 特点:线程不安全,集合元素可以为 null,不能保证元素的排列顺序HashSet 的底层实现原理:
当向 HashSet 添加数据时,首先调用 HashCode 方法决定数据存放在数组中的位置,该位置上没有其他元素,
则将数据直接存放,若该位置上有其他元素,调用 equals 方法进行比较。若返回 true 则认为两个数据相同, 若返回 false,则以链表的形式将该数据存在该位置上,(jdk1.8)如果数量达到 8 则将链表换成红黑树。
HashSet 的底层就是一个 HashMap,向 HashSet 中添加的数据实际上添加到了 HashMap 中的 key 里。所以 HashMap 的 key 可以看成是 Set 的集合。
LinkedHashSet 特点:继承了 HashSet,底层实现原理和 HashSet 一样,可以安照元素添加的顺序进行遍历根据元素的 hashCode 值来决定元素的存储位置,它维护了一张链表该链表记录了元素添加的顺序。
底层就是一个 LinkedHashMap。
TreeSet 特点:底层为红黑树;可以安照指定的元素进行排序;TreeSet 中的元素类型必须保持一致, 底层就是 TreeMap。TreeSet 必须(自然排序)实现 Comparable 接口,重写 compareTo()方法, 按照某个属性进行排序,相结合添加元素或(定制排序)创建一个 Comparator 实现类的对象,
并传入到 TreeSet 的构造器中,按照某个属性进行排序,向集合添加元素。定制排序比自然排序灵活。如果即有自然排序又有定制排序谁起作用? 定制排序
Map 接口
Map 的特点:
Map 存储的是键值对(key,value),Map 中的 key 是无序的且不可重复的,所有的 key 可以看成是一个 set 集合。Map 中的 key 如果是自定义类的对象必须重写 hashCode 和 equals 方法,Map 中的 value 是无序的可重复的, 所有的 value 可以看成是 Collection 集合,Map 中的 value 如果是自定义类的对象必须重写 equals 方法, Map 中的键值对可以看成是一个一个的 Entry.Entry 所存放的位置是由 key 来决定的。
Entry 是无序的不可重复的。主要的实现类:HashMap,LinkedHashMap,TreeMap,HashTable. HashMap 特点
1.底层是一个数组 + 链表 + 红黑树(jdk1.8)
2.数组的类型是一个 Node 类型
3.Node 中有 key 和 value 的属性
4.根据 key 的 hashCode 方法来决定 Node 存放的位置
5.线程不安全的 ,可以存放 null HashMap 的底层实现原理:
当我们向 HashMap 中存放一个元素(k1,v1),先根据 k1 的 hashCode 方法来决定在数组中存放的位置。
如果该位置没有其它元素则将(k1,v1)直接放入数组中,如果该位置已经有其它元素(k2,v2),调用k1 的equals 方法和 k2 进行比较。
如果结果为 true 则用 v1 替换 v2,如果返回值为 false 则以链表的形式将(k1,v1)存放, 当元素达到 8 时则会将链表替换成红黑树以提高查找效率。
HashMap 的构造器:new HashMap() :创建一个容量为 16 的数组,加载因子为 0.75。当我们添加的数据超过 12 时底层会进行扩容,扩容为原来的 2 倍。
LinkedHashMap:继承了 HashMap 底层实现和 HashMap 一样.
可以安照元素添加的顺序进行遍历底层维护了一张链表用来记录元素添加的顺序。
TreeMap 特点:可以对 Key 中的元素安照指定的顺序进行排序 ( 不能对 value 进行排序)
HashTable 特点:线程安全的 ,不可以存放 null,map 中的 key 不能重复,如果有重复的,后者的value 覆盖前者的 value
四大作用域和九大内置对象
四大作用域:
page :当前页面有效时间最短(页面执行期) request :HTTP 请求开始到结束这段时间session :HTTP 会话开始到结束这段时间application :服务器启动到停止这段时间
九大内置对象:
request :请求对象 作 用 域 Request response : 响 应 对 象 作 用 域 Page pageContext :页面上下文对象 作用域 Page session :会话对象 作用域 Session
application :应用程序对象 作用域 Application out :输出对象 作用域 Page
config :配置对象 作用域 Page page :页面对象 作用域 Page exception :例外对象 作用域 page
jsp 和 servlet 的区别
1.jsp 经编译后就变成了 Servlet.(JSP 的本质就是 Servlet,JVM 只能识别 java 的类, 不能识别 JSP 的代码,Web 容器将 JSP 的代码编译成JVM 能够识别的 java 类)
2.jsp 更擅长表现于页面显示,servlet 更擅长于逻辑控制.
3.Servlet 中没有内置对象,Jsp 中的内置对象都是必须通过 HttpServletRequest 象,
HttpServletResponse 对象以及 HttpServlet 对象得到.
Jsp 是 Servlet 的一种简化,使用 Jsp 只需要完成程序员需要输出到客户端的内容,Jsp 中的 Java 脚本如何镶嵌到一个类中,由 Jsp 容器完成。
而 Servlet 则是个完整的 Java 类,这个类的 Service 方法用于生成对客户端的响应。
servlet 生命周期
1.加载和实例化
2.初始化
3.请求处理
4.服务终止
加载(服务器启动时,会到 web.xml 文件中去找到 Servlet 文件的配置并创建 servlet 的实例)
→初始化(init()此方法只执行一次) →执行(service(),doGet(),doPost()) →销毁(销毁 destory()) service():
方法本身包含了 doGet()和 doPost().如果服务器发现了 service()方法,则不再执行 doGet(),doPost().
一般不建议去重写父类的 service 方法.因为重写了此方法 doGet 方法和 doPost 方法将得不到利用. 没有 service()方法默认执行doGet()方法.
cookie 和 session 区别以及 JWT 与 Session 的差异
1、cookie 数据存放在客户的浏览器上,session 数据放在服务器上。
2、cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗,考虑到安全应当使用 session。3、session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用 cookie。
4、单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。
5、可以考虑将登陆信息等重要信息存放为 session,其他信息如果需要保留,可以放在 cookie 中。
1.Session 是在服务器端的,而 JWT 是在客户端的。
2.Session 方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
3.JWT 方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
4.Session 的状态是存储在服务器端,客户端只有 session id;而 Token 的状态是存储在客户端。
JWT 与 OAuth 的区别
OAuth2 是一种授权框架 ,JWT 是一种认证协议。无论使用哪种方式切记用 HTTPS 来保证数据的安全性
OAuth2 用在使用第三方账号登录的情况(比如使用 weibo, qq, github 登录某个 app) JWT 是用在前后端分离, 需要简单的对后台 API 进行保护时使用。
Cookie 和 LocalStorage 和 sessionStorage 的区别
转
发和重定向的区别
转发:浏览器地址栏不变,1 次请求,request 请求,可以访问 web-inf,可以共享 request 请求域数据,只能跳转工程内的资源
重定向:浏览器变化,2 次请求,response 响应,不能访问 web-inf,不可以共享 request 请求域数据,可以跳转任意资源
饿汉于懒汉单例模式
单例模式设计:
第一步:私有化构造器
第二步:提供一个公共静态返回该类实例对象的方法
饿汉式:先初始化对象,Single 类一进内存,就已经创建好了对象。
class Single{
private Single(){}
private static Single s=new Single(); public static Single getInstance()
{
return s;
}
}
懒汉式:对象是方法被调用时,才初始化,也叫做对象的延时加载。
class Single{ //Single 类进内存,对象还没存在,只有调用了 getInstance 方法时,才建立对象
private Single(){}
private static Single s=null;
public static synchronize Single getInstance()
{
if(s==null){ s=new single();
}
return s;
}
}
操作共享的数据有多条,会出现线程安全问题,在方法加一个同步
过滤器和拦截器的区别
①拦截器是基于 java 的反射机制的,而过滤器是基于函数回调。
②拦截器不依赖与 servlet 容器,过滤器依赖与 servlet 容器。
③拦截器只能对 action 请求起作用,而过滤器则可以对几乎所有的请求起作用。
④拦截器可以访问 action 上下文、值栈里的对象,而过滤器不能访问。
⑤在 action 的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
⑥拦截器可以获取 IOC 容器中的各个 bean,而过滤器就不行,这点很重要,在拦截器里注入一个 service,可以调用业务逻辑。
#和$的区别
#{}和${}的区别
#{} 在 mapper 的配置文件的 sql 语句中,它是占位符, 相当于 ? 号。
${} 在 mapper 的配置文件的 sql 语句中,它是原样输出变量的值,然后以字符串拼接的功能进行操作。
${} 中只能写 value,或者是@Param 命名参数后的参数名称
在输出参数的时候,我们并不推荐使用 ${} 来输出。因为可能会导至 sql 注入问题的存在。
什么是 SQL 注入?
如果 SQL 是根据用户输入拼出来,如果用户故意输入可以让后台解析失败的字符串,这就是 SQL 注入
例如,用户在输入密码的时候,输入’ or 1=1’, 这样,后台的程序在解析的时候,拼成的 SQL 语句,可能是这样的:
select count(1) from tab where user=userinput and pass=’’ or 1=1;
看这条语句,可以知道,在解析之后,用户没有输入密码,加了一个恒等的条件 1=1,这样,这段 SQL 执行的时候, 返回的 count 值肯定大于 1 的,如果程序的逻辑没加过多的判断,这样就能够使用用户名 userinput 登陆,而不需要密码。
防止 SQL 注入,首先要对密码输入中的单引号进行过滤,再在后面加其它的逻辑判断,或者不用这样的动态SQL 拼。
&&和&与|和||的区别?
&和&&的区别?
&和&&左边的式子为 true 的时候,右边的式子都会执行。
左边的式子为 false 的时候。&右边的式子仍然会执行。&&右边的式子将不再执行。
|和||的区别?
|和||左边的式子为 false 的时候,右边的式子都会执行。
左边的式子为 true 的时候。|右边的式子仍然会执行。||右边的式子将不再执行。
final finally finalize 区别?
final 修饰符,用来修饰变量,方法和类,分别表示属性不可变,方法不可被重写,类不可被继承,finally 是异常语句中处理语句,
表示总是执行;finalize 表示在垃圾回收机制时使该对象状态恢复的方法
int 和 Integer 的区别?
1、Integer 是 int 的包装类,int 则是 java 的一种基本数据类型
2、Integer 变量必须实例化后才能使用,而 int 变量不需要
3、Integer 实际是对象的引用,当 new 一个 Integer 时,实际上是生成一个指针指向此对象;而 int 则是直接存储数据值
4、Integer 的默认值是 null,int 的默认值是 0
equals 与==的区别?
:如果两边是基本数据类型,那么比较的是具体的值。如果==两边是引用数据类型,那么比较的是地址值。 (两个对象是否指向同一块内存)
equals:如果没有重写 equals 方法那么调用的是 Object 中的 equals 方法,比较的是地址值。如果重写了 euqlas 方法(比属性内容)那么就比较的是对象中属性的内容。
StringBuff 和 StringBuilder 及 String 区别?
String 类是不可变类,任何对 String 的改变都会引发新的 String 对象的生成;
StringBuffer 是可变类,任何对它所指代的字符串的改变都不会产生新的对象,线程安全的。StringBuilder 是可变类,线性不安全的,不支持并发操作,不适合多线程中使用,但其在单线程中的性能比StringBuffer 高。
Override 和 Overload 的含义去区别?
1.Override 特点
1、覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;
2、覆盖的方法的返回值必须和被覆盖的方法的返回一致;
3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
4、方法被定义为 final 不能被重写。
5、对于继承来说,如果某一方法在父类中是访问权限是 private,那么就不能在子类对其进行重写覆盖,如果定义的话,
也只是定义了一个新方法,而不会达到重写覆盖的效果。(通常存在于父类和子类之间。)
2.Overload 特点
1、在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序
当然,同一方法内的几个参数类型必须不一样,例如可以是 fun(int, float), 但是不能为 fun(int, int) 2、不能通过访问权限、返回类型、抛出的异常进行重载;
3、方法的异常类型和数目不会对重载造成影响;
4、重载事件通常发生在同一个类中,不同方法之间的现象。
5、存在于同一类中,但是只有虚方法和抽象方法才能被覆写。
抽象类和接口及普通类的区别?
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象, 接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。 同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果
7、抽象类里可以没有抽象方法
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可继承接口,并可多继承接口,但类只能单根继承。
堆和栈的区别?
一.堆栈空间分配区别:
1.栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2.堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收,分配方式倒是类似于链表。
二.堆栈缓存方式区别:
1.栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2.堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。 所以调用这些对象的速度要相对来得低一些。
三.堆栈数据结构区别:
堆(数据结构):堆可以被看成是一棵树,如:堆排序; 栈(数据结构):一种先进后出的数据结构。
Spring Bean 生命周期
实例化 bean 对象
设置对象属性
检查 Aware 相关接口并设置相关依赖
BeanPostPreocessor 前置处理
检查是否是 InitialliziingBean 以决定是否调用 afterPropertesSet 方法
检查是否配置有自定义的 init-method
BeanPostProcessor 后置处理
注册必要的 Destrunction 相关回调接口
使用中
是否实现 DisposableBean 接口
是否配置有自定义的 Destory 方法
JDK、JRE、JVM 的区别?
JDK ( Java 开发工具包)= JRE(Java 运行环境) + 开发工具集(例如 Javac 编译工具等)
JRE (Java 运行环境)= JVM (Java 虚拟机)+ Java SE 标准类库
值传递和引用传递的区别?
值传递:会创建副本,函数中无法改变原始对象
引用传递:不会创建副本,函数中可以改变原始对象
值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。 引用传递:也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数, 在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。
4 种访问控制符区别?
访问权限 类 包 子类 其他包
public
∨
∨
∨
∨
protect ∨ ∨ ∨ ×
default ∨ ∨ × ×
private ∨ × × ×
装箱和拆箱,类型转换
装箱:值类型转换为引用对象,一般是转换为 System.Object 类型或值类型实现的接口引用类型; 拆箱:引用类型转换为值类型,注意,这里的引用类型只能是被装箱的引用类型对象;
拆箱与装箱就是值类型与引用类型的转换
throw 和 throws 区别
throw 代表动作,表示抛出一个异常的动作;
throws 代表一种状态,代表方法可能有异常抛出;
throw 用在方法实现中,而 throws 用在方法声明中;
throw 只能用于抛出一种异常,而 throws 可以抛出多个异常。
PreparedStatement 比 Statement 区别?
第一:statement 执行的 SQL 语句必须是一个完整的 SQL,而对于 PreparedStatement 来说,可以使用“?” 作为
SQL 语句当中的占位符,然后使用 PreparedStatement 的 setXXX 方法来给占位符赋值,最后在执行;
第二:使用 Statement 时,如果 SQL 当中出现了“‘”或者“-”等符号时,需要使用转义字符来进行转义,而在
PreparedStatement 当中,如果占位符的值当中有这些符号,PreparedStatement 会自动的进行转义;
第三:PreparedStatement 会讲 SQL 语句进行预编译,每次执行的时候只需要将参数设置给相应的占位符就可以运行。而使用 Statement 时,SQL 语句时每次都要进行编译,所以 PreparedStatement 的效率相对较高。
doGet()方法和 doPost()方法区别?
get 方式 参数在地址栏中显示 通过?name=""&id=""这种形式传递的 不安全 只能传递 2kb 的能容
post 方式 底层是通过流的形式传递 不限制大小 上传的时候必须用 Post 方式
doGet:路径传参。效率高,安全性差doPOST:实体传参。效率第,安全性好
null 和 undefind 的区别?
undefined 是访问一个未初始化的变量时返回的值,而 null 是访问一个尚未存在的对象时所返回的值。
Error 和 Exception 的区别?
Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才 能修正。
一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。
对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序 恢复运行,
而不应该随意终止异常。
阻塞和非阻塞以及同步和异步的区别?
1.同步,就是我调用一个功能,该功能没有结束前,我死等结果。
2.异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)
3.阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
4.非阻塞,就是调用我(函数),我(函数)立即返回,通过 select 通知调用者同步 IO 和异步 IO 的区别就在于:数据拷贝的时候进程是否阻塞
阻塞 IO 和非阻塞 IO 的区别就在于:应用程序的调用是否立即返回
Ajax 异步和同步
同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。
异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
1.同步通信方式要求通信双方以相同的时钟频率进行,而且准确协调,通过共享一个单个时钟或定时脉冲源保证发送方和接收方的准确同步,效率较高;
2.异步通信方式不要求双方同步,收发方可采用各自的时钟源,双方遵循异步的通信协议,以字符为数据传输单位,发送方传送字符的时间间隔不确定,发送效率比同步传送效率低。
使用者可以同步或异步实现服务调用。从使用者的观点来看,这两种方式的不同之处在于:
同步——使用者通过单个线程调用服务;该线程发送请求,在服务运行时阻塞,并且等待响应。
异步——使用者通过两个线程调用服务;一个线程发送请求,而另一个单独的线程接收响应。
事务的 ACID 和事务的隔离性?
1)原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
2)一致性(Consistent):事务结束后系统状态是一致的;
3)隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
4)持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在 故障发生后重建数据。
脏读:事务 A 读到了事务 B 未提交的数据。
不可重复读:事务 A 第一次查询得到一行记录 row1,事务 B 提交修改后,事务 A 第二次查询得到 row1,但列内容发生了变化,侧重于次数,
侧重于 update
幻读:事务 A 第一次查询得到一行记录 row1,事务 B 提交修改后,事务 A 第二次查询得到两行记录 row1 和 row2, 侧重于内容,侧重于 insert
线程的 sleep 和 wait 区别?
sleep()不释放同步锁,wait()释放同步锁.
sleep 可以用时间指定来使他自动醒过来,如果时间不到你只能调用 interreput()来强行打断; wait()可以用 notify()直接唤起.
sleep 和 wait 的区别还有:
1。这两个方法来自不同的类分别是 Thread 和 Object
2。最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3。wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用
线程的状态(阶段)?
创建、就绪、运行、阻塞、终止。
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。该状态的线程位于“可运行线程池”中,
变得可运行,只等待获取 CPU 的使用权。即在就绪状态的进程除 CPU 之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态, 才有机会转到运行状态。
http 和 https 的区别?
1、https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用。
2、http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议。
3、http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
4、http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全。
常见的运行时异常?
NullPointerException - 空指针引用异常ClassCastException - 类型强制转换异常。IllegalArgumentException - 传递非法参数异常。ArithmeticException - 算术运算异常
ArrayStoreException - 向数组中存放与声明类型不兼容对象异常IndexOutOfBoundsException - 下标越界异常NegativeArraySizeException - 创建一个大小为负数的数组错误异常NumberFormatException - 数字格式异常
SecurityException - 安全异常
UnsupportedOperationException - 不支持的操作异常
BIO 和 NIO 区别?
互联网 强调的是信息/数据在网络之间的流通,
BIO:堵塞式 IO,相当于轮船运输
NIO:非堵塞式 IO:面向缓冲区(buffer),基于通道(chanel)的 io 操作,相当于火车运输,效率高文件->双向通道((缓冲区))->程序
http 常见的状态码
200 OK //客户端请求成功
302 found //重定向
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的 URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable//服务器当前不能处理客户端的请求,一段时间后可能恢复正常
Hashmap 为什么线程不安全,如何让它线程安全
HashMap 在 put 的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是 rehash,这个会重新将原数组的内容重新 hash 到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行 put 操作,如果 hash 值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在 get 时会出现死循环,所以 HashMap 是线程不安全的。
使用java.util.Hashtable 类,此类是线程安全的。
使用java.util.concurrent.ConcurrentHashMap,此类是线程安全的。
使用java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的 Map,并在此 Map
上进行操作。
怎么加快程序访问速度
硬件上:加大网络带宽、和服务器内存
代码的处理:静态页面、缓存、优化 sql、创建索引等方案
怎样进行程序性能调优
系统性能就是两个事:
Throughput ,吞吐量。也就是每秒钟可以处理的请求数,任务数。
Latency,系统延迟。系统在处理一个请求或一个任务时的延迟。那么 Latency 越好,能支持的 Throughput
就会越高。因为 Latency 短说明处理速度快,于是就可以处理更多的请求。
解决方案:
提高吞吐量:分布式集群,模块解藕,设计模式
系统延迟:异步通信
冒泡排序和自然排序及定制排序怎么实现的或者手写出来
冒泡排序
int[] arr={6,3,8,2,9,1};
System.out.println(“排序前数组为:”); for(int num:arr){
System.out.print(num+" ");
}
for(int i=0;i<arr.length-1;i++){//外层循环控制排序趟数
for(int j=0;j<arr.length-1-i;j++){//内层循环控制每一趟排序多少次if(arr[j]>arr[j+1]){
int temp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=temp;
}
}
}
System.out.println(); System.out.println(“排序后的数组为:”); for(int num:arr){
System.out.print(num+" ");
}
自然排序
1、定义一个类(文章中为 Employee)实现Comparable 接口
2、重写 Comparable 接口中的 compareTo()方法3、在 compareTo()中按指定属性进行排序
public class Employee implements Comparable{
public int compareTo(Object o) { if (o instanceof Employee) {
Employee e = (Employee) o;
return this.name.compareTo(e.name);//按 name 进行排序
}
return 0;
}
}
定制排序
1.创建一个 Compartor 实现类的对象,并传入到 TreeSet 的构造器中
2.重写 compare 方法
3.安照某个属性进行排序
4.向集合中添加元素
TreeSet set = new TreeSet(new Comparator() { @Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Student && o2 instanceof Student) { Student s1 = (Student)o1;
Student s2 = (Student)o2;
int s = s1.getAge() - s2.getAge(); if(s == 0) {
return s1.getName().compareTo(s2.getName());
}
return s;
}
return 0;
}
});
set.add(new Student(“aaa”, 18));
set.add(new Student(“bbb”, 8));
set.add(new Student(“fff”, 38));
set.add(new Student(“ccc”, 28)); System.out.println(set);
在使用定制排序或是自然排序时,在其用到的类中都要重写 hashCode()与 equals()方法
三种遍历方式?
第一种遍历方法和输出结果。
for(int i=1,i<list.size(),i++){ System.out.println(list.get(i));
}
第二种用 foreach 循环。加强型 for 循环。推荐方式。
for(String string:list){ System.out.println(string);
}
第三钟迭代器
List list=new ArrayList<>(); list.add(“abc”);
list.add(“ghi”);
for(Iterator it=list.iterator();it.hasNext()😉{ System.out.println(it.next());
}
讲讲线程的创建及实现线程几种方式之间的区别
Java 中线程的创建
继承 Thread 类:重写该类的 run()方法。
实现 Runnable 接口:并重写该接口的 run()方法,该 run()方法同样是线程执行体,创建 Runnable 实现类的实例,并以此实例作为 Thread 类的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
使用 Callable 和 Future 接口创建线程:具体是创建 Callable 接口的实现类,并实现 clall()方法。并使用FutureTask 类来包装Callable 实现类的对象,且以此 FutureTask 对象作为Thread 对象的target 来创建线程。
线程池:线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程 来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队, 但他们要等到其他线程完成后才启动。
继承 Thread 类,并重写里面的 run 方法
class A extends Thread{ public void run(){
for(int i=1;i<=100;i++){
System.out.println(" "+i);
}
}
}
Aa = new A(); a.start();
实现 Runnable 接口,并实现里面的 run 方法
class B implements Runnable{ public void run(){
for(int i=1;i<=100;i++){
System.out.println(" "+i);
}
}
}
Bb = new B();
Thread t = new Thread(b); t.start();
实现 Callable
class A implements Callable{ public String call() throws Exception{
//…
}
}
FutureTask ft = new FutureTask<>(new A()); new Thread(ft).start();
线程池
ExcutorService es = Executors.newFixedThreadPool(10); es.submit(new Runnable(){//任务});
es.submit(new Runnable(){//任务});
…
es.shutdown();
实现 Runnable 和实现 Callable 的区别?
实现 Callable 接口,任务可以有返回值,Runnable 没有。实现 Callable 接口,可以指定泛型,Runnable 没有。
实现 Callable 接口,可以在 call 方法中声明异常,Runnable 没有。
Runnable 和 Thread 二者的区别?
实现 Runnable 接口的方式,更适合处理有共享资源的情况。实现 Runnable 接口的方式,避免了单继承的局限性。
线程的创建
同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。线程分为五个阶段:创建、就绪、运行、阻塞、终止。
Java 线程的五种基本状态:
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的 start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说执行了 t.start()此线程立即就会执行;
运行状态(Running):当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用), 它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、 join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。
如何实现线程的同步?
为何要使用同步? java 允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
线程同步(5 种同步方式)
1.同步方法
2.同步代码块
3.使用特殊域变量(volatile)实现线程同步
4.使用重入锁实现线程同步
5.使用局部变量实现线程同步
讲一下 ThreadLocal 类。
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道 ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量;ThreadLocal 在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。但是要注意,虽然 ThreadLocal 能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal 要大;
ReentrantReadWriteLock:读写锁
为什么要使用读写锁?
多线程同时读一个资源类没有任何问题,但是有一个线程去写共享资源,就不应该让其他线程可以对该资源进行读或者写。
读和读的线程可以共存,读和写的线程不能共存,写和写的线程不能共存。未使用读写锁之前:
一个线程正在写入共享资源的时候,其他线程有写入和读取的共享资源操作,导致数据不一致。而使用 ReentrantLock 只能保证一个线程读,不能让其他线程同时读取,所以需要使用ReentrantReadWriteLock 解决原子性和独占性,解决并发性和数据的一致性。
独占锁:该锁一次只能被一个线程持有,ReentrantLock 和 Synchronized 都是独占锁。
共享锁:该锁可以被多个线程持有。
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
说明:ReentrantReadWriteLock 的读锁是共享锁:lock.readLock().lock();,写锁是独占锁:
lock.writeLock().lock();
CountDownLatch:倒计时器
作用:让一些线程阻塞,直到其他线程完成一系列的操作之后才被唤醒。有几个方法:
CountDownLatch(int count) //实例化一个倒计数器,count 指定计数个数
countDown() // 计数减一
await() //等待,当计数减到 0 时,所有线程并行执行
使用 CountDownLatch 的代码:
public class Demo {
public static void main(String[] args) throws Exception{ CountDownLatch countDownLatch = new CountDownLatch(5); for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 工作任务完成,离开公
司");
}
countDownLatch.countDown();
},String.valueOf(i)).start();
");
}
}
countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"\t 最后将公司门锁死,离开公司
控制台:
CyclicBarrier:循环栅栏
作用:就是会让所有线程都等待完成后才会继续下一步行动。举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。
使用 CyclicBarrier 的代码:
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier;
public class Demo {
public static void main(String[] args) throws Exception{
CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{System.out.println(“朋友全部到了,才开始吃饭”);});
for (int i = 1; i <= 6; i++) { final int tempInt = i;
new Thread(()->{ System.out.println(tempInt+“朋友到了”); try {
cyclicBarrier.await();
} catch (InterruptedException e) { e.printStackTrace();
} catch (BrokenBarrierException e) { e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
控制台:
Semaphore:信号灯
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。多个线程抢多个资源,只有当一个线程使用资源完成之后,其他线程才可以抢占使用共享资源。下面案例是有六台车抢三个停车位
使用 Semaphore 的代码:
public class Demo {
public static void main(String[] args) throws Exception{
//模拟三个停车位
Semaphore semaphore = new Semaphore(3);
//模拟六台车
for (int i = 1; i <= 6; i++) { new Thread(()->{
try { semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t 抢到车位");
Thread.sleep(2000); System.out.println(Thread.currentThread().getName()+"\t 停车 2 秒后离开
车位");
} catch (InterruptedException e) { e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
控制台:
Java 自定义类加载器与双亲委派模型
启动类加载器(Bootstrap)C++ 扩展类加载器(Extension)Java
应用程序类加载器(AppClassLoader)Java
双亲委派模型工作原理:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(ClassNotFoundException),子加载器才会尝试自己去加载。
讲讲 jvm 的组成与调优,内存模型,tomcat 调优
t
omcat 调优:
增加 JVM 堆内存大小
修复 JRE 内存泄漏
线程池设置
压缩
数据库性能调优
Tomcat 本地库
JVM 调优:
JVM 由类加载器子系统、运行时数据区、执行引擎以及本地方法接口组成,jvm 调优分以下三点: 堆大小设置
-Xms – 指定初始化时化的堆内存,默认为物理内存的 1/64
-Xmx – 指定最大的内存,默认为物理内存的 1/4
-XX:+PrintGCDetails:输出详细的 GC 处理日志
在重启你的 Tomcat 服务器之后,这些配置的更改才会有效。
回收器选择
辅助信息:JVM 提供了大量命令行参数,打印信息,供调试使用;
设计模式在项目中如何体现
1、模板方法模式 :定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,如 JdbcTemplate 2、代理 :spring 的 Proxy 模式在 aop 中有体现
3、观察者 :定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 spring 中 Observer 模式常用的地方是 listener 的实现。如ApplicationListener。
4、适配器(Adapter ) :MethodBeforeAdviceAdapter 类
5、策略模式 :使用了 java 的继承和多态 。简单理解:执行多个事情时,创建多个对象
案例 1:加减法计算器,定义一个计算类接口,加法和减法类都实现它,加的时候传入加法对象。
案例 2:导出 excel,pdf,word 时,分别创建不同的对象
6、单例模式 :解决一个全局使用的类频繁的创建与销毁
7、工厂模式 :分为三种:简单工厂,工厂方法,抽象工厂 。根据“需求”生产“产品”,解耦“需求”“工厂”和“产品”。
简单工厂:通过构造时传入的标识来生产产品,不同产品都在同一个工厂中生产,每新增加一种产品, 需要改工厂类,来判断,这种判断会随着产品的增加而增加,给扩展和维护带来麻烦 。简单工厂项目案例:根据传入的 不同(比如 1 对应支付流水,2 对应订单流水),生成不同类型的流水号。
工厂方法:(使一个类的使用延迟到子类) 。其中的工厂类根据传入的 A.class 类型,反射出实例。 产品接口,产品类 A,产品类 B,工厂类可以生成不同的产品类对象,如果要随着产品的增加而增加,工厂类不变,只需新增一个产品类 C 即可。 项目案例:邮件服务器,有三种协议,POP3,IMAP,HTTP,把这三种做完产品类,在定义个工厂方法
抽象工厂:一个工厂生产多个产品,它们是一个产品族,不同的产品族的产品派生于不同的抽象产品。
讲讲 linux 命令awk、cat、sort、cut、grep、uniq、wc、top、find、sed 等作用
awk:相较于 sed 常常作用于一整个行的处理,awk 则比较倾向于一行当中分成数个『字段』来处理。 因此,awk
相当的适合处理小型的数据数据处理
cat:主要用来查看文件内容,创建文件,文件合并,追加文件内容等功能。sort:功能:排序文本,默认对整列有效
cut:cut 命令可以从一个文本文件或者文本流中提取文本列
grep:是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来uniq:功能:去除重复行,只会统计相邻的
wc:功能: 统计文件行数、字节、字符数
top:用来监控 Linux 的系统状况,比如 cpu、内存的使用find:功能: 搜索文件目录层次结构
sed:sed 是一种在线编辑器,它一次处理一行内容
Spring 框架
Spring 管理 bean 的作用域,为什么不会被 GC 处理?
当通过 spring 容器创建一个 Bean 实例时,不仅可以完成 Bean 实例的实例化,还可以为 Bean 指定特定的作用域。
Spring 支持如下 5 种作用域:
singleton:单例模式,在整个 Spring IoC 容器中,使用 singleton 定义的 Bean 将只有一个实例
prototype:原型模式,每次通过容器的 getBean 方法获取 prototype 定义的 Bean 时,都将产生一个新的
Bean 实例
request:对于每次 HTTP 请求,使用 request 定义的 Bean 都将产生一个新实例,即每次 HTTP 请求将会产生不同的 Bean 实例。只有在 Web 应用中使用 Spring 时,该作用域才有效
session:对于每次 HTTP Session,使用 session 定义的 Bean 都将产生一个新实例。同样只有在 Web 应用中使用 Spring 时,该作用域才有效
globalsession:每个全局的 HTTP Session,使用 session 定义的 Bean 都将产生一个新实例。典型情况下, 仅在使用 portlet context 的时候有效。同样只有在 Web 应用中使用 Spring 时,该作用域才有效
说明:其中比较常用的是 singleton 和 prototype 两种作用域。
1.对于 singleton 作用域的 Bean:每次请求该 Bean 都将获得相同的实例。容器负责跟踪Bean 实例的状态, 负责维护 Bean 实例的生命周期行为;
2.对于 prototype 作用域的 Bean:程序每次请求该 id 的 Bean,Spring 都会新建一个 Bean 实例,然后返回给程序。在这种情况下,Spring 容器仅仅使用 new 关键字创建 Bean 实例,一旦创建成功,容器不在跟踪实例,也不会维护 Bean 实例的状态。
Java 在创建 Java 实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。
prototype 作用域 Bean 的创建、销毁代价比较大。而 singleton 作用域的 Bean 实例一旦创建成功, 可以重复使用。因此,除非必要,否则尽量避免将 Bean 被设置成 prototype 作用域。如果不指定Bean 的作用域,Spring 默认使用 singleton 作用域。spring 底层使用 map 来存放 bean 实体,而 map 的键值是强引用,所以不会被 GC,可以重复使用。
Spring 对 bean 是如何解析
所谓 bean 的解析就是将我们的 xml 文件中的 bean 解析出来,上面的入口看到使用的是ClassPathXmlApplicationContext 来获取 ApplicationContext,所以,分析的入口也就从ClassPathXmlApplicationContext 类中相应的构造函数开始。
getBean 方法开始创建过程,getBean()有一系列的重载方法,最终都是调用 doGetBean() 方法
getSingleton 方法尝试从缓存中获取单例bean
当前bean 是单例且缓存不存在则通过getSingleton(String beanName, ObjectFactory<?> singletonFactory) 方法创建单例对象
主要包含下下面三个主要方法:
createBeanInstance 方法用于创建Bean 实例
populateBean 方法主要给Bean 进行属性注入
initializeBean 方法主要处理各种回调
Spring 核心特性?
Spring 的核心特性就是 IOC 和 AOP,IOC(Inversion of Control),即“控制反转”;AOP
(Aspect-OrientedProgramming),即“面向切面编程”。
IOC:IOC,另外一种说法叫 DI(Dependency Injection),即依赖注入。它并不是一种技术实现,而是一种设计思想。在任何一个有实际开发意义的程序项目中,我们会使用很多类来描述它们特有 的功能,并且通过类与类之间的相互协作来完成特定的业务逻辑。这个时候,每个类都需要负责管理与自己有交互的类的引用和依赖,代码将会变的异常难以维护和极度的高耦合。而 IOC 的出现正是用来解决这个问题,我们通过 IOC 将这些相互依赖对象的创建、协调工作交给 Spring 容器去处理, 每个对象只需要关注其自身的业务逻辑关系就可以了。在这样的角度上来看,获得依赖的对象的方式,进行了反转,变成了由 spring 容器控制对象如何获取外部资源(包括其他对象和文件资料等等)。
AOP:面向切面编程,往往被定义为促使软件系统实现关注点的分离的技术。系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
讲讲 Spring 的 IOC(DI)和AOP 动态代理
传统的程序开发(不用 IOC):举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有
长得漂亮身材又好的 mm,然后打听她们的兴趣爱好、qq 号、电话号、ip 号、iq 号………,想办法认识她们,投其
所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。
IOC:
IoC 是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友, 比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个美眉,我们只需要去和她谈恋爱、结婚就行了。
总结控制反转:所有的类都会在 spring 容器中登记,告诉 spring 你是个什么东西,你需要什么东西, 然后 spring 会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring 来控制,也就是说控制对象生存周期的不再是引用它的对象,而是 spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被 spring 控制,所以这叫控制反转。
理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”
●谁依赖于谁:当然是应用程序依赖于 IoC 容器;
●为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源;
●谁注入谁:很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象;
●注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
AOP 的各种实现
AOP 就是面向切面编程,我们可以从以下几个层面来实现 AOP
在编译期修改源代码
在运行期字节码加载前修改字节码
在运行期字节码加载后动态创建代理类的字节码
AOP 各种实现机制的比较
以下是各种实现机制的比较:
AOP 里的公民
Joinpoint:拦截点,如某个业务方法
Pointcut:Joinpoint 的表达式,表示拦截哪些方法。一个 Pointcut 对应多个 Joinpoint
Advice:要切入的逻辑
Before Advice:在方法前切入
After Advice:在方法后切入,抛出异常则不会切入
After Returning Advice:在方法返回后切入,抛出异常则不会切入
After Throwing Advice:在方法抛出异常时切入
Around Advice:在方法执行前后切入,可以中断或忽略原有流程的执行
Spring 框架 AOP 执行原理简单说下?
什么是动态代理:AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
主要有两种方式:JDK 动态代理和 CGLIB 动态代理。
JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。
CGLIB(Code GenerationLibrary),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。
AOP 在事务管理方面,Spring 使用 AOP 来完成声明式的事务管理有 annotation 和 xml 两种形式。开发中,方便代码编写,很多时候都是在 spring 配置文件中配置事务管理器并开启事务控制注解。在业务类或业务类方法中添加@Transactional 实现事务控制。
Spring 分布式事务如何处理的?
第一种方案:可靠消息最终一致性,需要业务系统结合 MQ 消息中间件实现,在实现过程中需要保证消息的成功发送及成功消费。即需要通过业务系统控制 MQ 的消息状态
第二种方案:TCC 补偿性,分为三个阶段 TRYING-CONFIRMING-CANCELING。每个阶段做不同的处理。
TRYING 阶段主要是对业务系统进行检测及资源预留
CONFIRMING 阶段是做业务提交,通过 TRYING 阶段执行成功后,再执行该阶段。默认如果 TRYING 阶段执行成功,CONFIRMING 就一定能成功。
CANCELING 阶段是回对业务做回滚,在 TRYING 阶段中,如果存在分支事务 TRYING 失败,则需要调用
CANCELING 将已预留的资源进行释放。
SpringBoot 框架
Springboot 的特点?
Springboot 用来简化 spring 应用的初始搭建以及开发过程 使用特定的方式来进行配置
(properties 或 yml 文件)
提供了各种启动器,独立运行,可以创建独立的 spring 引用程序 main 方法运行
Springboot 嵌入的 Tomcat 无需部署 war 文件
自动配置,无代码生成和 XML 配置,简化 maven 配置
Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
bootstrap 配置文件有以下几个应用场景。
1.使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
2.一些固定的不能被覆盖的属性;
3.一些加密/解密的场景
Spring Boot 的配置文件有哪几种格式?它们有什么区别?
.properties 和 .yml,它们的区别主要是书写格式不同。
在 properties 文件中是以”.”进行分割的, 在 yml 中是用”:”进行分割;
yml 的数据格式和 json 的格式很像,都是 K-V 格式,并且通过”:”进行赋值;
在 yml 中缩进一定不能使用 TAB,否则会报很奇怪的错误;(缩进只能用空格!!!!)
每个 k 的冒号后面一定都要加一个空格;
使用 spring cloud 的 maven 进行构造的项目,在把 properties 换成 yml 后,一定要进行 mvn clean insatll
properties 的优先级高于 yml。即如果两个文件中都配置了端口号,只有 properties 中的端口号有效,而
yml 文件中端口配置无效
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 比如关闭数据源自动配置:@SpringBootApplication(exclude =
{ DataSourceAutoConfiguration.class })。
这就是 spring boot 的核心功能,自动配置。就是根据当前引入的 JAR 包进行自动配置,
比如: 引入了 jackson 的 jar 包,那么就会自动配置 json 转换,所以可以使用@ResponseBody 进行转换
引入了 spring boot 的 web 模块,就会自动配置 web.xml 等与 web 项目相关的内容,所以这些配置都不需要我们自己配了。
@ComponentScan:Spring 组件扫描。
开启 Spring Boot 特性有哪几种方式?
要成为一个 spring boot 项目,首先就必须在 pom.xml 中继承 spring-boot-starter-parent,同时指定其版本。
导 入 spring-boot-dependencies 项 目 依 赖 : spring-boot-starter-parent 的 源 码 中 又 继 承 了
spring-boot-dependencies。
Spring Boot 需要独立的容器运行吗?
引入依赖:spring-boot-starter-web:代表 web 模块,在这个模块中含了许多 JAR 包,有 spring 相关的 jar, 内置 tomcat 服务器,jackson 等,这些 web 项目中常用的的功能都会自动引入。所以可以不需要独立容器运行。
运行 Spring Boot 有哪几种方式?
打包用命令或者放到容器中运行
用 Maven 命令(spring-boot:run)/ Gradle 插件运行
直接执行 main 方法运行
Spring Boot 自动配置原理是什么?
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。
如何理解 Spring Boot 中的 Starters?
Starters 可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。
Starters 包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖.
例如:
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-test test如何在 Spring Boot 启动的时候运行一些特定的代码?
可以实现接口 ApplicationRunner 或者 CommandLineRunner,这两个接口实现方式一样,它们都只提供了一个 run 方法.
实现 CommandLineRunner 接口的代码:
import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component;
@Component
public class MyCommandLineRunner implements CommandLineRunner{
@Override
public void run(String… var1) throws Exception{
System.out.println(“This will be execute when the project was started!”);
}
}
实现 ApplicationRunner 接口的代码:
import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component;
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments var1) throws Exception{
System.out.println(“MyApplicationRunner class will be execute when the project was started!”);
}
}
Spring Boot 有哪几种读取配置的方式?
Spring Boot 可以通过 @PropertySource,@Value,@Environment, @ConfigurationProperties 来绑定变量
读取 application 文件
@Value 注解读取方式
@ConfigurationProperties 注解读取方式
读取指定文件
@PropertySource+@Value 注解读取方式
@PropertySource+@ConfigurationProperties 注解读取方式
@Environment 读取方式
Spring boot 和 spring cloud 的区别与联系
Spring boot 是 Spring 的一套快速配置脚手架,可以基于 spring boot 快速开发单个微服务,Spring Boot, 看名字就知道是 Spring 的引导,就是用于启动 Spring 的,使得 Spring 的学习和使用变得快速无痛。不仅适合替换原有的工程结构,更适合微服务开发。
Spring Cloud 基于 Spring Boot,为微服务体系开发中的架构问题,提供了一整套的解决方案——服务注册与发现,服务消费,服务保护与熔断,网关,分布式调用追踪,分布式配置管理等。
说明:
Spring Cloud 是一个基于 Spring Boot 实现的云应用开发工具,它是关注全局的服务治理框架;
Spring boot 专注于快速,方便集成的单个个体,使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置;
MyBatis 框架
mybatis 是一个基于 java 的持久层框架,它内部封装了 jdbc,不需要花费精力去处理加载驱动、创建连接等的过程,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。
MyBatis 支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的POJO 映射成数据库中的记录。
提供了很多第三方插件(分页插件 / 逆向工程);
能够与 Spring 很好的集成;
MyBatis 相当灵活,SQL 写在 XML 里,从程序代码中彻底分离,解除 sql 与程序代码的耦合,便于统一管理,支持编写动态 SQL 语句。
提供映射标签,支持对象与数据库的 ORM 字段关系映射。
SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持,而 JDBC 提供了可扩展性,所以只要这个数据库有针对 Java 的 jar 包就可以就可以与 MyBatis 兼容),开发人员不需要考虑数据库的差异性。
对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择。
SpringMVC 框架
SpringMVC 框架的工作流程和常用注解
1、用户向服务器发送请求,请求被 SpringMVC 的前端控制器 DispatcherServlet 截获。
2、DispatcherServlet 对请求的 URL(统一资源定位符)进行解析,得到 URI(请求资源标识符),然后根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象,包括 Handler 对象以及 Handler 对象对应的拦截器,这些对象都会被封装到一个 HandlerExecutionChain 对象当中返回。
url 和 uri 的区别?URI 包括 URL 和 URN 两个类别,个人的身份证号就是 URN,个人的家庭地址就是 URL, URN 可以唯一标识一个人,而 URL 可以告诉邮递员怎么把货送到你手里。
3、DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter。HandlerAdapter 的设计符合面向对象中的单一职责原则,代码结构清晰,便于维护,最为重要的是,代码的可复制性高。HandlerAdapter 会被用于处理多种 Handler,调用 Handler 实际处理请求的方法。
4、提取请求中的模型数据,开始执行 Handler(Controller)。在填充 Handler 的入参过程中,根据配置,spring 将帮助做一些额外的工作
消息转换:将请求的消息,如 json、xml 等数据转换成一个对象,将对象转换为指定的响应信息。
数据转换:对请求消息进行数据转换,如 String 转换成 Integer、Double 等。
数据格式化:对请求的消息进行数据格式化,如将字符串转换为格式化数字或格式化日期等。
数据验证:验证数据的有效性如长度、格式等,验证结果存储到 BindingResult 或 Error 中。
5、Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象,ModelAndView 对象中应该包含视图名或视图模型。
6、根据返回的 ModelAndView 对象,选择一个合适的 ViewResolver(视图解析器)返回给DispatcherServlet。
7、ViewResolver 结合 Model 和 View 来渲染视图。
8、将视图渲染结果返回给客户端。
以上 8 个步骤,DispatcherServlet、HandlerMapping、HandlerAdapter 和 ViewResolver 等对象协同工作,完成 SpringMVC 请求—>响应的整个工作流程,这些对象完成的工作对于开发者来说都是不可见的,开发者并不需要关心这些对象是如何工作的,开发者,只需要在 Handler(Controller)当中完成对请求的业务处理。
组件型注解:
@Component 在类定义之前添加@Component 注解,他会被 spring 容器识别,并转为 bean。@Repository 对 Dao 实现类进行注解 (特殊的@Component)
@Service 用于对业务逻辑层进行注解, (特殊的@Component) @Controller 用于控制层注解 , (特殊的@Component)
请求和参数型注解:
@RequestMapping:用于处理请求地址映射,可以作用于类和方法上。@RequestParam:用于获取传入参数的值
@PathViriable:用于定义路径参数值
@ResponseBody:作用于方法上,可以将整个返回结果以某种格式返回,如 json 或 xml 格式。
@CookieValue:用于获取请求的 Cookie 值
Dubbo+Zookeeper 服务框架
Dubbo:
简单的介绍一下Dubbo?(Dubbo 是什么)
dubbo 就是个服务调用的东东。为什么怎么说呢?因为 Dubbo 是由阿里开源的一个 RPC 分布式框架。那么 RPC 是什么呢?就是不同的应用部署到不同的服务器上,应用之间想要调用没有办法直接调用,因为不在一个内存空间,需要通过网络通讯来调用,或者传达调用的数据。而且 RPC 会将远程调用的细节隐藏起来,让调用远程服务像调用本地服务一样简单。
Dubbo 有哪些组件?
主要有五个角色/核心组件,分为是 Container(容器)、Provider(服务的提供方)、Registry(注册中心)、Consumer(服务的消费方)、Monitor(监控中心)。
Container 容器:主要负责启动、加载、运行服务提供者;
Registry 注册中心:注册中心只负责地址的注册和查找
Monitor 监控中心:监控中心负责统计各服务调用次数、调用时间
Dubbo 支持什么协议?
Dubbo 协议:缺省协议、采用了单一长连接和 NIO 异步通讯、使用线程池并发处理请求,能减少握手和加大并发效率
Zookeeper:
Zookeeper 的实现原理?(工作原理)
Zookeeper 会维护一个类似于标准的文件系统的具有层次关系的数据结构。这个文件系统中每个子目录项都被称为 znode 节点,这个 znode 节点也可以有子节点,每个节点都可以存储数据,客户端也可以对这些 node 节点进行 getChildren,getData,exists 方法,同时也可以在 znode tree 路径上设置 watch(类似于监听),当 watch 路径上发生节点 create、delete、update 的时候,会通知到client。client 可以得到通知后,再获取数据,执行业务逻辑操作。Zookeeper 的作用主要是用来维护和监控存储的 node 节点上这些数据的状态变化,通过监控这些数据状态的变化,从而达到基于数据的集群管理。
为什么要用 zookeeper 作为 dubbo 的注册中心?能选择其他的吗?
Zookeeper 的数据模型是由一系列的 Znode 数据节点组成,和文件系统类似。zookeeper 的数据全部存储在内存中,性能高;zookeeper 也支持集群,实现了高可用;同时基于 zookeeper 的特性, 也支持事件监听(服务的暴露方发生变化,可以进行推送),所以 zookeeper 适合作为 dubbo 的注册中心使用。redis、Simple 也可以作为 dubbo 的注册中心来使用。项目中主要用 zookeeper 做了什么?(作用)作为注册中心用;主要是在服务器上搭建 zookeeper,其次在 spring 管理的 dubbo 的配置文件中配置(暴露方和消费方都需要配置)
对分布式,dubbo,zookeeper 说的不太清楚
分布式是从项目业务角度考虑划分项目整个架构。可以将项目基于功能模块划分再分别部署。Dubbo 是实现分布式项目部署框架。在 zookeeper 是 dubbo 分布式框架的注册中心,管理服务的注册和调用。
Zookeeper 待机的情况下,dubbo 如何工作?
Zookeeper 的作用:
zookeeper 用来注册服务和进行负载均衡,哪一个服务由哪一个机器来提供必需让调用者知道, 简单来说就是 ip 地址和服务名称的对应关系。当然也可以通过硬编码的方式把这种对应关系在调用方业务代码中实现,但是提供服务的机器挂掉,调用者无法知晓,如果不更改代码会继续请求挂掉的机器提供服务。 而 zookeeper 通过心跳机制可以检测挂掉的机器并将挂掉机器的 ip 和服务对应关系从列表中删除。至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添 加机器来提高运算能力。通过添加新的机器向 zookeeper 注册服务,服务的提供者多了能服务的客户就多了。
Dubbo 的作用:
是管理中间层的工具,在业务层到数据仓库间有非常多服务的接入和服务提供者需要调度,dubbo 提供一个框架解决这个问题。注意这里的 dubbo 只是一个框架,至于你架子上放什么是完全取决
于你的,就像一个汽车骨架,你需要配你的轮子引擎。这个框架中要完成调度必须要有一个分布式的注册中心,储存所有服务的元数据,你可以用 zk,也可以用别的,只是大家都用 zk。
zookeeper 和 dubbo 的关系:
Dubbo 的将注册中心进行抽象,使得它可以外接不同的存储媒介给注册中心提供服务,有ZooKeeper,Memcached,Redis 等。引入了 ZooKeeper 作为存储媒介,也就把 ZooKeeper 的特性引进来。
首先是负载均衡,单注册中心的承载能力是有限的,在流量达到一定程度的时候就需要分流,负载均衡就是为了分流而存在的,一个 ZooKeeper 集群配合相应的 Web 应用就可以很容易达到负载均衡;
资源同步,单单有负载均衡还不够,节点之间的数据和资源需要同步,ZooKeeper 集群就天然具备有这样的功能:命名服务。
命名服务:将树状结构用于维护全局的服务地址列表,服务提供者在启动的时候,向 ZK 上的指定节点
/dubbo/${serviceName}/providers 目录下写入自己的 URL 地址,这个操作就完成了服务的发布。 其他特性还有 Mast 选举,分布式锁等。
Dubbo,Zookeeper,Nginx 说不清楚
Dubbo 是 Alibaba 开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo 采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方
(Provider)和服务消费方(Consumer)两个角色。
ZooKeeper 是 Apache 的一个顶级项目,为分布式应用提供高效、高可用的分布式协调服务,提供了诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知和分布式锁等分布式基础服务。由于 ZooKeeper 便捷的使用方式、卓越的性能和良好的稳定性,被广泛地应用于诸如 Hadoop、HBase、Kafka 和 Dubbo 等大型分布式系统中。
Nginx 是一款自由的、开源的、高性能的 HTTP 服务器和反向代理服务器;同时也是一个 IMAP、POP3、SMTP 代理服务器;Nginx 可以作为一个 HTTP 服务器进行网站的发布处理,另外 Nginx 可以作为反向代理进行负载均衡的实现。
如何保证 dubbo 高可用?
zookeeper 宕机与 dubbo 直连:在实际生产中,假如 zookeeper 注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用的本地缓存进行通讯,这只是 dubbo 健壮性的一种。
dubbo 的健壮性表现:
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
注册中心的作用在于保存服务提供者的位置信息,我们可以完全可以绕过注册中心——采用 dubbo 直连,即在服务消费方配置服务提供方的位置信息。点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。
xml 配置方式
<dubbo:reference id=“userService” interface=“com.zang.gmall.service.UserService” url=“dubbo://localhost:20880” />
注解上直接添加
@Reference(url = “127.0.0.1:20880”) UserService userService;
集群下 dubbo 负载均衡配置
在集群负载均衡时,Dubbo 提供了 4 种均衡策略,如:Random LoadBalance(随机均衡算法)、RoundRobin LoadBalance(权重轮循均衡算法)、LeastAction LoadBalance(最少活跃调用数均衡算法)、ConsistentHash LoadBalance(一致性 Hash 均衡算法)。缺省时为 Random 随机调用。
@Reference(loadbalance = “roundrobin”) UserService userService;
服务方方法级别配置(基于 xml 配置的权重轮询均衡算法)
<dubbo:service interface=“com.zang.gmall.service.UserService”
<dubbo:method name=“getUserAddressList” loadbalance=“roundrobin”></dubbo:method>
</dubbo:service>
权重设置
当不设置负载均衡策略,即采用默认的 Random LoadBalance(随机均衡算法)时,默认每个服务的权重相同,我们可以通过设置权重来分配访问的随机性。权重默认为 100,可以在暴露服务的同时设置。
服务降级
当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略(不调用服务即返回为空 or 调用失败返回为空)。
向注册中心写入动态配置覆盖规则:
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); Registry registry = registryFactory.getRegistry(URL.valueOf(“zookeeper://10.20.153.10:2181”)); registry.register(URL.valueOf(“override://0.0.0.0/com.foo.BarService?category=configu rators&dynamic=false&application=foo&mock=force:return+null”));
其中:
mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
通过操作管理控制台也可以方便的进行服务降级集群容错
在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。集群容错模式主要有以下几种:
Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。消费方服务级注解添加(不能到方法级)
Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数。
Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
集群模式配置方法:在服务提供方和消费方配置集群模式
整合 hystrix
Hystrix 旨在通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包, 以及监控和配置等功能。spring boot 官方提供了对 hystrix 的集成,直接在 pom.xml 里加入依赖:
org.springframework.cloud spring-cloud-starter-netflix-hystrix 1.4.4.RELEASE然后在 Application 类上增加@EnableHystrix 来启用 hystrix starter:
配置服务提供方:在 Dubbo 的Provider 上增加@HystrixCommand 配置,这样子调用就会经过 Hystrix
代理。
配置服务消费方:对于 Consumer 端,则可以增加一层 method 调用,并在 method 上配置
@HystrixCommand 。当调用出错时,会走到 fallbackMethod = “reliable” 的调用里。
@HystrixCommand 注解设置的 reliable 调用方法的里的参数要和 method 的参数保持一致。
Zookeeper 怎样进行服务治理。
接受提供者的接口信息和提供者 ip 地址进行存储,然后管理消费者和提供者之间调用关系!
如果 zookeeper 服务挂了怎么办,服务调用可以进行吗?
注册中心对等集群,任意一台宕掉后,会自动切换到另一台
注册中心全部宕掉,服务提供者和消费者仍可以通过本地缓存通讯服务提供者无状态,任一台宕机后,不影响使用
服务提供者全部宕机,服务消费者会无法使用,并无限次重连等待服务者恢复
Dubbo 有 3 次重试,假如新消息被重复消费怎么处理
去掉超时重试机制
服务端增加幂等校验,服务器加入校验机制,如果这个消息已被消费就不再重复消费
Dubbo 的通信原理?
Dubbo 底层使用 hessain2 进行二进制序列化进行远程调用
Dubbo 底层使用 netty 框架进行异步通信。NIO
高并发,高可用,负载均衡
高并发解决方案:
1)数据层
数据库集群和库表散列
分表分库
开启索引
开启缓存
表设计优化
Sql 语句优化
缓存服务器(提高查询效率,减轻数据库压力)
搜索服务器(提高查询效率,减轻数据库压力)
图片服务器分离
2)项目层
采用面向服务分布式架构(分担服务器压力,提高并发能力)
采用并发访问较高的详情系统采用静态页面,HTML 静态化 freemaker
使用页面缓存
用 ActiveMQ 使得业务进一步进行解耦,提高业务处理能力
使用分布式文件系统存储海量文件
3)应用层
Nginx 服务器来做负载均衡
Lvs 做二层负载
镜像
处理高并发常见的方法有哪些?
1、HTML 静态化 freemaker
其实大家都知道,效率最高、消耗最小的就是纯静态化的 html 页面,所以我们尽可能使我们的网站上的页面采用静态页面来实现,这个最简单的方法其实也是最有效的方法。但是对于大量内容并且频繁更新的网站,我们无法全部手动去挨个实现,于是出现了我们常见的信息发布系统 CMS, 像我们常访问的各个门户站点的新闻频道,甚至他们的其他频道,都是通过信息发布系统来管理和实现的,信息发布系统可以实现最简单的信息录入自动生成静态页面,还能具备频道管理、权限管理、自动抓取等功能,对于一个大型网站来说,拥有一套高效、可管理的 CMS 是必不可少的。除了门户和信息发布类型的网站,对于交互性要求很高的社区类型网站来说,尽可能的静态化也是提高性能的必要手段,将社区内的帖子、文章进行实时的静态化,有更新的时候再重新静态化也是大量使用的策略,像 Mop 的大杂烩就是使用了这样的策略,网易社区等也是如此。同时,html 静态化也是某些缓存策略使用的手段,对于系统中频繁使用数据库查询但是内容更新很小的应用,可以考虑使用 html 静态化来实现,比如论坛中论坛的公用设置信息,这些信息目前的主流论坛都可以进行后台管理并且存储再数据库中,这些信息其实大量被前台程序调用,但是更新频率很小,可以考虑将这部分内容进行后台更新的时候进行静态化,这样避免了大量的数据库访问请求。
2、图片服务器分离
大家知道,对于 Web 服务器来说,不管 是 Apache、IIS 还是其他容器,图片是最消耗资源的,于是我们有必要将图片与页面进行分离,这是基本上大型网站都会采用的策略,他们都有独立的图片服务器,甚至很多台图片服务器。这样的架构可以降低提供页面访问请求的服务器系统压力,并且可以保证系统不会因为图片问题而崩溃,在应用服务器和图片服务器 上,可以进行不同的配置
优化,比如 apache 在配置 ContentType 的时候可以尽量少支持,尽可能少的 LoadModule,保证更高的系统消耗和执行效率。
3、数据库集群和库表散列
大型网站都有复杂的应用,这些应用必须使用数据库,那么在面对大量访问的时候,数据库的瓶颈很快就能显现出来,这时一台数据库将很快无法满足应用,于是我们需要使用数据库集群或者库表散列。在数据库集群方面,很多数据库都有自己的解决方案,Oracle、Sybase 等都有很好的方案, 常用的 MySQL 提供的 Master/Slave 也是类似的方案,您使用了什么样的 DB,就参考相应的解决方案来实施即可。上面提到的数据库集群由于在架构、成本、扩张性方面都会受到所采用 DB 类型的限制,于是我们需要从应用程序的角度来考虑改善系统架构,库表散列是常用并且最有效的解决方案。我们在应用程序中安装业务和应用或者功能模块将数据库进行分离,不同的模块对应不同的数据库或者表,再按照一定的策略对某个页面或者功能进行更小的数据库散列,比如用户表,按照用户 ID 进行表散列,这样就能够低成本的提升系统的性能并且有很好的扩展性。sohu 的论坛就是采用了这样的架 构,将论坛的用户、设置、帖子等信息进行数据库分离,然后对帖子、用户按照板块和 ID 进行散列数据库和表,最终可以在配置文件中进行简单的配置便能让系统随时增加一台低成本的数据库进来补充系统性能。
4、缓存
缓存一词搞技术的都接触过,很多地方用到缓存。网站架构和网站开发中的缓存也是非常重要。这里先讲述最基本的两种缓存。高级和分布式的缓存在后面讲述。架构方面的缓存,对 Apache 比较熟悉的人都能知道 Apache 提供了自己的缓存模块,也可以使用外加的 Squid 模块进行缓存,这两种方式均可以有效的提高 Apache 的访问响应能力。 网站程序开发方面的缓存,Linux 上提供的Memory Cache 是常用的缓存接口,可以在 web 开发中使用,比如用 Java 开发的时候就可以调用MemoryCache 对一些数据进行缓存和通讯共享,一些大型社区使用了这样的架构。另外,在使用web 语言开发的时候,各种语言基本都有自己的缓存模块和方法,PHP 有 Pear 的 Cache 模块,Java 就更多了。
5、镜像
镜像是大型网站常采用的提高性能和数据安全性的方式,镜像的技术可以解决不同网络接入商和地域带来的用户访问速度差异,比如 ChinaNet 和 EduNet 之间的差异就促使了很多网站在教育网内搭建镜像站点,数据进行定时更新或者实时更新。在镜像的细节技术方面,这里不阐述太深,有很多专业的现成的解决架构和产品可选。也有廉价的通过软件实现的思路,比如 Linux 上的 rsync 等工具。
6、负载均衡
负载均衡将是大型网站解决高负荷访问和大量并发请求采用的终极解决办法。 负载均衡技术发展了多年,有很多专业的服务提供商和产品可以选择。
高并发下电商网站的秒杀活动
秒杀实现思路
秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存, 当用户点击抢购时减少 redis 中的库存数量,当库存数为 0 时或活动期结束时,同步到数据库。产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成功后再写入数据库。所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
秒杀商品通常有两种限制:库存限制、时间限制。需求:
商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
运营商审核秒杀申请
秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。
商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为 0 或不在活动期范围内时无法秒杀。
秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
当用户秒杀下单 5 分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
秒杀架构设计理念
限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。
异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘 IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。
前端方案:浏览器端
页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过 CDN 来抗峰值。
禁止重复提交:用户提交之后按钮置灰,禁止重复提交
用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取 IP 限流
后端方案:服务端控制器层(网关层)
限制 uid(UserID)访问频率:我们上面拦截了浏览器访问的请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同一个访问 uid,限制访问频率。
服务层:上面只拦截了一部分访问请求,当秒杀的用户量很大时,即使每个用户只有一个请求,到服务层的请求数量还是很大。比如我们有 100W 用户同时抢 100 台手机,服务层并发请求压力至少为 100W。
采用消息队列缓存请求:既然服务层知道库存只有 100 台手机,那完全没有必要把 100W 个请求都传递到数据库啊,那么可以先把这些请求都写到消息队列缓存一下,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。
利用缓存应对读请求:对类似于 12306 等购票业务,是典型的读多写少业务,大部分请求是查询请求, 所以可以利用缓存分担数据库压力。
利用缓存应对写请求:缓存也是可以应对写请求的,比如我们就可以把数据库中的库存数据转移到 Redis 缓存中,所有减库存操作都在 Redis 中进行,然后再通过后台进程把 Redis 中的用户秒杀请求同步到数据库中。
数据库层:数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能 力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。
使用 Redis 缓存实现秒杀:
Redis 是一个分布式缓存系统,支持多种数据结构,我们可以利用 Redis 轻松实现一个强大的秒杀系统。我们可以采用 Redis 最简单的 key-value 数据结构,用一个原子类型的变量值(AtomicInteger) 作为 key,把用户 id 作为 value,库存数量便是原子变量的最大值。对于每个用户的秒杀,我们使用 RPUSH key value 插入秒杀请求, 当插入的秒杀请求数达到上限时,停止所有后续插入。然后我们可以在台启动多个工作线程,使用LPOP key 读取秒杀成功者的用户 id,然后再操作数据库做最终的下订单减库存操作。当然,上面 Redis 也可以替换成消息中间件如 ActiveMQ、RabbitMQ 等, 也可以将缓存和消息中间件 组合起来,缓存系统负责接收记录用户请求,消息中间件负责将缓存中的请求同步到数据库。
当商品库存数量不足时,如何保证不会超卖?当库存数量不足时,必须保证库存不能被减为负数, 如果不加以控制,库存被减为小于等于 0 的数,那么这就叫做超卖。
那么如何防止超卖的现象发生呢?
场景一: 如果系统并发要求不是很高:那么此时库存就可以存储在数据库中,数据库中加锁控制库存的超卖现象。
场景二:系统的并发量很大:如果系统并发量很大,那么就不能再使用数据库来进行减库存操作了,因为数据库加锁操作本身是以损失数据库的性能来进行控制数据库数据的一致性的。但是当并发量很大的时候,将会导致数据库排队,发生阻塞。因此必须使用一个高效的 nosql 数据库服务器来进行减库存。此时可以使用 redis 服务器来存储库存,redis 是一个内存版的数据库,查询效率相当的高,可以使用 watch来监控减库存的操作,一旦发现库存被减为 0,立马停止售卖操作。
做交易或是金融系统安全性需要从哪些方面考虑?没有用什么第三方可以框架?
ip 黑白名单,访问日志明细记录,防止重复提交,访问频率控制,分布式锁,数据前后端校验,自动对账任务处理,互联网金融项目一般情况下,不建议自动重试,最好结合对账系统,人工进行处理,写好人工处理的接口就好。其他就是控制好数据的一致性了,这个最重要,其次还要保证接口的幂等性,不要重复处理订单。这些是最基本的安全控制了。像这类网站用户的输入数据一般都不会太多,一般敏感词过滤,广告之类的可以忽略,如果有的话还要控制这些。安全框架选 shiro 了, 在系统中分配好角色就好了,控制好用户的资源访问。其他的用 springmvc 就够了。
讲一下高并发下的电商项目每台服务器的集群数量:
项目中一共 15 台项目服务,那么为了每一台高可用一主一备,但首页项目高并发设为四台服务器,
则一共 32 台项目服务器,再加 redis 集群用了 3 台,为了每一台高可用一主一备一共 6 台,fastdfs 一个 trackerServer 一个 storageServer 搭建集群一共 6 台,solr 集群 7 台服务器,nginx 为了高可用一主一备一共 2 台,mysql 数据库集群 3 台!activemq 消息中间件高可用 2 台;
共计:58 台服务器!
介绍一下高并发的电商项目:
我最近的一个项目是一个电商项目,我主要负责的是后台管理和商品详情的模块,然后也会参与到购物车和订单模块。这个项目是以 SpringBoot 和 mybatis 为框架,应为 springBoot 相对于 SSM 来说
配置方面,还有操作方面简单很多。然后是采用 zookeeper 加 dubbo 分布式架构和 RPC 远程调用, 因为他 Dubbo 实现了软负载均衡,其特点是成本低,但也会有缺点,就是负载能力会受服务器本身影响,然后为了解决软负载均衡的缺点,我们使用了 Nginx 进行负载均衡的轮询算法,但 Nginx 主要在我们项目还是实现反向代理,就是可以防止外网对内网服务器的恶性攻击、缓存以减少服务器的压力和访问安全控制。基础模块就有后台管理,商品详情,订单,支付,物流情况,库存服务。然后 SpringBoot 整合 Thymeleaf 模块技术开发项目商品详情模块,easyUI 开发后台管理项目。至于我负责的两个模块呢,就是后台管理和商品详情,其中呢使用了 sku 和 spu 的数据表结构进行增删改查,spu 就好比我们要买一台 Mate20,但是我们没有选择它是什么配置,那么关于详细的配置就是 sku 了,就是我要买一台 Mate20,黑色,内存是 128G 的。商品详情和商品列表模块使用 Nginx 实现集群,使用 Redis 解决应用服务器的 cpu 和内存压力,减少 io 的读操作,减轻 io 的压力,使用分布式锁防止 Redis 缓存击穿。其中 Redis 的作用我是觉得挺大的,因为他可以防止过多的用户去直接访问我们的数据库,当然,Redis 也会在高并发的时候宕机,在使用 Redis 做缓存的时候, 我们使用 Redis 持久化功能,防止 Redis 宕机后数据丢失,如果 Redis 宕机了,用户就会大量的去 访问数据库,从而我们数据库也会崩溃吧。这个时候我们就用了一个分布式锁,用户需要获得一个锁才能访问我们的数据库,当然啦,并不只是只有一个锁,而是锁的数量是有限的,当一位用户查完了数据之后,锁就会释放,给下位用户,这也就是服务降降级。没有获得锁的用户,页面就一直刷新直到自己拿到锁为止。redis 提供了持久化功能——RDB 和 AOF。通俗的讲就是将内存中的数据写入硬盘中。在实际应用中,用户如果要查询商品的话呢,首先回到 Redis 缓存里面找的,如果找不到,就会到数据库里面找,然后缓存到 Redis 中,那么下一次或者下一个用户需要查找这个数据就不必到数据库中查找了!然后我还参与了购物车和订单模块的开发。购物车模块里面呢,我先和您讲下他的业务逻辑吧。就像你逛网页淘宝一样,在没有登录的时候,把东西放入购物车,它是不会和你的账号里的商品合并的,这个时候,商品就会以 cookie 的形式,放到你的浏览器里面。这个时候如果你想购买这些商品的时候,你就要登录,这个时候就会使用到单点登录这一个技术。用户跳转到订单页面的时候,我们会用拦截器去进行判断用户是否已经登录。我们是用 cookie 中是否有 token,如果没有 token 的话就跳转到登录页面,然后生成 token,至于 token 的生成呢,我们是用本地的 IP,用户的 id,保存在 map 中,还有一个常量,这个我们通常会以项目名称来命名的。至于为什么要token 呢,其实是因为 cookie 是不太安全的,它很容易被伪造,所以我们就需要 token, 然后有了 token 之后,我们用 JWT 这个盐值生成最后的 token。并把它保存到 cookie 当中。下一次支付的时候我们也还会用到这个 token,用一个加密算法再去运算验证一下就可以了!然后就是合并购物车了。这个的话我所知道的就是将客户端的 cookie 复印一份到缓存中进行修改然后送回客户端进行覆盖,再接着就是数据库的修改了。那这个如果登陆了的就直接从数据库中取得数据跳到订单系统了。然后订单模块里面,简单来说就是从购物车中勾选的商品迁移到订单里面。但是呢订单模块其实是会联系到另外两个模块的,就是库存和支付。如果你点击了提交订单,商品就会在购物车里移除。然后我们提交订单避免他反复的提交同一个订单,就会通过交易码防止订单重复提交。我们会吧 tradecode 放在缓存里面,以用户 id 为 key 商品的交易为 value 在 Redis 里面保存这个交易码。到最后选好收货地址,留言之后,提交订单了,就会用自己的 tradecode 和在 Redis 里面通过用户的 id 去获取 tradecode 进行对比,如果能跳转到支付页面,那么缓存中的交易码就会删除掉。到最后就是支付功能,这一步的话我是不太清楚其中的技术点了,只知道这个模块调用了支付宝的接口和用了消息队列,异步通知。
高并发下的一些优化: 数据层面的优化:
从数据库层面做优化,比如:索引,缓存,集群,读写分离,主从复制,分表,分库。
从数据库设计层面的优化:比如减少表关联,加入冗余字段
从缓存方面优化:比如 redis 实现数据缓存,减轻数据库压力
从搜索上进行优化:比如查找索引库
项目层面的优化:
采用面向服务的分布式架构:分担服务器压力 ,提高项目并发量。 比如 dubbox+zookeeper 分布式架构
采用分布式文件系统实现海量文件存储:如采用 fastdfs 实现海量图片存储,提高文件的访问速度。
采用 mq 使用服务进一步解藕:同步索引库,同步静态资源,短信发送
服务器层面的优化:
集群思想的使用:tomcat,zookeeper,redis,mysql 等
Tomcat 异步通信的使用,tomcat 连接池配置
高可用解决方案:
通常企业级应用系统(特别是政府部门和大企业的应用系统)一般会采用安规的软硬件设备,如 IOE
(IBM 的小型机、Oracle 数据、EMC 存储设备)系列。而一般互联网公司更多地采用 PC 级服务器
(x86),开源的数据库(MySQL)和操作系统(Linux)组建廉价且高容错(硬件故障是常态)的应用集群。
目的:保证服务器硬件故障服务依然可用,数据依然保存并能够被访问。主要的手段?
数据和服务的①冗余备份以及②失效转移:
对于服务而言,一旦某个服务器宕机,就将服务切换到其他可用的服务器上;
对于数据而言,如果某个磁盘损坏,就从备份的磁盘(事先就做好了数据的同步复制)读取数据。
高可用的服务
①分级管理:核心应用和服务具有更高的优先级,比如用户及时付款比能否评价商品更重要;
②超时设置:设置服务调用的超时时间,一旦超时,通信框架抛出异常,应用程序则根据服务调度策略选择重试 or 请求转移到其他服务器上
③异步调用:通过消息队列等异步方式完成,避免一个服务失败导致整个应用请求失败的情况。不是所有服务都可以异步调用,对于获取用户信息这类调用,采用异步方式会延长响应时间,得不偿失。对于那些必须确认服务调用成功后才能继续进行下一步的操作的应用也不适合异步调用。
④服务降级:网站访问高峰期间,为了保证核心应用的正常运行,需要对服务降级。降级有两种手段:
拒绝服务,拒绝较低优先级的应用的调用,减少服务调用并发数,确保核心应用的正常运行;
关闭功能,关闭部分不重要的服务,或者服务内部关闭部分不重要的功能,以节约系统开销,为核心应用服务让出资源;
⑤幂等性设计:保证服务重复调用和调用一次产生的结果相同; 高可用的数据
保证数据高可用的主要手段有两种:一是数据备份,二是失效转移机制;
①数据备份:又分为冷备份和热备份,冷备份是定期复制,不能保证数据可用性。热备份又分为异步热备和同步热备,异步热备是指多份数据副本的写入操作异步完成,而同步方式则是指多份数据副本的写入操作同时完成。关系数据库的热备机制就是通常所说的主从同步机制,实践中通常
使用读写分离的方法来访问 Master 和 Slave 数据库,也就是说写操作只访问 Master 库,读操作均访问 Slave 库。
②失效转移:若数据服务器集群中任何一台服务器宕机,那么应用程序针对这台服务器的所有读写操作都
要重新路由到其他服务器,保证数据访问不会失败。
网站运行监控
”不允许没有监控的系统上线“
(1)监控数据采集
①用户行为日志收集:服务器端的日志收集和客户端的日志收集;目前许多网站逐步开发基于实时计算框架 Storm 的日志统计与分析工具;
②服务器性能监控:收集服务器性能指标,如系统 Load、内存占用、磁盘 IO 等,及时判断,防患于未然;
③运行数据报告:采集并报告,汇总后统一显示,应用程序需要在代码中处理运行数据采集的逻辑;
(2)监控管理
①系统报警:配置报警阀值和值守人员联系方式,系统发生报警时,即使工程师在千里之外,也可以被及时通知;
②失效转移:监控系统在发现故障时,主动通知应用进行失效转移;
③自动优雅降级:为了应付网站访问高峰,主动关闭部分功能,释放部分系统资源,保证核心应用服务的正
常运行;—>网站柔性架构的理想状态其他手段:
主从切换:很好理解,当其中一台机器的服务宕机后,对于服务调用者来说,能够迅速的切换到其他可用服务,从服务升级为主服务,这种切换速度应当控制在秒级别(几秒钟)。当宕机的服务恢复之后, 自动变为从服务,主从服务角色切换。主从切换一定是要付出代价的,所以当主服务恢复之后,也就不再替换现有的主服务。
负载均衡:当服务的请求量比较高的时候,一台服务不能满足需求,这时候需要多台机器提供同样的服务,将所有请求分发到不同机器上。高可用架构中应该具有丰富的负载均衡策略和易调节负载的方式。甚至可以自动化智能调节,例如由于机器性能的原因,响应时间可能不一样,这时候可以向性能差的机器少一点分发量,保证各个机器响应时间的均衡。
易横向扩展:当用户量越来越多,已有服务不能承载更多的用户的时候,便需要对服务进行扩展,扩展的方式最好是不触动原有服务,对于服务的调用者是透明的。
负载均衡:
什么是负载均衡?
当一台服务器的性能达到极限时,我们可以使用服务器集群来提高网站的整体性能。那么,在服务器集群中,需要有一台服务器充当调度者的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况将请求分配给某一台后端服务器去处理。
简单介绍 5 种负载均衡:
(1)HTTP 重定向负载均衡。
这种负载均衡方案的优点是比较简单;
缺点是浏览器需要每次请求两次服务器才能拿完成一次访问,性能较差。
(2)DNS 域名解析负载均衡
优点是将负载均衡工作交给 DNS,省略掉了网络管理的麻烦; 缺点就是 DNS 可能缓存 A 记录,不受网站控制。
(3)反向代理负载均衡。优点是部署简单;
缺点是反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。
(4)IP 负载均衡。
优点:IP 负载均衡在内核进程完成数据分发,较反向代理均衡有更好的处理性能。缺点:负载均衡的网卡带宽成为系统的瓶颈。
(5)数据链路层负载均衡。
避免负载均衡服务器网卡带宽成为瓶颈,是目前大型网站所使用的最广的一种负载均衡手段。
详情介绍 5 种负载均衡:
(1)HTTP 重定向负载均衡。
原理:当用户向服务器发起请求时,请求首先被集群调度者截获;调度者根据某种分配策略,选择一台服务器,并将选中的服务器的 IP 地址封装在 HTTP 响应消息头部的 Location 字段中,并将响应消息的状态码设为 302,最后将这个响应消息返回给浏览器。当浏览器收到响应消息后,解析Location 字段,并向该 URL 发起请求,然后指定的服务器处理该用户的请求,最后将结果返回给用户。
优点:比较简单
缺点:调度服务器只在客户端第一次向网站发起请求的时候起作用。当调度服务器向浏览器返回响应信息后,客户端此后的操作都基于新的 URL 进行的(也就是后端服务器),此后浏览器就不会与调度服务器产生关系,浏览器需要每次请求两次服务器才能拿完成一次访问,性能较差。而且调度服务器在调度时,无法知道当前用户将会对服务器造成多大的压力,只不过是把请求次数平均分配给每台服务器罢了,浏览器会与后端服务器直接交互。
(2)DNS 域名解析负载均衡
原理:为了方便用户记忆,我们使用域名来访问网站。通过域名访问网站之前,首先需要将域名解析成 IP 地址,这个工作是由 DNS 域名服务器完成的。我们提交的请求不会直接发送给想要访问的网站,而是首先发给域名服务器,它会帮我们把域名解析成 IP 地址并返回给我们。我们收到 IP 之后才会向该 IP 发起请求。一个域名指向多个 IP 地址,每次进行域名解析时,DNS 只要选一个 IP 返回给用户,就能够实现服务器集群的负载均衡。
优点:配置简单,将负载均衡工作交给 DNS,省略掉了网络管理的麻烦;
缺点:集群调度权交给了 DNS 服务器,从而我们没办法随心所欲地控制调度者,没办法定制调度策略,没办法了解每台服务器的负载情况,只不过把所有请求平均分配给后端服务器罢了。某一台后端服务器发生故障时,即使我们立即将该服务器从域名解析中去除,但由于 DNS 服务器会有缓存,该 IP 仍然会在 DNS 中保留一段时间,那么就会导致一部分用户无法正常访问网站。不过动态DNS 能够让我们通过程序动态修改 DNS 服务器中的域名解析。从而当我们的监控程序发现某台服务器挂了之后,能立即通知 DNS 将其删掉。
调度策略:一般 DNS 提供商会提供一些调度策略供我们选择,如随机分配、轮询、根据请求者的地域分配离他最近的服务器。
随机分配策略:当调度服务器收到用户请求后,可以随机决定使用哪台后端服务器,然后将该服务器的 IP 封装在 HTTP 响应消息
的 Location 属性中,返回给浏览器即可。
轮询策略(RR) :调度服务器需要维护一个值,用于记录上次分配的后端服务器的 IP。那么当新的请求到来时,调度者将请求依
次分配给下一台服务器。
(3)反向代理负载均衡。
原理:反向代理服务器是一个位于实际服务器之前的服务器,所有向我们网站发来的请求都首先要经过反向代理服务器,服务器根据用户的请求要么直接将结果返回给用户,要么将请求交给后端服务器处理,再返回给用户。反向代理服务器就可以充当服务器集群的调度者,它可以根据当前后端服务器的负载情况,将请求转发给一台合适的服务器,并将处理结果返回给用户。
优点:
部署简单
隐藏后端服务器:与 HTTP 重定向相比,反向代理能够隐藏后端服务器,所有浏览器都不会与后端服务器直接交互,从而能够确保调度者的控制权,提升集群的整体性能。
故障转移 :与 DNS 负载均衡相比,反向代理能够更快速地移除故障结点。当监控程序发现某一后端服务器出现故障时,能够及时通知反向代理服务器,并立即将其删除。
合理分配任务 :HTTP 重定向和 DNS 负载均衡都无法实现真正意义上的负载均衡,也就是调度服务器无法根据后端服务器的实际负载情况分配任务。但反向代理服务器支持手动设定每台后端服务器的权重。我们可以根据服务器的配置设置不同的权重,权重的不同会导致被调度者选中的概率的不同。
缺点:
调度者压力过大 :由于所有的请求都先由反向代理服务器处理,那么当请求量超过调度服务器的最大负载时,调度服务器的吞吐率降低会直接降低集群的整体性能。
制约扩展 :当后端服务器也无法满足巨大的吞吐量时,就需要增加后端服务器的数量,可没办法无限量地增加,因为会受到调度服务器的最大吞吐量的制约。
粘滞会话:反向代理服务器会引起一个问题。若某台后端服务器处理了用户的请求,并保存了该用户的session 或存储了缓存,那么当该用户再次发送请求时,无法保证该请求仍然由保存了其 Session 或缓存的服务器处理,若由其他服务器处理,先前的 Session 或缓存就找不到了。
解决办法:
可以修改反向代理服务器的任务分配策略,以用户 IP 作为标识较为合适。相同的用户 IP 会交由同一台后端服务器处理,从而就避免了粘滞会话的问题。
可以在 Cookie 中标注请求的服务器 ID,当再次提交请求时,调度者将该请求分配给 Cookie 中标注的服务器处理即可。
(4)IP 负载均衡。
通过 NAT 实现负载均衡:响应报文一般比较大,每一次都需要 NAT 转换的话,大流量的时候,会导致调度器成为一个瓶颈。
通过直接路由实现负载均衡
VS/TUN 实现虚拟服务器
优点:IP 负载均衡在内核进程完成数据分发,较反向代理均衡有更好的处理性能。
缺点:负载均衡的网卡带宽成为系统的瓶颈,场景:某个服务器跑的应用非高峰期间都能达到
500M 以
上,晚高峰一般能够超过 1G,主流服务器的网卡都是千兆的,超过 1G 的流量明显会导致丢包的问题,此时又不
能停止业务对网卡进行更换。
(5)数据链路层负载均衡。
对于 linux 系统来说,数据链路层的解决方案就是实现多个网卡绑定联合提供服务,把多张网卡捆绑做成一个逻辑网卡。避免负载均衡服务器网卡带宽成为瓶颈,是目前大型网站所使用的最广的一种负载均衡手段。linux bonding 的七种模式,mod=0~6:平衡抡循环策略,主-备份策略,平衡策略, 广播策略,动态链接聚合,适配器传输负载均衡,适配器适应性负载均衡
分布式,集群,微服务,SOA
微服务和 SOA 有什么区别?
微服务剔除 SOA 中复杂的 ESB 企业服务总线,所有的业务智能逻辑在服务内部处理,使用 Http(Rest API) 进行轻量化通讯
SOA 强调按水平架构划分为:前、后端、数据库、测试等,微服务强调按垂直架构划分,按业务能力划分,每个服务完成一种特定的功能,服务即产品
SOA 将组件以 library 的方式和应用部署在同一个进程中运行,微服务则是各个服务独立运行。
传统应用倾向于使用统一的技术平台来解决所有问题,微服务可以针对不同业务特征选择不同技术平台, 去中心统一化,发挥各种技术平台的特长。
SOA 架构强调的是异构系统之间的通信和解耦合;(一种粗粒度、松耦合的服务架构)
微服务架构强调的是系统按业务边界做细粒度的拆分和部署
分布式架构:
把系统按照模块拆分成多个子系统,多个子系统分布在不同的网络计算机上相互协作完成业务流程,系统之间需要进行通信。
优点:
1.把模块拆分,使用接口通信,降低模块之间的耦合度。
2.把项目拆分成若干个子项目,不同的团队负责不同的子项目。
3.增加功能时只需要再增加一个子项目,调用其他系统的接口就可以。
4.可以灵活的进行分布式部署。
缺点:
1.系统之间交互需要使用远程通信,接口开发增加工作量。
2.各个模块有一些通用的业务逻辑无法共用。
基于 soa 的架构
SOA:面向服务的架构。可以把工程拆分成服务层、表现层两个工程。服务层中包含业务逻辑,只需要对外提供服务即可。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。
分布式架构和 soa 架构有什么区别?
SOA,主要还是从服务的角度,可以将工程拆分成服务层、表现层两个工程。
分布式,主要还是从部署的角度,将应用按照访问压力进行归类,主要目标是充分利用服务器的资源,避免资源分配不均
集群:
一个集群系统是一群松散结合的服务器组,形成一个虚拟的服务器,为客户端用户提供统一的服务。对于这个客户端来说,通常在访问集群系统时不会意识到它的服务是由具体的哪一台服务器提
供。集群的目的,是为实现负载均衡、容错和灾难恢复。以达到系统可用性和可伸缩性的要求。集群系统一般应具高可用性、可伸缩性、负载均衡、故障恢复和可维护性等特殊性能。一般同一个工程会部署到多台服务器上。
常见的 tomcat 集群,Redis 集群,Zookeeper 集群,数据库集群分布式与集群的区别:
分布式是指将不同的业务分布在不同的地方。 而集群指的是将几台服务器集中在一起,实现同一业务。一句话:分布式是并联工作的,集群是串联工作的。
分布式中的每一个节点,都可以做集群。 而集群并不一定就是分布式的。
举例:就比如新浪网,访问的人多了,他可以做一个群集,前面放一个响应服务器,后面几台服务器完成同一业务,如果有业务访问的时候,响应服务器看哪台服务器的负载不是很重,就将给哪一台去完成。而分布式,从窄意上理解,也跟集群差不多,但是它的组织比较松散,不像集群,有一个组织性,一台服务器垮了,其它的服务器可以顶上来。分布式的每一个节点,都完成不同的业务, 一个节点垮了,哪这个业务就不可访问了。
分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提升效率。
举例:如果一个任务由 10 个子任务组成,每个子任务单独执行需 1 小时,则在一台服务器上执行
该任务需 10 小时。采用分布式方案,提供 10 台服务器,每台服务器只负责处理一个子任务,不考虑子任务间的依赖关系,执行完这个任务只需一个小时。(这种工作模式的一个典型代表就是Hadoop 的 Map/Reduce 分布式计算模型)而采用集群方案,同样提供 10 台服务器,每台服务器都能独立处理这个任务。假设有 10 个任务同时到达,10 个服务器将同时工作,1 小时后,10 个任务同时完成,这样,整身来看,还是 1 小时内完成一个任务!
讲讲分布式事务的异步通信问题解决方案
问题介绍:一个消息发送过去了,不管结果如何发送端都不会原地等待接收端。直到接收端再推送回来回执消息,
发送端才直到结果。但是也有可能发送端消息发送后,石沉大海,杳无音信。这时候就需要一种机制能够对这种不
确定性进行补充。
解决方案:
你给有很多笔友,平时写信一去一回,但是有时候会遇到迟迟没有回信的情况。那么针对这种偶尔出现的情况,你
可以选择两种策略。一种方案是你发信的时候用定个闹钟,设定 1 天以后去问一下对方收没收到信。另一种方案就
是每天夜里定个时间查看一下所有发过信但是已经一天没收到回复的信。然后挨个打个电话问一下。
第一种策略就是实现起来就是延迟队列,第二种策略就是定时轮询扫描。
二者的区别是延迟队列更加精准,但是如果周期太长,任务留在延迟队列中时间的就会非常长, 会把队列变得冗长。比如用户几天后待办提醒,生日提醒。那么如果遇到这种长周期的事件,而且
并不需要精确到分秒级的事件,可以利用定时扫描来实现,尤其是比较消耗性能的大范围扫描, 可以安排到夜间执行。
分布式事务
一、两阶段提交(2PC)
两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性。在实现方面,在 .NET 中,可以借助 TransactionScop 提供的 API 来编程实现分布式系统中的两阶段提交,比如 WCF 中就有实现这部分功能。不过在多服务器之间,需要依赖于 DTC 来完成事务一致性
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能 100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案
二、补偿事务(TCC)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
Try 阶段主要是对业务系统做检测及资源预留
Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm
阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
优点: 跟 2PC 比起来,实现以及流程相对简单了一些,但数据的一致性比 2PC 也要差一些
缺点: 缺点还是比较明显的,在 2,3 步中都有可能失败。TCC 属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用 TCC 不太好定义及处理
三、本地消息表(异步确保)(使用最多的技术方案)
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交, 也就是说他们要在一个数据库里面。然后消息会经过 MQ 发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。四、MQ 事务消息
有一些第三方的 MQ 是支持事务消息的,比如 RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的 MQ 都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段 Prepared 消息,会拿到消息的地址。
第二阶段执行本地事务
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了 RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了 Prepared 消息,它会向消息发送者确认,所以生产方需要实现一个 check 接口,RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
分布式架构 session 共享问题,如何在集群里边实现共享,分布式 session 跨域问题
用了 CAS,所有应用项目中如果需要登录时在 web.xml 中配置过滤器做请求转发到 cas 端工作原理是在 cas 登录后会给浏览器发送一个票据(ticket),浏览器 cookie 中会缓存这个 ticket,在登录其他项目时会拿着浏览器的 ticket 转发到 cas,到 cas 后根据票据判断是否登录。单点登录是相互信任的系统模块登录一个模块后,其他模块不需要重复登录即认证通过。采用 CAS 单点登录框架, 首先 CAS 有两大部分:客户端和服务端。服务端就是一个 web 工程部署在 tomcat 中。在服务端完成用户认证操作。每次访问系统模块时,需要去 CAS 完成获取 ticket。当验证通过后,访问继续操作。对于 CAS 服务端来说,我们访问的应用模块就是 CAS 客户端。
什么是跨域?
当异步请求时,访问的请求地址的协议、ip 地址、端口号任意一个与当前站点不同时,就会涉及跨域访问。
什么时候涉及跨域问题?
当涉及前端异步请求的时候才涉及跨域。解决方案:
jQuery 提供了 jsonp 实现
W3C 标准提供了 CORS(跨域资源共享)解决方案。
用了 CAS,所有应用项目中如果需要登录时在 web.xml 中配置过滤器做请求转发到 cas 端工作原理是在 cas 登录后会给浏览器发送一个票据(ticket),浏览器 cookie 中会缓存这个 ticket,在登录其他项目时会拿着浏览器的 ticket 转发到 cas,到 cas 后根据票据判断是否登录。
Redis 缓存
什么是 Redis?
redis 是内存中的数据结构存储系统,一个 key-value 类型的非关系型数据库,可持久化的数据库, 相对于关系型数据库(数据主要存在硬盘中),性能高,因此我们一般用 redis 来做缓存使用;并
且 redis 支持丰富的数据类型,比较容易解决各种问题,因此 redis 可以用来作为注册中心, 数据库、缓存和消息中间件。Redis 的 Value 支持 5 种数据类型,string、hash、list、set、zset(sorted set);
String 类型:一个 key 对应一个 value
Hash 类型:它的 key 是 string 类型,value 又是一个 map(key-value),适合存储对象。
List 类型:按照插入顺序的字符串链表(双向链表),主要命令是 LPUSH 和 RPUSH,能够支持反向查找和遍历
Set 类型:用哈希表类型的字符串序列,没有顺序,集合成员是唯一的,没有重复数据,底层主要是由一个 value 永远为 null 的 hashmap 来实现的。
zset 类型:和 set 类型基本一致,不过它会给每个元素关联一个 double 类型的分数(score),这样就可以为成员排序,并且插入是有序的。
你还用过其他的缓存吗?这些缓存有什么区别?都在什么场景下去用?
对于缓存了解过 redis 和 memcache
Memcache 和 redis 的区别:
数据支持的类型:redis 不仅仅支持简单的 k/v 类型的数据,同时还支持 list、set、zset、hash 等数据结构的存储;memcache 只支持简单的 k/v 类型的数据,key 和 value 都是 string 类型
可靠性:memcache 不支持数据持久化,断电或重启后数据消失,但其稳定性是有保证的;redis 支持数据持久化和数据恢复,允许单点故障,但是同时也会付出性能的代价
性能上:对于存储大数据,memcache 的性能要高于 redis
应用场景:
Memcache:适合多读少写,大数据量的情况(一些官网的文章信息等)
Redis:适用于对读写效率要求高、数据处理业务复杂、安全性要求较高的系统
案例:分布式系统,存在 session 之间的共享问题,因此在做单点登录的时候,我们利用 redis 来模拟了 session 的共享,来存储用户的信息,实现不同系统的 session 共享;
对 redis 的持久化了解不?
redis 的持久化方式有两种:
RDB(半持久化方式):按照配置不定期的通过异步的方式、快照的形式直接把内存中的数据持久化到磁盘的一个 dump.rdb 文件(二进制的临时文件)中,redis 默认的持久化方式,它在配置文件
(redis.conf)中。
优点:只包含一个文件,将一个单独的文件转移到其他存储媒介上,对于文件备份、灾难恢复而言,比较实用。
缺点:系统一旦在持久化策略之前出现宕机现象,此前没有来得及持久化的数据将会产生丢失
RDB 持久化配置:
Redis 会将数据集的快照 dump 到 dump.rdb 文件中。此外,我们也可以通过配置文件来修改 Redis
服务器 dump 快照的频率,在打开 6379.conf 文件之后,我们搜索 save,可以看到下面的配置信息:
save 900 1 #在 900 秒(15 分钟)之后,如果至少有 1 个 key 发生变化,则 dump 内存快照。
save 300 10 #在 300 秒(5 分钟)之后,如果至少有 10 个 key 发生变化,则 dump 内存快照。
save 60 10000 #在 60 秒(1 分钟)之后,如果至少有 10000 个 key 发生变化,则 dump 内存快照。
AOF(全持久化的方式):把每一次数据变化都通过 write()函数将你所执行的命令追加到一个appendonly.aof 文件里面,Redis 默认是不支持这种全持久化方式的,需要在配置文件(redis.conf) 中将 appendonly no 改成 appendonly yes
优点:数据安全性高,对日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机问题,也不会破坏日志文件中已经存在的内容;
缺点:对于数量相同的数据集来说,aof 文件通常要比 rdb 文件大,因此 rdb 在恢复大数据集时的速度大于 AOF;
AOF 持久化配置:
在 Redis 的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会都调用 fsync 刷新到 aof 文件,非常慢,但是安全;
appendfsync everysec #每秒钟都调用 fsync 刷新到 aof 文件中,很快,但是可能丢失一秒内的数据,推荐使用,兼顾了速度和安全;
appendfsync no #不会自动同步到磁盘上,需要依靠 OS(操作系统)进行刷新,效率快,但是安全性就比较差;
二种持久化方式区别:
AOF 在运行效率上往往慢于 RDB,每秒同步策略的效率是比较高的,同步禁用策略的效率和 RDB 一样高效;
如果缓存数据安全性要求比较高的话,用 aof 这种持久化方式(比如项目中的购物车);
如果对于大数据集要求效率高的话,就可以使用默认的。而且这两种持久化方式可以同时使用。
做过 redis 的集群吗?你们做集群的时候搭建了几台,都是怎么搭建的?
Redis 的数据是存放在内存中的,不适合存储大数据,大数据存储一般公司常用 hadoop 中的 Hbase 或者MogoDB。redis 主要用来处理高并发的,用我们的项目来说,电商项目如果并发大的话,一台单独的 redis 是不能足够支持我们的并发,这就需要我们扩展多台设备协同合作,即用到集群。
Redis 搭建集群的方式有多种,例如:客户端分片、Twemproxy、Codis 等,但是 redis3.0 之后就支持redis-cluster 集群,这种方式采用的是无中心结构,每个节点保存数据和整个集群的状态,每个节点都和其他所有节点连接。如果使用的话就用 redis-cluster 集群。集群这块是公司运维搭建的,具体怎么搭建不是太了解。
我们项目中 redis 集群主要搭建了 6 台,3 主(为了保证 redis 的投票机制)3 从(高可用),每个主服务器都有一个从服务器,作为备份机。所有的节点都通过 PING-PONG 机制彼此互相连接;客户端与 redis 集群连接,只需要连接集群中的任何一个节点即可;Redis-cluster 中内置了 16384 个哈希槽,Redis-cluster 把所有的物理节点映射到【0-16383】slot 上,负责维护。
Redis 有事务吗?
Redis 是有事务的,redis 中的事务是一组命令的集合,这组命令要么都执行,要不都不执行,保证一个事务中的命令依次执行而不被其他命令插入。redis 的事务是不支持回滚操作的。redis 事务的实现,需要用到 MULTI(事务的开始)和 EXEC(事务的结束)命令 ;
缓存穿透
缓存查询一般都是通过 key 去查找 value,如果不存在对应的 value,就要去数据库中查找。如果这个 key 对应的 value 在数据库中也不存在,并且对该 key 并发请求很大,就会对数据库产生很大的压力,这就叫缓存穿透。
解决方案:
对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃。
将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉, 从而避免了对底层存储系统的查询压力。
如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存, 但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
当缓存服务器重启或者大量缓存集中在一段时间内失效,发生大量的缓存穿透,这样在失效的瞬间对数据库的访问压力就比较大,所有的查询都落在数据库上,造成了缓存雪崩。 这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
解决方案:
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
可以通过缓存 reload 机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀
做二级缓存,或者双缓存策略。A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期。
redis 的安全机制(你们公司 redis 的安全这方面怎么考虑的?)
漏洞介绍:redis 默认情况下,会绑定在 bind 0.0.0.0:6379,这样就会将 redis 的服务暴露到公网上, 如果在没有开启认证的情况下,可以导致任意用户在访问目标服务器的情况下,未授权就可访问redis 以及读取redis 的数据,攻击者就可以在未授权访问 redis 的情况下可以利用redis 的相关方法, 成功在 redis 服务器上写入公钥,进而可以直接使用私钥进行直接登录目标主机;
解决方案:
1.禁止一些高危命令。修改 redis.conf 文件,用来禁止远程修改 DB 文件地址,比如 rename-command FLUSHALL “” 、rename-command CONFIG"" 、rename-command EVAL “”等;
2.以低权限运行 redis 服务。为 redis 服务创建单独的用户和根目录,并且配置禁止登录;
3.为 redis 添加密码验证。修改 redis.conf 文件,添加 requirepass mypassword;
4.禁止外网访问 redis。修改 redis.conf 文件,添加或修改 bind 127.0.0.1,使得 redis 服务只在当前主机使用;
5.做 log 监控,及时发现攻击;
redis 的哨兵机制(redis2.6 以后出现的):
监控:监控主数据库和从数据库是否正常运行;
提醒:当被监控的某个 redis 出现问题的时候,哨兵可以通过 API 向管理员或者其他应用程序发送通知;
自动故障迁移:主数据库出现故障时,可以自动将从数据库转化为主数据库,实现自动切换;
具体的配置步骤参考的网上的文档。要注意的是,如果 master 主服务器设置了密码,记得在哨兵的配置文件(sentinel.conf)里面配置访问密码
redis 中对于生存时间的应用
Redis 中可以使用 expire 命令设置一个键的生存时间,到时间后 redis 会自动删除; 应用场景:
1.设置限制的优惠活动的信息;
2.一些及时需要更新的数据,积分排行榜;
3.手机验证码的时间;
4.限制网站访客访问频率;
能讲下redis 的具体使用场景吗?使用redis 存储长期不改变的数据完全可以使用也看静态化,那么你们当时是为什么会使用 redis?
redis 在项目中应用:
主要应用在门户网站首页广告信息的缓存。因为门户网站访问量较大,将广告缓存到 redis 中,可以降低数据库访问压力,提高查询性能。
应用在用户注册验证码缓存。利用 redis 设置过期时间,当超过指定时间后,redis 清理验证码,使过期的验证码无效。
用在购物车模块,用户登陆系统后,添加的购物车数据需要保存到 redis 缓存中。
使用 redis 主要是减少系统数据库访问压力。从缓存中查询数据,也提高了查询性能,挺高用户体验度。
Redis 分布式锁理解
获取锁的时候,使用 setnx 加锁,并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的 value 值为一个随机生成的 UUID,通过此在释放锁的时候进行判断。
获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。
SETEX:如果 key 已经存在, SETEX 命令将覆写旧值。
SETNX:若给定的 key 已经存在,则 SETNX 不做任何动作。
Redis 怎么设置过期的?
设置过期:this.redisTemplate.expire(“max”,tempTime,TimeUnit.SECONDS);
讲到 redis 缓存的时候说不清楚
redis 中项目中的使用场景:
主要应用在门户网站首页广告信息的缓存。因为门户网站访问量较大,将广告缓存到 redis 中,可以降低数据库访问压力,提高查询性能。
应用在用户注册验证码缓存。利用 redis 设置过期时间,当超过指定时间后,redis 清理验证码,使过期的验证码无效。
用在购物车模块,用户登陆系统后,添加的购物车数据需要保存到 redis 缓存中。
技术角度分析:
Redis 如何实现负载的?采用 Hash 槽来运算存储值,使用 CRC16 算法取模运算,来保证负载问题。
Redis 缓存穿透问题?将数据查询出来如果没有强制设置空值,并且设置过期时间,减少频繁查询数据库。
使用 redis 主要是减少系统数据库访问压力。从缓存中查询数据,也提高了查询性能,挺高用户体验度。
Redis 中对一个 key 进行自增或者自减操作,它是原子性的吗?
是原子性的。对于 Redis 而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。Redis 的操作之所以是原子性的,是因为 Redis 是单线程的。对 Redis 来说,执行 get、set 以及 eval 等 API,都是一个一个的任务,这些任务都会由 Redis 的线程去负责执行,任务要么执行成功,要么执行失败,这就是 Redis 的命令是原子性的原因。Redis 本身提供的所有 API 都是原子操作,Redis 中的事务其实是要保证批量操作的原子性。
项目添加 Redis 缓存后,持久化具体怎么实现的。
RDB:保存存储文件到磁盘;同步时间为 15 分钟,5 分钟,1 分钟一次,可能存在数据丢失问题。
AOF:保存命令文件到磁盘;安全性高,修改后立即同步或每秒同步一次。
上述两种方式在我们的项目中都有使用到,在广告轮播的功能中使用了 redis 缓存,先从 redis 中获取数据,无数据后从数据库中查询后保存到 redis 中。采用默认的 RDB 方式。
怎么提高 redis 缓存利用率?
从业务场景分析,预计会高频率用到的数据预先存放到 redis 中,
可以定时扫描命中率低的数据,可以直接从 redis 中清除。
Redis 宕机之后,购物车中的数据如何处理?如何缓解 mysql 压力?
用 redis 保存的*.rdb 文件恢复即可。另外 redis 还有 AOF 功能,启动时可以自动恢复到前一条查询。这样做在一定程度上减少数据丢失。但重启 redis 会需要从关系型数据库中读取数据,增大 mysql 的压力。依据实际情况,如果 redis 之前有主从复制,则可在其他节点 redis 上拿到数据。如果公司没钱,则只能暂时限制客户端访问量,优先恢复 redis 数据。
Redis 和 mysql 数据同步,是先删除 redis 的数据还是先删除 Mysql 的数据?
不管是先写库,再删除缓存;还是先删缓存,再写库,都有可能出现数据不一致的情况。因为写和读是并发的,没法保证顺序,如果删了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。 如果是 redis 集群,或者主从模式,写主读从,由于 redis 复制存在一定的时间延迟,也有可能导致数据不一致。这时候,考虑先删除数据库内容,再删 redis。因为在库存等实时数据都是直接在数据库中读取,从业务逻辑上来说,我们允许查询时的数据缓存误差,但是不允许结算时的数据存在误差。
Redis 中 watch 机制和原理
我们常用 redis 的 watch 和 multi 来处理一些涉及并发的操作,redis 的 watch+multi 实际是一种乐观锁。watch 命令描述:WATCH 命令可以监控一个或多个键,一旦其中有一个键被修改(或删除), 之后的事务就不会执行。监控一直持续到 EXEC 命令(事务中的命令是在 EXEC 之后才执行的,所以在 MULTI 命令后可以修改 WATCH 监控的键值)
讲讲缓存的设计和优化,缓存和数据库一致性同步解决方案
降低后端负载:对于高消耗的 SQL:join 结果集、分组统计结果;对这些结果进行缓存。
加速请求响应
大量写合并为批量写:如计数器先 redis 累加再批量写入 DB
超时剔除:例如 expire
主动更新:开发控制生命周期(最终一致性,时间间隔比较短)
缓存空对象
布隆过滤器拦截
命令本身的效率:例如 sql 优化,命令优化
网络次数:减少通信次数
降低接入成本:长连/连接池,NIO 等。
IO 访问合并
目的:要减少缓存重建次数、数据尽可能一致、减少潜在危险。解决方案:
互斥锁 setex,setnx:
如果 set(nx 和 ex) 结果为 true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
如果 setnx(nx 和 ex) 结果为 false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间 ( 例如这里是 50 毫秒,取决于构建缓存的速度 ) 后,重新执行函数,直到获取到数据。
永远不过期:
热点 key,无非是并发特别,重建缓存时间比较长,如果直接设置过期时间,那么时间到的时候, 巨大的访问量会压迫到数据库上,所以要给热点 key 的 val 增加一个逻辑过期时间字段,并发访问的时候,判断这个逻辑字段的时间值是否大于当前时间,大于了说明要对缓存进行更新了,那么这个时候,依然让所有线程访问老的缓存,因为缓存并没有设置过期,但是另开一个线程对缓存进行重构。等重构成功,即执行了 redis set 操作之后,所有的线程就可以访问到重构后的缓存中的新的内容了
从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
一致性问题:
先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。
先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同数据在做更新,发现队列里有一个请求了,那么就不要放新的操作进去了,用一个 while(true)循环去查询缓存,循环个 200MS 左右再次发送到队列里去,然后同步等待缓存更新完成。
MySQL 数据库和 SQL 优化与 JDBC
为什么 InnoDB 支持事务而 myisam 不支持
MyISAM:这个是默认类型,它是基于传统的ISAM 类型,ISAM 是Indexed Sequential Access Method (有索引的顺序访问方法) 的缩写,它是存储记录和文件的标准方法.与其他存储引擎比较,MyISAM 具有检查和修复表格的大多数工具. MyISAM 表格可以被压缩,而且它们支持全文搜索.它们不是事务安全的,而且也不支持外键。如果事物回滚将造成不完全回滚,不具有原子性。如果执行大量的 SELECT,MyISAM 是更好的选择。
InnoDB:这种类型是事务安全的.它与 BDB 类型具有相同的特性,它们还支持外键.InnoDB 表格速度很快.具有比 BDB 还丰富的特性,因此如果需要一个事务安全的存储引擎,建议使用它.如果你的数据执行大量的INSERT 或 UPDATE,出于性能方面的考虑,应该使用 InnoDB 表
SQL 语句中关于查询语句的优化你们是怎么做的?
1.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
2.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
3.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
4.尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
5.in 和 not in 也要慎用,否则会导致全表扫描
6.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。
7.应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
8.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
9.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相一致。
10.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
11.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间, 其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
12.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
13.使用连接(JOIN)来代替子查询(Sub-Queries)
14.使用联合(UNION)来代替手动创建的临时表
MySQL 索引使用限制
不要在列上进行运算
select * from users where YEAR(adddate)<2007;
将在每个行上进行运算,这将导致索引失效而进行全表扫描,
因此我们可以改成 select * from users where adddate<‘2007-01-01’;
like 查询是以%开头不使用索引
如果使用 like。like “%aaa%” 不会使用索引而 like “aaa%”可以使用索引。
select * from users where name like '%aaa%'不会使用索引select * from users where name like 'aaa%'可以使用索引
使用短索引
例如,如果有一个 CHAR(255)的列,如果在前 10 个或 20 个字符内,多数值是惟一的,
那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和 I/O 操作。
索引不会包含 NULL 列,IS NULL /IS NOT NULL 不使用索引
复合索引中如果有一列含有 NULL 值那么这个组合索引都将失效,一般需要给默认值 0 或者 ’ '字符串
最左匹配,任何一个索引的最左前缀可以通过使用优化器来查找行
不按索引最左列开始查询(多列索引) 例如: index(‘c1’, ‘c2’, ‘c3’) , where ‘c2’ = ‘aaa’ 不使用索引,
where ‘c2’ = ‘aaa’ and ‘c3’ = ‘sss’ 不能使用索引。where ‘c1’ = ‘aaa’ and ‘c2’ = ‘bbb’ 可以使用索引。index(‘c1’)靠最左可以使用索引。
多列索引,不是使用的第一部分,则不会使用索引
查询中某个列有范围查询,则其右边的所有列都无法使用查询(多列查询)。
where c1= ‘xxx’ and c2 like = ‘aa%’ and c3=’sss’
该查询只会使用索引中的前两列,c3 将不能使用到索引,因为 like 是范围查询。
检索排序
一个查询语句中,既有检索又有排序并且是不同的字段,且这两个列上都有单列索引(独立索引), 那么只有其中一个列用到索引,因为查询优化器在做检索和排序中不能同时使用两个不同的索引。
索引散列度
通过索引扫描的记录超过了表总行数的 30%(估计值),则查询优化器认为全表扫描的效率更高,所以会变成全表扫描查询。
隐式转换:如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
隐式转换导致的索引失效。比如,表的字段 tu_mdn 定义为 varchar(20), 但在查询时把该字段作为 number 类型当做 where 条件,这样会导致索引失效. 错误的例子:select * from test where tu_mdn=13333333333;
正确的例子:select * from test where tu_mdn='13333333333’;
条件中有 or
即使其中有条件带索引也不会使用(这也是为什么尽量少用 or 的原因)
注意:要想使用 or,又想让索引生效,只能将 or 条件中的每个列都加上索引
使用全表扫描要比使用索引快,则不使用索引,数据唯一性差(一个字段的取值只有几种时)的字段不要使用索引
比如性别,只有两种可能数据。意味着索引的二叉树级别少,多是平级。这样的二叉树查找无异于全表扫描
频繁更新的字段不要使用索引
比如 logincount 登录次数,频繁变化导致索引也频繁变化,增大数据库工作量,降低效率
where 子句里对索引列使用不等于(<>),使用索引效果一般,不使用索引
数据库创建表的时候会有哪些考虑呢?
项目中使用的是 MySQL 数据库,数据库创建表时要考虑:
大数据字段最好剥离出单独的表,以便影响性能
使用 varchar,代替 char,这是因为 varchar 会动态分配长度,char 指定为 20,即时你存储字符“1”,它依然是 20 的长度
给表建立主键,看到好多表没主键,这在查询和索引定义上将有一定的影响
避免表字段运行为 null,如果不知道添加什么值,建议设置默认值,特别 int 类型,比如默认值为 0,在索引查询上,效率立显。
建立索引,聚集索引则意味着数据的物理存储顺序,最好在唯一的,非空的字段上建立,其它索引也不是越多越好,索引在查询上优势显著,在频繁更新数据的字段上建立聚集索引,后果很严重,插入更新相当忙。
组合索引和单索引的建立,要考虑查询实际和具体模式
有了解过大数据层面的分库分表吗?以及 mysql 的执行计划吗?
分库:通过 Mycat 结点来管理不同服务器上的数据库,每个表最多存 500 万条记录
分表:重直切割,水平切割
MySql 提供了 EXPLAIN 语法用来进行查询分析,在 SQL 语句前加一个"EXPLAIN"即可。mysql 中的
explain 语法可以帮助我们改写查询,优化表的结构和索引的设置,从而最大地提高查询效率。
有了解过数据库中的表级锁和行级锁吗?乐观锁和悲观锁你有哪些了解?
MySQL 的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM 和 MEMORY 存储引擎采用的是表级锁(table-level locking);InnoDB 存储引擎既支持行级锁
( row-level locking),也支持表级锁,但默认情况下是采用行级锁。MySQL 主要锁的特性可大致归纳如下:
表级锁: 开销小,加锁快;不会出现死锁(因为 MyISAM 会一次性获得 SQL 所需的全部锁);锁定粒度大, 发生锁冲突的概率最高,并发度最低。
行级锁: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
乐观锁:通过 version 版本字段来实现
悲观锁:通过 for update 来实现
Sql 层面: 一、悲观锁
排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操 作该部分数据。这将防止其他进程读取或修改表中的数据。
实现:大多数情况下依靠数据库的锁机制实现
一般使用 select …for update 对所选择的数据进行加锁处理,例如 select * from account where name=”Max” for update, 这条 sql 语句锁定了 account 表中所有符合检索条件(name=”Max”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
二、乐观锁
如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。
实现:大多数基于数据版本(Version)记录机制实现
具体可通过给表加一个版本号或时间戳字段实现,当读取数据时,将 version 字段的值一同读出, 数据每更新一次,对此 version 值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。
代码层面:
悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待, 直到锁被释放.
乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。
Mysql 优化有没有工具
三个 MySQL 性能测试工具:The MySQL Benchmark Suite、MySQL super-smack、MyBench。除了第一个为 MySQL 性能测试工具,其他两个都为压力测试工具。
你有了解 mysql 的隔离级别吗?mysql 默认的隔离级别是什么?
数据库事务的隔离级别有四种,隔离级别高的数据库的可靠性高,但并发量低,而隔离级别低的数据库可靠性低,但并发量高,系统开销小。
1.READ UNCIMMITTED(未提交读)
2.READ COMMITTED(提交读)
3.REPEATABLE READ(可重复读)
4.SERIALIZABLE(可串行化)
mysql 默认的事务处理级别是’REPEATABLE-READ’,也就是可重复读。
怎样进行数据库性能调优
一:应用程序优化
把数据库当作奢侈的资源看待,在确保功能的同时,尽可能少地动用数据库资源。
不要直接执行完整的 SQL 语法,尽量通过存储过程实现数据库操作。
客户与服务器连接时,建立连接池,让连接尽量得以重用,以避免时间与资源的损耗。
非到不得已,不要使用游标结构,确实使用时,注意各种游标的特性。
二:基本表设计优化
表设计遵循第三范式。在基于表驱动的信息管理系统中,基本表的设计规范是第三范式。
分割表。分割表可分为水平分割表和垂直分割表两种:水平分割是按照行将一个表分割为多个表。
引入中间表。
三: 数据库索引优化
索引是建立在表上的一种数据组织,它能提高访问表中一条或多条记录的特定查询效率。
聚集索引:该索引中键值的逻辑顺序决定了表中相应行的物理顺序。聚集索引确定表中数据的物理顺序。
非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同.
MySQL 存储过程
SQL 语句需要先编译然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的 SQL 语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。存储过程是可编程的函数,在数据库中创建并保存,可以由 SQL 语句和控制结构组成。当想要在不同的应用程序或平台上执行相同的函数,或者封装特定功能时,存储过程是非常有用的。数据库中的存储过程可以看做是对编程中面向对象方法的模拟,它允许控制数据的访问方式。
存储过程的优点:
1.增强 SQL 语言的功能和灵活性:存储过程可以用控制语句编写,有很强的灵活性,可以完成复杂的判断和较复杂的运算。
2.标准组件式编程:存储过程被创建后,可以在程序中被多次调用,而不必重新编写该存储过程的 SQL 语句。而且数据库专业人员可以随时对存储过程进行修改,对应用程序源代码毫无影响。
3.较快的执行速度:如果某一操作包含大量的 Transaction-SQL 代码或分别被多次执行,那么存储过程要比批处理的执行速度快很多。因为存储过程是预编译的。在首次运行一个存储过程时查询,优化器对其进行分析优化,并且给出最终被存储在系统表中的执行计划。而批处理的 Transaction-SQL 语句在每次运行时都要进行编译和优化,速度相对要慢一些。
4.减少网络流量:针对同一个数据库对象的操作(如查询、修改),如果这一操作所涉及的 Transaction-SQL 语句被组织进存储过程,那么当在客户计算机上调用该存储过程时,网络中传送的只是该调用语句,从而大大减少网络流量并降低了网络负载。
5.作为一种安全机制来充分利用:通过对执行某一存储过程的权限进行限制,能够实现对相应的数据的访问权限的限制,避免了非授权用户对数据的访问,保证了数据的安全。
MySQL 存储过程的创建语法:
CREATE PROCEDURE 过程名([[IN|OUT|INOUT] 参数名 数据类型[,[IN|OUT|INOUT] 参数名 数据类型…]]) [特性 …] 过程体
DELIMITER //
CREATE PROCEDURE myproc(OUT s int) BEGIN
SELECT COUNT(*) INTO s FROM students; END
// DELIMITER ;
分隔符:MySQL 默认以";“为分隔符,如果没有声明分割符,则编译器会把存储过程当成 SQL 语句进行处理,因此编译过程会报错,所以要事先用“DELIMITER //”声明当前段分隔符,让编译器把两个”//"之间的内容当做存储过程的代码,不会执行这些码;“DELIMITER ;”的意为把分隔符还原。
参数:存储过程根据需要可能会有输入、输出、输入输出参数,如果有多个参数用","分割开。MySQL 存储过程的参数用在存储过程的定义,共有三种参数类型,IN,OUT,INOUT:
IN 参数的值必须在调用存储过程时指定,在存储过程中修改该参数的值不能被返回,为默认值
OUT:该值可在存储过程内部被改变,并可返回
INOUT:调用时指定,并且可被改变和返回
过程体:过程体的开始与结束使用 BEGIN 与 END 进行标识。
怎么实现数据量大、 并发量高的搜索
创建 Elasticsearch/solr 索引库,数据量特别大时采用 Elasticsearch/solr 分布式集群。
JDBC 的理解
JDBC(Java DataBase Connectivity,java 数据库连接)是一种用于执行 SQL 语句的 Java API,可以为多种关系数据库提供统一访问,它由一组用 Java 语言编写的类和接口组成。JDBC 提供了一种基准, 据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。有了 JDBC,向各种关系数据发送 SQL 语句就是一件很容易的事。换言之,有了 JDBC API,就不必为访问 Sybase 数据库专门写一个程序,为访问 Oracle 数据库又专门写一个程序,或为访问 Informix 数据库又编写另一个程序等等,程序员只需用 JDBC API 写一个程序就够了,它可向相应数据库发送 SQL 调用。
AngularJS 框架
AngularJS 四大特征?
MVC 模式
Model:数据,其实就是 angular 变量($scope.XX);
View: 数据的呈现,Html+Directive(指令);
Controller:操作数据,就是 function,数据的增删改查;
双向绑定
首先我们要理解数据绑定。我们看到的网站页面中,是由数据和设计两部分组合而成。将设计转换成浏览器能理解的语言,便是 html 和 css 主要做的工作。而将数据显示在页面上,并且有一定的交互效果(比如点击等用户操作及对应的页面反应)则是 js 主要完成的工作。很多时候我们不可能每次更新数据便刷新页面(get 请求),而是通过向后端请求相关数据,并通过无刷新加载的方式进行更新页面(post 请求)。那么数据进行更新后,页面上相应的位置也能自动做出对应的修
改,便是数据绑定。在以前的开发模式中,这一步一般通过 jq 操作 DOM 结构,从而进行更新页面。但这样带来的是大量的代码和大量的操作。如果能在开始的时候,便已经确定好从后端获取的数据到页面上需要进行的操作,当数据发生改变,页面的相关内容也自动发生变化,这样便能极大地方便前端工程师的开发。在新的框架中(angualr,react,vue 等),通过对数据的监视,发现变化便根据已经写好的规则进行修改页面,便实现了数据绑定。可以看出,数据绑定是 M(model,数据) 通过 VM(model-view,数据与页面之间的变换规则)向 V(view)的一个修改。而双向绑定则是增加了一条反向的路。在用户操作页面(比如在 Input 中输入值)的时候,数据能及时发生变化, 并且根据数据的变化,页面的另一处也做出对应的修改。有一个常见的例子就是淘宝中的购物车, 在商品数量发生变化的时候,商品价格也能及时变化。这样便实现了 V——M——VM——V 的一个双向绑定。这里是区别于 Jquery 的,jq 操作的是 dom 对象,angularJS 操作的是变量
依赖注入
对象在创建时,其依赖的对象由框架来自动创建并注入进来。控制器就是通过依赖注入的方式实现对服务的调用。
模块化设计
高内聚低耦合法则
高内聚:每个模块的具体功能具体实现
低耦合:模块之间尽可能的少用关联和依赖
官方提供的模块 ng(最核心)、ngRoute(路由)、ngAnimate(动画)
用户自定义的模块 angular.module(‘模块名’,[ ])
Nginx 服务器
什么是 Nginx?
nginx 本是一个 web 服务器和反向代理服务器,但由于丰富的负载均衡策略,常常被用于客户端可真实的服务器之间,作为负载均衡的实现。用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。
请列举 Nginx 的一些特性?
反向代理/L7 负载均衡器
嵌入式 Perl 解释器
动态二进制升级
可用于重新编写 URL,具有非常好的 PCRE 支持
nginx 和 apache 的区别?
轻量级,同样起 web 服务,比 apache 占用更少的内存及资源
抗并发,nginx 处理请求是异步非阻塞的,而 apache 则是阻塞型的,在高并发下 nginx 能保持低资源低消耗高性能
高度模块化的设计,编写模块相对简单
最核心的区别在于 apache 是同步多进程模型,一个连接对应一个进程;nginx 是异步的,多个连接(万级别)可以对应一个进程
nginx 是如何实现高并发的?
一个主进程,多个工作进程,每个工作进程可以处理多个请求,每进来一个 request,会有一个 worker进程去处理。但不是全程的处理,处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request,并等待请求返回。那么,这个处理的 worker 继续处理其他请求,而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走。由于 web server 的工作性质决定了每个 request 的大部份生命都是在网络传输中,实际上花费在 server 机器上的时间片不多。这是几个进程就解决高并发的秘密所在。即@skoo 所说的 webserver 刚好属于网络 io 密集型应用,不算是计算密集型。
请解释 Nginx 如何处理 HTTP 请求?
Nginx 使用反应器模式。主事件循环等待操作系统发出准备事件的信号,这样数据就可以从套接字读取,在该实例中读取到缓冲区并进行处理。单个线程可以提供数万个并发连接。
在 Nginx 中,如何使用未定义的服务器名称来阻止处理请求?
只需将请求删除的服务器就可以定义为:Server {listen 80; server_name “ “ ;return 444;}这里,服务器名被保留为一个空字符串,它将在没有“主机”头字段的情况下匹配请求,而一个特殊的 Nginx 的非标准代码 444 被返回,从而终止连接。7、 使用“反向代理服务器”的优点是什么?答:反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和 web 服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用 web 托管服务时。
请列举 Nginx 服务器的最佳用途?
Nginx 服务器的最佳用法是在网络上部署动态 HTTP 内容,使用 SCGI、WSGI 应用程序服务器、用于脚本的 FastCGI 处理程序。它还可以作为负载均衡器。
请解释 Nginx 服务器上的 Master 和 Worker 进程分别是什么?
Master 进程:读取及评估配置和维持 Worker 进程:处理请求
请解释你如何通过不同于 80 的端口开启 Nginx?
为了通过一个不同的端口开启 Nginx,你必须进入/etc/Nginx/sites-enabled/,如果这是默认文件, 那么你必须打开名为“default”的文件。编辑文件,并放置在你想要的端口:Like server {listen 81;}
请解释是否有可能将Nginx 的错误替换为 502 错误、503?
502 =错误网关 503 =服务器超载 有可能,但是您可以确保 fastcgi_intercept_errors 被设置为 ON, 并使用错误页面指令。Location / { fastcgi_pass 127.0.01:9001; fastcgi_intercept_errors on; error_page 502 =503/error_page.html; #… }
在 Nginx 中,解释如何在URL 中保留双斜线?
要在 URL 中保留双斜线,就必须使用 merge_slashes_off;语法:merge_slashes [on/off]默认值: merge_slashes on 环境: http,server
请解释 ngx_http_upstream_module 的作用是什么?
ngx_http_upstream_module 用于定义可通过 fastcgi 传递、proxy 传递、uwsgi 传递、memcached 传递和 scgi 传递指令来引用的服务器组。
请解释什么是 C10K 问题?
C10K 问题是指无法同时处理大量客户端(10,000)的网络套接字。
请陈述 stub_status 和 sub_filter 指令的作用是什么?
Stub_status 指令:该指令用于了解 Nginx 当前状态的当前状态,如当前的活动连接,接受和处理当前读/ 写/等待连接的总数
Sub_filter 指令:它用于搜索和替换响应中的内容,并快速修复陈旧的数据
解释 Nginx 是否支持将请求压缩到上游?
您可以使用 Nginx 模块 gunzip 将请求压缩到上游。gunzip 模块是一个过滤器,它可以对不支持“gzip” 编码方法的客户机或服务器使用“内容编码:gzip”来解压缩响应。
解释如何在 Nginx 中获得当前的时间?
要获得 Nginx 的当前时间,必须使用 SSI 模块、 d a t e g m t 和 date_gmt 和 dategmt和date_local 的变量。Proxy_set_header THE-TIME $date_gmt;
用 Nginx 服务器解释-s 的目的是什么?
用于运行 Nginx -s 参数的可执行文件。
解释如何在 Nginx 服务器上添加模块?
在编译过程中,必须选择 Nginx 模块,因为 Nginx 不支持模块的运行时间选择。
什么是反向代理和正向代理?
正向代理:被代理的是客户端,比如通过 XX 代理访问国外的某些网站,实际上客户端没有权限访问国外的网站,客户端请求 XX 代理服务器,XX 代理服务器访问国外网站,将国外网站返回的内容传给真正的用户。用户对于服务器是隐藏的,服务器并不知道真实的用户。
反向代理:被代理的是服务器,也就是客户端访问了一个所谓的服务器,服务器会将请求转发给后台真实的服务器,真实的服务器做出响应,通过代理服务器将结果返给客户端。服务器对于用户来说是隐藏的,用户不知道真实的服务器是哪个。
说明:关于正向代理和反向代理,听起来比较绕,仔细理解,体会也不难明白到底是什么意思。用nginx 做实现服务的高可用,nginx 本身可能成为单点,遇见的两种解决方案,一种是公司搭建自己的 DNS,将请求解析到不同的 NGINX,另一只是配合 keepalive 实现服务的存活检测。
Nginx 静态页面服务跳转到购物车跨域问题
在 Nginx 中部署了静态页面,添加购物车时必须从静态页面跳转到购物车系统,实现购物车添加操作。由于在静态页面中使用 angularJS 实现的跳转,发现跳转到购物车系统完全没有问题,但是并不能跳转回到购物车系统页面。
问题分析:从静态详情系统跳转到购物车系统,会存在跨域问题,因此不能进行回调函数的数据传递。所以在回调函数中的页面跳转就不能实现。
解决方案:使用 angularJS 跨域调用及 springmvc 跨域配置,解决问题。
FastDFS 分布式文件系统
简单介绍一下 FastDFS?
开源的分布式文件系统,主要对文件进行存储、同步、上传、下载,有自己的容灾备份、负载均衡、线性扩容机制;
FastDFS 架构主要包含 Tracker(跟踪) server 和 Storage(组,卷) server。客户端请求 Tracker server 进行文件上传、下载的时候,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker server:跟踪器或者调度器,主要起负载均衡和调度作用。通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。
Storage server:存储服务器,作用主要是文件存储,完成文件管理的所有功能。客户端上传的文件主要保存在 Storage server 上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统去管理文件。
存储服务器采用了分组/分卷的组织方式。
整个系统由一个组或者多个组组成;
组与组之间的文件是相互独立的;
所有组的文件容量累加就是整个存储系统的文件容量;
一个组可以由多台存储服务器组成,一个组下的存储服务器中的文件都是相同的,组中的多台存储服务器起到了冗余备份和负载均衡的作用;
在组内增加服务器时,如果需要同步数据,则由系统本身完成,同步完成之后,系统自动将新增的服务器切换到线上提供使用;
当存储空间不足或者耗尽时,可以动态的添加组。只需要增加一台服务器,并为他们配置一个新的组, 即扩大了存储系统的容量。
为什么要使用 FastDFS 作为你们的图片服务器?
首先基于 fastDFS 的特点:存储空间可扩展、提供了统一的访问方式、访问效率高、容灾性好 等特点,再结合我们项目中图片的容量大、并发大等特点,因此我们选择了 FastDFS 作为我们的图片服务器;Nginx 也可以作为一台图片服务器来使用,因为 nginx 可以作为一台 http 服务器来使用, 作为网页静态服务器,通过 location 标签配置;在公司中有的时候也用 ftp 作为图片服务器来使用。
FastDFS 中文件上传下载的具体流程?
客户端上传文件后生成一个 file_id,返回给客户端,客户端利用这个 file_id 结合 ip 地址,生成一个完成图片的 url,保存在数据库中。生成的那个 file_id 用于以后访问该文件的索引信息。
FastDFS 文件下载的流程
ActiveMQ 信息队列
什么是消息队列?
就是消息的传输过程中保存消息的容器。
消息队列都解决了什么问题? 异步,并行,解耦,排队
消息模式? 订阅,点对点重复消费
Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
丢消息
用持久化消息
非持久化消息及时处理不要堆积
启动事务,启动事务后,commit()方法会负责等待服务器的返回,也就不会关闭连接导致消息丢失。
消息重发
消息被重新传递给客户端:
使用事务会话,并调用滚退()。
在调用 commit()之前关闭事务会话。
会话使用 CLIENT_ACKNOWLEDGE 签收模式,并 Session .recover()重发被调用。
客户端连接超时(也许正在执行的代码要比配置的超时周期更长)。
什么是 ActiveMQ?
activeMQ 是一种开源的,面向消息的中间件,用来系统之间进行通信的
activemq 的原理
原理就是生产者生产消息,把消息发送给 activemq。Activemq 接收到消息, 然后查看有多少个消费者, 然后把消息转发给消费者, 此过程中生产者无需参与。 消费者接收到消息后做相应的处理和生产者没有任何关系
对比 RabbitMQ
RabbitMQ 的协议是 AMQP,而 ActiveMQ 使用的是 JMS 协议。顾名思义 JMS 是针对 Java 体系的传输协议,队列两端必须有 JVM,所以如果开发环境都是 java 的话推荐使用 ActiveMQ,可以用 Java 的一些对象进行传递比如 Map、Blob(二进制大数据)、Stream 等。而 AMQP 通用行较强,非 java 环境经常使用,传输内容就是标准字符串。另外一点就是RabbitMQ 用Erlang 开发,安装前要装Erlang 环境,比较麻烦。ActiveMQ 解压即可用不用任何安装。
对比 KafKa
Kafka 性能超过 ActiveMQ 等传统 MQ 工具,集群扩展性好。弊端是:
在传输过程中可能会出现消息重复的情况,
不保证发送顺序
一些传统 MQ 的功能没有,比如消息的事务功能。
所以通常用 Kafka 处理大数据日志。
对比 Redis
其实 Redis 本身利用 List 可以实现消息队列的功能,但是功能很少,而且队列体积较大时性能会急剧下降。对于数据量不大、业务简单的场景可以使用。
如何解决消息重复问题
所谓消息重复,就是消费者接收到了重复的消息,一般来说我们对于这个问题的处理要把握下面几点,
消息不丢失
消息不重复执行
一般来说我们可以在业务段加一张表,用来存放消息是否执行成功,每次业务事物 commit 之后,告知服务端,已经处理过该消息,这样即使你消息重发了,也不会导致重复处理,大致流程如下:业务端的表记录已经处理消息的 id,每次一个消息进来之前先判断该消息是否执行过,如果执行过就放弃, 如果没有执行就开始执行消息,消息执行完成之后存入这个消息的 id
关于事务控制
获取 session 链接的时候 设置参数 默认不开启
producer 提交时的事务
事务开启 只执行 send 并不会提交到队列中,只有当执行 session.commit()时,消息才被真正的提交到队列中。
事务不开启 只要执行 send,就进入到队列中。
事务开启,签收必须写 收到消息后,消息并没有真正的被消费。
消息只是被锁住。一旦出现该线程死掉、
Session.SESSION_TRANSACTED 抛异常,或者程序执行了
session.rollback()那么消息会释放,重新
回到队列中被别的消费端再次消费。
事务不开启,签收方式选择
Session.AUTO_ACKNOWLEDGE 只要调用 comsumer.receive 方法 ,自动确认。
需要客户端执行
consumer 接收时的事务 事务不开启,签收方式选择 message.acknowledge(),否则视为未提交
状态,线程结束后,其他线程还可以接
Session.CLIENT_ACKNOWLEDGE 收到。这种方式跟事务模式很像,区别是不能手动回滚,而且可以单独确认某
个消息。手动签收
事务不开启,签收方式选择
Session.DUPS_OK_ACKNOWLEDGE 在 Topic 模式下做批量签收时用的,可以提高性能。但是某些情况消息可能会被重复提交,使用这种模式的 consumer 要可以处理重复提交的问题。
持久化
通过 producer.setDeliveryMode(DeliveryMode.PERSISTENT) 进行设置,持久化的好处就是当 activemq
宕机的话,消息队列中的消息不会丢失。非持久化会丢失。但是会消耗一定的性能。
哪些情况用到 activeMq?
商品上架后更新 ES 索引库时、更新静态页时、发送短信时,提交订单后清除购物车中的数据,支付时修改订单状态,添加支付信息,支付成功时修改订单状态,修改支付信息,更新库存时
ActiveMQ 消息队列不消费
去 ActiveMQ.DLQ 里找找
什么是 ActiveMQ.DLQ?
一旦消息的重发尝试超过了为重发策略配置的最大重发次数,一个“Poison ACK”被发送回 the broker,让他知道消息被认为是毒丸。the broker 然后接收消息并将其发送到死信队列,以便以后可以进行分析。
在 activemq 中死信队列叫做 ActiveMQ.DLQ。所有无法传递的消息将被发送到这个队列,这很难管理。
可以在 Activemq.xml 配置文件的目标策略映射中设置个体死信策略,它允许您为队列或主题指定特定的死信队列前缀。
MQ 消费者接收不到消息怎么办?
处理失败指的是 MessageListener 的 onMessage 方法里抛出 RuntimeException。
Message 头里有两个相关字段:Redelivered 默认为 false,redeliveryCounter 默认为 0。
消息先由 broker 发送给 consumer,consumer 调用 listener,如果处理失败,本地 redeliveryCounter++,给broker 一个特定应答,broker 端的 message 里 redeliveryCounter++,延迟一点时间继续调用,默认 1s。超过 6 次,则给 broker 另一个特定应答,broker 就直接发送消息到 DLQ。
如果失败 2 次,consumer 重启,则 broker 再推过来的消息里,redeliveryCounter=2,本地只能再重试 4
次即会进入 DLQ。
重试的特定应答发送到 broker,broker 即会在内存将消息的 redelivered 设置为 true,redeliveryCounter++, 但是这两个字段都没有持久化,即没有修改存储中的消息记录。所以 broker 重启时这两个字段会被重置为默认值。
怎样解决 activeMQ 的消息持久化问题?
A:持久化为文件
这个你装 ActiveMQ 时默认就是这种,只要你设置消息为持久化就可以了。涉及到的配置:
涉及到的代码:
producer.Send(request, MsgDeliveryMode.Persistent, level, TimeSpan.MinValue);
B:持久化为 MySql
加载驱动 jar,为数据中创建三个数据库表,存储 activemq 的消息信息
如果 activeMQ 的消息没有发送成功,怎样确保再次发送成功。
重新传递消息,ActiveMQ 在接收消息的 Client 有以下几种操作的时候,需要重新传递消息:
Client 用了 transactions(事务),且在 session 中调用了 rollback()
Client 用了 transactions,且在调用 commit()之前关闭
Client 在 CLIENT_ACKNOWLEDGE 的传递模式下,在 session 中调用了 recover()
确保客户端有几种状态,检测状态,只要提交了那就说明客户端成功!
MQ 丢包如何解决
transaction 机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。然而缺点就是吞吐量下降了。所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ 就会发送一个 Ack 给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了.如果 rabiitMQ 没能处理该消息,则会发送一个 Nack 消息给你,你可以进行重试操作。
如果 activeMQ 的服务挂了,怎么办?
在通常的情况下,非持久化消息是存储在内存中的,持久化消息是存储在文件中的,它们的最大限制在配置文件的节点中配置。但是,在非持久化消息堆积到一定程度,内存告急的时候, ActiveMQ 会将内存中的非持久化消息写入临时文件中,以腾出内存。虽然都保存到了文件里,但它和持久化消息的区别是,重启后持久化消息会从文件中恢复,非持久化的临时文件会直接删除。
考虑高可用,实现 activemq 集群。
消息发送失败怎么处理,发送数据,数据库已经保存了数据,但是 redis 中没有同步, 怎么办。或者说如何做到消息同步。
消息发送失败,可以进行消息的重新发送,可以配置消息的重发次数。如果消息重发完毕后,消息还没有接受成功,重启服务。
ActiveMQ 存在发出消息太大,造成消息接受不成功,怎么解决?
多个线程从 activeMQ 中取消息,随着业务的扩大,该机器占用的网络带宽越来越高。仔细分析发现,mq 入队时并没有异常高的网络流量,仅仅在出队时会产生很高的网络流量。最终发现是 Spring 的 jmsTemplate 与 Activemq 的 prefetch 机制配合导致的问题,怎么解决?研究源码发现jmsTemplate 实现机制是:每次调用 receive()时都会创建一个新的 consumer 对象,用完即销毁。正常情况下仅仅会浪费重复创建consumer 的资源代价,并不至于产生正常情况十倍百倍的网络流量。但是 activeMQ 有一个提高性能的机制 prefetch,此时就会有严重的问题。
prefetch 机制:
每次 consumer 连接至 MQ 时,MQ 预先存放许多 message 到消费者(前提是 MQ 中存在大量消息),预先存放 message 的数量取决于 prefetchSize(默认为 1000)。此机制的目的很显然,是想让客户端代码用一个 consumer 反复进行receive 操作,这样能够大量提高出队性能。此机制与 jmsTemplate 配合时就会产生严重的问题,每次 jmsTemplate.receive(),都会产生 1000 个消息的网络流量, 但是因为jmsTemplae 并不会重用consumer,导致后面999 个消息都被废弃。反复jmsTemplate.receive()时,表面上看不出任何问题,其实网络带宽会造成大量的浪费。
解决方案:
若坚持使用 jmsTemplate,需要设置 prefetch 值为 1,相当于禁用了 activeMQ 的 prefetch 机制,此时感觉最健壮, 就算多线程,反复调用 jmsTemplate.receive()也不会有任何问题。但是会有资源浪费,因为要反复创建 consumer 并频繁与服务器进行数据通信,但在性能要求不高的应用中也不算什么问题。
不使用jmsTemplate,手工创建一个consumer,并单线程反复使用它来receive(),此时可以充分利用prefetch机制。配合多线程的方式每个线程拥有自己的一个 consumer,此时能够充分发挥 MQ 在大吞吐量时的速度优势。
切记避免多线程使用一个 consumer 造成的消息混乱。大吞吐量的应用推荐使用方案 2,能够充分利用 prefetch 机制提高系 MQ 的吞吐性能。
activeMQ 存在运行时间长了以后,收不到消息的现象,怎么解决?
时间长了就会出现,卡死,新的数据不能从队列接听到。只能重启程序。解决方案:
不要频繁的建立和关闭连接:JMS 使用长连接方式,一个程序,只要和 JMS 服务器保持一个连接就可以了,不要频繁的建立和关闭连接。频繁的建立和关闭连接,对程序的性能影响还是很大的。这一点和 jdbc 还是不太一样的。
Connection 的 start()和 stop()方法代价很高:JMS 的 Connection 的 start()和 stop()方法代价很高,不能经常调用。我们试用的时候,写了个 jms 的 connection pool,每次将 connection 取出 pool 时调用 start()方法,归还时调用 stop()方法,然而后来用 jprofiler 发现,一般的 cpu 时间都耗在了这两个方法上。
start()后才能收消息:Connection 的 start()方法调用后,才能收到 jms 消息。如果不调用这个方法,能发出消息,但是一直收不到消息。不知道其它的 jms 服务器也是这样。
显式关闭 Session:如果忘记了最后关闭 Connection 或 Session 对象,都会导致内存泄漏。这个在我测试的时候也发现了。本来以为关闭了 Connection,由这个 Connection 生成的 Session 也会被自动关闭,结果并非如此,Session 并没有关闭,导致内存泄漏。所以一定要显式的关闭 Connection 和 Session。
对 Session 做对象池:对 Session 做对象池,而不是 Connection。Session 也是昂贵的对象,每次使用都新建和关闭,代价也非常高。而且后来我们发现,原来 Connection 是线程安全的,而 Session 不是,所以后来改成了对 Session 做对象池,而只保留一个 Connection。
集群:ActiveMQ 有强大而灵活的集群功能,但是使用起来还是会有很多陷阱。
Elasticsearch 全文搜索
简单介绍一下 Elasticsearch 全文搜索
ElasticSearch 是一个基于 Lucene 的搜索服务器。通过 HTTP 使用 JSON 进行数据索引,用于分布式全文检索,解决人们对于搜索的众多要求。
ES 的用途
ES 在系统中主要完成商品搜索功能,提高搜索性能。
lucene 与 elasticsearch(solr)有什么区别?
lucene 只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来的应用。好比 lucene 是类似于 jdk,而搜索引擎软件就是 tomcat 的。elasticsearch 和 solr, 这两款都是基于 lucene 的搭建的,可以独立部署启动的搜索引擎服务软件。
基本概念:
cluster 集群 整个 elasticsearch 默认就是集群状态,整个集群是一份完整、互备的数据。
node 节点 集群中的一个节点,一般只一个进程就是一个 node
shard 分片 分片,即使是一个节点中的数据也会通过 hash 算法,分成多个片存放,默认是
5 片。
index 逻辑数据库 相当于 rdbms 的 database, 对于用户来说是一个逻辑数据库,虽然物理上会被分多个 shard 存放,也可能存放在多个 node 中。
type 类似于 rdbms 的 table,但是与其说像 table,其实更像面向对象中的 class , 同一
Json 的格式的数据集合。
document 类似于 rdbms 的 row、面向对象里的 object
field 相当于字段、属性
与 MySQL 对比
利用 kibana 学习 elasticsearch restful api (DSL)
Kibana 是一个开源分析和可视化平台,可视化操作 Elasticsearch 。Kibana 可以用来搜索,查看和与存储在 Elasticsearch 索引中的数据进行交互。可以轻松地进行高级数据分析,并可在各种图表, 表格和地图中显示数据。ES 提供了基于 JSON 的 query DSL 查询语言
es 中保存的数据结构
这两个对象如果放在关系型数据库保存,会被拆成 2 张表,但是 elasticsearch 是用一个 json 来表示一个 document。所以它保存到 es 中应该是:
es 的 java 客户端的选择
目前市面上有两类客户端
一种是 TransportClient 为代表的 ES 原生客户端,不能执行原生 dsl 语句必须使用它的 Java api 方法。
一种是以 Rest Api 为主的 missing client,最典型的就是 jest。 这种客户端可以直接使用 dsl 语句拼成的字符串,直接传给服务端,然后返回 json 字符串再解析。
两种方式各有优劣,但是最近 elasticsearch 官网,宣布计划在 7.0 以后的版本中废除TransportClient。以 RestClient 为主。在官方的 RestClient 基础上,进行了简单包装的 Jest 客户端,就成了首选,而且该客户端也与 springboot 完美集成。
中文分词:elasticsearch 本身自带的中文分词,就是单纯把中文一个字一个字的分开,根本没有词汇的概念。
es 使用的问题:
es 大量的写操作会影响 es 性能,因为 es 需要更新索引,而且 es 不是内存数据库,会做相应的 io 操作。
而且修改某一个值,在高并发情况下会有冲突,造成更新丢失,需要加锁,而 es 的乐观锁会恶化性能问题。
解决思路:
用 redis 做精确计数器,redis 是内存数据库读写性能都非常快,利用 redis 的原子性的自增可以解决并发写操作。redis 每计 100 次数(可以被 100 整除)我们就更新一次 es ,这样写操作就被稀释了 100 倍,这个倍数可以根据业务情况灵活设定。
增量同步索引库
推荐使用 MQ(RabbitMQ)原理:使用 MQ 做增量同步,即当修改数据之后就将此数据发送至 MQ, 由 MQ 将此数据同步到 ES 上
ES 索引中使用了 IK 分词器,你们项目中使用到了分词器的哪种工作模式?
IK 分词器,基本可分为两种模式,一种为 smart 模式,一种为非 smart 模式。例如:张三说的确实在理
smart 模式的下分词结果为:张三 | 说的 | 确实 | 在理
非 smart 模式下的分词结果为:张三 | 三 | 说的 | 的确 | 的 | 确实 | 实在 | 在理
区别:
可见非 smart 模式所做的就是将能够分出来的词全部输出;
smart 模式下,IK 分词器则会根据内在方法输出一个认为最合理的分词结果,这就涉及到了歧义判断。
怎么分词
使用第三方的分词器 IKAnalyzer,会按照中国人用此习惯自动分词。
ES 高亮不能显示的问题
前台使用 angularJS 加载搜索结果,但是发现高亮不能展示。
问题原因:angularJS 底层使用 ajax,异步加载高亮信息返回给页面后,页面没有刷新,就直接显示返回的数据。此时会把所有的数据作为普通的文本数据进行加载。因此就没有高亮的效果。
解决方案:使用 angularJS 过滤器过滤文本数据,此时 angularJS 过滤器把 html 文本数据解析为浏览器能识别的 html 标签。高亮就能展示了。
简单介绍一下 Es 全文检索在整个系统中的应用,在更新索引库的同时会产生索引碎片, 这个碎片是如何处理的?
根据商品的名称,分类,品牌等属性来创建索引进行商品搜索。更新索引库时会先删除索引,然后再重建。而对于删除聚集索引,则会导致对应的非聚集索引重建两次(删除时重建,建立时再重建). 直接删除碎片。
单点登录
早期单一服务器,用户认证
缺点:单点性能压力,无法扩展
WEB 应用集群,session 共享模式
解决了单点性能瓶颈。问题:
1.多业务分布式数据独立管理,不适合统一维护一份 session 数据。
2.分布式按业务功能切分,用户、认证解耦出来单独统一管理。
3.cookie 中使用 jsessionId 容易被篡改、盗取。
4.跨顶级域名无法访问。
分布式,SSO(single sign on)模式
解决 :
1.用户身份信息独立管理,更好的分布式管理。
2.可以自己扩展安全策略
3.跨域不是问题
缺点:
认证服务器访问压力较大。
业务流程图
认证中心模块(oauth 认证)
数据库表:user_info,并添加一条数据!密码应该是加密的!
在设计密码加密方式时 一般是使用 MD5+盐的方式进行加密和解密。
登录功能
业务:
1.用接受的用户名密码核对后台数据库
2.将用户信息写入 redis,redis 中有该用户视为登录状态。
3.用 userId+当前用户登录 ip 地址+密钥生成 token
4.重定向用户到之前的来源地址,同时把 token 作为参数附上。
生成 token
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录。JWT 最重要的作用就是对 token 信息的防伪作用。
JWT 的原理,一个 JWT 由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行 base64 编码得到 JWT。
公共部分:主要是该 JWT 的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
私有部分:用户自定义的内容,根据实际需要真正要封装的信息。
签名部分:根据用户信息+盐值+密钥生成的签名。如果想知道 JWT 是否是真实的只要把 JWT 的信息取出来,加上盐值和服务器中的密钥就可以验证真伪。所以不管由谁保存 JWT,只要没有密钥就无法伪造。
例如:usrInfo+ip=密钥。
base64 编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以吧 base64 编码解成明文,所以不要在 JWT 中放入涉及私密的信息,因为实际上 JWT 并不是加密信息。
验证功能
功能:当业务模块某个页面要检查当前用户是否登录时,提交到认证中心,认证中心进行检查校验, 返回登录状态、用户 Id 和用户名称。
业务:
1.利用密钥和 IP 检验 token 是否正确,并获得里面的 userId
2.用 userId 检查 Redis 中是否有用户信息,如果有延长它的过期时间。
3.登录成功状态返回。
业务模块页面登录情况检查
问题:
1.由认证中心签发的 token 如何保存?
2.难道每一个模块都要做一个 token 的保存功能?
3.如何区分请求是否一定要登录?
回答:
登录成功后将 token 写到 cookie 中
加入拦截器:首先这个验证功能是每个模块都要有的,也就是所有 web 模块都需要的。在每个 controller 方法进入前都需要进行检查。可以利用在 springmvc 中的拦截器功能。因为咱们是多个 web 模块分布式部署的,所以不能写在某一个 web 模块中,可以是一个公共的 web 模块,加入拦截器。
检验方法是否需要验证用户登录状态:为了方便程序员在 controller 方法上标记,可以借助自定义注解的方式。比如某个 controller 方法需要验证用户登录,在方法上加入自定义的@LoginRequie。
什么是 CAS?
中央认证服务,企业级单点登录解决方案。CAS(Central Authentication Service),是耶鲁大学开发的单点登录系统(SSO,single sign-on),应用广泛,具有独立于平台的,易于理解,支持代理功能。CAS 系统在各个大学如耶鲁大学、加州大学、剑桥大学、香港科技大学等得到应用。CAS 的设计目的:
为多个 Web 应用提供单点登录基础设施,同时可以为非 Web 应用但拥有 Web 前端的功能服务提供单点登录的功能;
简化应用认证用户身份的流程;
将用户身份认证集中于单一的 Web 应用,让用户简化他们的密码管理,从而提高安全性;而且,当应用需要修改身份验证的业务逻辑时,不需要到处修改代码。
1.CAS Server 需要独立部署,主要负责对用户的认证工作;
2.CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。
单点登录的访问或者跨域问题
单点登录:单点登录是相互信任的系统模块登录一个模块后,其他模块不需要重复登录即认证通过。项目采用的是 CAS 单点登录框架完成的。首先 CAS 有两大部分。客户端和服务端。服务端就是一个 web 工程部署在 tomcat 中。在服务端完成用户认证操作。每次访问系统模块时,需要去 CAS 完成获取 ticket。当验证通过后,访问继续操作。对于 CAS 服务端来说,我们访问的应用模块就是 CAS 客户端。
跨域问题:首先明白什么是跨域。什么时候涉及跨域问题。当涉及前端异步请求的时候才涉及跨域。那什么是跨域呢?当异步请求时,访问的请求地址的协议、ip 地址、端口号任意一个与当前站点不同时, 就会涉及跨域访问。解决方案:1、jQuery 提供了 jsonp 实现 2、W3C 标准提供了 CORS(跨域资源共享) 解决方案。
单点登陆如果在另一台电脑上登陆并修改了密码怎么办?
单点登录系统(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。当用户第一次访问应用系统的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据 ticket;用户再访问别的应用的时候,就会将这个 ticket 带上,作为自己认证的凭据,应用系统接受到请求之后会把 ticket 送到认证系统进行校验,检查 ticket 的合法性。如果通过校验,用户就可以在不用再次登录的情况下访问应用系统 2 和应用系统 3 了。要实现 SSO,需要以下主要的功能:
所有应用系统共享一个身份认证系统:认证成功后,认证系统应该生成统一的认证标志(ticket),返还 给用户。另外,认证系统还应该对 ticket 进行效验,判断其有效性。
所有应用系统能够识别和提取 ticket 信息:要实现 SSO 的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对 ticket 进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。当用户在另一终端登陆并修改密码,则对应的ticket 附带的信息会发生改变,导致原有 ticket 因无法通过校验而失效。因此要求用户使用新的密码重新登陆。
锁
什么是死锁,怎么解决死锁,表级锁和行级锁,悲观锁与乐观锁以及线程同步锁区别
死锁:你去面试,面试官问你,你告诉我什么是死锁我就让你进公司。你回答说你让我进公司,我就告诉你什么是死锁。
死锁产生的原因:
系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
进程运行推进顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。
死锁的四个必要条件:
互斥条件:资源不能被共享,只能由一个进程使用。
请求与保持条件:进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
不可抢占条件:有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
循环等待条件:若干个进程形成环形链,每个都占用对方申请的下一个资源。
死锁的避免与预防:
死锁预防:破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
死锁避免:避免是指进程在每次申请资源时判断这些操作是否安全,例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
死锁检测:死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
死锁解除:这是与死锁检测结合使用的,它使用的方式就是剥夺。即将某进程所拥有的资源强行收回, 分配给其他的进程。
其他锁:
表级锁: 开销小,加锁快;不会出现死锁(因为 MyISAM 会一次性获得 SQL 所需的全部锁);锁定粒度大, 发生锁冲突的概率最高,并发度最低。
行级锁: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。通过 for update 来实现
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观
锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了观锁的一种实现方式 CAS 实现的。通过 version 版本字段来实现
同步锁:
场景:在开发中,遇到耗时的操作,我们需要把耗时的逻辑放入子线程中执行,防止卡顿。二个线程分别执行两个任务,同时执行完成,同时解析文件,获取数据后,同时插入数据库,由于插入的表比较多,这样容易出现插入错乱的 bug。
采用 synchronized:
声明该方法为同步方法,如果一个方法正在执行,别的方法调用,则处于等待状态。当这个方法执行完成后,可以调用解锁方法,wait():释放占有的对象锁,线程进入等待池。
区别:
synchronized 是在JVM 层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock 使用代码实现的,系统无法自动释放锁,需要在代码中 finally 子句中显式释放锁 lock.unlock();在并发量比较小的情况下,使用 synchronized 是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时 ReentrantLock 是个不错的方案。
Sql 层面的锁: 一、悲观锁
排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。
实现:大多数情况下依靠数据库的锁机制实现
一般使用 select …for update 对所选择的数据进行加锁处理,例如 select * from account where name=”Max” for update, 这条 sql 语句锁定了 account 表中所有符合检索条件(name=”Max”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
二、乐观锁
如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。
实现:大多数基于数据版本(Version)记录机制实现
具体可通过给表加一个版本号或时间戳字段实现,当读取数据时,将 version 字段的值一同读出, 数据每更新一次,对此 version 值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。
代码层面:
悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待, 直到锁被释放.
乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。
分布式锁的问题
针对分布式锁的实现,目前比较常用的有以下几种方案:
基于数据库实现分布式锁
基于缓存(redis,memcached,tair)实现分布式锁
基于 zookeeper 实现分布式锁
分布式锁(zookeeper,redis,数据库)如何实现
一、基于数据库实现的分布式锁基于表实现的分布式锁
CREATE TABLE methodLock
(
id
int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
method_name
varchar(64) NOT NULL DEFAULT ‘’ COMMENT ‘锁定的方法名’,
desc
varchar(1024) NOT NULL DEFAULT ‘备注信息’,
update_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘保存数据时间,自动生成’,
PRIMARY KEY (id
),
UNIQUE KEY uidx_method_name
(method_name
) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=‘锁定中的方法’;
当我们想要锁住某个方法时,执行以下 SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因为我们对 method_name 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql:
delete from methodLock where method_name =‘method_name’
上面这种简单的实现有以下几个问题:
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。
当然,我们也可以有其他方式解决上面的问题。
数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
非阻塞的?搞一个 while 循环,直到 insert 成功再返回成功。
非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁
基于排他锁实现的分布式锁
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于 MySql 的InnoDB 引擎,可以使用以下方法来实现加锁操作:
public boolean lock(){ connection.setAutoCommit(false); while(true){
try{
result = select * from methodLock where method_name=xxx for update; if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
在查询语句后面增加 for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){ connection.commit(); }
通过 connection.commit();操作来释放锁。这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。
问题:
阻塞锁? for update 语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。但是还是无法直接解决数据库单点、可重入和公平锁的问题。总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优点:直接借助数据库数据库实现分布式锁的缺点:
会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。
操作数据库需要一定的开销,性能问题需要考虑。
二、基于缓存的分布式锁
相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。目前有很多成熟的缓存产品,包括 Redis,memcached 等。这里以 Redis 为例来分析下使用缓存实现分布式锁的方案。基于 Redis 实现分布式锁在网上有很多相关文章,其中主要的实现方式是使用Jedis.setNX 方法来实现。
public boolean trylock(String key) {
ResultCode code = jedis.setNX(key, “This is a Lock.”); if (ResultCode.SUCCESS.equals(code))
return true; else
return false;
}
public boolean unlock(String key){ ldbTairManager.invalid(NAMESPACE, key);
}
以上实现方式同样存在几个问题:
单点问题。
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在 redis 中,其他线程无法再获得到锁。
这把锁只能是非阻塞的,无论成功还是失败都直接返回。
这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的 key 在 redis
中已经存在。无法再执行 setNX 操作。
这把锁是非公平的,所有等待的线程同时去发起 setNX 操作,运气好的线程能获取锁。
当然,同样有方式可以解决。现在主流的缓存服务都支持集群部署,通过集群来解决单点问题。
没有失效时间?redis 的 setExpire 方法支持传入失效时间,到达时间之后数据会自动删除。
非阻塞?while 重复执行。
非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。
非公平?在线程获取锁之前先把所有等待的线程放入一个队列中,然后按先进先出原则获取锁。
redis 集群的同步策略是需要时间的,有可能 A 线程 setNX 成功后拿到锁,但是这个值还没有更新到 B 线程执行 setNX 的这台服务器,那就会产生并发问题。redis 的作者 Salvatore Sanfilippo,提出了 Redlock 算法,该算法实现了比单一节点更安全、可靠的分布式锁管理(DLM)。Redlock 算法假设有 N 个 redis 节点,这些节点互相独立,一般设置为 N=5,这 N 个节点运行在不同的机器上以保持物理层面的独立。
算法的步骤如下:
客户端获取当前时间,以毫秒为单位。
客户端尝试获取 N 个节点的锁,(每个节点获取锁的方式和前面说的缓存锁一样),N 个节点以相同的key 和 value 获取锁。客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置 5-50ms。这样可以在有 redis 节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。
客户端计算在获得锁的时候花费了多少时间,方法是用当前时间减去在步骤一获取的时间,只有客户端获得了超过 3 个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
客户端获取的锁的时间为设置的锁超时时间减去步骤三计算出的获取锁花费时间。
如果客户端获取锁失败了,客户端会依次删除所有的锁。 使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。
但是,有一位分布式的专家写了一篇文章《How to do distributed locking》,质疑 Redlock 的正确性。该专家提到,考虑分布式锁的时候需要考虑两个方面:性能和正确性。如果使用高性能的分布式锁, 对正确性要求不高的场景下,那么使用缓存锁就足够了。如果使用可靠性高的分布式锁,那么就需要考虑严格的可靠性问题。而 Redlock 则不符合正确性。为什么不符合呢?专家列举了几个方面。现在很多编程语言使用的虚拟机都有 GC 功能,在 Full GC 的时候,程序会停下来处理 GC,有些时候 Full GC 耗时很长,甚至程序有几分钟的卡顿,文章列举了 HBase 的例子,HBase 有时候 GC 几分钟,会导致租约超时。而且 Full GC 什么时候到来,程序无法掌控,程序的任何时候都可能停下来处理 GC,比如下图,客户端 1 获得了锁,正准备处理共享资源的时候,发生了 Full GC 直到锁过期。这样,客户端 2 又获得了锁,开始处理共享资源。在客户端 2 处理的时候,客户端 1 Full GC 完成, 也开始处理共享资源,这样就出现了 2 个客户端都在处理共享资源的情况。给锁带上 token,token 就是 version 的概念,每次操作锁完成,token 都会加 1,在处理共享资源的时候带上 token,只有指定版本的 token 能够处理共享资源。
使用缓存实现分布式锁:
优点:性能好。
缺点:实现过于负责,需要考虑的因素太多。
基于 Zookeeper 实现的分布式锁
基于 zookeeper 临时有序节点可以实现的分布式锁。大致思想即为:每个客户端对某个方法加锁时, 在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。来看下Zookeeper 能不能解决前面提到的问题。
锁无法释放?使用 Zookeeper 可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在 ZK 中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session 连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
非阻塞锁?使用 Zookeeper 可以实现阻塞的锁,客户端可以通过在 ZK 中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper 会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
不可重入?使用 Zookeeper 也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一
下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
单点问题?使用 Zookeeper 可以有效的解决单点问题,ZK 是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
公平问题?使用 Zookeeper 可以解决公平锁问题,客户端在 ZK 中创建的临时节点是有序的,每次锁被释放时,ZK 可以通知最小节点来获取锁,保证了公平。
问题又来了,我们知道 Zookeeper 需要集群部署,会不会出现 Redis 集群那样的数据同步问题呢?
Zookeeper 是一个保证了弱一致性即最终一致性的分布式组件。采用称为 Quorum Based Protocol 的数据同步协议。假如 Zookeeper 集群有 N 台 Zookeeper 服务器(N 通常取奇数,3 台能够满足数据可靠性同时有很高读写性能,5 台在数据可靠性和读写性能方面平衡最好),那么用户的一个写操作,首先同步到 N/2 + 1 台服务器上,然后返回给用户,提示用户写成功。基于 Quorum Based Protocol 的数据同步协议决定了 Zookeeper 能够支持什么强度的一致性。在分布式环境下,满足强一致性的数据储存基本不存在,它要求在更新一个节点的数据,需要同步更新所有的节点。这种同步策略出现在主从同步复制的数据库中。但是这种同步策略,对写性能的影响太大而很少见于实践。因为Zookeeper 是同步写 N/2+1 个节点,还有 N/2 个节点没有同步更新,所以 Zookeeper 不是强一致性的。用户的数据更新操作,不保证后续的读操作能够读到更新后的值,但是最终会呈现一致性。牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可。
Zookeeper 是否满足因果一致性,需要看客户端的编程方式。不满足因果一致性的做法
A 进程向 Zookeeper 的/z 写入一个数据,成功返回
A 进程通知 B 进程,A 已经修改了/z 的数据
B 读取 Zookeeper 的/z 的数据
由于 B 连接的 Zookeeper 的服务器有可能还没有得到 A 写入数据的更新,那么 B 将读不到 A 写入的数据
满足因果一致性的做法
B 进程监听 Zookeeper 上/z 的数据变化
A 进程向 Zookeeper 的/z 写入一个数据,成功返回前,Zookeeper 需要调用注册在/z 上的监听器,Leader 将数据变化的通知告诉 B
B 进程的事件响应方法得到响应后,去取变化的数据,那么 B 一定能够得到变化的值
这里的因果一致性提现在 Leader 和 B 之间的因果一致性,也就是是 Leader 通知了数据有变化,第二种事件监听机制也是对 Zookeeper 进行正确编程应该使用的方法,所以,Zookeeper 应该是满足因果一致性的, 所以我们在基于 Zookeeper 实现分布式锁的时候,应该使用满足因果一致性的做法,即等待锁的线程都监听 Zookeeper 上锁的变化,在锁被释放的时候,Zookeeper 会将锁变化的通知告诉满足公平锁条件的等待线程。可以直接使用 zookeeper 第三方库客户端,这个客户端中封装了一个可重入的锁服务。