近期有很多同事面试,跟他们交流了下整理些基础的面试题
1. 因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
2. HashMap不是线程安全的,ConcurrentHashMap是线程安全的。
3. ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的。锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
4. ConcurrentHashMap具体是怎么实现线程安全的呢,肯定不可能是每个方法加synchronized,那样就变成了HashTable。
从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的Segment(类似HashTable),根据key.hashCode()来决定把key放到哪个HashTable中。
在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中:
ConcurrentHashMap中默认是把segments初始化为长度为16的数组。
(1)Thread1和Thread2先后进入Segment.put方法时,Thread1会首先获取到锁,可以进入,而Thread2则会阻塞在锁上:
**以上就是ConcurrentHashMap的工作机制,通过把整个Map分为N个Segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认提升16倍**
①、Iterator:迭代器
它是Java集合的顶层接口(不包括 map 系列的集合,Map接口 是 map 系列集合的顶层接口)
②、Collection:
List 接口和 Set 接口的父接口
③、List :
有序,可以重复的集合。
由于 List 接口是继承于 Collection 接口,所以基本的方法如上所示。
List 接口的三个典型实现:
a. List list1 = new ArrayList();
底层数据结构是数组,查询快,增删慢;线程不安全,效率高
b. List list2 = new Vector();
底层数据结构是数组,查询快,增删慢;线程安全,效率低,几乎已经淘汰了这个集合
c. List list3 = new LinkedList();
底层数据结构是链表,查询慢,增删快;线程不安全,效率高edis和秒杀、单点登陆、negix、dubbox
④、Set:
典型实现 HashSet()是一个无序,不可重复的集合
1、Set hashSet = new HashSet();
①、HashSet:不能保证元素的顺序;不可重复;不是线程安全的;集合元素可以为 NULL;
②、其底层其实是一个数组,存在的意义是加快查询速度。我们知道在一般的数组中,元素在数组中的索引位置是随机的,元素的取值和元素的位置之间不存在确定的关系,因此,在数组中查找特定的值时,需要把查找值和一系列的元素进行比较,此时的查询效率依赖于查找过程中比较的次数。而 HashSet 集合底层数组的索引和值有一个确定的关系:index=hash(value),那么只需要调用这个公式,就能快速的找到元素或者索引。
③、对于 HashSet: 如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同。
当向HashSet集合中存入一个元素时,HashSet会先调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值决定该对象在HashSet中的存储位置
1.1、如果 hashCode 值不同,直接把该元素存储到 hashCode() 指定的位置
1.2、如果 hashCode 值相同,那么会继续判断该元素和集合对象的 equals() 作比较
1.2.1、hashCode 相同,equals 为 true,则视为同一个对象,不保存在 hashSet()中
1.2.2、hashCode 相同,equals 为 false,则存储在之前对象同槽位的链表上,这非常麻烦,我们应该约束这种情况,即保证:如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同。
注意:每一个存储到 哈希 表中的对象,都得提供 hashCode() 和 equals() 方法的实现,用来判断是否是同一个对象
对于 HashSet 集合,我们要保证如果两个对象通过 equals() 方法返回 true,这两个对象的 hashCode 值也应该相同。
2、Set linkedHashSet = new LinkedHashSet();
①、不可以重复,有序
因为底层采用 链表 和 哈希表的算法。链表保证元素的添加顺序,哈希表保证元素的唯一性
3、Set treeSet = new TreeSet();
TreeSet:有序;不可重复,底层使用 红黑树算法,擅长于范围查询。
* 如果使用 TreeSet() 无参数的构造器创建一个 TreeSet 对象, 则要求放入其中的元素的类必须实现 Comparable 接口所以, 在其中不能放入 null 元素
* 必须放入同样类的对象.(默认会进行排序) 否则可能会发生类型转换异常.我们可以使用泛型来进行限制
*
以上三个 Set 接口的实现类比较:
共同点:1、都不允许元素重复
2、都不是线程安全的类,解决办法:Set set = Collections.synchronizedSet(set 对象)
⑤、Map:key-value 的键值对,key 不允许重复,value 可以
1、严格来说 Map 并不是一个集合,而是两个集合之间 的映射关系。
2、这两个集合没每一条数据通过映射关系,我们可以看成是一条数据。即 Entry(key,value)。Map 可以看成是由多个 Entry 组成。
3、因为 Map 集合即没有实现于 Collection 接口,也没有实现 Iterable 接口,所以不能对 Map 集合进行 for-each 遍历。
⑥、Map 和 Set 集合的关系
1、都有几个类型的集合。HashMap 和 HashSet ,都采 哈希表算法;TreeMap 和 TreeSet 都采用 红-黑树算法;LinkedHashMap 和 LinkedHashSet 都采用 哈希表算法和红-黑树算法。
2、分析 Set 的底层源码,我们可以看到,Set 集合 就是 由 Map 集合的 Key 组成。
arraylist初始创建参数没有指定长度时默认时长度为10
ArrayList 采用的是数组形式来保存对象的,这种方式将对象放在连续的位置中,所以最大的缺点就是插入删除时非常麻烦(删除麻烦)
LinkedList 采用的将对象存放在独立的空间中,而且在每个空间中还保存下一个链接的索引 但是缺点就是查找非常麻烦 要丛第一个索引开始(查找麻烦)
1、无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用 cookie 时将无法使用 cookie。
2、在存储的数据量方面:session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象。
Cookie本身是有大小和个数的限制.Session没有限制.Cookie的数据保存在客户端,Session数据保存在服务器端.
会话级别的Cookie:默认的Cookie.关闭浏览器Cookie就会销毁.
持久级别的Cookie:可以设置Cookie的有效时间.那么关闭浏览器Cookie还会存在. 手动销毁持久性Cookie. setMaxAge(0)---前提是有效路径必须一致.
session何时创建和销毁?作用范围:
* 创建:服务器端第一次调用getSession()创建session.
* 销毁:三种情况销毁session:
* 1.session过期. 默认过期时间为30分钟.
* 2.非正常关闭服务器.如果正常关闭session序列化到硬盘.
* 3.手动调用session.invalidate();
* 作用范围:多次请求.(一次会话)
4个特性:
一致性,持久性,原子性,隔离性
不考虑隔离性残生两类问题:
写问题:
读问题:
脏读-------解决方法设置隔离级别read committed(Oracle的默认隔离级别)
不可重复度--解决方法设置隔离级别repeatable read(mysql的默认隔离级别)
虚读-------解决方法设置隔离级别Serializable
事务的7种传播级别:
1) PROPAGATION_REQUIRED
(propagation_required ) ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
2)PROPAGATION_SUPPORTS
(propagation_supports),从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
3)PROPAGATION_MANDATORY
(propagation_mandatory), 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
4)PROPAGATION_REQUIRES_NEW
(propagation_require_new),从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。
5)PROPAGATION_NOT_SUPPORTED
(propagation_not_supported),这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。
6)PROPAGATION_NEVER
(propagation_never),该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
7)PROPAGATION_NESTED
(propagation_nested),字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
分布式事务一致性
方案一:补偿事务
添加用户的事务:
伪代码:
Int addUser(User user){
start transaction;
//调 dao 添加用户 异常 rollback return 0;
commit;
return 1;
}
//添加用户的补偿事务
int Compensate_addUser(User user){
//调 dao 删除新添加用户;
}
同理新增分数的逻辑
伪代码
Int addScore(int userId ,int Score){
start transaction;
//调 dao 给指定用户添加积分 异常 rollback return 0;
commit;
return 1;
}
//添加积分的补偿事务
int Compensate_addScore(int userId,int score){
//调 dao 删除给定 id 的积分
}
注册系统中的完整实现逻辑:
要保证添加用户和增加积分一致性,可能要写这样的代码:
int register(User user){
//执行第一个事务
Int flag =addUser( user);
If(flag=1){
//第一个添加用户的事务执行成功,执行第二个事务加积分!
flag=addScore(userid, score);
if(flag=1){
//第二个事务成功,则成功!
return 1;
}else{
//第二个事务执行失败,执行第一个事务的补偿事务
compensate_addUser(User user)
}
}
}
该方案的不足是:
(1)不同的业务要写不同的补偿事务,不具备通用性
(2)没有考虑补偿事务的失败
(3)如果业务流程很复杂,if/else 会嵌套非常多层
方案二:两阶段提交
单库操作是通过一个大事务来保证数据的一致性,如果分库操作的话就会变成多个小事务
如上例
Start Transaction;
addUser(User user) ; anyException rollback;
addScore(int userId, int socre); anyException rollback;
commit;
一个事务,分成执行与提交两个阶段,执行的时间其实是很长的,而 commit 的执行其实是很快的,于是整个执行过程的时间轴
第一个事务执行 200ms,
提交 1ms;
第二个事务执行 300ms,
提交 1ms;
那在什么时候系统出现问题,会出现不一致呢?
回答:第一个事务成功提交之后,最后一个事务成功提交之前,如果出现问题(例如服务器重启,数据库异常等),都可能导致数据不一致。
如果改变事务执行与提交的时序,变成事务先执行,最后一起提交,情况会变成什么样呢:
第一个事务执行 200ms;
第二个事务执行 300ms;
第一个事务提交 1ms;
第二个事务提交 1ms;
那在什么时候系统出现问题,会出现不一致呢?
问题的答案与之前相同:第一个事务成功提交之后,最后一个事务成功提交之前,如果出现问题(例如服务器重启,数据库异常等),都可能导致数据不一致。
这个变化的意义是什么呢?
方案一总执行时间是 502ms,最后 301ms 内出现异常都可能导致不一致;
方案二总执行时间也是 502ms,但最后 1ms 内出现异常才会导致不一致;
虽然没有彻底解决数据的一致性问题,但不一致出现的概率大大降低了!
事务提交后置降低了数据不一致的出现概率,会带来什么副作用呢?
回答:事务提交时会释放数据库的连接,第一种方案,第一个库事务提交,数据库连接就释放了,后置事务提交的方案,所有库的连接,要等到所有事务执行完才释放。这就意味着,数据库连接占用的时间增长了,系统整体的吞吐量降低了。
工厂模式(Factory Pattern)
定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。
使用场景:jdbc连接数据库,硬件访问,降低对象的产生和销毁
Builder(建造者模式):
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
使用场景:
● 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。
Decorator(装饰模式)
动态地给一个对象添加一些额外的职责。就扩展功能而言, 它比生成子类方式更为灵活。
使用场景:
● 需要扩展一个类的功能,或给一个类增加附加功能。
Singleton(单例模式)
保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式是最简单的设计模式之一,但是对于Java的开发者来说,它却有很多缺陷。在九月的专栏中,David Geary探讨了单例模式以及在面对多线程(multi-threading)、类装载器(class loaders)和序列化(serialization)时如何处理这些缺陷。
使用场景:
● 要求生成唯一序列号的环境;
Proxy(代理模式)
为其他对象提供一个代理以控制对这个对象的访问。
抽象工厂模式(Abstract Factory Pattern)
为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
使用场景:
一个对象族(或是一组没有任何关系的对象)都有相同的约束。
涉及不同操作系统的时候,都可以考虑使用抽象工厂模式
恶汉懒加载模式
存储层:存储引擎,列类型,范式规范
1. Mysql中主要的两种存储引擎: MyISAM 和 InnoDB
2. 如何选择mysql数据库的存储引擎呢?(即对MyISAM和InnoDB的选择)
1) 要求执行sql执行速度快,并且数据完整性要求不是很严格的情况下,选MyISAM.例如:cms(内容管理系统),论坛,贴吧,微博.
2) 数据完整性要求非常严格的,必须使用InnoDB引擎,例如:银行系统,网上商城
3) 具体的情况可以根据存储引擎的不同来具体选择.
3. 其他的引擎的介绍:
Memory:数据至于内存的存储引擎,拥有极高的插入,更新和查询的效率.但是会占用和数据量成正比的内存空间,并且内容会在重启之后消失.
Archive:归档存储引擎,只支持数据的查询和写入,经常用于存储写日志信息.
设计层(单台服务器):索引,缓存,分区(分表)
架构层:读写分离(主从复制)
SQL语句层:更合适的SQL语句
1. MyBatis是一个优秀的持久层框架,它对jdbc操作数据库的过程进行封装,使开发者只需要关注 SQL 本身
2. 执行流程:加载配置文件---产生sessionFactory--产生SQL session--加载Executor执行器---加载MappedStatement
1. Spring 体系结构:有核心容器、数据访问、Web、AOP、Test五大模块
2. Spring 优点:方便解耦,简化开发;AOP 编程的支持;声明式事务的支持等
3. DI:依赖注入,在 spring 创建对象的过程中,对象所依赖的属性通过配置注入对象中
4. IOC 控制反转,是指对象实例化权利由 spring 容器来管理
5. Aop面向切面编程,配置切点表达式
1、用户发送请求至前端控制器(DispatcherServlet
2、(DispatcherServlet)收到请求调用(HandlerMapping)处理器映射器。
3、处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给(DispatcherServlet)前端控制器。
4、(DispatcherServlet)前端控制器通过(HandlerAdapter)处理器适配器调用处理器
5、执行处理器(Controller,也叫后端控制器)。
6、(Controller)执行处理器执行完成返回(ModelAndView)
7、(HandlerAdapter)处理器适配器将controller执行处理器执行结果ModelAndView返回给(DispatcherServlet)前端控制器
8、(DispatcherServlet)前端控制器将ModelAndView传给(ViewReslover)视图解析器
9、(ViewReslover)视图解析器解析后返回具体View
10、(DispatcherServlet)前端控制器对View进行渲染视图(即将模型数据填充至视图中)。
11、(DispatcherServlet)前端控制器响应用户
1、springmvc的入口是一个servlet即前端控制器,而struts2入口是一个filter过虑器。
2、springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。
3、Struts2采用值栈存储请求和响应的数据,通过OGNL存取数据, springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。
1. 默认使用时会占电脑内存的1/64
2. 当使用超过虚拟机的内存60%时,虚拟机内存会扩容成电脑的1/4
3. 而电脑再发现虚拟机使用没有超过内存的30%时,会回退成原本的默认占电脑内存的1/64
按代的垃圾回收机制
新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC 。
老年代(Old generation):对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC。
持久代(Permanent generation)也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:
1、所有实例被回收
2、加载该类的ClassLoader 被回收
3、Class 对象无法通过任何途径访问(包括反射)
Spring Boot让我们的Spring应用变的更轻量化
为所有Spring开发者更快的入门
开箱即用,提供各种默认配置来简化项目配置
内嵌式容器简化Web项目
没有冗余代码生成和XML配置的要求
1. Redis通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
2. 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
3. Redis 优势:
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行
丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性
redis缓存 (缓存击穿,雪崩)解决方案
方案 1(后台刷新):
在缓存过期之前,通过后台线程或者 job 主动更新缓存。例如,缓存的过期时间为 30 分钟,而后台 job 则每隔 29 分钟执行一次(job 中查询出最新的数据并写入到缓存中)。这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定、cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。
方案 2(检查更新):
将缓存 key 的过期时间(绝对时间)也一起保存到缓存中(可以拼接,也可以加新字段,也可以采用单独的 key 保存,反正需要两者建立好关联关系)。在每次执行 get 操作后,都将 get 出来的缓存过期时间与当前系统时间做一个对比,如果发现缓存过期时间-当前系统时间<=1 分钟,则主动更新缓存。这样就能保证缓存中始终是最新的(和方案 A 的思路本质上一样,就是为了保证缓存“始终是最新的”且“永不过期”),不用担心缓存失效和一致性的问题。当然,这个 1 分钟只是举例,可以根据实际情况定义或者配置的。这种方案在特殊情况下也会有问题。假设缓存过期时间是 11:30 分,而 11:29到 11:30 这 1 分钟时间里恰好没有 get 请求过来,恰好请求都在 11:30 分的时候并发过来,那就悲剧了。这种情况比较极端,但并不是没有可能。因为“高并发”也可能是阶段性在某个时间点爆发。
方案 3(分级缓存):
分级缓存。采用 L1 和 L2 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。请求优先从 L1 缓存获取数据,如果 L1 缓存未命中则加锁,只有 1 个线程获取到锁,改线程从数据库中读取,再将数据 set 到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。
方案 D(互斥锁):
加锁等待。采用互斥锁的方式。注意,不能直接在缓存加载逻辑判断时直接采用 synchronize。使用互斥锁的方式来实现,本次采用 Java 中的 ReentranLock。在实际分布式场景中,可以使用 redis、tair、zookeeper 等提供的分布式锁来实现,这部分相对比较简单参考相应组件的锁机制实现就 ok 了。
angularJs的特点就是有mvc、双向绑定、di、模块化
1. 指令:
1.1 ng-model
ng-click
ng-app
ng-controller
ng-repeat
2. 控制器
1. //模块
var app = angluar.module{"模块名称",[]}
//控制器
app.controller("myc",function($scope){
//变量
$scope.variable={};
//方法
$scope.add=function(){
}
})
3. 自定义服务
4. 控制区继承
![](https://i.imgur.com/EAzdvOc.png)
1、什么是高并发
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
2. 高并发相关常用的一些指标有响应时(Response Time),吞吐量(Throughput),每秒查询率 QPS(Query Per Second),并发用户数等。
吞吐量:单位时间内处理的请求数量。
3. 如何提升系统的并发能力
互联网分布式架构设计,提高系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。
垂直扩展:提升单机处理能力。垂直扩展的方式又有两种:(不推荐)
(1)增强单机硬件性能,例如:增加 CPU 核数如 32 核,升
(2)提升单机架构性能,例如:使用 Cache 来减少 IO 次数,
水平扩展:只要增加服务器数量,就能线性扩充系统性能。
1)反向代理层的水平扩展
反向代理层的水平扩展,是通过“DNS 轮询”实现的:dns-server 对于一个域名配置了多个解析 ip,每次 DNS 解析请求来访问 dns-server,会轮询返回这些 ip。
当 nginx 成为瓶颈的时候,只要增加服务器数量,新增 nginx 服务的部署,增加一个外网 ip,就能扩展反向代理层的性能,做到理论上的无限高并发。
2)站点层的水平扩展
站点层的水平扩展,是通过“nginx”实现的。通过修改 nginx.conf,可以设置多个 web后端。
当 web 后端成为瓶颈的时候,只要增加服务器数量,新增 web 服务的部署,在 nginx配置中配置上新的 web 后端,就能扩展站点层的性能,做到理论上的无限高并发。
3)服务层的水平扩展
服务层的水平扩展,是通过“服务连接池”实现的。
站点层通过 RPC-client 调用下游的服务层 RPC-server 时,RPC-client 中的连接池会建立与下游服务多个连接,当服务成为瓶颈的时候,只要增加服务器数量,新增服务部署,在RPC-client 处建立新的下游服务连接,就能扩展服务层性能,做到理论上的无限高并发。如果需要优雅的进行服务层自动扩容,这里可能需要配置中心里服务自动发现功能的支持。
4)数据层的水平扩展
在数据量很大的情况下,数据层(缓存,数据库)涉及数据的水平扩展,将原本存储在一台服务器上的数据(缓存,数据库)水平拆分到不同服务器上去,以达到扩充系统性能的目的。
互联网数据层常见的水平拆分方式有这么几种,以数据库为例:
总结
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
提高系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。前者垂直扩展可以通过提升单机硬件性能,或者提升单机架构性能,来提高并发性,但单机性能总是有极限的,互联网分布式架构设计高并发终极解决方案还是后者:水平扩展。
互联网分层架构中,各层次水平扩展的实践又有所不同:
(1)反向代理层可以通过“DNS 轮询”的方式来进行水平扩展;
(2)站点层可以通过 nginx 来进行水平扩展;
(3)服务层可以通过服务连接池来进行水平扩展;
(4)数据库可以按照数据范围,或者数据哈希的方式来进行水平扩展;
各层实施水平扩展后,能够通过增加服务器数量的方式来提升系统的性能,做到理论上的性能无限。