redis
缓存雪崩(多个key过期)
缓存雪崩:
我们可以简单的理解为:由同一时间热点缓存大面积失效,(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
解决办法:
大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时将缓存失效时间分散开。
或者设置热点数据永不过期,有更新操作的时候更新下缓存,电商首页用这个策略,保险
缓存击穿(当单个key过期)
问题:一些设置了过期时间的key,这些key可能会在某些时间点被超高并发访问,是一种非常热点的数据,这个时候请求发现缓存过期了,会直接读取db,大并发可能会搞垮db
解决方案0:热点数据永不过期
解决方案1:使用互斥锁:(SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。)
业界比较常用的方式是使用metex。简单的来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(setnx)去set一个mutex key,当操作成功时,再进行load db的操作并设置缓存,否则就重试整个get缓存的方法
优点:思路简单、保证一致性
缺点:代码复杂度增大、存在死锁的风险
1 | String get(String key) { |
解决方案2:异步构建缓存
在这种方案下,构建缓存采取异步策略,会从线程池中取线程来异步构建缓存,从而不会让所有的请求直接怼到数据库上。该方案redis自己维护一个timeout,当timeout小于System.currentTimeMillis()时,则进行缓存更新,否则直接返回value值。
优点:用户无需等待
缺点:无法保证缓存一致性
集群环境的redis代码如下所示:
1 | String get(final String key) { |
解决方案3:
布隆过滤器
优点:思路简单、保证一致性、性能强
缺点:代码复杂度增大、需要另外维护一个集合来存放缓存的key、不支持删值操作
缓存穿透
问题:查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果db查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到db去,在流量大时,db可能就挂掉了
比如说:查询一个不存在id都是1开始自增上去的,如果查询一个id为-1的值
解决方案:
- 接口添加校验,比如用户鉴权,参数合法校验,比如id<0的直接拦截等。
1.最常见的就是布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
2.更简单粗暴的方法:如果一个查询返回的数据为空(不管数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,设置一个比较短的过期时间
双写一致问题
数据库跟缓存同时操作肯定会出现不一致的问题
解决方案:延时双删策略
先删除redis,更新数据库,删除redis
参考文章:https://www.cnblogs.com/rjzheng/p/9041659.html
redis与memcached有什么区别?为什么选用redis作为缓存的中间件
- redis支持复杂结构,如果需要缓存支持复杂的结构和操作,redis 是不错的选择
- redis原生支持集群模式:memcached需要依赖客户端来实现往集群中分片写入数据。
性能对比
redis只是用单核,而memcached可以使用多核,所有平均每一个核上redis在存储小数据时比memcached性能更高,而在100k以上的数据中,memcached性能要高于redis
redis线程模型
内部使用文件事件处理器 file event handler,这个是单线程的,所以redis才叫单线程的模型,它采用io多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理。
文件事件处理器,结构包括四个部分:
- 多个socket
- io多路复用程序
- 文件时间分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Redis有哪些数据结构啊?
String、Hash、List、Set、SortedSet
以上五个基本类型,如果你是中高级用户的话,还有HyperLogLog、Geo、Pub、Sub
如果还想加分,还可以说玩过Redis Module,像BloomFilter、RedisSearch、Redis-ML
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果过期时间设置的过于集中,到过期的时候,Redis可能会出现短暂的卡顿现象,严重的话会出现缓存雪崩,一般在时间上加一个随机值,使得过期时间分散一些
电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点涌入大量访问,就可能缓存雪崩
你使用过Redis分布式锁么,它是怎么回事?
先拿setnx来争抢锁,抢到之后,再用Expire给锁加一个过期时间防止锁忘记释放。
如果在setnx之后,执行expire之前,进程意外重启维护了,会怎么样?
这个时候你一定要给予惊讶的反馈,是哦,这个锁就永远得不到释放了,紧接着,你抓了抓自己的头发,故作思考:我记得set指令有个非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的
假如redis里有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将他们全部找出来?
使用keys指令可以扫出指定模式的key列表
继续追问:如果这个redis正在给线上的业务提供服务,那么使用keys指令会有什么问题?
redis是单线程的,使用keys会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,这个时候可以使用scan指令,scan指令可以无阻塞的取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,单整体花费的时间会比直接使用keys指令长
redis是怎么持久化的?服务主从数据是怎么交互的?
rdb做镜像全量持久化,aof做增量持久化。因为rdb会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用rdb持久化文件重构内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态
这里很好理解的,把rdb理解为一整个表全量的数据,aof理解为每次操作的日志就好了,服务器重启的时候先把表的数据全部搞进去,但是可能不完整,这个时候再回放一下日志,数据不就完整了嘛。不过redis本身的机制是aof持久化开启且存在aof文件时,优先加载aof文件;aof关闭或者aof文件不存在时,加载rdb文件;加载aof/rdb文件成功后,redis启动成功;aof/rdb文件存在错误时,redis启动失败并打印错误信息
对方追问如果突然机器停电会怎样?
取决于aof日志sync属性的配置,如果不要求心梗,在每一条写指令时都sync一下磁盘,就不会丢失数据,但是在高性能要求下每次都sync是不现实的,一般都使用定时sync,比如1s/1次,这个时候最多就会丢失1s的数据
对方继续追问rdb的原理是什么?
这个问题给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行rdb操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会主键和子进程分离开来
Pipeline有什么好处,为什么要用Pipeline?
可以将多次io往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目
是否使用过redis集群,集群的高可用怎么保证,集群的原理是什么?
redis Sentinel着眼于高可用,在master宕机时会自动将slave升级为master,继续提供服务 。
redis cluster 着眼于扩展性,在单个redis内存不足时,使用cluster进行分片存储
哨兵、持久化、主从、手撕LRU
在上面了解完基础知识已经一些缓存的常见问题之后,聊聊下面的
为什么redis那么快
先看一下关系型数据库跟redis本质上的区别,如下图:
Redis采用的是单进程单线程模型的kv数据库,由c编写,官方提供的数据是达到10w的qps(每秒内查询次数)
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换消耗,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
- 使用多路i/o复用模型,非阻塞io;
- 使用底层模型不同,他么之间底层实现方式以及与客户端之间通信的应用协议不一样,redis直接自己构建了vm机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
我可以问一下啥是上下文切换吗?为啥可能线程不安全?
好比你看一本英文书,你看到第十页发现有个单词不会,你加了个书签,然后去查字典,过了一会你回来继续从书签那里读,ok目前为止没问题。
问题来了,你去查字典的时候,别人过来翻了一下你的书,然后走了,然后你回来了,你再看书的时候发现书不是你看的那一页了。
那他是单线程,我们服务器都是多核的,那不是浪费吗?
虽然他是单线程的,但是我们可以单机开多个实例啊
既然提到了单机会有瓶颈,那你们是怎么解决这个瓶颈的?
我们用到了集群的部署方式也就是redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster支撑n个redis mater node,并且每个master node 都可以挂载多个salve node
这样整个Redis就可以横向扩容了,如果你要吃成更大数据量的缓存,那就横向扩容更多的master节点,每个master节点就能存放更多的数据了。
那么问题来了,他们之间是怎么进行数据交互的?以及redis 是怎么进行持久化的?Redis数据都在内存中,一断电或者重启不就木有了吗?
是的,持久化的话是redis高可用中的重要一点,因为redis数据在内存的特性,持久化必须有,我了解方式有两种:
RDB:是对redis中的数据进行周期性的持久化
AOF:对每条写入命令作为日志,以[只追加]【append-only】的方式写入到一个日志文件中,没有任何磁盘寻址的开销,所以很快,有点像mysql中binlog
这两种方式都可以把redis内存中的数据持久化到磁盘上,rdb更适合做冷备,aof更适合做热备
tip:两种机制全部开启的时候,redis在重启的时候回默认使用aof去重新构建数据,因为aof的数据是比rdb更完整的
那这两种机制各自优缺点是啥?
RDB
优点:
会生成多个数据文件,每个数据文件都代表了某一时刻redis里面的数据,这种方式,很适合做冷备
rdb对redis性能影响较小,是因为在同步时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比aof来得快
缺点:
rdb都是快照文件,都是默认五分钟甚至更久的时间才生成一次,意味着两次同步之间五分钟的数据可能全部丢失,aof则最多丢1秒的数据
还有就是rdb在生成数据快照的时候,如果文件很大, 客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候它刚搞在这个时候fork了一个子进程去生成一个大快照
AOF
优点:一秒生成一个,只追加的方式写,性能惊人
缺点:
一样的数据aof文件比rdb还要大,aof开启后redis支持写的qps会比rdb支持写的要低
那两者如何选择呢?
两个都要,出现事故,第一时间用rdb恢复,在用aof补全
Redis还有其他保证集群高可用的方式吗?
还要哨兵集群sentinel(森提nou)
哨兵必须用三个实例去保证自己的健壮性,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用
一个机器挂了,剩下两个机器需要选举出来一个执行故障转移,如果只有两台机器的话,挂了一个就剩下一个了,没有哨兵去允许故障转移了
Redis的同步机制
redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存,加载完成后,通知主节点将期间修改的操作记录同步到点进行重放就完成了同步过程。后续的增量数据通过aof日志同步即知,有点类似数据库的binlog。
说一下他的内存淘汰机制
Redis的过期策略有两种,定期删除+惰性删除
定期删除:默认100ms就随意抽一些设置了过期时间的key,检查是否过期,过期就删除
为什么不扫描全部设置了过期时间的key呢?
因为太慢,浪费资源
如果没随机到很多key,里面不就存在大量的无效key了?
好问题,不是还有惰性删除吗
惰性删除:我不主动删,我懒,等你来查询了,我看看有没有过期,过期就删了还不给你返回,没过期就那么挂着
1.13 双写一致性、并发竞争、线程模型
mysql
索引
索引有哪些数据结构
Hash、B+
去创建索引的时候,可以选择索引的类型
为什么hash、完全平衡二叉树、b树、b+树都可以优化查询,mysql为什么喜欢b+树?
先说hash索引,字段值所对应的数据下标是哈希随机算出来的,可能会出现hash冲突。
举例:where name = ‘鸡蛋’,“鸡蛋”可以直接hash出他的数组下标,然后直接从数据中取出来
举例2:where name >’鸡蛋’,那么hash表就无能为力了,他可以精确查询,但是不支持范围查询,就算做成索引,速度也很慢,要扫全表。
hash表的适合场景?
hash表是无序的。
redis、Memcached这些nosql的中间件。kv结构的
说的是无序的hash表,有没有有序的结构?
有序数组,等值查询和范围查询都很好
缺点:适合做静态数据,因为增删改会改变他的结构
适用:静态存储的索引啊,比如说2019年的支付宝账单,等等历史记录,都是不会变动的数据
二叉树
二叉树是有序的,所以支持范围查询,但是时间复杂度是o(log(n)),为了维持这个时间复杂度,更新的时间复杂度也得是o(log(n)),那就得保持这棵树是完全平衡二叉树了
索引页不止在内存里存储的,也要落盘持久化的,如果数据多了,树就很高了,查询成本随着树变高而高
如果公司为了节约成本用的机械盘,来一次千万级别的查询,那不得慢死了
B树
b树比完全平衡二叉树要矮,原因是b树中的一个节点可以存储多个元素
B+树
同样的元素,b+树会比b树要“胖”,原因在于B+树中的非叶子节点会冗余一份在叶子节点中,并且叶子节点之间用指针相连
B+树 优势
Hash不支持范围查询
二叉树太高
只有B树可以跟b+比一比
b树一个节点存储多个元素,相对于完全平衡二叉树的树高整体降低了,磁盘io效率提高了
b+树是b树的升级版,只是把非叶子节点冗余一下,这样可以提高范围查找的效率:原因是会有指针指向下一个节点的叶子节点
一个b+树节点可以存储多少个元素?
懵逼,换个角度,b+树中一个节点到底多大合适?
b+树中一个节点为一页或者页的倍数最合适
基础
回表
是什么?
大概就是有个主键为ID的索引,和一天个普通字段 name 的索引,我们在普通字段上搜索:
select * from table where name = ‘“丙丙”
执行的流程是:先查询name索引上的“丙丙”,然后找到他id是2,最后去主键索引,找到id为2对应的值。
回到主键索引树搜索的过程,就是回表。
覆盖索引可以避免回表
覆盖索引
是什么?
比如说刚才的 select * ,查询所有的,如果我们只是需要id,那么其实name字段的索引上就已经有了,就不需要回表了
很多联合索引的建立,就是为了支持覆盖索引,特定的业务能极大的提升效率
char 、varchar的区别是什么?
varchar 是变长,而char是长度固定的,如果你的内容是固定大小的,使用char性能更好
truncate与delete的区别是什么?
truncate 是永久删出表中的每一行,且不可恢复
什么是触发器?
触发器是指一段代码,当触发某个事件时,自动执行这些代码。在mysql数据库中有如下6种触发器:1.Before Insert 2.After Insert 3.Before Update 4.After Update 5.Before Delete 6.After Delete
float与double的区别是什么?
fioat存储至多8位十进制数,内存占用4字节
double存储至多18位十进制数,内存占用8字节
如果在mysql获取当前日期
1 | select current_date(); |
如何查询第n高的工资
1 | select distinct(salary) from employee order by salary Desc limit n-1,1 |
请说明innodb和mylsam的区别
innodb,支持事务
innodb,支持崩溃后的恢复
请列举三个以上表引擎
innodb 、MylSAM、Memory
varchar和text的区别
- varchar可以指定字符数,text不能指定
- 内部存储varchar是存入的实际字符数+1个字节(n<=255)或2个字节(n>255),text是实际字符数+2个字节
- text不能有默认值,默认值为null
- varchar可以直接创建索引,text创建索引要指定前多少个字符,varchar查询速度快于text,在都创建索引的情况下,text的索引几乎不起作用。
- 查询text需要创建临时表
varchar(50)中50的含义
最多存放50个字符
varchar(50)和(200)存储“hello”所占用的空间是一样的,但是200在排序时会消耗更多内存。
int(20)中20的含义
是指显示字符的长度,不影响内部存储,只是当定义了ZEROFILL时,前面补多少个0
索引、主键、唯一索引、联合索引的区别,对数据库性能有什么影响?
- 唯一索引:数据列不允许重复,可以null,一个表可以创建多个
- 主键索引:一定是唯一索引,不允许null,一个表只能有一个
- 主键可以与外键构成曹肇完整性约束,防止数据不一致。
- 联合索引:将多个列组合在一起创建索引,可以覆盖多个列。
- 外检索引:基本不用,只有innodb类型的表才可以使用
- 全文索引:mysql自带的全文索引只能用于mylSAM,并且只能对英文进行全文检索(基本不用)
最左前缀
顾名思义,就是最左优先,在创建多列索引时,需要根据业务需求,where字句中使用最频繁的一列放在最左边。
1 | 以 index(a,b,c) 为例,(注意和顺序有关) |
- 组合索引的生效原则是 从前往后依次使用生效,如果中间某个索引没有使用,那么断点前面的索引部分起作用,断点后面的索引没有起作用;
联合索引最左匹配原则
最左前缀匹配原则
在mysql建立联合索引时会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配,示例:
对列col1、列col2和列col3建一个联合索引
1 | KEY test_col1_col2_col3 on test(col1,col2,col3); |
联合索引 test_col1_col2_col3 实际建立了(col1)、(col1,col2)、(col,col2,col3)三个索引。
1 | SELECT * FROM test WHERE col1=“1” AND clo2=“2” AND clo4=“4” |
上面这个查询语句执行时会依照最左前缀匹配原则,检索时会使用索引(col1,col2)进行数据匹配。
注意
索引的字段可以是任意顺序的,如:
1 | SELECT * FROM test WHERE col1=“1” AND clo2=“2” |
这两个查询语句都会用到索引(col1,col2),mysql创建联合索引的规则是首先会对联合合索引的最左边的,也就是第一个字段col1的数据进行排序,在第一个字段的排序基础上,然后再对后面第二个字段col2进行排序。其实就相当于实现了类似 order by col1 col2这样一种排序规则。
有人会疑惑第二个查询语句不符合最左前缀匹配:首先可以肯定是两个查询语句都包含索引(col1,col2)中的col1、col2两个字段,只是顺序不一样,查询条件一样,最后所查询的结果肯定是一样的。既然结果是一样的,到底以何种顺序的查询方式最好呢?此时我们可以借助mysql查询优化器explain,explain会纠正sql语句该以什么样的顺序执行效率最高,最后才生成真正的执行计划。
为什么要使用联合索引
- 减少开销。建一个联合索引(col1,col2,col3),实际相当于建了(col1),(col1,col2),(col1,col2,col3)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销!
- 覆盖索引。对联合索引(col1,col2,col3),如果有如下的sql: select col1,col2,col3 from test where col1=1 and col2=2。那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作。减少io操作,特别的随机io其实是dba主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。
- 效率高。索引列越多,通过索引筛选出的数据越少。有1000W条数据的表,有如下sql:select from table where col1=1 and col2=2 and col3=3,假设假设每个条件可以筛选出10%的数据,如果只有单值索引,那么通过该索引能筛选出1000W10%=100w条数据,然后再回表从100w条数据中找到符合col2=2 and col3= 3的数据,然后再排序,再分页;如果是联合索引,通过索引筛选出1000w10% 10% *10%=1w,效率提升可想而知!
引申
对于联合索引(col1,col2,col3),查询语句SELECT * FROM test WHERE col2=2;是否能够触发索引?
大多数人都会说NO,实际上却是YES。
原因:
1 | EXPLAIN SELECT * FROM test WHERE col2=2; |
观察上述两个explain结果中的type字段。查询中分别是:
- type: index
- type: ref
index:这种类型表示mysql会对整个该索引进行扫描。要想用到这种类型的索引,对这个索引并无特别要求,只要是索引,或者某个联合索引的一部分,mysql都可能会采用index类型的方式扫描。但是呢,缺点是效率不高,mysql会从索引中的第一个数据一个个的查找到最后一个数据,直到找到符合判断条件的某个索引。所以,上述语句会触发索引。
ref:这种类型表示mysql会根据特定的算法快速查找到某个符合条件的索引,而不是会对索引中每一个数据都进行一一的扫描判断,也就是所谓你平常理解的使用索引查询会更快的取出数据。而要想实现这种查找,索引却是有要求的,要实现这种能快速查找的算法,索引就要满足特定的数据结构。简单说,也就是索引字段的数据必须是有序的,才能实现这种类型的查找,才能利用到索引。
索引算法
btree是最常用的,也是mysql默认的算法,因为它不仅仅可以用在=、>、>=、<、<=和between这些比较操作符上,也可以用于like操作符,只要他的查询条件不是一通配符开头的常量
1 | select * from user where name like ‘jack%’---使用索引 |
hash索引只能用于对等比较,例如=、<=>(相当于=)操作符。由于是一次定位数据,不像btree索引需要从根节点到枝节点,最后才能访问到叶节点这样多次io访问,所以hash索引的效率远高于btree索引
索引的设计原则
- 适合索引的列是出现在where字句中的列,或者连接字句中指定的列
- 基数较小的列,索引效果差,没必要在此列建立索引
- 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
- 不要过度索引,占用额外的磁盘空间,并降低写操作的性能。在修改表能容的时候,索引进行更新甚至重构
mysql中in 和exists区别
- 如果查询的两个表大小想打,那么in和exists差别不大
- 如果一个小表,一个大表,则子查询表大的用exists,子查询表小的用in
- not in 和 not exists 如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not exists 的子查询依然能用到表上的索引。所以无论哪个表达,用not exists 都比not in 要快
mysql的关联查询语句有哪些?
6中关联查询:1 交叉连接(cross join);2 内连接(inner join);3 外连接(left join、right join);4 联合查询( union 与 union all);5 全连接(full join);
内连接分为三类
- 等值连接:on a.id=b.id
- 不等值连接:on a.id >b.id
- 自连接 :select * from a t1 inner join a t2 on t1.id = t2.id
外连接(left join、right join)
- 左匹配连接,以左表为主,先查询出主表,按照on后的关联条件匹配右表,没有匹配到的用null填充,可以简写成left join
- 右匹配连接,同上
联合查询(uninon 与 union all)
1 | select * from a union select * from b union 。。。 |
- 就是把多个结果集集中在一起,union前的结果为准,联合查询的列数要相等,相同的记录行会合并
- 如果使用union all,不会合并重复的记录行
- union效率高于union all
mysql的隔离级别和对应的问题
Read Uncomitted(读未提交)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结。
本级别很少用于实际应用,因为它的性能也不必其他级别好多事
问题:脏读:读取未提交的数据
Read Committed(读已提交)
大多数数据库系统的默认隔离级别(但不是mysql默认的)。满足了隔离的简单定义:一个事务只能看见已提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理期间可能会有新的commit,所以同一select 可能返回不同结果。
Repeatable Read(可重复读-默认级别)
这是mysql默认的事务隔离界别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。
不过理论上会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当前用户读取某一范围的数据行是,另一个书屋又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有心的“幻影”行。InnoDB和Falcon存储引擎通过多版本并发控制(mvcc)机制解决了该问题
Serializable(可串行化)
这是最高的隔离级别,通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,就是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。这四种隔离界别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就有几列数据是未查询出来的,如果此时插入和另外一个事务插入的数据,就会报错。
在数据库中如何优化?
- 对查询进行优化,尽量避免全表扫描,首先应考虑在where 及order by 涉及的列上建立索引
- 应尽量避免在where 字句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描
- 应尽量避免在where字句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
- 应尽量避免在where字句中使用or来连接条件,如果一个字段有索引,一个字段没有索引,将导致全表扫描
- in 和 not in 也要慎用,否则会导致全表扫描
- like 模糊全匹配也将导致全表扫描
mysql的基础架构,画一下图
连接器是啥?
我们要查询db,第一步就是去连接db,这个连接器就是db跟我们对接的。
他负责跟客户端建立连接、获取权限、维持、管理连接
连接的时候回经过tcp握手,然后身份验证,然后输入账号密码就好了
验证ok后,我们就脸上mysql这个服务器了,但是这个时候处于空闲状态
怎么查看空闲连接列表?
show processList
其中Command列显示为sleep的这一行,就表示系统里面有一个空闲连接
如果数据库的客户端太久没有相应,连接器就自动断开了,这个时间参数是wait_timeout控制住的,默认时长是8小时。超时之后可以重新连接
除了重新连接,还有其他方法吗?
使用长连接
但是优缺点:长连接后,内存飙升很快,我们知道mysql在执行过程中临时使用的内存是管理在连接对象里面的。只有在断开连接的时候才能得到释放,如果长连接,这一部分内存就得不到释放,就oom了,在jvm里面就频繁fgc
mysql缓存
mysql拿到一个查询请求之后,会先到缓存里面查询看看,之前是否执行过这条
如果同一条语句执行两次,第一次明显比后面的慢,这就是因为缓存的存在
跟redis类似,只要你之前执行过的语句,都会在内存里面用key-value形式存储着
为什么缓存弊大于利
缓存很容易失效,只要对表有任何的更新,这个表的所有查询缓存都会被清空,出现:缓存还没使用就被清空。
只有类似于配置表这种的,适用缓存
查询的时候不想用缓存、想用缓存该如何操作?
可以显示调用,把query_cache_type设置成DEMAND,这样sql默认不适用缓存,想用缓存就用sql_cache。
缓存在mysql 8.0之后就取消了
缓存查询完了,应该做啥呢?
缓存没有命中的情况下,开始执行语句了,你写的sql有没有语法错误,这是接下来比较关心的点。
他会先词法分析,一条sql那么多单词,要识别每个字符代表的是什么?表名?列名?关键字?
然后开始语法分析,根据此法分析的结果,语法分析会判断你sql的对错,错了会提示你哪里错了
下一步:优化器
主要优化什么:我们的表可能会建立很多索引,优化的有一步就是要确认使用哪个索引、执行顺序进行优化,条件那么多、先查哪个表,还是县关联?
最后就是执行了,交给 执行器去做
线程池
什么是线程池?
线程池的基本思想是对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡的)的线程,池中的线程执行调度由池管理器来处理,当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象带来的性能开销,节省了系统的资源
使用线程池的好处
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用
- 运行线程池能有效的控制线程最大并发数,可以根据系统的承受能力
- 对线程进行一些简单的管理:比如:延迟执行、定时循环执行的策略等,运用线程池都能进行很好的实现
线程池的主要组件
一个线程池包括以下四个基本组成部分
- 线程池管理器(ThreadPool):用于创建并管理线程池,创建、销毁线程池,添加新任务;
- 工作线程(WorkThread):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务
- 任务接口(Task):每个任务都必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等。
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
ThreadPoolExecutor类
这个类的线程池中最核心的一个类
线程池的主要处理流程
- 线程数量未达到corePoolSize,则新建一个新城(核心线程)执行任务
- 线程数量达到了corePools,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maxImumPoolSize,就会抛出异常
四中拒绝策略
- AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略
- DIscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式
- DIsCardOldSetPolicy:将消息队列中的一个任务替换为当前新进来的任务执行
- CallerRunPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
5.5 java通过Executors提供四种线程池
CacheThreadPool:可缓存线程池
- 线程数无限制
- 有空闲线程则复用空闲线程,若没有则新建线程,一定程度减少频繁创建、销毁线程,减少系统开销
FixedThreadPool():定长线程池
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
ScheduledThreadPool
- 定时线程池
- 支持定时及周期性任务执行。
SingleThreadExecutor:单线程化的线程池
- 有且仅有一个工作线程任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
5.6 线程池参数设置
参数的设置跟系统的负载有直接关系,下面为系统负载的相关参数:
- tasks,每秒需要处理的任务数(针对系统需求)
- threadtasks,每个线程每秒可处理任务数(针对线程本身)
- responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒
corePoolSize
系统每秒有tasks个任务需要处理理,则每个线程每钞可处理threadtasks个任务。,则需要的线程数为:tasks/threadtasks,即tasks/threadtasks个线程数。
假设系统每秒任务数为100 ~ 1000,每个线程每钞可处理10个任务,则需要100 / 10至1000 / 10,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,因为系统每秒任务数为100 ~ 1000,即80%情况下系统每秒任务数小于1000 * 20% = 200,则corePoolSize可设置为200 / 10 = 20。
queueCapacity
任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为 所有核心线程每秒处理任务数 * 每个任务响应时间 = 每秒任务总响应时间 ,即(corePoolSizethreadtasks)responsetime: (2010)2=400,即队列长度可设置为400。
maxPoolSize
当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(tasks - queueCapacity)/ threadtasks 即(1000-400)/10,即60个线程,可将maxPoolSize设置为60。
队列长度设置过大,会导致任务响应时间过长,切忌以下写法:
1 | LinkedBlockingQueue queue = new LinkedBlockingQueue(); |
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。
keepAliveTime
当负载降低时,可减少线程数量,当线程的空闲时间超过keepAliveTime,会自动释放线程资源。默认情况下线程池停止多余的线程并最少会保持corePoolSize个线程。
allowCoreThreadTimeout
默认情况下核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。
5.7 线程池的五种状态
线程池的初始化状态running,能够接受新任务,以及对已添加的任务进行处理。
线程池出在shutdown状态时,不接受新任务,但能处理已添加的任务。调用线程池的shutdown()接口时,线程池由running->shutdown
线程池出在stop状态时,不接受新任务,不处理已添加的任务,并且会终端正在处理的任务。调用线程池的shutdownNow()接口时,线程池由(running or shutdown) ->stop。
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
5.8 关闭线程池
线程池提供两种关闭方法:shutDown()和shutDownNow()
shutDown()
当线程池调用该方法时,线程池的状态则like变成shutdown状态,此时,则不能再往线程池中添加任何任务,否则将会抛出异常;但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出
shutdownNow()
执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理线程池中等待的任务,当然,会返回那些未执行的任务;不代表立刻就能退出,可能要等待所有正在执行的任务都执行完才能退出
5.9 各种场景下怎么设置线程数
高并发、任务执行时间短的业务
线程池线程数可以设置为cpu核心数+1,减少线程上下文的切换
并发不高、任务执行时间长的业务
这个需要判断执行时间是耗在那个地方了?
- io操作耗时,也就是io密集型的任务,因为io操作并不占用cpu,所有不要让所有的cpu闲下来,可以适当加大线程池中的线程数目(cpu核心数*2),让cpu处理更多的业务
- 计算操作耗时,也就是cpu密集型任务,那么久设置cpu核心数+1,线程数设置的少一些,减少线程上下文的切换。
并发高、业务执行时间长的业务
解决这种类型任务的关键不在于线程池,而在于整体架构的设计
线程
五个状态
new(新建)
Runnable(就绪)
Running(运行)
Blocked(阻塞)
死亡
线程的创建方式到底有几种
声明为Thread的子类,并重写run方法
1
2
3
4
5
6
7
8
9
10
11
12 public class MyThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() + " Thread running...");
}
public static void main(String[] args) throws IOException {
new MyThread().start();
System.in.read();
}
}实现Runnable接口
Runnable的优点:
业务代码与线程类创建启动等逻辑解耦
Runnable可以复用,Thread则需要每次创建
类可以实现多个接口,而不可以继承多个对象
1
2
3
4
5
6
7
8
9
10
11
12
13 public class MyRunnable implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " Runnable running...");
}
public static void main(String[] args) throws IOException {
new Thread(new MyRunnable()).start();
System.in.read();
}
}
// 有点归根结底
创建线程只有一种方式,就是创建Thread对象,而构建一个线程的方式有多重,比如:创建线程类,实现Runnable接口,创建线程池,FutureTask等等
线程池创建线程:实际是由默认的工厂代为创建Thread类来实现的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 // Executors中的DefaultThreadFactory
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
//由上newThread()方法可知,即使是线程池,本质上还是使用Thread的创建线程。Callable和FutureTask创建线程,本质其实也是Thread
1
2
3
4
5
6
7
8
9
10
11
12 public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
public class FutureTask<V> implements RunnableFuture<V> {
......
private volatile Thread runner;
......定时器Timer:它的TimerTask其实也是实现了Runnable接口,可以看下TimerTask这个抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class TimerExample {
public static void main(String[] args) {
Timer timer = new Timer();
// 每隔1s打印下自己的名字
timer.scheduleAtFixedRate(new TimerTask() {
public void run() {
System.out.println(Thread.currentThread().getName() + " timer running...");
}
}, 1000, 1000);
}
}
线程和进程的区别
线程是进程的子集,一个进程可以有多个线程;
不同的进程使用不同的内存空间,所有单线程共享一片相同的内存空间
用Runnable还是Thread
推荐用Runnable,因为可以继承多个接口
Thread类中的start()和run()方法有什么区别?
start()被用来启动新创建的线程,而且start()内部调用了run()方法;
run(),只会在原来的线程中调用,没有新的线程启动
Runnable和Callable有什么不同
都代表那些要在不同的线程中执行的任务。Runnable从1.0开始就有了,Callable在1.5增加的;
Callable的call()方法可以返回值和抛出异常
乐观锁、悲观锁
简单说说乐观锁、悲观锁,他们对应的实现cas、Synchronized、ReentrantLock
锁只能升级,不能降级
乐观锁
说一说cas
Compare and Swap 比较并且替换,是乐观锁的一种实现方式,是一种轻量级锁,juc中很多工具类的实现是基于cas的
cas是怎么实现线程安全的?
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已修改,则重新执行读取流程,无法处理aba问题
cas操作长时间不成功的话,会导致一直自旋,相当于死循环了,cpu压力很大
乐观锁在项目中的实践?
比如订单表,比如流水表,为了防止并发安问题,就会加入cas的校验过程,保证线程安全,但并不是所有场景都适用的。
cas性能很好,但是Synchronized性能不咋地,为啥1.8之后反而多了Synchronized?
synchronized之前一直都是重量级的锁,但是后来java官方对他进行过升级 的,他现在采用的是锁升级的方式去做的。
针对synchronized获取锁的方式,jvm使用了锁升级的优化方式,就是先使用偏向锁优先同一进程然后再次获取锁,如果失败,就升级为cas轻量级锁,如果失败就会端在自旋,防止线程被系统挂起,最后如果以上都失败就升级为重量级锁;所有是一步步升级上去的,一开始也是通过很多轻量级的方式锁定的
悲观锁
什么是悲观锁,悲观锁,就是默认你每次都是渣男,每次都要提防着你是的
Synchronized
实现原理
无论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步
是如何保证同一时刻只有一个线程可以进入临界区呢?
jvm层面的synchronized加锁,是最常见的线程同步收之一
synchronized,代表这个方法加锁,相当于不管哪一个线程(比如线程A),运行到这个方法的时候,都要检查有没有其他线程B(或者c、d)正在用这个方法(或者该类方法啊其他同步方法),有的话要等他运行完之后,再运行,没有的话,锁定调用这,然后直接运行。
分别从他对对象、方法、代码块三方面加锁,去介绍怎么保证线程安全的
同步方法和同步代码块底层都是通过monitor来实现同步的
对象加锁
在jvm中,对象在内存中分为三块区域:对象头(header)、实例数据(Instance data)、对齐填充(padding)
对象头
以hotpot虚拟机为例,对象头主要包括两部分数据:mark word(标记字段)、klass pointer(类型指针)
- mark word:默认存储对象的hashcode,分代年龄和锁标志位信息。它会根据对象的状态复制自己的存储空间,也就是说:在运行期间mark word里面存储的数据会随着锁标志位的变化而变化
- klass point :对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
可以看到对象头保存了锁标志位和指向monitor对象的起始地址,如图所示,右侧就是对象对应的monitor对象
当monitor被某个线程持有后,就会处于锁定状态,如同种的 Owner部分,会指向持有monitor 对象的进程
另外 Monitor中海油两个队列分别是 EntryList和waitList,主要是用来存放进入及等待获取锁的线程,如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行
如果一个对象有多个资源,就不需要将整个对象加锁
由于每个对象都有锁,可以使用虚拟对象来上锁 Synchronized(new obj)
ReentrantLock
在介绍ReentrantLock之前,先介绍AQS(AbstractQueuedSynchronizer)
AQS:队列同步器,是实现ReentrantLock的基础
AQS有一个state标记位,值为1时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表
什么是aba
什么是ABA
就是一个线程把值改成了b,又一个线程把值改回了a,对于当前线程来说,发现他的值还是a,所有就不知道这个值到底有没有被人修改过,如果只追求最后结果正确,这是没关系的
但实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯
如果和解决ABA问题?
用版本号去保证就好, 比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号+1,伪代码如下
1 | update a set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样 |
除了版本号还有别的方法保证吗
比如时间戳也可以,查询的时候把时间戳一起查询出来,对的上才修改并且更新值的时候一起修改更新时间,方法很多,都是跟版本号异曲同工
Hashmap
6.1.1成员变量
1。集合的初始化容量(必须是2的n次幂)
1 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 |
HashMap构造方法可以指定集合的初始化容量大小:
1 | Hash(int 初始大小) //构造一个带指定初始容量和默认加载因子(0.75)的空HashMap |
由此可见,当想HashMap中添加一个元素的时候,需要根据key的hash值,去确定其在数组中的具体位置。HashMap为了存取高效,要尽量减少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就是把数据存储到哪个链表中的算法
这个算法实际就是取模,hash%length,计算机中直接求余效率远不如位运算。所有源码中做了优化,使用hash&(length-1),而实际上hash%length等于hash&(length-1)的前提是lengtn是2的n次幂。
hash&(length-1)是计算数组的索引的
为什么是2的n次幂
答:减少hash冲突,提高性能
如果不是2的n次幂会怎么样?比如如果是9?
答:会通过无符号右移 按位或运算变为比指定容量大的最小的2的n次幂
2。默认的负载因子,默认值是0.75
1 | static final float DEFAULT_LOAD_FACTOR = 0.75f; |
达到这个之后,会扩容,rehash
3。集合最大容量
1 | static final int MAXIMUM_CAPACITY = 1 << 30;//集合最大容量的上线是2的30次幂 |
4。当链表的值超过8则会转红黑树(1.8新增)
1 | static final int TREEIFY_THRESHOLD = 8; |
链表中节点的分布符合泊松分布,也就是说转变为红黑树的概率非常小
5。当链表的长度小于6则会从红黑树转回链表
1 | static final int UNTREEIFY_THRESHOLD = 6; |
6。当map里面的数量超过这个值时,表中的桶才会树化,否则桶内元素太多时会扩容,而不是树化沦为了避免进行扩容、树化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD(8)
1 | static final int MIN_TREEIFY_CAPACITY = 64;//桶中结构转化为红黑树时对应数组长度最小的值 |
7。table用来初始化(必须是2的n次幂)(★★★★)
1 | transient Node<K,V>[] table ; |
table在jdk1.8中是由数组+链表+红黑树来组成的结构其中table就是hashMap中的数组,jdk8之前的数组类型是Entry<K,V>类型,jdk8之后就是Node<K,V>类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据的
8。HashMap中存放元素的个数(★★★★)
1 | //存放元素的个数,注意这个不等于数组的长度 |
size为HashMap中k-v的实时数量,不是数组table的长度。
9。用来记录hashMap的修改次数
1 | transient int modCount;//每次扩容和更改map结构的计数器 |
10。用来调整大小下一个容量的值计算方式(容量*负载因子)(边界值)
1 | int threshold; |
11。哈希表的加载因子(★★★★)
1 | final float loadFactor; |
问题1:为什么架子啊因子设置是0.75,为什么不是是0.5呢?
考虑到hash冲突的问题,
6.1.2构造方法
HashMap中重要的构造方法,他们分别如下:
1.构造一个空的hashMap,默认初始容量(16)和默认负载因子(0.75)
1 | public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; |
2.构造一个具有指定出事容量和默认负载因子的HashMap(0.75)
1 | public HashMap(int initialCapacity) { |
3.构造一个具有指定的初始容量和负载因子的HashMap(★★★★★)
1 | public HashMap(int initialCapacity, float loadFactor) { |
6.1.3 成员方法
增加方法
put方法是比较复杂的,实现步骤大致如下:
- 先通过hash值计算出key映射到哪个桶(就是数组)
- 如果桶上没有碰撞冲突,则直接插入
- 如果出现碰撞冲突,则需要处理冲突
- 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据
- 否则采用传统的链式方法插入,如果链的长度达到临界值,则把链表转化为红黑树
- 如果同种存在重复的键,则为该键替换为新值value;
- 如果size大于阈值threshold,则进行扩容
具体的方法如下
1 | public V put(K key, V value) { |
将链表转为红黑树
扩容方法 resize
什么时候才需要扩容?
第一次是在putVal的时候扩容
补充:当HashMap中的其中一个链表的对象个数如果达到8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到64,那么这个链表会变成红黑树,节点类型由node变为treenode
当然,如果映射关系被移除后,下次执行resize方法时判断树节点个数低于6个,也会再把树转换为链表
扩容是在做什么?
jdk8之前:
reHash
创建新的数组
遍历放到新的数组
jdk8开始:
扩容避免了reHash,如何避免的呢?不需要重新计算hash;扩容之后要么还是在原来的索引位置,要么就是在(原来的索引位置+旧容量)的索引位置
HashMap的扩容是什么?
面试题:谈谈你对hashMap的理解
hashmap是一种存取高效,但是不保证有序的容器。数据结构是数组+链表+红黑树,是解决hash冲突的产物,它实现了Map接口,采用kv键值对存储数据,并且实现了浅拷贝和序列化
默认大小是16,阈值是0.75,初始化大小必须是2的幂,最大是2的30次方,数组中存储的链表节点Entry类实现于Map.Entry接口,实现了对节点的通用操作
HashMap提供了四种构造方法,默认构造、指定初始容量构造、指定初始容量和阈值构造、基于Map的构造。虽然是构造方法,但是真正从初始化是在第一次添加操作里面实现的
第一次添加操作时,先判断数组有没有初始化,如果没有则先初始化
添加操作的流程:
- 先判断是否初始化
- 再判断传入的key是否为空,为空则保存在table[0]位置
- key不为空的就对key进行hash,hash的结果再&数组的长度就得到存储的位置
- 如果存储位置为空则创建节点,不为空说明存在冲突
- 解决冲突HashMap会先遍历链表,如果有相同的value就更新旧值,否则构建节点添加到链表头
- 添加还要先判断存储的节点数量是否达到阈值,达到阈值就要进行扩容
- 扩容2倍,是新建数组,所以要先转移节点,转移时都重新计算存储位置,可能保持不变,可能为就容量+位置
- 扩容结束后新插入的元素也得再hash一遍才能插入
获取节点的操作流程:
- 判断是否为空,为空就去table[0]找值
- 不为空,也是先hash,&数组长度计算下标位置
- 遍历找相同的key返回值
HashMap是并发不安全的容器,并发添加操作中会出现丢失更新的问题;因为采用头插法在并发扩容时会产生环形链表的问题,导致cpu达到100%
解决并发问题可以采用
- java类库提供的Collections工具包下的Collections.synchronizedMap()方法,返回一个线程安全的map
- 使用并发包下的ConcurrentHashMap。它采用的是分段锁机制实现线程安全
- 使用HashTable(不推荐)
回答顺序:数据结构+继承结构+基本字段+构造方法+添加操作+扩容操作+并发问题+与1.8的区别
知识点扩展
说说浅拷贝与深拷贝的区别
基本数据类型:直接存储在栈中
引用数据类型:存储的是该对象在栈中引用,真实数据存放在堆内存里。
这两种都是针对于Object和Array这样的引用类型数据的
浅拷贝只复制指向某个对象的指针,而不是复制对象本身,新旧对象还是共享同一块内存,拷贝对象的时候只对基本数据类型进行了拷贝,对于引用数据类型只是进行了引用的传递,没有真实的创建一个新的对象。
深拷贝会创造一个一模一样的对象,新旧对象不共享内存
Collections.synchronizedMap()和Hashtable的区别
HashMap和Hashtable的区别
Hashtable是个过时的集合类
两者都实现了map接口,两者介乎等价,除了:Hashtable是synchronized的,这意味着线程安全,多个线程可以共享一个hashtable,java5提供了concurrentHashMap,他是Hashtable的替代,比Hashtable扩展性更好
HashMap Hashtable 线程是否安全 否 是 效率 高 低 null key 和null value 是 否 底层数据结构 数组+链表+红黑树 实现方式 继承AbstractMap类 继承了Dictionary类,是jdk1.0添加,好像已经过时了 初始化容量不同 16 11 扩容机制不同 翻倍 翻倍+1 迭代器不同 Iterator是fail-fast的 Enumerator不是fail-fast的 快速失败fail-fast是啥?原理是啥
是java集合中的一种机制,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception
原理:在遍历的过程中使用一个modCount遍历用来记录内容变化,如果发生变化,他的值就会改变,每当迭代器使用hashNext、next遍历下一个元素之前,都会检测modCount变量
fail-fast场景?
java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改,算是一种安全机制吧。
安全失败 fail-safe
java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
Hashtable效率低的原因?
看过源码,会发现,他对数据的操作都会上锁
为啥Hashtable不允许键值为null?
Hashtable在put空值的时候回直接抛异常,HashMap却做了特殊处理
Hashtable使用的是安全失败机制,这种机制会使你此次读取到的数据不一定是最新的数据,如果你使用null值,就会使得它无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。
*ConcurrentHashMap *
ConcurrentHashMap的数据结构,以及并发度这么高?
底层基于数组+链表组成的,不过在jdk1.7与18中稍有不同
1.7版本
先看1.7 如图所示:是有segment数组、HashEntry组成,和HashMap一样,仍然是数组+链表
HashEntry跟HashMap差不多,不同的是,他使用了volatile修饰他的value还有next。
ConcurrentHashMap采用了分段锁,其中Segment继承于ReentrantLock。不会像Hashtable那样不管是put还是get操作都需要做同步处理,理论上他支持Segment数量的并发。
每当一个线程占用锁访问一个Segment时,不会影响到其他的Segment。
put:第一步的时候回尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则尝试自旋获取锁,如果重试的次数达到了Max_SCAN_RETRIES则改为阻塞锁获取,保证能获取成功
get:将key通过hash之后定位到具体的Segment,再通过一次Hash定位到具体的元素上。由于HashEntry中的value是volatile的,保证了内存可见性,所有每次获取的都是最新值,整个过程不需要加锁
虽然1.7支持每个Segment并发访问,但是还是存在一些问题?基本还是数组+链表的方式,查询时候,还得遍历链表,效率低下,跟1.7的HashMap存在一样的问题
1.8版本
基本抛弃了原有的Segment分段锁,采取了CAS+Synchronized来保证并发安全性,跟HashMap很像,也把之前的HashEntry改成了node,但是作用不变
值的存取过程?以及怎么保证线程安全?
put操作还是比较复杂的,大概有一下几个步骤:
- 根据key计算出Hashcode。
- 判断是否需要进行初始化。
- 即为当前key定位出的node,如果为空表是当前位置可以写入数据,利用cas尝试写入,失败则自选保证成功。
- 如果当前位置的hashCode == moved == -1,则需要进行扩容
- 如果都不满足,则利用synchronized锁写入数据
- 如果数量大于 TREEIFY_THRESHOLD,则要转换为红黑树
这里所说的cas是什么?自旋又是什么?
cas是乐观锁的一种实现方式,是一种轻量级锁,juc中很多工具类的实现就是基于cas的
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
cas就一定能保证数据没被别的线程修改过吗?
并不能,比如经典的ABA问题,CAS就无法判断了
什么是ABA
就是一个线程把值改成了b,又一个线程把值改回了a,对于当前线程来说,发现他的值还是a,所有就不知道这个值到底有没有被人修改过,如果只追求最后结果正确,这是没关系的
但实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯
如果和解决ABA问题?
用版本号去保证就好, 比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号+1,伪代码如下
1
2 update a set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样除了版本号还有别的方法保证吗
比如时间戳也可以,查询的时候把时间戳一起查询出来,对的上才修改并且更新值的时候一起修改更新时间,方法很多,都是跟版本号异曲同工
cas性能很好,但是Synchronized性能不咋地,为啥1.8之后反而多了Synchronized?
synchronized之前一直都是重量级的锁,但是后来java官方对他进行过升级 的,他现在采用的是锁升级的方式去做的。
针对synchronized获取锁的方式,jvm使用了锁升级的优化方式,就是先使用偏向锁优先同一进程然后再次获取锁,如果失败,就升级为cas轻量级锁,如果失败就会端在自旋,防止线程被系统挂起,最后如果以上都失败就升级为重量级锁;所有是一步步升级上去的,一开始也是通过很多轻量级的方式锁定的
先来一张1.7图:
jdk1.7从图中可以看出,Hashtable的锁是加在整个表上的,而ConcurrentHashMap是加在segment(每个段上的),这样我们在堆segment1操作的时候,同时也可以对segment2中的数据操作,这样效率会高很多。需要hash两次,第一次Hash定位到segment,第二次Hash定位到元素所在的链表的头部
jdk1.8
jdk8中ConcurrentHashMap参考了jdk8HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用cas操作(CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。)从jdk8来看,它的数据结构已经接近HashMap,只是增加了同步的操作来控制并发
网络tcp常见面试题
为什么建立连接是三次握手?
两个目的:信息对等、防止超时
先从信息对等的角度来看,如下图,双方只有确定四类信息,才能建立连接,第二次握手之后,从b机器的视角看还有两个红色的no是无法确定的,只有在第三次握手之后才能确认完毕
防止脏数据,ttl网络报文的生存时间往往都会超过tcp请求超时时间,如果两次握手就建立连接,传输数据并释放连接后,第一次超时的请求才到达b机器,b机器会认为是a创建新的连接请求,然后确认统一创建连接,因为a的状态不是xx,所以直接丢了b的确认数据,以至于最后只是b机器单方面的创建连接完毕
tcp断开连接为什么是四次?
为什么四次?
确保数据能够完成传输
第一次:**
A—>B:我要断开连接
第二次:
B—>A:我答应你,但是要等我处理完数据
第三次
B—>A:我处理完了,咱们断开连接把
第四次
A—->B:我答应你,咱俩掰了
快速排序
快速排序经典的呢,叫单轴快排,改进的叫双轴快排
他的排序思想就是:在数组中选择一个元素作为轴进行排序
第一次将大于轴的元素放在轴右边,小于轴的元素放在轴左边,这样就完成了第一次排序,然后对轴两边分区选轴进行递归排序,直到只剩下一个元素时返回。
双轴快排使用到Arrays中是sort方法,所处理的都是基本数据类型
空间复杂度是 ologn,如果不考虑递归的话,可以达到o1
链表,反转链表
二叉树遍历
并发
volatile
可见性(✅)
会强制将修改的值写入主内存,并且其他用过的工作内存的值都无效了
原子性(❌)
自增不是原子操作,volatile也无法保证对变量的任何操作都是原子性的
valatile只能保证对单次读、写的原子性
有序性(✅❌一定程度)
volatile禁止指令重排有两层意思:
- 当程序执行到volatile变量的读操作或者写操作时,在他前面的操作肯定已经全部进行,且结果对后面的操作可见;在他后面的操作肯定都还没有执行
- 在进行指令优化时,不能讲volatile变量访问的语句放在他后面执行,也不能放到前面执行
使用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于他,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile无法保证操作的原子性
使用volatile必须具备以下两个条件(实际就是保证使用场景是原子性操作)
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
多线程
1、为什么用线程池
使用线程池主要是为了解决:
通过重用线程池中的线程,来减少每个线程的创建销毁的性能开销
对线程进行一些维护和管理,比如定时开始,周期执行,并发控制等等
不要用多线程去治理长连接,肯定是异步的网络模型nio,不在这里多说。
2、线程池参数什么意思?
corePoolSize
常驻核心线程数,即本地任务执行完毕,核心线程也不会被销毁
maximumPoolSize
线程池最大线程数,必须大于等于1,如果待执行的线程数大于此值,需要借助第五个参数(workQueue)的帮助,缓存在队列中,如果与核心线程数一致的话,即固定了线程池大小
keepAliveTime
线程池中的线程空闲时间,当空闲时间达到此值时,线程会被销毁,只剩下核心线程
TimeUnit
时间单位,结合上面这个一起用,通常是秒
workQueue
表示缓存队列,请求的线程数大于线程池最大线程数时,线程进入BlockingQueue阻塞队列
threadFactory
线程工厂,用来生产一组相同任务的线程,线程池的命名就是通过给这个factory增加组名前缀来实现的
handler
表示执行拒绝策略的对象,当超过第五个为参数workQueue的任务缓存区上线的时候,就可以通过该策略处理请求,这是一种简单的限流保护
3、线程池中的threadpoolexecutor,每个参数是干嘛用的?
4、说一下线程池内部使用规则
5、用过AtomicInteger吗?怎么用的
6、用过threadlocal吗?怎么用的
7、程序、进程、线程的区别?举个现实的例子说明
8、java中通过哪些方式创建多线程类?分别使用代码说明,并调用之
9、Thread类有没有实现Runnable接口?
10、当调用一个线程对象的start方法后,线程马上进入运行
11、下面的代码,实际上有几个线程在运行
12、线程的几种状态
13、说说:sleep、yield、join、wait方法的区别
join
谁调用了join,谁就会被阻塞,直到join执行完毕
当先线程等待,调用此方法的线程执行结束再继续执行
比如:在main方法中调用t.join(),那么main会进入阻塞状态,一直等待t线程执行完毕,main方法再恢复到就绪状态,准备继续执行
sleep
需要制定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态。
该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。
14、为什么不推荐使用stop和destory方法来结束线程
15、写个代码说明,终止线程的典型方式
16、A线程的优先级是10,b线程的优先级是1,那么当进行调度时一定会调用A吗?
17、synchronized加在static关键字前和普通方法前的区别?
Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”,不同的对象没有竞争关系
Synchronized修饰static静态方法,实际上是对该类对象加锁,俗称“类锁”,这个类所有的对象竞争一把锁
结论:类锁和对象锁不同,他们之间不会产生互斥。
18、使用Timer和TimerTask实现定时执行,定时在每天下午17:00执行
19、wait方法被调用时,所在线程是否会释放所持有的锁资源?sleep方法呢?
20、wait、notify、notifyAll是在Thread类中定义的方法吗?作用分别是什么?
21、notify是唤醒所在对象watipool中的一个线程吗?
java虚拟机
说一说jvm的内存模型(每一个模块都说)
方法区(Method Area)
方法区主要是放一下类似类定义、常量、编译后的代码、静态变量等,在jdk1.7中hotspot vm 的实现就把他放在了永久代中,这样的好处是可以直接使用堆中的gc算法来管理,但坏处就是经常会出现内存溢出。
所以在1.8中,取消了永久代,用元空间取而代之,元空间直接使用本地内存,perm(永久代)区的字符串常量在堆内存
常量池中的string实际是保存在堆内存中的
堆(heap)
oom故障最主要的发源地,几乎存储着所有的实例对象,堆由垃圾收集器自动回收,由各子线程共享使用,通常情况下,堆占用的内存是内存区域中最大的
对的内存空间既可以固定大小,也可以运行时动态调整,但是通常为了避免堆空间的不断扩容与收缩,在线上环境时,jvm最大堆空间和最小堆空间设置成一样,避免gc后调整大小带来的额外压力
新生代:1个eden区+2个survivor区
绝大部分对象在eden区生成,当eden区满了的时候,会触发young gc。垃圾回收的时候,在eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被已送到Survivor区。
Survivor分为s0 和 s1两块内存空间,那么送到哪块呢?每次ygc的时候,他们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。
如果ygc要移送的对象大于Survivor区容量的上限,则直接移交给老年代。
每个对象都有一个计数器,每次ygc都会加1,-XX:MaxTenuring Threshold参数能配置计数器的值达到某个阈值的时候,对象从新生代晋升至老年代,如果该参数配置为1,那么从新生代的eden区直接移送到年老代。
默认值是15,可以在Survivor区交换14次之后,晋升至老年代。
流程图如下:
![image-20200114135528237](img/尚学堂 java 300 讲.assets/image-20200114135528237.png)
如果Survivor区无法放下,会尝试在老年代中进行分配,如果老年代也放不下,则会触发「fgc」,如果『fgc』后依然放不下,则抛出oom,所以给jvm设置运行参数-XX:+heapDumpOnOutOfMemoryError,让jvm遇到oom异常时能输出堆信息,特别是相隔数月才出现的oom异常尤为重要
虚拟机栈
jvm在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等,方法开始执行前就先创建栈帧入栈,执行完后就出栈
只有位于栈顶帧才是有效的, 称为“当前栈”
本地方法栈
和虚拟机栈类似,不过是专门给native方法用的
程序计数器
占用很小的一块区域,我们知道jvm执行代码是一行一行执行字节码文件,所有需要一个计数器来记录当前执行的行数
说一说垃圾回收吧?有哪些垃圾回收算法
标记-清除
分为两个阶段,标记阶段、清除阶段
在标记阶段,首先通过根节点(gc roots),标记所有从根节点开始的对象,未被标记的对象是未被引用的垃圾对象,然后,在清除阶段,清除所有未被标记的对象
缺点:会带来大量的空间碎片,在分配一个较大连续空间时容易触发fgc
标记-整理
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象整理到内存空间的一端,形成连续的已使用空间,最后把已使用空间之外的部分全部清理掉,这样就不会产生空间碎片
标记-复制
为了能够并行的标记和整理,将空间分为了两块,每次只激活其中一块,垃圾回收时只需要把存活的对象复制到另一块未激活的空间上,然后把未激活标记为已激活,把已激活标记未激活,然后清理原空间中的对象
场景:作为主流的ygc算法进行新生代的垃圾回收
分代收集算法
目前虚拟机使用回收算法,解决了标记整理不适合老年代的问题
将内存分为各个年代,一般是来年代,和新生代,永久代(jdk1.8中被元空间替代)
新生代存活率低,可以使用复制算法
老年代对象存活率高,没有额外空间对他进行分配担保,所以只能使用标记清除或者标记整理
如何判断一个对象是否应该回收
方法1:引用计数,该对象每被一个地方引用,计数器就加+1,引用失效时,计数器-1,计数器为0时的对象就不能再被使用
缺点:很难解决循环引用的问题
方法2:可达性分析法:通过gc roots作为七点,从这些节点开始,向下搜索,当一个对象到gc roots没有任何引用链,这个对象就是不可用的,至少两次标记
什么可以作为GC ROOT呢?
类静态属性中引用的对象(方法区)
常量引用的的对象(方法区)
虚拟机栈中引用的对象(栈帧中的本地变量表)
jni引用的对象(本地方法栈)
除了垃圾回收,还有哪些工作会造成cpu负载过高,100%负载,并给出排查过程
- 使用top命令查找占用cpu高的进程pid
- 显示线程列表 ps -mp 35867 -o THREAD,tid,time—-找到占用cpu过高的tid
- 将需要的线程tid转换为16进制 printf “%x\n” 35889
- 打印线程堆栈信息 jstack 35867 |grep 8c31 -A 30
cms收集器的特点(标记-清除)
回收停顿时间比较短,四个步骤完成工作
- 初始标记(只是标记一下gc roots能直接关联到的对象,速度很快)
- 并发标记(进行gc roots tracing 追踪)
- 重新标记(为了修正并发标记期间因用户程序持续运行产生变动的那一部分对象的标记记录,比1长,比2短)
- 并发清除()
缺点
第1、3步需要stw
对cpu资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
因为标记-清除,会产生大量碎片,可以配置参数,强制jvm在fgc后对老年代进行压缩,但是会stw
G1★★★★
四个步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
g1在jdk1.7中是推荐使用,jdk11是默认的
将堆空间分割成若干个大小相同的区域,即region,包括eden、Survivor、old、Humongous四种类型,Humongous是特殊的ol的类型,专门存放大对象,防止了反复拷贝移动
这样的划分方式,意味着不需要一个连续的内存空间管理对象,g1采用的是mark-copy,不产生大量的空间碎片
g1提供两种gc模式,ygc,mixedgc,两种都是stw的
面试题
String a = ‘’abc”; 和 String b = new String(“abc”);是不是一样的?为什么?他们对应的内存空间分别是什么?
不一样
String a = ‘’abc”是在现在栈中创建一个string类的对象引用变量,然后查找栈中有没有存放“abc”,如果没有则将“abc”放入栈,并令a指向abc
new String(abc)是在堆中存放,每次new 都会在堆中存放一个
equals比较值是否相等,==比较地址是否相等
Object o = new Object()
解释一下对象的创建过程(半初始化)
1
2
3
4
5 static class T{
int m = 8;
}
T t = new T();编译后的字节码如下
new
为对象申请了一块内存,m此时是默认值,半初始化
dup
invokespecial #3<T.
> 这里才会把m设置为8,第1、3步之间就是半初始化状态
astore_1
return
dcl单例(Double Check Lock)到底需不需要volatile问题
上锁前检查一次,上锁后再检查一次,要不要在定义的 时候加个volatile?
阻止对这块内存的访问指令重排序 。需要,出现的几率很小,但是还是需要
对象在内存中的存储布局
普通对象 new xx()
- 对象头markword
- 类型指针classpointer:这个类型是什么类型?指向xx.class
- 实例数据instance data 比如说m=1,对象的数据
- 对齐padding ,比如最后算出来是30个字节?不是8的倍数怎么办?长成32
数组 T[] a = new T[5]
- 对象头
- 类型指针:数组是什么类型?int?T?
- 数组长度(length 4字节)
- 实例数据
- 对齐
对象头具体包括什么
- 锁的信息
- gc的信息:被回收多少次了
- hashCode
对象头占用8个字节(64位虚拟机)
什么时候回产生hashcode?当然是调用未重写的hashcode()方法以及System.identityHashCode的时候
对象怎么定位
直接指针是hotspot使用的方式
句柄方式:
优点:对象小,垃圾回收时不用频繁改动t
缺点:两次访问
直接指针
对象怎么分配(栈上-线程本地-eden-old)
该代码在内存中占用多少字节
要看压不压缩
一个object占多少字节?
16
new int[]{} 是24
常用框架、技术
spring
spring框架是一个轻量级的控制反转和面向切面的容器框架
轻量
从大小到开销来说都是轻量的,spring框架开源字啊一个只有1m大小的jar文件中发布,而且spring所处理的开销也是微不足道的
非侵入式
spring应用中的对象不依赖于spring的特定类
IOC(控制反转)
一个对象依赖的其他对象会通过被动的方式传递进来,而不是这个对象自己创建或者主动查找依赖对象
AOP(面向切面)
只完成业务逻辑,并不负责其他的系统级的关注点
容器
定义了bean是如何创建、配置、管理的
框架模块
你可以不必将应用完全基于spring,可以自由的挑选适用你的模块,而忽略其余的模块
核心容器
这是spring框架最基础的部分,它提供了依赖注入特征来实现容器对bean的管理。这里最基本的概念是BeanFactory,它是任何spring应用的核心。BeanFactory是工厂模式的一个实现,他使用ioc将应用配置和依赖说明从实际的应用代码中分离出来
应用上下文(context)模块
核心模块的beanFactory使spring成为了一个容器,而上下文模块使他成为一个框架,这个模块扩展了BeanFactory的概念,增加了对国际化消息、事件传播以及验证的支持
另外,这个模块提供了许多企业服务,例如电子邮件、JNDI访问、EJB集成、远程以及时序调度(scheduling)服务。也包括了对模版框架例如Velocity和FreeMarker集成的支持。
AOP模块
JDBC抽象和DAO模块
使用jdbc经常导致大量的重复代码:连接、创建于巨,处理结果集,关闭链接。
这两个模块抽取了这些重复代码,因此可以保持数据库访问代码干净简洁。
这个模块还使用了aop模块为spring应用中的对象提供了事务管理服务
对象/关系映射集成
orm模块,spring并不试图实现自己的orm解决方案,而是为集中流行orm框架提供了集成方案。
spring的事务管理支持这些orm框架中的每一个也包括jdbc。
WEB 模块
web上下文模块建立于应用上下文模块之上,提供了一个合适于web应用的上下文。另外,这个模块还提供了一些面向服务支持。例如:实现文件上传的multipart请求,它也提供了spring 和其它web框架的集成,比如Struts、WebWork。
mvc框架
spring为构建web应用提供了一个功能全面的mvc框架。虽然spring可以很容易的与其它mvc框架集成,例如struts,但spring的mvc框架使用ioc对控制逻辑和业务对象提供了完全的分离。
ioc
使用到的设计模式
1.简单工厂模式
应用场景:又叫静态工厂,不属于23中设计模式。实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类
BeanFactory就是简单工程模式的体现,根据传入一个唯一的标识来获取bean对象
2.工厂模式
应用场景:通常由应用程序直接使用new创建新的对象,为了将对象的创建和使用相分离,采用工厂模式,即应用程序将对象的创建及初始化职责交给工厂对象。
一般情况下,应用程序有自己的工厂对象来创建bena,如果将应用程序自己的工程对象交给spring管理,那么spring管理就不是普通bean,而是工程bean
3.单例模式
应用场景:保证一个类只有一个实例,并提供一个访问它的全局访问点
spring中的单例模式只完成了后半句,即提供了全局的访问点BeanFactory,但没有从构造器级别去空值单例,这是因为spring管理的是任意的java对象,spring下默认的bean均为单例
4.原型模式-Prototype
应用场景:原型模式就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节
所谓原型模式,就是java中的克隆技术,以某个对象为原型,复制出新的对象,显然新的对象具备原型对象的特点,效率噶(避免了重新执行构造过程步骤)
5.代理模式–Proxy
应用场景:为其他对象提供一种代理以控制对这个对象的访问。从结构上看和Decorator(装饰器)模式类似,但是proxy是控制,更像是一种对功能的闲置,而Decorator是增加职责。
spring中代理模式体现在aop中,比如cglibAopProxy,和jdkAopProxy
6.策略模式–Strategy
应用场景:定义一系列的算法,把它们一个个封装起来,并且使他们可互相替换,最终执行结果是固定的,执行过程和执行逻辑不一样
spring中在实例化对象的时候用到了Strategy模式
7.模板方法模式–TemplateMethod
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤
TemplateMethod模式一般是需要继承的。执行流程固定,但是中间实现步骤有细微差别
springorm数据类型
8.委派模式–Delegate
应用场景:不属于23中设计模式,是面向对象设计模式中常用的一种模式。这种模式的原理为类b和类a是两个没有任何关系的类,b具有和a一模一样的方法和属性;并且调用b中的方法,属性就是调用a中同名的方法和属性,b好像就是一个受a委托授权的终结。第三方的代码不需要指定a的存在,也不需要和a发生直接的联系,通过b就可以直接使用a的功能,这样既能够使用到a的各种功能,又能够很好的将a保护起来,一举两得
要和代理模式区分开来,持有被委托人的引用,不关心过程,只关心结果
DIspatcher
9.适配器模式–Adapter
springAOP模块对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型的支持实际上是借助适配器模式来实现的,这样的好处是使得框架允许用户向框架中加入自己想要支持任何一种通知雷丁,这三种通知类型是springAOP模块定义的,他们是AOP联盟定义的Advice的子类型
10.装饰器模式–Decorator
应用场景:比如说项目需要多数据库连接,用户每次访问都会根据需要去访问不同的数据库
首先想到的是在spring的applicationContext中配置所有的DataSource。这些DataSource可能是各种不同类型的,比如不同的数据库oracle、mysql,也可能是不同的数据源:比如Apache提供的org.apache.commons.dbcp.BasicDataSource、Spring提供的org.springframework.jndi.JndiObjectFactoryBean等。然后SessionFactory根据客户的每次请求,将DataSource属性设置成不同的数据源,以到达切换数据源的目的。
spring中用的包装器模式在类名上有两种表现:一种是类名中含有Wrapper、另一种是类名中喊有Decorator。基本上都是动态的给一个对象添加一些额外的职责。
io流包装、数据源包装等
11.观察者模式–Observer
应用场景:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变是,所有依赖于它的对象都得到通知并被自动更新
Spring中Observer模式常用的地方是Listener的实现。如:ApplicationListener
一般由两个角色组成:发布者和订阅者(观察者),观察者通常有一个回调,也可以没有,监听器、日志收集
设计模式对比
设计模式 | 一句话归纳 |
---|---|
工厂模式Factory | 只对结果负责,不要三无产品 |
单例模式Singleton | 保证独一无二 |
适配器模式Adapter | 需要一个转换头(兼容) |
装饰器模式 Decorator | 需要包装,但不改变本质(同宗同源) |
代理模式Proxy | 办事要求人,所以找代理 |
观察者模式Observer | 完成时通知我 |
策略模式Strategy | 我行我素,达到目的就行 |
模板模式Template | 流程标准化,原料自己加 |
委派模式Delegate | 干活是你的(普通员工),功劳是我的(项目经理) |
原型模式prototype | 拔一根好,吹出千万个 |
编程思想总结
spring思想 | 应用场景(特点) | 一句话归纳 |
---|---|---|
aop | 面向切面变成,找出多个类中中有一定规律的代码,开发时拆开,运行时再合并,例如aop日志 | 解耦,专人做专事 |
oop | 面向对象变成,归纳总结生活中的一切事务 | 封装、集成、多态 |
bop | 面向bean编程,面向bean(普通java类)设计程序 | 一切从bean开始 |
ioc | 控制翻转,将new对象的动作交给spring管理,并由spring保存已创建的对象(ioc容器) | 转交控制权(控制翻转) |
DI/DL | 依赖注入或者依赖查找,spring不仅保存自己创建的对象,而保存对象与对象之间的关系 注入即赋值,主要三种方式:构造方法、set方法、直接赋值 |
先理清关系再赋值 |
cglib与jdk的区别是:
创建代理的消耗
cglib不适合频繁创建,适合创建一次,长期使用
spring bean 的加载、注入过程
- 执行该对象的构造方法
- 执行set参数注入方法
- 执行BeanNameAware的实现方法获取bean的id
- 执行BeanFactoryAware的实现方法获取bean的工厂
- 执行BeanPostProcessor的postProcessBeforeInitalization处理方法
- 执行In
说一下对spring的理解,ioc和aop在项目里怎么用的
spring 是一个开源框架,处于mvc的控制层,能应对需求的快速变化,主要原因是它有一种面向切面编程(aop)的优势
其次它提升了系统性能,是因为通过依赖倒置机制(ioc),系统中用到的对象不是在系统加载时就全部实例化,而是在调用到这个类时才会实例化该对象
优点
- 降低了组件之间的耦合性,实现了软件各层之间的解耦
- 可以使用容易提供的众多服务,事务管理、消息服务、日志记录等
- 容器提供了aop,利用它很容易实现如权限拦截、运行期监控等功能
Spring中的aop技术是设计模式中的动态代理模式
AOP的两种实现方式?哪个效率更高?为什么?
jdk动态代理、cglib
JDK动态代理具体实现原理
- 通过实现InvocationHandle接口创建自己的调用处理器
- 通过Proxy类指定ClassLoader对象和一组interface来创建动态代理
- 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型
- 通过构造函数创建动态代理实例,构造时调用处理器对象作为参数传入;
jdk动态代理是面向接口的代理模式,如果被代理目标没有接口那么spring也无能为力,Spring通过java的 反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。
CGLib动态代理
强大的、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过cglib集成要被动态代理的类,重写父类的方法,实现Aop切面变成
两者的对比
jdk是面向接口的
cglib是通过字节码底层集成要代理类来实现(如果被代理类被final修饰,那么会失败)
性能
主要体现在如下的两个指标中
- cglib所创建的动态搭理对象在实际运行时候的性能要比jdk高,大概10倍
- cglib在创建对象的时候所花费的时间比jdk要高,大概8倍
因此,对于Singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用cglib动态代理,反之使用jdk
spring事务的传播机制
有如下几种
PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
常用的主要由三个:Required、RequresNew、Nested
- Required:简单理解就是事务方法会判断是否存在事务,有事务就用已有的,没有就重新开启一个
- RequiresNew:简单理解就是开启新事务,若当前已有事务,挂起当前事务。新开启的事务和之前的事务无关,拥有自己的锁和隔离级别,可以独立提交和回滚,内层事务执行期间,外层事务挂起。内层事务执行完毕,外层事务恢复执行
- Nested:简单理解就是:嵌套事务,如果外部事务回滚,则嵌套事务也回滚!!外部事务提交的时候,嵌套事务才会被提交。嵌套事务回滚不会影响外部事务
如果想事务一起执行可以用Required满足大部分场景,如果不想让执行的子事务的结果影响到父事务的提交,可以将子事务设置为RequiresNew
简单说一下IOC、DI
Inversion on Control,控制翻转,对象的创建交给外部容器完成,这个就叫做控制翻转
Dependency injection,依赖注入、处理对象的依赖关系
区别:
- 控制反转,解决对象创建的问题
- 依赖注入,在创建完对象后,对象的关系的处理就是依赖注入
springboot
核心配置文件是哪几个?他们的区别是啥?
applicaion和boostrap配置文件
application配置文件主要用于springboot项目的自动化配置
boostrap配置文件主要用于
一些固定的不能被覆盖的属性,一些加密解密的场景
配置文件有哪几种格式?有什么区别?
properties和yml
区别就是书写格式不同
另外 yml 格式不支持@propertySource注解导入配置
事务是怎么实现的?
基于@Transactional注解
整体事务控制流程
- 当@Transactional注解的方法被外部的代码调用时,spring在运行时为方法所在类生成一个aop代理对象。
- 代理对象根据@transactional的属性,决定是否由事务拦截器TransactionInterceptor对此方法进行事务拦截。
- 在进行事务拦截时,会先开启事务,然后执行业务代码,根据执行是否出现异常,通过抽象事务管理来进行rollback或者commit。
数据库引擎是否支持事务?
mysql的mylsam不支持事务
如果事务生效,库和表的引擎必须是InnoDB
当事务方法被本类内部方法调用时,@Transactional 注解并不生效,因为,只有被当前类以外的调用时,才会由spring生成的代理对象来管理
一定要确保所使用的数据源加载了事务管理器(配置文件写一下就好)
springboot的核心注解是哪个?他主要由哪几个注解组成?
启动类上面的注解是@springbootApplication,它是核心注解,包含了以下三个注解
@springbootConfiguration:组合了@Configuration注解,实现配置文件的功能
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
@ComponentScan:spring组件扫描
开启springboot 特性有哪几种方式?
- 继承spring-boot-starter-parent项目
- 导入spring-boot-dependencies项目依赖
springboot需要独立的容器运行吗?
可以不需要,内置了tomcat/jetty等容器。
springboot 配置加载顺序
- properties
- yaml文件
- 系统环境变量
- 命令行参数
springboot可以兼容老spring项目吗?如何做?
可以兼容,使用@ImportResource注解导入老spring项目配置文件
保护springboot应用有哪些方法?
- 在生产中使用https
- 启用csrf保护
- 使用内容安全策略防止xss攻击
private不能事务,基于aop实现的,aspectj可以??
springboot默认的代理是cglib??
spring cloud
微服务之间是如何独立通讯的?
- 同步:rpc、rest等
- 异步:消息队列
ribbon和feign的区别?
- 都是客户端的负载均衡工具,feign的底层是通过ribbon实现的,是对riboon的封装
- ribbon使用httpclient或者restTemplate模拟http请求,步骤繁琐。
- feign采用接口+注解的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建http请求。就像调用自身工程的方法一样调用
注册中心用的什么?
用的nacos= eureka+config
nacos优点?
- nacos自带配置中心,且提供了管理界面
- 动态刷新,eureka需要配合mq实现配置动态刷新,nacos采用netty保持tcp场链接实时推送
- nacos可用根据业务和环境进行分组管理
- 默认提供权重设置功能,调整承载流量压力
- nacos支持由客户端或服务端发起的健康检查,eureka是由客户端发起心跳
- nacos支持对服务在线管理,eureka只是预览服务状态
选型建议?
采用eureka防范的考虑
- 想用spring cloud 原生全家桶
- 想用本地文件和git作为配置管理的,将配置与服务分开管理
- 考虑短期的稳定性
采用Nacos方案的考虑
- 想在线对服务进行上下线和流量管理
- 不想采用MQ实现配置中心动态刷新
- 不想新增配置中心生产集群
- 考虑引入spring cloud alibaba生态
eureka 和zookeeper都可以提供服务注册和发现的功能,请说说两个的区别?
zookeeper保证的是cp,Eureka保证的ap
zookeeper在选举期间注册服务谈话,虽然服务最终会回复,但是选举期间不可用的
eureka各个节点是平等关系,只要有一台eureka就可以保证服务可以用,查询到的数据并不是最新的
自我保护机制会导致:
eureka不再从注册列表移除因长时间没有收到心跳而应该过期的服务
eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点(高可用)
当网络稳定是,当前实例新的注册信息会被同步到其他节点中(最终一致性)
eureka可以很好的应对因为网络故障导致部分节点失去联系的情况,而不会像zookeeper一样使得整个注册系统瘫痪
eureka可以看做是一个工程,而zookeeper只是一个进程
springcloud是如何实现服务发现和注册的?
服务在发布时指定对应的服务名(包括了ip地址和端口)将服务注册到注册中心(eureka或者zookeeper或者nacos)
在main方法添加@enableDiscoveryClient 同一个服务修改端口就可以启动多个实例
调用方的话:传递服务名称通过注册中心获取所有的可用实例,通过负载均衡策略调用(ribbon和feign)对用的服务
mybatis
说一下mybatis与hibernate的区别
共同点
都是通过orm对象关系映射框架,都是持久层数据框架
不同点
- hibernate重量级框架,Mybatis是轻量级框架
- Hibernate对jdbc的封装比较深,对开发者写sql的要求高,只要通过hql语句操作对象即可完成数据的持久化操作了
- Mybatis也是对jdbc的封装,但是没有H那么深,他的sql语句都在配置里,也可以通过重新配置里sql,来实现数据优化
- 处理大数据的时候,建议使用Mybatis,它优化sql更方便
RocketMq
核心模块
- rocketmq-broker:接受生产者发来的消息并存储(通过调用rocketmq-stroe),消费者从这里取得消息
- rocketmq-client:提供发送、接收消息的客户端API
- rocketmq-namesrv:NameServer,类似于Zookeeper,这里保存着消息的TopicName,队列运行时的元信息
- rocketmq-common:通用的一些类、方法、数据结构等
- rocketmq-remoting:基于Netty4的client/Server + fastjson序列化 + 自定义二进制协议
- rocketmq-store:消息、索引存储等
- rocketmq-filtersrv:消息过滤器(一般用tag就可以)
- rocketmq-tools:命令行工具
四大核心组成部分
他主要有四大核心:NameServer、Broker、Producer以及Consumer
可以看到,他啥都是可以集群的,这是他吞吐量大,高可用的原因之一
集群的模式也很花哨,可以支持多master模式、多master多slave异步复制模式、多master多slave同步双写模式
而且这个模式好像kafka啊,废话,rocketmq 本身就是阿里基于kafka的很多特性研发的
分别介绍一下各个集群组成部分
NameServer
主要负责对数据源的管理,包括了对于topic和路由信息的管理。
类似于Dubbo中zookeeper,但NameServer与Zookeeper相比更轻量。主要是因为每个NameServer节点互相之间是独立的,没有任何信息交互。
NameServer压力不会太大,平时的开销主要是维持心跳和提供Topic-Broker的关系数据。
但是有一点需要注意,Brker想NameServer发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多(万级别),会导致一次心跳中,光Topic的数据就几十m,网络情况差的话,网络传输失败,心跳失败,导致NameServer误认为Broker心跳失败
NameServer被设计成几乎无状态的,可以横向扩展,节点之间相互之间无通信,通过部署多台机器来标记自己是个伪集群。
每个Broker在启动的时候都会到NameServer注册, Producer在发送消息前会根据Topic到NameServer获取到Broker的路由信息,Consumer也会定时获取Topic的路由信息。
所有从功能上看NameServer应该是和Zookeeper差不多,据说RocketMq的早期版本确实使用的Zookeeper,后来改为了自己实现的NameServer
Producer
生产者,负责产生消息,一般是由业务系统负责产生消息
- Producer又用户进行分布式部署,消息由Producer通过多种负责均衡模式发送到Broker集群,发送延时低,支持快速失败。
- RocketMq提供了三种方式发送消息:同步、异步、单向
- 同步发送:指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,重要通知邮件,营销短信等
- 异步发送:指发送方发出数据后,不等待接收方发回响应,就发送下个数据包,一般用于对响应时间不敏感的业务
- 单向发送:值负责发送消息而不等待服务器的回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集
Broker
消息中转角色,负责存储消息,转发消息
Broker是具体提供业务的服务器,单个broker节点与所有的NameServer节点保持长连接及心跳,并会定时将Topic信息注册到NameServer,顺带一提底层的通信和连接都是基于netty实现的
Broker负责消息存储,以Topic为维度支持轻量级的队列,单机可以支撑上完队列规模,支持消息推拉模型
官网上说:具有上亿级消息堆积能力,同事可严格保证消息的有序性
Consumer
消费者,负责消费消息,一般是由后台系统负责异步消费
Consumer也由用户部署,支持push和pull两种消费模式,支持集群消费和广播消费,提供实时的消息订阅机制
Pull:拉取,主动从消息服务器拉取消息,只要批量拉取到消息,用户应用就会启动消费过程
Push:推送,封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所有Push被称为被动消费;从实现上看还是从消费服务器中拉取消息,不同于pull的是 push首先要注册消费监听器,当监听器触发后才开始消费消息。
一次完成的通讯流程是什么样的?
Producer 与 NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic陆游信息,并向Topic服务的Broker Master建立长连接,且定时向Broker发送心跳
Producer 只能将消息发送到Broker master,但是Consumer不一样,它同时和提供Topic的Master 和Slave建立长连接,既可以从Master订阅,也可以从Slave订阅消息
优点
- 单机吞吐量:十万级
- 可用性:非常高,分布式架构
- 消息可靠性:经过参数优化配置,消息可以做到0丢失
- 功能支持:mq功能较为完善,还是分布式的,扩展性好
- 支持10亿级别的消息堆积,不会因为堆积导致性能下降
- 源码是java,我们可以自己阅读源码,定制自己公司的mq,可以掌控
- 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况
- 稳定性更值得信赖,经历了多次双11
缺点
- 支持的客户端语言不多,java、c++(不成熟)
- 社区活跃度不是特别活跃的那种
- 没有在mq核心中去实现jms等接口,有些系统要迁移需要修改大量代码
消息去
陌陌面试题
java多线程的实现?
二叉树后序遍历
网络io模型
三次握手每次在干嘛?
两个有序表的第n 和第n+1 大的数,不要额外空间,时间复杂度优化到nlogn?
设计模式
单例模式
优点
只创建一个实例,节省内存开销
减少了系统的性能开销,创建、回收对象都对性能有影响
提供了对唯一实例的受控访问
允许可变数目的实例
缺点
- 没有抽象层,因此扩展有很大的困难
- 单例类的职责过重,一定程度上违背了“单一职责原则”
- 滥用单例将带来一些负面问题:比如实例化的对象长时间不被利用,系统会认为是垃圾而回收
使用场景
- web中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
- 要求生产唯一序列号
- 创建一个对象需要消耗的资源过多,比如I/O与数据库的连接等
关键代码
构造函数是私有的
注意事项
getInstance()方法中需要使用同步锁synchronized(Singleton.class)防止多线程同事进入造成instance被多次实例化。
懒汉
1 | /** |
饿汉
1 | public class HungrySingleton { |
创建线程安全的单例有哪些实现方法?
1 | 双检锁/双重校验锁 |
登记式/静态内部类
1 | public class Singleton{ |
枚举
1 | public enum Singleton{ |
ArrayList
ArrayList有用过吗?它是一个什么东西?可以用来做什么?
就是一个数组列表,主要用来状态数据,如果装在的是基本数据类型(int、long、boolean、short、byte、double、char、float)的时候,只能存储他们对应的包装类,它的主要底层实现是数组
与它类似是LinkedList、和LinkedList相比,它的查找和访问元素的速度较快,但是增删慢
小结:底层是数组实现的存储
特点:查询效率高,增删效率低,线程不安全,使用频率高
线程不安全,为啥还使用它呢?
因为正常使用的场景中,都是用来查询,不会涉及太频繁的增删,如果涉及频繁的增删,可以使用LinkedList,如果要线程安全就使用Vector,这就是三者的区别,实际开发中还是ArrayList使用的最多。
不存在一个集合工具查询效率高,增删效率也高,线程还是安全的。
做的也都是一些线下的系统,没啥并发
它的底层是数组,数组是定长的,如果我们不断的往里面添加数据的话,不会有问题吗?
ArrayList可以通过构造方法在初始化的时候指定底层数组的大小
如果是使用无参构造初始化,则赋值底层数据一个默认空数组,只有真正对数据进行添加是,才会分配默认的初始容量10
可以看下它的无参构造和有参构造,无参就是默认大小,有参会判断参数
数组是有长度限制的,而ArrayList是可以存放任意数量对象,长度不受限制,那么他是怎么实现的呢?
实现比较简单,他就是通过数组扩容的方式去实现的。
比如说现在有一个长度为10的数组,需要新增第11个了,发现已经满了,那么会怎么做呢?
第一步:他会重新定义一个10+10/2的数组也就是一个长度为15的数组
第二步:把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数组的地址切换到新地址,ArrayList就这样完成了一次扩容
能具体说下1.7和1.8初始化的时候的区别么?
初始化的时候:1.7以前会调用this(10)的时候才是真正的容量为10,1.7开始就是默认走空数组了,只有第一次add的时候才会变成10
ArrayList的默认数组大小为什么是10?
不清楚。。。
为什么增删慢?
因为他的数组,是连续的内存空间,比如说删除一个的话,需要移动后面所有的
ArrayList(int initialCapacity)会不会初始化数组大小?
会初始化数组大小!但是List没,那size就没变,set下标和size比较的那就报错了。
ArrayList插入删除一定慢么?
取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。
它的删除是怎么实现的呢?
删除跟新增是一样的,不过叫是叫删除,但是在代码里可以发现,他还是在copy一个数组,举个例子:要删除index5这个位置
那么代码就复制一个index5+1到最后的数组,然后把它放到index开始的位置
index5的位置就被成功“删除了”,起始就是被覆盖了
ArrayList是线程安全的么?
当然不是,线程安全的数组容器是Vector
Vector的实现很简单,就是把所有的方法统统加上Synchronized就完事了。
你也可以不使用vector,用Collections.synchronizedList把一个普通的ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized
ArrayList用来做队列合适么?
队列一般是FIFO(先进先出)的,如果用ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,或者反过来。
但是无论如何总会有一个操作涉及到数组数据的迁移,耗费性能
结论:不适合
那数组适合做队列吗?
数组是非常适合的
比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。
另外著名的Disruptor开源Library也是用环形数组来实现的超高性能队列,具体原理不做解释,比较复杂。
简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组。
ArrayList的遍历和LinkedList遍历性能比较如何?
ArrayList快得多,内存是连续的,cpu的内部缓存结构会缓存连续的内存片段,可以大幅度降低读取内存的性能开销。
总结:
ArrayList就是动态数组,可以看成Array的复杂版本,提供了冬天的增删,实现了ICollection和IList接口,灵活的设置数组的大小等好处
面试频率不如HashMap和ConcurrentHashMap
秒杀系统设计
场景
要定点秒杀100件手机
问题
高并发
时间短,瞬间用户量大
超卖
恶意请求
黄牛几十台机器脚本秒杀,模拟个十几万人的请求
链接暴露
前端暴露了地址,或者开发人员自己知道了,猛点
数据库
每秒上万甚至十几万的qps打到数据库,基本gg
解决办法
服务单一职责
微服务设计思想,再用分布式的部署方式
给秒单独的服务,单独的库
好处:就算挂了, 也不会影响其他服务
秒杀连接加盐
把url动态化,就连写代码的人都不知道,通过md5之类的加密算法随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。
redis集群
单机redis顶不住,那就多找几个兄弟,秒杀本来就是读多写少
redis集群、主从同步、读写分离、还可以高点哨兵,开启持久化直接无敌高可用
nginx
高性能的web服务器,并发也是随便几万不是梦,但是tomcat只能顶几百的并发啊。那简单啊负载均衡嘛,一台服务器几百,那就多搞点,在秒杀的时候多租点流量机
恶意请求拦截也需要用它,一般单个用户请求次数太夸张,不像真人的请求在网关那一层就得拦截掉了
资源静态化
秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所有页面一般是不会经过后端的,但是前段也要有自己的服务器啊,那就把能提前放到cdn服务器的东西都放进去,反正把能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力
按钮控制
秒杀前按钮置灰,到点了才能点
这是防止在快到秒杀前的时间疯狂请求服务器,这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点了再给按钮可以用
点击一次之后也得置灰几秒,防止一直点
限流
前端限流:跟按钮控制类似,防止一直点
后端限流:秒杀的时候肯定是涉及到了后续的订单生成和支付操作,一旦秒杀产品卖完了,return一个false,前端直接秒杀结束
真正的限流还会有限流组件,比如阿里的Sentinel、Hystrix等
库存预热
秒杀的本质,就是对库存的争夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,对开发很不友好,而且数据库顶不住啊
提前把商品的库存加载到redis中去,让整个流程都在redis里做,然后等秒杀结束了,再异步的去修改库存就好了
但是用redis的话就有一个问题了,我们上面说了主从,然后回去读库然后再判断有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了:比如只剩下一个库存了,高并发,四个服务器一起查询大家发现都还剩一个,都觉得自己抢到了,都去扣库存了,结果变成-3了,这样就发生了超卖
Lua:lua脚本类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作,一个脚本=查库存+扣减库存,如果是0 了直接false
削峰填谷
说到这里就知道是说mq了,卖100个东西直接100个请求,我觉得没问题,但万一秒杀一万个,10万个呢,服务器挂了
把他放消息队列,然后一点点消费去改库存不就好了嘛
消息队列基础
经典场景,需要熟烂于心:异步、削峰、解耦
异步
一个下单流程:本来需要100ms,后来产品说要加上积分,流程中加上积分扣减,200ms了
产品说要加上优惠券,300毫秒了
产品说要发短信,400毫秒了
再后来。。。
让产品加的这三点可以异步
面试官:异步,我用线程、线程池去做不一样吗?
为什么不能用线程去做?
用线程的话,扣积分、扣优惠券、发短信是不是都需要单独的接口?,每次加一个流程是不是代码都要改动?
但是用了消息队列,问题迎刃而解啊:
你下单了,你就把你支付成功的消息告诉别的系统,他们收到了去处理就好了,来多少类似的需求,只需要对应的人员去监听该消息就行了
那你的流程走完了,别人没成功怎么办?
起始不需要考虑,业务系统本身就是自己开发人员维护的,你扣积分失败和我下单有什么关系?
削峰
平时流量低,但是秒杀时流量猛增,你的服务器,redis,mysql各自的承受能力都不一样,全部流量赵丹全收肯定有问题啊,直接就挂了
怎么办
把请求放到队列里面,每秒消费多少请求,就看自己的服务器处理能力,你能处理5000qps,你就消费这么多人,可能会比正常的慢一点,但是不至于打挂服务器,等流量高峰下去了,你的服务器也就没压力了
分布式事务
消息去重、重复消费
原则:使用业务端逻辑保持幂等性
策略:保证每条消息都要唯一编号(比如唯一流水号),且保证消息处理成功与去重表的日志同时出现。
建立一个消息表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键或约束,就算出现重复消费的情况,也会导致主键冲突,以后就不再处理这条消息了
消息重复
Qos:Quality of Service 服务质量
消息领域对投递的定义分为:
- 最多一次
- 至少一次
- 仅一次
几乎所有的mq产品都声称自己做到了 至少一次
既然是至少一次,那避免不来消息重复,尤其是在分布式网络环境下
消息可用性
当我们选择好了集群模式之后,那么我们需要关系的就是怎么去存储和复制这个数据,RocketMq对消息的刷盘提供了同步和异步的策略来满足我们。
同步刷盘:如果刷盘超时则会返给FLUSH_DISK_TIMEOUT,如果是异步刷盘不会返回刷盘相关信息,选择同步刷盘可以尽最大程度满足我们的消息不会丢失。
除了存储有选择之后,我们的主从同步提供了同步和异步两种模式来进行复制,当然选择同步可以提升可用性,但是消息的发送RT时间会下降10%左右。
RocketMq采用的是混合型的存储结构,即为Broker单个实例下所有的队列公用一个日志数据文件(即为COmmitLog)来存储
Kafka采用的是独立型的存储结构,每个队列一个文件。
RocketMq采用混合型存储结构的缺点在于:会存在较多的随机读操作,因此读的效率偏低。同时消费消息需要依赖ConsumeQueue,构建该逻辑消费队列需要一定开销。
顺序消费
生产者消费者一般需要保证顺序消费的话,可能就是一个业务场景下的,比如订单的创建、支付、发货、收货。
那么这些东西是不是一个订单号呢?一个订单的肯定是一个订单号啊
一个topic下游多个队列,为了保证发送有序,RocketMq提供了MessageQueueSelector队列选择机制,他有三种实现:
我们可以使用Hash取模法,让同一个订单发送到同一个队列中,再使用同步发送包,只有同个订单创建消息发送成功,再发送支付消息。这样就保证了发送有序
Rokcet的topic内的队列机制,可以保证存储满足FIFO(先进先出),剩下的只需要消费者顺序消费即可
分布式事务
Half Message(半消息)
是指暂时不能倍Consumer消费的消息。Producer已经把消息成功发送到了Broker端,但此消息被标记为暂不能投递状态,处于该状态下的消息成为半消息,需要Producer对消息的二次确认后,Consumer才能去消费它。
消息回查
由于网络闪断,生产者重启等原因,导致Producer端一直没有对半消息进行二次确认,这时Brocker服务器会定时扫描长期处于半消息的消息,会主动询问Producer端,该消息的最终状态(Commit 或者 Rollback),该消息即为消息回查。
A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。
当A服务知道Half Message发送成功后,那么开始第3步执行本地事务。
执行本地事务(会有三种情况1、执行成功。2、执行失败。3、网络等原因导致没有响应)
如果本地事务成功,那么Product像Brock服务器发送Commit,这样B服务就可以消费该message。
如果本地事务失败,那么Product像Brock服务器发送Rollback,那么就会直接删除上面这条半消息。
如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。
消息过滤
Broker端消息过滤
在broker中,按照Consumer 的要求做过滤,优点是减少了对于 Consumer 无用消息的网络传输。缺点是增加了Broker的负担,实现相对复杂。
Consumer 端消息过滤
这种过滤完全可由应用完全自定义实现,但是缺点是很多无用的消息要传到Consumer端
Broker的Buffer问题
Broker的buffer通常指的是Broker中一个队列的内存Buffer大小,这类Buffer通常大小有限。
另外,RokcetMq没有内存Buffer概念,RocketMq的队列都是持久化磁盘,数据定时清除。
RockertMq同其他mq有个非常显著的区别,RocketMq的内存Buffer抽象成一个无线长度的队列,不管有多少数据进来都能装得下,这个无线是有前提的,Broker会定期删除过期的数据。
例如Broker只保存三天的消息,那么Buffer长度虽然无线,但是3天前的数据会被从队尾删除。
回溯消费
回溯消息是只Concumer已经消费成功的消息,由于业务上的需求需要重新消费,要支持此功能,Broker在向Consumer投递消息成功后,消息仍然需要保留。并且重新消费一般是按时间维度。
RocketMq支持按照时间回溯消费,可以精确到秒,可以向前,向后
消息堆积
消息中间件的主要功能就是异步解耦,还有个重要功能是挡住前端的数据洪峰,保证后端系统的稳定性
消息堆积有两种
- 堆积在内存Buffer,一旦超过内存Buffer,可以根据一定的丢弃策略来丢弃消息,这种情况堆积能力主要在于内存Buffer大小,而且消息堆积后,性能下降不大
- 堆积在持久化存储系统中,例如db,kv存储,文件记录形式。当消息不能再内存命cache命中时,要不可避免的访问磁盘,会产生大量读io,读io的吞吐量直接决定了消息堆积后的访问能力。
评估消息堆积能力主要有以下四点:
- 消息能堆积多少条,多少字节?即消息的堆积容量
- 消息堆积后,发消息的吞吐量大小,是否会受堆积影响?
- 正常消费的Consumer是否受影响
- 访问堆积在磁盘的消息是,吞吐量有多大?
定时消息
定时消息是指消息发到Broker后,不能立刻被Consumer消费,要到特定的时间点或者等待特定的时间后才能被消费。
RocketMq支持定时消息,但是不支持任意时间精度,支持特定的level,例如5s,10s,1m等
如果要支持任意的时间精度,需要在Broker做,必须要做消息排序,再涉及到持久化,那么消息排序要不可避免的产生巨大性能开销