java基础
hashmap
jdk8数据结构:数组+链表+红黑树
计算key下标:hashcode(key),将table的容量与hash值做“与”运算,得到哈希table的bucket下标
hash冲突:先根据key确定在哈希table中的下标,找到对应的bucket,遍历链表(或红黑树),做替换/插入操作。在JDK7中,新增结点是使用头插法,但在JDK8中,在链表使用尾插法,将待新增结点追加到链表末尾
链表转红黑树:当链表超过 8 且数据总量超过 64 时会转红黑树。如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
HashMap、HashTable区别:
共同点:
底层都是使用哈希表 + 链表的实现方式。
区别:
HashTable线程安全,HashMap线程不安全;
HashMap 的put方法流程?
以JDK 8为例,简要流程如下:
1、首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
2、如果数组是空的,则调用 resize 进行初始化;
3、如果没有哈希冲突直接放在对应的数组下标里;
4、如果冲突了,且 key 已经存在,就覆盖掉 value;
5、如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
6、如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
java.util.concurrent.ConcurrentHashMap如何保证线程安全
ConcurrentHashMap中的数组设计分为大数组Segment和小数组HashEntry,jdk7是分段锁,jdk8是对头结点加锁来保证线程安全
线程池
使用场景:接口合并返回,任务批量处理,异步执行,mq消息批量消费
核心参数:
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。
RejectedExecutionHandler(拒绝策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。n AbortPolicy:直接抛出异常。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
执行流程:
判断线程池的状态,如果不是RUNNING状态,直接执行拒绝策略
如果当前线程数 < 核心线程池,则新建一个线程来处理提交的任务
如果当前线程数 > 核心线程数且任务队列没满,则将任务放入阻塞队列等待执行
如果 核心线程池 < 当前线程池数 < 最大线程数,且任务队列已满,则创建新的线程执行提交的任务
如果当前线程数 > 最大线程数,且队列已满,则执行拒绝策略拒绝该任务
线程池参数设置
corePoolSize:20
queueCapacity:队列长度可设置为400
maxPoolSize:60
线程池中的锁
mainLock
一个是workers 对象:线程不安全的 HashSet,一个是 largestPoolSize 变量:曾经出现过的最大线程数
Worker 对象:worker 类存在的主要意义就是为了维护线程的中断状态,自定义 worker 类的大前提是为了维护中断状态,因为正在执行任务的线程是不应该被中断的
ArrayList和LinkedList的大致区别如下:
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
jvm垃圾回收算法
java中判断对象可回收:“GC Roots”的对象作为起点,从这些起点开始往下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象没有和任何引用链相连,即称为该对象不可达(图论的说法),认为该对象死亡
垃圾回收算法:
标记-清除算法
复制算法
标记-整理算法
分代回收算法
G1收集器
内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记一压缩(Mark一Compact)算法
G1 GC的垃圾回收过程:
年轻代GC (Young GC):应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;然后从年轻代区间移动存活对象到Survivor区间或者老年区间
老年代并发标记过程 (Concurrent Marking):当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程
混合回收(Mixed GC):标记完成马上开始混合回收过程
mysql
MyISAM和InnoDB的区别:
MyISAM:不支持事务,支持表级锁,即每次操作是对整个表加锁;
InnoDb:支持ACID的事务,支持事务的四种隔离级别;支持行级锁及外键约束:因此可以支持写并发;
如何防止sql注入
1. 代码层防止sql注入攻击的最佳方案就是sql预编译,将参数用''包含
mybatis 提供了两种支持动态 sql 的语法:#{} 以及 $ { }, 其最大的区别则是前者方式能够很大程度防止sql注入(安全),后者方式无法防止Sql注入 。
#{}将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。
使用${},典型情况就是 动态参数,动态指定表名,排序时使用order by 动态参数
索引优化
分析 SQL 的性能
使用 EXPLAIN 命令来分析 SQL 的 执行计划
type(重要)
system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。
const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。
eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式,常用于使用主键或唯一索引的所有字段作为连表条件。
ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。
index_merge:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。
range:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。
index:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。
ALL:全表扫描。
key(重要)
key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。
Extra(重要)
这列包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。常见的值如下:
Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。
Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY。
Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。
Using index condition:表示查询优化器选择使用了索引条件下推这个特性。
Using where:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。
Using join buffer (Block Nested Loop):连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。
这里提醒下,当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。#
读写分离:读写分离主要应对的是数据库读并发
部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制。
系统将写请求交给主数据库处理,读请求交给从数据库处理。
实现方式:
1. 代理方式:我们可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。提供类似功能的中间件有 MySQL Router(官方)、Atlas(基于 MySQL Proxy)、MaxScale、MyCat。
2. 组件方式:推荐使用 sharding-jdbc ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。
主从复制原理:
主库将数据库中数据的变化写入到 binlog
从库连接主库
从库会创建一个 I/O 线程向主库请求更新的 binlog
主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收
从库的 I/O 线程将接收的 binlog 写入到 relay log 中。
从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。
如何避免主从延迟?
强制将读请求路由到主库处理
延迟读取
如何尽量减少延迟
从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据;
从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。
分库分表:解决 MySQL 的存储压力
常见的分片算法
哈希分片:求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。
范围分片:按照特性的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 id 为 1~299999 的记录分到第一个库, 300000~599999 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
引入分库分表之后,会给系统带来什么挑战呢?
join 操作:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。
事务问题:分布式事务
分布式 ID:
redis
redis快的原因:
Redis是基于内存操作,需要的时候需要我们手动持久化到硬盘中
Redis高效数据结构,对数据的操作也比较简单
Redis是单线程模型,从而避开了多线程中上下文频繁切换的操作
使用多路I/O复用模型,非阻塞I/O
Reids中数据过期策略采用定期删除+惰性删除策略。定时删除策略就是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求
Redis 内存淘汰机制有以下几种策略:
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(推荐使用)
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。
在 Redis 中存储value,常用的 5 种数据类型和应用场景如下:
String: 缓存、计数器、分布式锁等。SDS 简单动态字符串
List: 链表、队列、微博关注人时间轴列表等。
Hash: 用户信息、Hash 表等。
Set: 去重、赞、踩、共同好友等。zipList 压缩列表
Zset: 访问量排行榜、点击量排行榜等。skipList 跳跃表
Redis 的单线程指的是 Redis 的网络 IO (6.x 版本后网络 IO 使用多线程)以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。
一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
Redis 6.0采用多个IO线程来处理网络请求,网络请求的解析可以由其他线程完成,然后把解析后的请求交由主线程进行实际的内存读写。提升网络请求处理的并行度,进而提升整体性能。
但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。
spring/springboot/springcloud
消息队列
kafka和rocketmq对比:Kafka使用 scala 编写,rocketmq是java编写
(1)Kafka的业务应用场景主要定位于日志传输;对于复杂业务支持不够
(2)阿里很多业务场景对数据可靠性、数据实时性、消息队列的个数等方面的要求很高。
(10)消息查询机制
Kafka不支持消息查询
RocketMQ支持根据Message Id查询消息,也支持根据消息内容查询消息
(8)定时/延时消息
Kafka不支持定时消息;
RocketMQ支持定时消息
(7)消费失败重试机制
Kafka消费失败不支持重试
RocketMQ消费失败支持定时重试,每次重试间隔时间顺延。
(5) 支持的队列数
Kafka单机超过64个队列/分区,消息发送性能降低严重;
RocketMQ 单机支持最高5万个队列,性能稳定
结论:长远来看,RocketMQ 胜出,这也是适合业务处理的原因之一
(2) 性能
Kafka单机写入 TPS 号称在百万条/秒;
RocketMQ 大约在10万条/秒。
结论:追求性能的话,Kafka单机性能更高。
(3) 可靠性
RocketMQ支持异步/同步刷盘;异步/同步Replication;
Kafka使用异步刷盘方式,异步Replication。
结论:RocketMQ所支持的同步方式提升了数据的可靠性。
微服务
远程调用Dubbo与Feign对比
Dubbo
利用Netty,TCP传输,单一、异步、长连接,适合数据量小、高并发和服务提供者远远少于消费者的场景。
dubbo默认使用socket长连接,即首次访问建立连接以后,后续网络请求使用相同的网络通道。
Feign
Feign基于Http传输协议,底层实现是rest。在高并发场景下性能不够理想,成为性能瓶颈
http协议是应用层协议,HTTP/1.0在交互之前需要进行tcp三次握手,握手成功之后才能进行数据传输。
分布式事务
分布式事务的实现主要有以下 6 种方案:
2PC 方案
TCC 方案
本地消息表
本地消息表的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。
本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。
MQ事务
Saga事务
最大努力通知方案
最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。
最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。