GitHub地址:github.com/zaiyunduan1…,如果对你有帮助欢迎Star
- 数据库
- mysql
- redis
- java
- java虚拟机
- java并发
- java容器
- java基础
- 计算机网络
- 数据结构与算法
- 操作系统/Linux
- 设计模式
- 场景题和设计题
- 流行框架
数据库
mysql
为什么用自增列作为主键
-
如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择主键作为聚集索引、如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引、如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。
-
数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)
-
如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页
-
如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
为什么使用数据索引能提高效率
- 数据索引的存储是有序的
- 在有序的情况下,通过索引查询一个数据是无需遍历索引记录的
- 极端情况下,数据索引的查询效率为二分法查询效率,趋近于 log2(N)
B+树索引和哈希索引的区别
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互链接,是有序的
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可,是无序的哈希索引的优势:
- 等值查询。哈希索引具有绝对优势(前提是:没有大量重复键值,如果大量重复键值时,哈希索引的效率很低,因为存在所谓的哈希碰撞问题。)
哈希索引不适用的场景:
- 不支持范围查询
- 不支持索引完成排序
- 不支持联合索引的最左前缀匹配规则
通常,B+树索引结构适用于绝大多数场景,像下面这种场景用哈希索引才更有优势:
在HEAP表中,如果存储的数据重复度很低(也就是说基数很大),对该列数据以等值查询为主,没有范围查询、没有排序的时候,特别适合采用哈希索引,例如这种SQL:
select id,name from table where name='李明'; — 仅等值查询
复制代码
而常用的InnoDB引擎中默认使用的是B+树索引,它会实时监控表上索引的使用情况,如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”建立哈希索引(在InnoDB中默认开启自适应哈希索引),通过观察搜索模式,MySQL会利用index key的前缀建立哈希索引,如果一个表几乎大部分都在缓冲池中,那么建立一个哈希索引能够加快等值查询。
注意:在某些工作负载下,通过哈希索引查找带来的性能提升远大于额外的监控索引搜索情况和保持这个哈希表结构所带来的开销。但某些时候,在负载高的情况下,自适应哈希索引中添加的read/write锁也会带来竞争,比如高并发的join操作。like操作和%的通配符操作也不适用于自适应哈希索引,可能要关闭自适应哈希索引。
B树和B+树的区别
- B树,每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为nul,叶子结点不包含任何关键字信息。
- B+树,所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接,所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
为什么说B+比B树更适合实际应用中操作系统的文件索引和数据库索引?
-
B+的磁盘读写代价更低 B+的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
-
B+-tree的查询效率更加稳定 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
mysql联合索引
- 联合索引是两个或更多个列上的索引。对于联合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a 、 a,b 、 a,b,c 3种组合进行查找,但不支持 b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效。
- 利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知 道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处。
什么情况下应不建或少建索引
- 表记录太少
- 经常插入、删除、修改的表
- 数据重复且分布平均的表字段,假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。
- 经常和主字段一块查询但主字段索引值比较多的表字段
MySQL分区
什么是表分区?
表分区,是指根据一定规则,将数据库中的一张表分解成多个更小的,容易管理的部分。从逻辑上看,只有一张表,但是底层却是由多个物理分区组成。
表分区与分表的区别
分表:指的是通过一定规则,将一张表分解成多张不同的表。比如将用户订单记录根据时间成多个表。
分表与分区的区别在于:分区从逻辑上来讲只有一张表,而分表则是将一张表分解成多张表。
表分区有什么好处?
- 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。 2. 和单个磁盘或者文件系统相比,可以存储更多数据
- 优化查询。在where语句中包含分区条件时,可以只扫描一个或多个分区表来提高查询效率;涉及sum和count语句时,也可以在多个分区上并行处理,最后汇总结果。
- 分区表更容易维护。例如:想批量删除大量数据可以清除整个分区。
- 可以使用分区表来避免某些特殊的瓶颈,例如InnoDB的单个索引的互斥访问,ext3问价你系统的inode锁竞争等。
分区表的限制因素
- 一个表最多只能有1024个分区
- MySQL5.1中,分区表达式必须是整数,或者返回整数的表达式。在MySQL5.5中提供了非整数表达式分区的支持。
- 如果分区字段中有主键或者唯一索引的列,那么多有主键列和唯一索引列都必须包含进来。即:分区字段要么不包含主键或者索引列,要么包含全部主键和索引列。
- 分区表中无法使用外键约束
- MySQL的分区适用于一个表的所有数据和索引,不能只对表数据分区而不对索引分区,也不能只对索引分区而不对表分区,也不能只对表的一部分数据分区。
如何判断当前MySQL是否支持分区?
命令:show variables like '%partition%' 运行结果:
mysql> show variables like '%partition%'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | have_partitioning | YES | +-------------------+-------+ 1 row in set (0.00 sec) have_partintioning 的值为YES,表示支持分区。
MySQL支持的分区类型有哪些?
- RANGE分区: 这种模式允许将数据划分不同范围。例如可以将一个表通过年份划分成若干个分区
- LIST分区: 这种模式允许系统通过预定义的列表的值来对数据进行分割。按照List中的值分区,与RANGE的区别是,range分区的区间范围值是连续的。
- HASH分区 :这中模式允许通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区。例如可以建立一个对表主键进行分区的表。
- KEY分区 :上面Hash模式的一种延伸,这里的Hash Key是MySQL系统产生的。
四种隔离级别
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
- Repeatable read (可重复读):可避免脏读、不可重复读的发生。
- Read committed (读已提交):可避免脏读的发生。
- Read uncommitted (读未提交):最低级别,任何情况都无法保证。
关于MVVC
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,现阶段几乎所有的RDBMS,都支持了MVCC。
- LBCC:Lock-Based Concurrency Control,基于锁的并发控制。
- MVCC:Multi-Version Concurrency Control,基于多版本的并发控制协议。纯粹基于锁的并发机制并发量低,MVCC是在基于锁的并发控制上的改进,主要是在读操作上提高了并发量。
在MVCC并发控制中,读操作可以分成两类:
- 快照读 (snapshot read):读取的是记录的可见版本 (有可能是历史版本),不用加锁(共享读锁s锁也不加,所以不会阻塞其他事务的写)。
- 当前读 (current read):读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
行级锁定的优点:
- 当在许多线程中访问不同的行时只存在少量锁定冲突。
- 回滚时只有少量的更改
- 可以长时间锁定单一的行。
行级锁定的缺点:
- 比页级或表级锁定占用更多的内存。
- 当在表的大部分中使用时,比页级或表级锁定速度慢,因为你必须获取更多的锁。
- 如果你在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其它锁定明显慢很多。
- 用高级别锁定,通过支持不同的类型锁定,你也可以很容易地调节应用程序,因为其锁成本小于行级锁定。
MySQL 触发器简单实例
- CREATE TRIGGER <触发器名称> --触发器必须有名字,最多64个字符,可能后面会附有分隔符.它和MySQL中其他对象的命名方式基本相象.
- { BEFORE | AFTER } --触发器有执行的时间设置:可以设置为事件发生前或后。
- { INSERT | UPDATE | DELETE } --同样也能设定触发的事件:它们可以在执行insert、update或delete的过程中触发。
- ON <表名称> --触发器是属于某一个表的:当在这个表上执行插入、 更新或删除操作的时候就导致触发器的激活. 我们不能给同一张表的同一个事件安排两个触发器。
- FOR EACH ROW --触发器的执行间隔:FOR EACH ROW子句通知触发器 每隔一行执行一次动作,而不是对整个表执行一次。
- <触发器SQL语句> --触发器包含所要触发的SQL语句:这里的语句可以是任何合法的语句, 包括复合语句,但是这里的语句受的限制和函数的一样。
什么是存储过程
简单的说,就是一组SQL语句集,功能强大,可以实现一些比较复杂的逻辑功能,类似于JAVA语言中的方法;
ps:存储过程跟触发器有点类似,都是一组SQL集,但是存储过程是主动调用的,且功能比触发器更加强大,触发器是某件事触发后自动调用;
有哪些特性
- 有输入输出参数,可以声明变量,有if/else, case,while等控制语句,通过编写存储过程,可以实现复杂的逻辑功能;
- 函数的普遍特性:模块化,封装,代码复用;
- 速度快,只有首次执行需经过编译和优化步骤,后续被调用可以直接执行,省去以上步骤;
DROP PROCEDURE IF EXISTS `proc_adder`;
DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int)
BEGIN
#Routine body goes here...
DECLARE c int;
if a is null then set a = 0;
end if;
if b is null then set b = 0;
end if;
set sum = a + b;
END
;;
DELIMITER ;
set @b=5;
call proc_adder(0,@b,@s);
SELECT @s as sum;
create table tab2(
tab2_id varchar(11)
);
DROP TRIGGER if EXISTS t_ai_on_tab1;
create TRAILING t_ai_on_tab1
AFTER INSERT ON tab1
for EACH ROW
BEGIN
INSERT INTO tab2(tab2_id) values(new.tab1_id);
end;
INSERT INTO tab1(tab1_id) values('0001');
SELECT * FROM tab2;
复制代码
MySQL优化
- 开启查询缓存,优化查询
- explain你的select查询,这可以帮你分析你的查询语句或是表结构的性能瓶颈。EXPLAIN 的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的
- 当只要一行数据时使用limit 1,MySQL数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据
- 为搜索字段建索引
- 使用 ENUM 而不是 VARCHAR,如果你有一个字段,比如“性别”,“国家”,“民族”,“状态”或“部门”,你知道这些字段的取值是有限而且固定的,那么,你应该使用 ENUM 而不是VARCHAR。
- Prepared Statements Prepared Statements很像存储过程,是一种运行在后台的SQL语句集合,我们可以从使用 prepared statements 获得很多好处,无论是性能问题还是安全问题。Prepared Statements 可以检查一些你绑定好的变量,这样可以保护你的程序不会受到“SQL注入式”攻击
- 垂直分表
- 选择正确的存储引擎
key和index的区别
- key 是数据库的物理结构,它包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的)。包括primary key, unique key, foreign key 等
- index是数据库的物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引等;
Mysql 中 MyISAM 和 InnoDB 的区别有哪些?
区别:
-
InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
-
InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
-
InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
-
InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
-
Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;
如何选择:
-
是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
-
如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读写也挺频繁,请使用InnoDB。
-
系统奔溃后,MyISAM恢复起来更困难,能否接受;
-
MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
数据库表创建注意事项
一、字段名及字段配制合理性
-
剔除关系不密切的字段
-
字段命名要有规则及相对应的含义(不要一部分英文,一部分拼音,还有类似a.b.c这样不明含义的字段)
-
字段命名尽量不要使用缩写(大多数缩写都不能明确字段含义)
-
字段不要大小写混用(想要具有可读性,多个英文单词可使用下划线形式连接)
-
字段名不要使用保留字或者关键字
-
保持字段名和类型的一致性
-
慎重选择数字类型
-
给文本字段留足余量
二、系统特殊字段处理及建成后建议
-
添加删除标记(例如操作人、删除时间)
-
建立版本机制
三、表结构合理性配置
-
多型字段的处理,就是表中是否存在字段能够分解成更小独立的几部分(例如:人可以分为男人和女人)
-
多值字段的处理,可以将表分为三张表,这样使得检索和排序更加有调理,且保证数据的完整性!
四、其它建议
-
对于大数据字段,独立表进行存储,以便影响性能(例如:简介字段)
-
使用varchar类型代替char,因为varchar会动态分配长度,char指定长度是固定的。
-
给表创建主键,对于没有主键的表,在查询和索引定义上有一定的影响。
-
避免表字段运行为null,建议设置默认值(例如:int类型设置默认值为0)在索引查询上,效率立显!
-
建立索引,最好建立在唯一和非空的字段上,建立太多的索引对后期插入、更新都存在一定的影响(考虑实际情况来创建)。
redis
redis单线程问题
单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。
为什么说redis能够快速执行
- 绝大部分请求是纯粹的内存操作(非常快速)
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 非阻塞IO - IO多路复用
redis的内部实现
内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,不在io上浪费一点时间 这3个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能很差。redis为特殊的场景选择了合适的技术方案。
Redis关于线程安全问题
redis实际上是采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个redis操作的复合操作来说,依然需要锁,而且有可能是分布式锁。
使用redis有哪些好处?
- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 支持丰富数据类型,支持string,list,set,sorted set,hash
- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
redis相比memcached有哪些优势?
- memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
- redis的速度比memcached快很多
- redis可以持久化其数据
- Redis支持数据的备份,即master-slave模式的数据备份。
- 使用底层模型不同,它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- value大小:redis最大可以达到1GB,而memcache只有1MB
Redis主从复制
过程原理:
- 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
- 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
- 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
- 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
- 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
缺点:所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压力太大,使用主从从结构来解决
redis两种持久化方式的优缺点
- RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)
- AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。
- Redis 还可以同时使用 AOF 持久化和 RDB 持久化。当redis重启时,它会有限使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更加完整
RDB的优点:
-
RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。
-
RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。
-
RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
-
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
Redis 常见的性能问题都有哪些?如何解决?
- Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
- Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
redis 提供 6种数据淘汰策略
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
java
java虚拟机
图片来源(Hollis微信公众号)什么时候会触发full gc
- System.gc()方法的调用
- 老年代空间不足
- 永生区空间不足(JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据)
- GC时出现promotion failed和concurrent mode failure
- 统计得到的Minor GC晋升到旧生代平均大小大于老年代剩余空间
- 堆中分配很大的对象
可以作为root的对象:
- 类中的静态变量,当它持有一个指向一个对象的引用时,它就作为root
- 活动着的线程,可以作为root
- 一个Java方法的参数或者该方法中的局部变量,这两种对象可以作为root
- JNI方法中的局部变量或者参数,这两种对象可以作为root
例子:下述的Something和Apple都可以作为root对象。
public AClass{
public static Something;
public static final Apple;
''''''
}
复制代码
Java方法的参数和方法中的局部变量,可以作为root.
public Aclass{
public void doSomething(Object A){
ObjectB b = new ObjectB;
}
}
复制代码
新生代转移到老年代的触发条件
- 长期存活的对象
- 大对象直接进入老年代
- minor gc后,survivor仍然放不下
- 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代
G1和CMS的区别
- G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。
- CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
双亲委派模型中有哪些方法。用户如何自定义类加载器 。怎么打破双亲委托机制
- 双亲委派模型中用到的方法:
- findLoadedClass(),
- loadClass()
- findBootstrapClassOrNull()
- findClass()
- defineClass():把二进制数据转换成字节码。
- resolveClass()
自定义类加载器的方法:继承 ClassLoader 类,重写 findClass()方法 。
- 继承ClassLoader覆盖loadClass方法 原顺序
- findLoadedClass
- 委托parent加载器加载(这里注意bootstrap加载器的parent为null)
- 自行加载 打破委派机制要做的就是打乱2和3的顺序,通过类名筛选自己要加载的类,其他的委托给parent加载器。
即时编译器的优化方法
字节码可以通过以下两种方式转换成合适的语言:
- 解释器
- 即时编译器 即时编译器把整段字节码编译成本地代码,执行本地代码比一条一条进行解释执行的速度快很多,因为本地代码是保存在缓存里的
编译过程的五个阶段
- 第一阶段:词法分析
- 第二阶段:语法分析
- 第三阶段:词义分析与中间代码产生
- 第四阶段:优化
- 第五阶段:目标代码生成
java应用系统运行速度慢的解决方法
问题解决思路:
- 查看部署应用系统的系统资源使用情况,CPU,内存,IO这几个方面去看。找到对就的进程。
- 使用jstack,jmap等命令查看是JVM是在在什么类型的内存空间中做GC(内存回收),和查看GC日志查看是那段代码在占用内存。 首先,调节内存的参数设置,如果还是一样的问题,就要定位到相应的代码。
- 定位代码,修改代码(一般是代码的逻辑问题,或者代码获取的数据量过大。)
内存溢出是什么,什么原因导致的
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。为了解决Java中内存溢出问题,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用GC函数来释放内存,因为不同的JVM实现者可能使用不同的算法管理GC,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是中断式执行GC。但GC只能回收无用并且不再被其它对象引用的那些对象所占用的空间。Java的内存垃圾回收机制是从程序的主要运行对象开始检查引用链,当遍历一遍后发现没有被引用的孤立对象就作为垃圾回收。
引起内存溢出的原因有很多种,常见的有以下几种:
-
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
-
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
-
代码中存在死循环或循环产生过多重复的对象实体;
-
使用的第三方软件中的BUG;
-
启动参数内存值设定的过小;
内存溢出的解决
内存溢出虽然很棘手,但也有相应的解决办法,可以按照从易到难,一步步的解决。
第一步,就是修改JVM启动参数,直接增加内存。这一点看上去似乎很简单,但很容易被忽略。JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“OutOfMemory”错误。因此,-Xms,-Xmx参数一定不要忘记加。
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。在一个项目中,使用两个数据库连接,其中专用于发送短信的数据库连接使用DBCP连接池管理,用户为不将短信发出,有意将数据库连接用户名改错,使得日志中有许多数据库连接异常的日志,一段时间后,就出现“OutOfMemory”错误。经分析,这是由于DBCP连接池BUG引起的,数据库连接不上后,没有将连接释放,最终使得DBCP报“OutOfMemory”错误。经过修改正确数据库连接参数后,就没有再出现内存溢出的错误。
查看日志对于分析内存溢出是非常重要的,通过仔细查看日志,分析内存溢出前做过哪些操作,可以大致定位有问题的模块。
第三步,找出可能发生内存溢出的位置。重点排查以下几点:
-
检查代码中是否有死循环或递归调用。
-
检查是否有大循环重复产生新对象实体。
-
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
-
检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况。某个项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,用上面三个步骤解决不了,这就需要使用内存查看工具了。
内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线,如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。
通过以上四个步骤的分析与处理,基本能处理内存溢出的问题。当然,在这些过程中也需要相当的经验与敏感度,需要在实际的开发与调试过程中不断积累。
总体上来说,产生内存溢出是由于代码写的不好造成的,因此提高代码的质量是最根本的解决办法。有的人认为先把功能实现,有BUG时再在测试阶段进行修正,这种想法是错误的。正如一件产品的质量是在生产制造的过程中决定的,而不是质量检测时决定的,软件的质量在设计与编码阶段就已经决定了,测试只是对软件质量的一个验证,因为测试不可能找出软件中所有的BUG。
java并发
JAVA 线程状态转换图示
synchronized 的底层怎么实现
- 同步代码块(Synchronization)基于进入和退出管程(Monitor)对象实现。每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
-
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
-
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
-
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
- 被 synchronized 修饰的同步方法并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成
讲一下CAS
CAS,compare and swap的缩写,中文翻译成比较并交换。乐观锁用到的机制就是CAS,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试。
原理:
- CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
JDK文档说cas同时具有volatile读和volatile写的内存语义。
缺点:
-
ABA问题。 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化
-
循环时间长开销大。 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
-
只能保证一个共享变量的原子操作。 对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
线程池
Executor线程池框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
ThreadPoolExecutor执行的策略
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePools,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常
新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略
常见四种线程池
- CachedThreadPool():可缓存线程池。
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程
- 一定程序减少频繁创建/销毁线程,减少系统开销
- FixedThreadPool():定长线程池。
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
- ScheduledThreadPool():定时线程池。
- 支持定时及周期性任务执行。
- SingleThreadExecutor():单线程化的线程池。
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
四种拒绝策略
- AbortPolicy:拒绝任务,且还抛出RejectedExecutionException异常,线程池默认策略
- CallerRunPolicy:拒绝新任务进入,如果该线程池还没有被关闭,那么这个新的任务在执行线程中被调用
- DiscardOldestPolicy: 如果执行程序尚未关闭,则位于头部的任务将会被移除,然后重试执行任务(再次失败,则重复该过程),这样将会导致新的任务将会被执行,而先前的任务将会被移除。
- DiscardPolicy:没有添加进去的任务将会被抛弃,也不抛出异常。基本上为静默模式。
为什么要用线程池
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
- 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
对象锁和静态锁之间的区别
- 对象锁用于对象实例方法,
- 类锁用于类的静态方法或一个类的class对象。
- 类的对象实例可以有很多,不同对象实例的对象锁互不干扰,而每个类只有一个类锁
简述volatile字
两个特性
- 保证了不同线程对这个变量进行 读取 时的可见性,即一个线程修改 了某个变量的值 , 这新值对其他线程来说是立即可见的 。(volatile 解决了 线程间 共享变量
- 禁止进行指令重排序 ,阻止编译器对代码的优化
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,锁保证了原子性,而volatile保证可见性和有序性
happens-before 原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在 后面的操作
- 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
- volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以 得出操作 A 先行发生于操作 C
- 线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测 到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 T hread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始
Lock 和synchronized 的区别
-
Lock 是一个 接口,而 synchronized 是 Java 中的 关键字, synchronized 是 内置的语言实现;
-
synchronized 在 发生异常时,会 自动释放线程占有的锁,因此 不会导 致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放 锁,则很 可能造成死锁现象,因此用 使用 Lock 时需要在 finally 块中释放锁;
-
Lock 可以让 等待锁的线程响应中断 (可中断锁),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去, 不能够响应中 断 (不可中断锁);
-
通过 Lock 可以知道 有没有成功获取锁 (tryLock ( ) 方法 : 如果获取 了锁 ,回 则返回 true ;回 否则返回 false e, , 也就说这个方法无论如何都会立即返回 。 在拿不到锁时不会一直在那等待。 ),而 synchronized 却无法办到。
-
Lock 可以提高 多个线程进行读操作的效率( 读写锁)。
-
Lock 可以实现 公平锁,synchronized 不保证公平性。 在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 于 synchronized。所以说,在具体使用时要根据适当情况选择。
ThreadLocal(线程变量副本)
Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。 采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。 ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。 ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。 Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
通过Callable和Future创建线程
Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。
Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,我们必须等待它返回的结果。java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
什么叫守护线程,用什么方法实现守护线程(Thread.setDeamon()的含义)
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。 JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的,主要的作用是提供计数等等辅助的功能。
如何停止一个线程?
终止线程的三种方法:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 在定义退出标志exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,
thread.exit = true; // 终止线程thread
复制代码
-
使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。 使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程: thread.stop(); 虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
-
使用interrupt方法中断线程,使用interrupt方法来终端线程可分为两种情况:
- 线程处于阻塞状态,如使用了sleep方法。
- 使用while(!isInterrupted()){……}来判断线程是否被中断。 在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。
注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。
什么是线程安全?什么是线程不安全?
- 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
- 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据 在多线程的情况下,由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
java容器
HashSet和TreeSet区别
HashSet
- 不能保证元素的排列顺序,顺序有可能发生变化
- 不是同步的
- 集合元素可以是null,但只能放入一个null 当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
TreeSet
- TreeSet是SortedSet接口的唯一实现类
- TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象
讲一下LinkedHashMap
LinkedHashMap的实现就是HashMap+LinkedList的实现方式,以HashMap维护数据结构,以LinkList的方式维护数据插入顺序
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。 在遍历的时候会比HashMap慢TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器
利用LinkedHashMap实现LRU算法缓存(
- LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致
- LinkedList提供了一个boolean值可以让用户指定是否实现LRU)
Java8 中HashMap的优化(引入红黑树的数据结构和扩容的优化)
- if (binCount >= TREEIFY_THRESHOLD - 1) 当符合这个条件的时候,把链表变成treemap红黑树,这样查找效率从o(n)变成了o(log n) ,在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:
- 我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置
这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化: hashMap 1.8 哈希算法例图2
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”Map遍历的keySet()和entrySet()性能差异原因
Set> entrySet = map.entrySet();
Set set = map.keySet();`
复制代码
- keySet()循环中通过key获取对应的value的时候又会调用getEntry()进行循环。循环两次
- entrySet()直接使用getEntry()方法获取结果,循环一次
- 所以 keySet()的性能会比entrySet()差点。所以遍历map的话还是用entrySet()来遍历
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
复制代码
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
复制代码
java基础
抽象类和接口的对比
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口是完全不同的类型 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。你不可以使用其它修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。 |
多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
创建一个类的几种方法?
- 使用new关键字 → 调用了构造函数
- 使用Class类的newInstance方法 → 调用了构造函数
Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();
复制代码
- 使用Constructor类的newInstance方法 → 调用了构造函数
Constructor constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
复制代码
- 使用clone方法 → 没有调用构造函数
- 使用反序列化 }→ 没有调用构造函数
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();
复制代码
Redirect和forward
-
上图所示的间接转发请求的过程如下: 浏览器向Servlet1发出访问请求; Servlet1调用sendRedirect()方法,将浏览器重定向到Servlet2; 浏览器向servlet2发出请求; 最终由Servlet2做出响应。
-
上图所示的直接转发请求的过程如下: 浏览器向Servlet1发出访问请求; Servlet1调用forward()方法,在服务器端将请求转发给Servlet2; 最终由Servlet2做出响应。
什么是泛型,为什么要使用以及类型擦除。
- 泛型的本质就是“参数化类型”,也就是说所操作的数据类型被指定为一个参数。 创建集合时就指定集合元素的数据类型,该集合只能保存其指定类型的元素, 避免使用强制类型转换。
- Java 编译器生成的字节码是不包含泛型信息的,泛型类型信息将在 编译处理 时 被擦除,这个过程即 类型擦除。类型擦除可以简单的理解为将泛型 java 代码转 换为普通 java 代码,只不过编译器更直接点,将泛型 java 代码直接转换成普通 java 字节码。
类型擦除的主要过程如下:
- 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
- 移除所有的类型参数。
Object跟这些标记符代表的java类型有啥区别呢?
Object是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。
Error类和Exception类区别
- Error类和Exception类的父类都是throwable类,他们的区别是: Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。 Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行, 而不应该随意终止异常。
- Exception类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。
throw和throws区别
throw:(针对对象的做法) 抛出一个异常,可以是系统定义的,也可以是自己定义的
public void yichang(){
NumberFormatException e = new NumberFormatException();
throw e;
}
复制代码
throws:(针对一个方法抛出的异常) 抛出一个异常,可以是系统定义的,也可以是自己定义的。
public void yichang() throws NumberFormatException{
int a = Integer.parseInt("10L");
}
复制代码
- throws出现在方法函数头;而throw出现在函数体。
- throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常。
- 两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
.class 文件是什么类型文件
class文件是一种8位字节的二进制流文件
java中序列化之子类继承父类序列化
父类实现了Serializable,子类不需要实现Serializable
相关注意事项 1. 序列化时,只对对象的状态进行保存,而不管对象的方法; 2. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口; c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化; 3. 并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如: 1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。 2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现。
2,反过来父类未实现Serializable,子类实现了,序列化子类实例的时候,父类的属性是直接被跳过不保存,还是能保存但不能还原?(答案:值不保存)
解:父类实现接口后,所有派生类的属性都会被序列化。子类实现接口的话,父类的属性值丢失。
java中序列化之子类继承父类序列化
标识符
标识符可以包括这4种字符:字母、下划线、$、数字;开头不能是数字;不能是关键字
Integer i=new Integer(127);和Integer i=127;的区别
Integer i = 127的时候,使用Java常量池技术,是为了方便快捷地创建某些对象,当你需要一个对象时候,就去这个池子里面找,找不到就在池子里面创建一个。但是必须注意 如果对象是用new 创建的。那么不管是什么对像,它是不会放到池子里的,而是向堆申请新的空间存储。Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127之间的数时才可使用对象池。超过了就要申请空间创建对象了
int i1=128;
Integer i2=128;
Integer i3=new Integer(128);//自动拆箱
System.out.println(i1==i2);//true
System.out.println(i1==i3);//true
Integer i5=127;
Integer i6=127;
System.out.println(i5==i6);//true
Integer i5=127;
Integer ii5=new Integer(127);
System.out.println(i5==ii5);//false
Integer i7=new Integer(127);
Integer i8=new Integer(127);
System.out.println(i7==i8);//false
复制代码
手写单例模式
最好的单例模式是静态内部类,不要写双重检验
private static class LazySomethingHolder {
public static Something something = new Something();
}
public static Something getInstance() {
return LazySomethingHolder.something;
}
复制代码
为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?
Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法
Java中wait 和sleep 方法比较
-
这两个方法来自不同的类分别是Thread和Object
-
最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
-
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
-
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
-
sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
- 注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程
- wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生
hashCode和equals方法的关系
在有些情况下,程序设计者在设计一个类的时候为需要重写equals方法,比如String类,但是千万要注意,在重写equals方法的同时,必须重写hashCode方法。 也就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等; 如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同; 如果两个对象的hashcode值不等,则equals方法得到的结果必定为false; 如果两个对象的hashcode值相等,则equals方法得到的结果未知。
Object类中有哪些方法,列举3个以上(可以引导)
Object方法:equals()、toString()、finalize()、hashCode()、getClass()、clone()、wait()、notify()、notifyAll()
String s=new String("xyz")究竟创建String Object分为两种情况:
- 如果String常理池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象new String("xyz");
- 如果String常理池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象new String("xyz")。
什么是值传递和引用传递
值传递
public class TempTest {
private void test1(int a) {
a = 5;
System.out.println("test1方法中的a=" + a);
}
public static void main(String[] args) {
TempTest t = new TempTest();
int a = 3;
t.test1(11);
System.out.println("main方法中a=" + a);
}
}
复制代码
test1方法中的a=5 main方法中a=3 值传递:传递的是值的拷贝,传递后就互不相关了 引用传递:传递的是变量所对应的内存空间的地址
public class TempTest {
private void test1(A a) {
a.age = 20;
System.out.println("test1方法中a=" + a.age);
}
public static void main(String[] args) {
TempTest t = new TempTest();
A a = new A();
a.age = 10;
t.test1(a);
System.out.println("main方法中a=" + a.age);
}
}
class A {
public int age = 0;
}
复制代码
test1方法中a=20 main方法中a=20 传递前和传递后都指向同一个引用(同一个内存空间) 如果不互相影响,方法是在test1方法里面新new一个实例就可以了
讲一下netty
netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件
Nio的原理(同步非阻塞)
服务端和客户端各自维护一个管理通道的对象,我们称之为 selector,该对 象能检测一个或多个通道(channel)上的事件。我们以服务端为例,如果服务 端的 selector 上注册了读事件,某时刻客户端给服务端送了一些数据,阻塞 I/O 这时会调用 read()方法阻塞地读取数据,而 NIO 的服务端会在 selector 中添加 一个读事件。服务端的处理线程会轮询地访问 selector,如果访问 selector 时发 现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处 理线程会一直阻塞直到感兴趣的事件到达为止。
缓冲区Buffer、通道Channel、选择器Selector
缓冲区Buffer
- 缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。
通道Channel
- 通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不同之处在于 通道是双向 的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类,比如 InputStream 只能进行读取操作,OutputStream 只能进行写操作),而通道是双向的,可以用于读、写或者同时用于读写。
选择器(Selector )
- NIO 有一个主要的类 Selector,这个类似一个观察者,只要我们把需要探知 的 socketchannel 告诉 Selector,我们接着做别的事情, 当有事件发生时,他会 通知我们,传回一组 SelectionKey, 我们读取这些 Key, 就会获得我们刚刚注册 过的 socketchannel, 然后,我们从这个 Channel 中读取数据,放心,包准能 够读到,接着我们可以处理这些数据。
- Selector 内部原理实际是在做一个 对所注册的 channel 的轮询访问,不断 地轮询,一旦轮询到一个 channel 有所注册的事情发生,比如数据来了,他就 会站起来报告, 交出一把钥匙,让我们 通过这把钥匙来读取这个 channel 的内 容。
BIO和NIO的区别
- BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO的selector作用
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
为了实现Selector管理多个SocketChannel,必须将具体的SocketChannel对象注册到Selector,并声明需要监听的事件(这样Selector才知道需要记录什么数据),一共有4种事件:
- connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
- accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
- read:读事件,对应值为SelectionKey.OP_READ(1)
- write:写事件,对应值为SelectionKey.OP_WRITE(4)
每次请求到达服务器,都是从connect开始,connect成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。
所以,当SocketChannel有对应的事件发生时,Selector都可以观察到,并进行相应的处理。
计算机网络
GET 和 POST 区别
(GET) 请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的: /test/demo_form.asp?name1=value1&name2=value2
-
GET 请求可被缓存
-
GET 请求保留在浏览器历史记录中
-
GET 请求可被收藏为书签
-
GET 请求不应在处理敏感数据时使用
-
GET 请求有长度限制
-
GET 请求只应当用于取回数据 POST 方法 (POST) 请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的: POST /test/demo_form.asp HTTP/1.1 Host: w3schools.com name1=value1&name2=value2
-
POST 请求不会被缓存
-
POST 请求不会保留在浏览器历史记录中
-
POST 不能被收藏为书签
-
POST 请求对数据长度没有要求
dns使用的协议
既使用TCP又使用UDP
- 首先了解一下TCP与UDP传送字节的长度限制:
- UDP报文的最大长度为512字节,而TCP则允许报文长度超过512字节。当DNS查询超过512字节时,协议的TC标志出现删除标志,这时则使用TCP发送。通常传统的UDP报文一般不会大于512字节。
- 区域传送时使用TCP,主要有一下两点考虑:
- 辅域名服务器会定时(一般时3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。
- TCP是一种可靠的连接,保证了数据的准确性。
- 域名解析时使用UDP协议:
- 客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。虽然从理论上说,客户端也可以指定向DNS服务器查询的时候使用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。
幂等
一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.
Cookies和session区别
-
Cookies是一种能够让网站服务器把少量数据储存到客户端的硬盘或内存,或是从客户端的硬盘读取数据的一种技术。Cookies是当你浏览某网站时,由Web服务器置于你硬盘上的一个非常小的文本文件,它可以记录你的用户ID、密码、浏览过的网页、停留的时间等信息。 session: 当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。 cookie机制:采用的是在客户端保持状态的方案,而session机制采用的是在服务端保持状态的方案。同时我们看到由于服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助cookie机制来达到保存标识的目的。
-
Session是服务器用来跟踪用户的一种手段,每个Session都有一个唯一标识:session ID。当服务器创建了Session时,给客户端发送的响应报文包含了Set-cookie字段,其中有一个名为sid的键值对,这个键值Session ID。客户端收到后就把Cookie保存浏览器,并且之后发送的请求报表都包含SessionID。HTTP就是通过Session和Cookie这两个发送一起合作来实现跟踪用户状态,Session用于服务端,Cookie用于客户端
TCP粘包和拆包产生的原因
- 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
- 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度
- 以太网的payload大于MTU进行IP分片。MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成托干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上。
TCP粘包和拆包的解决策略
- 消息定长。例如100字节。
- 在包尾部增加回车或者空格符等特殊字符进行分割,典型的如FTP协议
- 将消息分为消息头和消息尾。
- 其它复杂的协议,如RTMP协议等。
三次握手
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据
四次挥手
- 客户端先发送FIN,进入FIN_WAIT1状态
- 服务端收到FIN,发送ACK,进入CLOSE_WAIT状态,客户端收到这个ACK,进入FIN_WAIT2状态
- 服务端发送FIN,进入LAST_ACK状态
- 客户端收到FIN,发送ACK,进入TIME_WAIT状态,服务端收到ACK,进入CLOSE状态
TIME_WAIT的状态就是主动断开的一方(这里是客户端),发送完最后一次ACK之后进入的状态。并且持续时间还挺长的。客户端TIME_WAIT持续2倍MSL时长,在linux体系中大概是60s,转换成CLOSE状态
TIME_WAIT
TIME_WAIT 是主动关闭链接时形成的,等待2MSL时间,约4分钟。主要是防止最后一个ACK丢失。 由于TIME_WAIT 的时间会非常长,因此server端应尽量减少主动关闭连接
CLOSE_WAIT
CLOSE_WAIT是被动关闭连接是形成的。根据TCP状态机,服务器端收到客户端发送的FIN,则按照TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果服务器端不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。此时,可能是系统忙于处理读、写操作,而未将已收到FIN的连接,进行close。此时,recv/read已收到FIN的连接socket,会返回0。
为什么需要 TIME_WAIT 状态?
假设最终的ACK丢失,server将重发FIN,client必须维护TCP状态信息以便可以重发最终的ACK,否则会发送RST,结果server认为发生错误。TCP实现必须可靠地终止连接的两个方向(全双工关闭),client必须进入 TIME_WAIT 状态,因为client可能面 临重发最终ACK的情形。
为什么 TIME_WAIT 状态需要保持 2MSL 这么长的时间?
如果 TIME_WAIT 状态保持时间不足够长(比如小于2MSL),第一个连接就正常终止了。第二个拥有相同相关五元组的连接出现,而第一个连接的重复报文到达,干扰了第二个连接。TCP实现必须防止某个连接的重复报文在连接终止后出现,所以让TIME_WAIT状态保持时间足够长(2MSL),连接相应方向上的TCP报文要么完全响应完毕,要么被 丢弃。建立第二个连接的时候,不会混淆。
TIME_WAIT 和CLOSE_WAIT状态socket过多
如果服务器出了异常,百分之八九十都是下面两种情况:
1.服务器保持了大量TIME_WAIT状态
2.服务器保持了大量CLOSE_WAIT状态,简单来说CLOSE_WAIT数目过大是由于被动关闭连接处理不当导致的。
一次完整的HTTP请求过程
域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户
讲一下长连接
- 一、基于http协议的长连接
- 在HTTP1.0和HTTP1.1协议中都有对长连接的支持。其中HTTP1.0需要在request中增加”Connection: keep-alive“ header才能够支持,而HTTP1.1默认支持.
- http1.0请求与服务端的交互过程:
- 客户端发出带有包含一个header:”Connection: keep-alive“的请求
- 服务端接收到这个请求后,根据http1.0和”Connection: keep-alive“判断出这是一个长连接,就会在response的header中也增加”Connection: keep-alive“,同是不会关闭已建立的tcp连接.
- 客户端收到服务端的response后,发现其中包含”Connection: keep-alive“,就认为是一个长连接,不关闭这个连接。并用该连接再发送request.转到a)
- 二、发心跳包。每隔几秒就发一个数据包过去
TCP如何保证可靠传输?
- 三次握手。
- 将数据截断为合理的长度。应用数据被分割成 TCP 认为最适合发送的数据块(按字节编号,合理分片)
- 超时重发。当 TCP 发出一个段后,它启动一个定时器,如果不 能及时收到一个确认就重发
- 对于收到的请求,给出确认响应
- 校验出包有错,丢弃报文段,不给出响应
- 对失序数据进行重新排序,然后才交给应用层
- 对于重复数据 , 能够丢弃重复数据
- 流量控制。TCP 连接的每一方都有固定大小的缓冲空间。TCP 的接收端 只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲 区溢出。
- 拥塞控制。当网络拥塞时,减少数据的发送。
详细介绍http
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。
特点
-
简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
-
灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
-
无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
-
无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
-
支持B/S及C/S模式。
请求消息Request
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本.
- 请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息 从第二行起为请求头部,HOST将指出请求的目的地.User-Agent,服务器端和客户端脚本都能访问它,它是浏览器类型检测逻辑的重要基础.该信息由你的浏览器来定义,并且在每个请求中自动发送等等
- 空行,请求头部后面的空行是必须的
- 请求数据也叫主体,可以添加任意的其他数据。
响应消息Response
- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
- 消息报头,用来说明客户端要使用的一些附加信息
- 空行,消息报头后面的空行是必须的
- 响应正文,服务器返回给客户端的文本信息。
状态码
- 200 OK //客户端请求成功
- 301 Moved Permanently //永久重定向,使用域名跳转
- 302 Found // 临时重定向,未登陆的用户访问用户中心重定向到登录页面
- 400 Bad Request //客户端请求有语法错误,不能被服务器所理解
- 401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
- 403 Forbidden //服务器收到请求,但是拒绝提供服务
- 404 Not Found //请求资源不存在,eg:输入了错误的URL
- 500 Internal Server Error //服务器发生不可预期的错误
- 503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
http的方法
- get:客户端向服务端发起请求,获得资源。请求获得URL处所在的资源。
- post:向服务端提交新的请求字段。请求URL的资源后添加新的数据。
- head:请求获取URL资源的响应报告,即获得URL资源的头部
- patch:请求局部修改URL所在资源的数据项
- put:请求修改URL所在资源的数据元素。
- delete:请求删除url资源的数据
URI和URL的区别
URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。 Web上可用的每种资源如HTML文档、图像、视频片段、程序等都是一个来URI来定位的
URI一般由三部组成:
- 访问资源的命名机制
- 存放资源的主机名
- 资源自身的名称,由路径表示,着重强调于资源。
URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。 URL是Internet上用来描述信息资源的字符串,主要用在各种WWW客户程序和服务器程序上,特别是著名的Mosaic。 采用URL可以用一种统一的格式来描述各种信息资源,包括文件、服务器的地址和目录等。
URL一般由三部组成:
- 协议(或称为服务方式)
- 存有该资源的主机IP地址(有时也包括端口号)
- 主机资源的具体地址。如目录和文件名等
HTTPS和HTTP的区别
- https协议需要到CA申请证书,一般免费证书很少,需要交费。
- http是超文本传输协议,信息是明文传输;https 则是具有安全性的ssl加密传输协 议。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
- http默认使用80端口,https默认使用443端口
https是如何保证数据传输的安全
https实际就是在TCP层与http层之间加入了SSL/TLS来为上层的安全保驾护航,主要用到对称加密、非对称加密、证书,等技术进行客户端与服务器的数据加密传输,最终达到保证整个通信的安全性。
- SSL/TLS协议作用:
- 认证用户和服务器,确保数据发送到正确的客户机和服务器;
- 加密数据以防止数据中途被窃取;
- 维护数据的完整性,确保数据在传输过程中不被改变。
数据结构与算法
动态规划的思想
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
快速排序的思想
在数组中找到一个基准数(pivot) 分区,将数组中比基准数大的放到它的右边,比基准数小的放到它的左边 继续对左右区间重复第二步,直到各个区间只有一个数,这时候,数组也就有序了。
快速排序算法是不稳定的算法
27 23 27 3 以第一个27作为pivot中心点,则27与后面那个3交换,形成 3 23 27 27,排序经过一次结束,但最后那个27在排序之初先于初始位置3那个27,所以不稳定。
堆排序的思想
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。 其基本思想为(大顶堆): 1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无须区; 2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n]; 3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
字典树
字典树主要有如下三点性质:
- 根节点不包含字符,除根节点意外每个节点只包含一个字符。
- 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符串不相同。
链表反转
- 首先让头节点与第一个元素节点断开,但是要注意在断开之前需要用p指针指向第一个元素节点来保存第一个元素节点的位置,然后再断开。在这里有一个指针q指向一个指针域为空的节点,这个节点用来做为链表反转后的最后一个节点。
- 让第二个元素节点的指针从指向第三个元素节点变为指向第一个元素节点,以此类推,直至指针p指向原链表最后一个元素。
- p指针指向NULL时,让原头节点的指针域指向原来最后一个元素节点。此时链表倒置已完成。
linkList reverse(linkList head){
linkList p,q,pr;
p = head->next;
q = NULL;
head->next = NULL;
while(p){
pr = p->next;
p->next = q;
q = p;
p = pr;
}
head->next = q;
return head;
}
复制代码
操作系统/Linux
进程有哪些状态
一般来说,进程有三个状态,即就绪状态,运行状态,阻塞状态。
- 运行态:进程占用CPU,并在CPU上运行
- 就绪态:进程已经具备运行条件,但是CPU还没有分配过来
- 阻塞态:进程因等待某件事发生而暂时不能运行
当然理论上上述三种状态之间转换分为六种情况;
- 运行——>就绪:1,主要是进程占用CPU的时间过长,而系统分配给该进程占用CPU的时间是有限的;2,在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行时,该进程就被迫让出CPU,该进程便由执行状态转变为就绪状态。
- 就绪——>运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU
- 运行——>阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如发生了I/O请求
- 阻塞——>就绪:进程所等待的事件已经发生,就进入就绪队列
以下两种状态是不可能发生的:
-
阻塞——>运行:即使给阻塞进程分配CPU,也无法执行,操作系统在进行调度时不会从阻塞队列进行挑选,而是从就绪队列中选取
-
就绪——>阻塞:就绪态根本就没有执行,谈不上进入阻塞态。
进程间通信方式
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
Linux中软链接和硬链接的区别
ln -s source dist # 建立软连接 ln source dist # 建立硬连接 建立硬链接时,链接文件和被链接文件必须位于同一个文件系统中,并且不能建立指向目录的硬链接
- 硬连接就像一个文件有多个文件名,
- 软连接就是产生一个新文件(这个文件内容,实际上就是记当要链接原文件路径的信息),这个文件指向另一个文件的位置
I/O多路复用
单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。尽量多的提高服务器的吞吐能力
select, poll, epoll 都是I/O多路复用的具体的实现
设计模式
设计模式主要分三个类型:创建型、结构型和行为型。
- 创建型有:
-
Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点
-
Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
-
Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
-
Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
-
Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
- 行为型有:
- Iterator,迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
- Observer,观察者模式:定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
- Template Method,模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。
- Command,命令模式:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
- State,状态模式:允许对象在其内部状态改变时改变他的行为。对象看起来似乎改变了他的类。
- Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。
- China of Responsibility,职责链模式:使多个对象都有机会处理请求,从而避免请求的送发者和接收者之间的耦合关系
- Mediator,中介者模式:用一个中介对象封装一些列的对象交互。
- Visitor,访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
- Interpreter,解释器模式:给定一个语言,定义他的文法的一个表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子
- Memento,备忘录模式:在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 结构型有:
- Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。
- Facade,外观模式:为子系统中的一组接口提供一致的界面,fa?ade提供了一高层接口,这个接口使得子系统更容易使用。
- Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问
- Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
- Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生成子类更加灵活。
- Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变化。
- Flyweight,享元模式
动态代理和静态代理有什么区别
静态代理
这种代理方式需要代理对象和目标对象实现一样的接口。在程序运行前,代理类的.class文件就已经存在了。
优点:
- 可以在不修改目标对象的前提下扩展目标对象的功能。
缺点:
- 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
- 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
动态代理
动态代理利用了JDK API,运用反射机制动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。
动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象
优点:
- 动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。
静态代理与动态代理的区别主要
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
- 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中
- 静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
- 静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
JDK中的动态代理和CGLIB
动态代理
-
JDK中的动态代理: 通过反射类Proxy以及InvocationHandler回调接口实现的,
-
动态代理缺点: JDK中所要进行动态代理的类必须要实现一个接口,也就是说只能对该类所实现接口中定义的方法进行代理,这在实际编程中具有一定的局限性,而且使用反射的效率也并不是很高。
CGLIB
-
CGLIB原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
-
CGLIB底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
-
CGLIB优点:它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。
-
CGLIB缺点:对于final方法,无法进行代理
场景题和设计题
场景题:设计判断论文抄袭的系统
- 一类是基于字符串比较的方法;另一类是基于词频统计的方法。
- 基于字符串比较的方法也称为数字指纹法,这类方法通过某种选取策略在文档中取一些字符串作为“指纹”,把指纹映射到Hash 表中,最后统计Hash
- 表中相同的指纹数目或者比率,作为文本相似度依据。
- 基于词频统计的方法也称为基于语义的方法。词频统计法源于信息检索技术中的向量空间模型,该类方法首先都要统计每篇文档中各个单词的出现次数,然后根据单词频度构成文档特征向量,最后采用点积、余弦或者类似方式度量两篇文档的特征向量,以此作为文档相似度的依据。
设计一个即时聊天的系统
- 用户通过客户端进入系统,向服务器发出消息,请求登陆。
- 服务器收到请求后,向客户端返回应答消息,表示同意接受该用户加入,并顺带将自己服务线程所在的监听端口号告诉用户。
- 客户端按照服务器应答中给出的端口号与服务器建立稳定的连接。
- 服务器通过该连接将当前在线用户的列表信息传给新加入的客户端。
- 客户端获得了在线用户列表,就可以独立自主地与在线的其他用户通信了。
- 当用户退出系统时要及时地通知服务器。
分布式系统事务一致性解决方案
分布式系统事务一致性解决方案 MQ(事务消息)
举个例子,Bob向Smith转账,那我们到底是先发送消息,还是先执行扣款操作?
好像都可能会出问题。如果先发消息,扣款操作失败,那么Smith的账户里面会多出一笔钱。反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith收不到钱。除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的思路呢?
下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:
设计高并发的系统?
- HTML 页面静态化 访问频率较高但内容变动较小,使用网站 HTML 静态化方案来优化访问速度。将社区 内的帖子、文章进行实时的静态化,有更新的时候再重新静态化也是大量使用的策略。 优势: 一、减轻服务器负担。 二、加快页面打开速度, 静态页面无需 访问 数据库,打开速度较动态页面有明显提高; 三、很多搜索引擎都会优先收录静态页面,不仅被收录的快,还收录的全,容易被搜 索引擎找到; 四、HTML 静态页面不会受程序相关漏洞的影响,减少攻击 ,提高安全性。
- 图片服务器和应用服务器相分离 现在很多的网站上都会用到大量的图片,而图片是网页传输中占主要的数据量,也是影 响网站性能的主要因素。因此很多网站都会将图片存储从网站中分离出来,另外架构一个 或多个服务器来存储图片,将图片放到一个虚拟目录中,而网页上的图片都用一个 URL 地 址来指向这些服务器上的图片的地址,这样的话网站的性能就明显提高了。 优势: 一、分担 Web 服务器的 I/O 负载-将耗费资源的图片服务分离出来,提高服务器的性能 和稳定性。 二、 能够专门对图片服务器进行优化-为图片服务设置有针对性的 缓存方案,减少带宽 成本,提高访问速度。 三、 提高网站的可扩展性-通过增加图片服务器,提高图片吞吐能力。
- 数据库 见“数据库部分的---如果有一个特别大的访问量到数据库上,怎么做优化?”。
- 缓存 尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与 数据库的交互,提高性能。 假如我们能减少数据库频繁的访问,那对系统肯定大大有利的。比如一个电子商务系 统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑这部分商品列表存放到缓 存(内存中去),这样不用每次访问数据库,性能大大增加。
- 镜像 镜像是冗余的一种类型,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本 即为镜像。
- 负载均衡 在网站高并发访问的场景下,使用负载均衡技术(负载均衡服务器)为一个应用构建 一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单 一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性。
- 并发控制 加锁,如乐观锁和悲观锁。
- 消息队列 通过 mq 一个一个排队方式,跟 12306 一样。
设计高负载的系统
- 应用无状态
- 有效使用缓存
- 应用拆分
- 数据库拆分
- 异步通信
- 非结构化数据存储 ( TFS,NOSQL)
- 监控、预警系统
- 配置统一管理
订票系统,某车次只有一张火车票,假定有 1w 个人同时打开 12306 网站来订票,如何解决并发问题?(可扩展到任何高并发网站要考虑的 并发读写问题
使用乐观锁,乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样既保证数据的并发 可读性 ,又保证保存数据的 排他性,保证性能的同时解决了并发带来 的脏数据问题。hibernate 中实现乐观锁。(乐观锁,使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。)
分布式与集群的区别是什么
分布式:一个业务分拆多个子业务,部署在不同的服务器上 集群:同一个业务,部署在多个服务器上
实时展现热门文章,比如近8小时点击量最大的文章前100名
- 数据接收
- 客户端会为了减轻服务器的压力而选择延迟合并点击请求进行批量发送
- 服务器肯定会有多台机器多进程部署来接受点击请求,接收到的请求在进行参数解析后,被发送到存储单元。为了减轻存储的压力,每个进程可能会使用小窗口聚合数据,每隔一小段时间将窗口内的数据聚合起来一起发给存储单元。
- 数据存储
- 使用kafka存,ZeroCopy机制并发量很高,数据持久化在磁盘里成本低。不过kafka的数据一般是有过期时间的,如果想完全记住用户的点击以便做长期的数据分析,需要要使用hdfs了
- 分布式TopN算法
- 用户太多,用户表按用户ID哈希分成了1024张子表。用户表里有一个字段score,表示这个用户的积分数。现在我们要计算前100名积分最多的用户以及积分数,该怎么查询?
- 如果是单个表,一个SQL也就搞定了,
- 如果是多个子表,你得在每个子表上都进行一次TopN查询,然后聚合结果再做一次TopN查询。子表查询可以多线程并行,提高聚合效率。
- 滑动窗口
- 8小时的滑动窗口,意味着新的数据源源不断的进来,旧的数据时时刻刻在淘汰,在业务可以差几分钟。
- 我们对时间片进行了切分,一分钟一个槽来进行计数,过期了8小时,移掉第一个,计算topn的帖子,维护窗口,移除过期的槽,然后统计topn,30s~60s调用一次
- 定时任务
- 每个子节点都会有一个定时任务去负责维持统计窗口,过期失效的统计数据,计算局部的topn热帖。
- 现在每个子节点都有了各自的局部topn热帖,那么还需要一个主节点去汇总这些局部热点,然后计算去全局热帖。
- 点击去重
- 首先要从客户端下手,客户端本身可以过滤一部分无效点击。同一篇文章在太短的时间内被当前用户反复点击,这个模式还是很好发现的。如果间隔时间比较长,那就是读者的回味点击,属于文章的正向反馈,应该记录下来
- 服务器还需要防止用户的防刷行为。如果缺失防刷控制,可以通过这种漏洞来使得自己的文章非法获得大量点击,进入热门文章列表,打上热门标签,被海量的用户看到,就会获得较大的经济效益,即使这篇文章内容本身吸引力并不足够。
如何解决电商网站超卖现象
超卖是什么
- 因为数据库底层的写操作和读操作可以同时进行,虽然写操作默认带有隐式锁(即对同一数据不能同时进行写操作)但是读操作默认是不带锁的,所以当用户1去修改库存的时候,用户2依然可以读到库存为1,导致两个用户同时减一次库存,所以出现了超卖现象。
解决方案
- 使用redis预减库存
- 当库存大于0,才能更新库存
update sk_goods_seckill set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0
- 添加唯一索引,UNIQUE KEY
u_uid_gid
(user_id
,goods_id
) USING BTREE,防止同一用户同一商品下两次订单 - 乐观锁,就是在数据库设计一个版本号的字段,每次修改都使其+1,这样在提交时比对提交前的版本号就知道是不是并发提交了,但是有个缺点就是只能是应用中控制,如果有跨应用修改同一条数据乐观锁就没办法了,这个时候可以考虑悲观锁。
- 悲观锁,就是直接在数据库层面将数据锁死,类似于oralce中使用select xxxxx from xxxx where xx=xx for update,这样其他线程将无法提交数据。
- 使用消息队列异步下单
mq异步调用失败,如何保证数据一致性?
- 按你的使用场景,推送数据必须得在数据创建事务成功之后执行,这里必须有个先后。你可以将推送这个操作异步执行,消息队列有一搬有ack机制,确保消息没丢失。这时候监听消息队列的程序会执行推送,如果推送成功做标记。如果推送失败也标记记录时间,也可以推到另一个消息队列约定多少分钟重试。实在不行就彻底标记失败,或者回滚之前创建的数据。这个才是最终一致性。
- 如果是并行的操作,就得使用消息队列的confirm机制了。
分布式锁的几种实现方式
- 基于数据库表
-
要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。
-
我们对method_name(方法名)做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。当方法执行完毕之后,想要释放锁的话就删除该记录
- 基于数据库排他锁
- 可以通过数据库的排他锁来实现分布式锁
- 在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引
- 值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
- 基于缓存
- 比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。
- 基于Zookeeper
- 每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
消息队列
四种MQ区别
如何保证消息队列是高可用的
- 集群,以rcoketMQ为例,有多master 模式、多master多slave异步复制模式、多 master多slave同步双写模式。
如何保证消息不被重复消费
原因:
- 消费者在消费后,会发送一个确认信息给消息队列,消息队列收到后会将该消息从消息队列中删除,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka每一个消息都有一个offset,kafka消费过消息后,需要提交offset,让消息队列知道自己已经消费过了。因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。
解决方法:
- 消息可以使用唯一id标识
- 如果拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
- 如果拿到这个消息做redis的set的操作,无论set几次结果都是一样的,set操作是算幂等操作。
- 使用第三方介质做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将< id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
流行框架
SpringMVC的工作原理
SpringMVC流程- 用户发送请求至前端控制器DispatcherServlet。
- DispatcherServlet收到请求调用HandlerMapping处理器映射器。
- 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter处理器适配器。
- HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
- Controller执行完成返回ModelAndView。
- HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
- ViewReslover解析后返回具体View。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
- DispatcherServlet响应用户。
请求 ---> DispatcherServlet(前端控制器)---> 调用HandlerMapping(处理器映射器)---> DispatcherServlet调用 HandlerAdapter(处理器适配器)---> 适配调用具体的Controller ---> 返回ModelAndView ---> 传给ViewReslover视图解析器 ---> 解析后返回具体View ---> 根据View进行渲染视图响应用户
MyBatis原理
MyBatis完成2件事情
- 封装JDBC操作
- 利用反射打通Java类与SQL语句之间的相互转换
MyBatis的主要成员
- Configuration MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中
- SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
- Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
- StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等
- ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所对应的数据类型
- ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
- TypeHandler 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
- MappedStatement MappedStatement维护一条select|update|delete|insert节点的封装
- SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
- BoundSql 表示动态生成的SQL语句以及相应的参数信息
MyBatis缓存
MyBatis提供查询缓存,用于减轻数据库压力,提高性能。MyBatis提供了一级缓存和二级缓存。
-
一级缓存是SqlSession级别的缓存,每个SqlSession对象都有一个哈希表用于缓存数据,不同SqlSession对象之间缓存不共享。同一个SqlSession对象对象执行2遍相同的SQL查询,在第一次查询执行完毕后将结果缓存起来,这样第二遍查询就不用向数据库查询了,直接返回缓存结果即可。一级缓存是MyBatis内部实现的一个特性,用户不能配置,默认情况下自动支持的缓存,用户没有定制它的权利
-
二级缓存是Application应用级别的缓存,它的是生命周期很长,跟Application的声明周期一样,也就是说它的作用范围是整个Application应用。MyBatis默认是不开启二级缓存的,可以在配置文件中使用如下配置来开启二级缓存
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
复制代码
后记
以上对面试相关的知识点做了系统的整理,希望对大家有所帮助。想要支持楼主的话,在 Github 上点个 Star,还有就是有些知识点是从网上优秀博客摘抄下来的,如果作者不希望文章部分内容被转载,可以通知楼主,会及时处理,感谢。