RabbitMQ
一、什么是消息队列?
消息队列是使用队列来通信的组件,就是个转发器,包含发消息,存消息,消费消息的过程。
我们通常说的消息队列 简称为 MQ 其实就是消息中间件。
现在业界流行的开源的消息中间件包括 RabbitMQ RocketMQ Kafka.
二、为什么使用消息队列?
1、应用解耦
例如:下单扣库存 调用库存服务无法访问,下单就会失败,订单微服务和库存微服务就会发生耦合关系
2、流量削峰
例如:在做秒杀的时候,避免流量暴涨,可以在应用面前加上消息队列 。假设秒杀系统
每秒可处理2千和请求 每秒却请求过来5千,可以引入消息队列 每秒系统从消息队列拉取2千
不用担心消息积压的问题,因为秒杀不会时刻都有请求过来,积压请求可以慢慢处理,
如何消息长度超过最大数量,可以直接抛弃请求和跳转错误页面。
3、异步处理
4、消息通讯
例如:点对点消息队列 、聊天室
5、远程调用
三、如何解决消息丢失问题?
一个消息从生产者产生,到被消费者消费 主要经过三个过程:
生产者 到 存储消息 Broker 到 消费者
三个阶段:
生产者保证不丢消息
存储端不丢消息
消费者不丢消息
生产者不丢消息?
RocketMQ消息中间件: 同步发送 异步发送 单向发送
存储端不丢消息?
持久化到磁盘 : 刷盘机制
MQ的刷盘机制
RocketMQ的消息会在消息的发送者发送到队列之后存储到本地磁盘上
消费端不丢消息?
消费者完成业务逻辑,在反馈回Broker说明消费成功
四、消息队列如何保证消息的顺序性?
搭建集群
假设先下单再付款 下单为M1 付款为 M2 , 为了保证消息的顺序性,
可以将M1、M2 发送到同一个server上,当M1发送完收到ack机制后,M2再发送。
可能存在网络延迟的问题,虽然M1先发送,但是它比M2晚到。
解决方案,将M1和M2发送同一个消费者,且发送M1后等到消费端ACK成功后,才发送M2
就可以了。
五、引入MQ之后的问题
1、系统可用性降低
例如:本来系统有四个服务,保证现在四个就可以,现在MQ又来了,现在还要保证MQ的可用,系统可用性降低。
2、系统复杂性提高
例如:如果三个服务不可用,交易服务可用。引入MQ之后,系统稳定性就靠MQ保证了。
这时候要考虑MQ里面的消息:如何避免丢失?顺序性?重复消费等,所以复杂性提高。
避免消息丢失可以使用MQ集群,顺序性消费可以把消息发到同一个分区,重复性消费可以在消费端做幂等性处理。
六、重复消费问题
1、那些场景会发现重复的消息
-
消息生产者产生了重复的消息
-
Kafka 和 RocketMQ 的 offset 被回调了;
-
消息消费者确认失败;
-
消息消费者确认时超时;
-
业务系统主动发起重试。
解决方案:做幂等 ,使用messageId做唯一索引。
七、数据一致性问题
我们都知道数据一致性分为:强一致性、弱一致性、最终一致性。
八、消息丢失问题
- 生产者产生消息时,由于网络原因发送到 MQ 失败了;
- MQ 服务器持久化,存储磁盘时出现异常;
- Kafka和RocketMQ 的 offset 被回调时,略过了很多消息;
- 消费者刚读取消息,已经 ACK 确认,但业务还没处理完,服务就被重启了。
解决方案:ACK确认机制
九、消息堆积问题
由于消费者速度小于生产速度
解决方案:不保证顺序的情况下,可以使用多线程来解决
保证顺序的情况下,可以使用多个队列对应多个线程
mq的工作模式
简单模式(队列模式)(点对点发送):一个生产者对应一个消费者 ,
工作模式(广播模式):一个生产者对应多个消费者,一次只能被一个消费者拿到消息,消费者不会拿到重复的消息
消息订阅模式(经常使用):多了个交换机,生产者发送给交换机,所有消费者都能收到消息
路由模式:和订阅模式相同,传递过程中加了key,key匹配上才能拿到消息
Redis
一、redis简介
Redis 是C语言开发的一个开源、高性能、键值对的内存数据库,可以用来做数据库、缓存、消息中间件等场景,是一种NoSQL(not-only sql,非关系型数据库)的数据库
二、Redis特点
- 优秀的性能,数据是存储在内存中,读写速度非常快,可支持并发10W QPS
- 单线程单进程,是线程安全的,采用IO 多路复用制
- 可作为分布式锁
- 支持五种数据类型
- 支持数据持久化到磁盘
- 可以作为消息中间件使用,支持消息发布及订阅
三、数据类型(5种)
string: 可以⽤来做最简单的数据缓存,可以缓存某个简单的字符串,也可以缓存某个json格式的字符 串,Redis分布式锁的实现就利⽤了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
hash: 可以⽤来存储⼀些key-value对,更适合⽤来存储对象
list: Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使⽤
set: 和列表类似,也可以存储多个元素,但是不能重复,集合可以进⾏交集、并集、差集操作,从⽽ 可以实现类似,我和某⼈共同关注的⼈、朋友圈点赞等功能
zset : 集合是有序的,有序集合可以设置顺序,可以⽤来实现排⾏榜功能
四、缓存
直接使用RedisTemplate
通过SpringCache集成Redis
五、使用缓存遇到的问题
1、数据一致性
我们只能在项目中使用策略降低缓存与数据库一致性的概率,是无法保障两者的强一致性,一般策略包括缓存更新机制,更新数据库后及时更新缓存、缓存失败时增加重试机制
2、缓存雪崩
假设A请求每秒处理5000个请求 ,但数据库每秒处理4000个请求,假设某一天缓存机器宕机了,
这时所有的请求都打在了数据库中,导致数据库扛不住宕机。
- 事故前:redis高可用方案,主从+哨兵,集群方案,避免全盘崩溃
- 事故中:较少数据库的压力,本地Ehcache缓存+限流及降级,避免超过数据库承受压力
- 事故后:做redis持久化,一旦Redis重启,可从磁盘中快速恢复数据
3、缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,用户(黑客)不断发起请求,导致请求直接查询数据库,这种恶意行为攻击场景的会直接导致数据库挂掉 。
- 可以在缓存中设置一个null值,让恶意的请求不会直接击垮数据库,每次访问的时候都去访问此缓存。
- 可以设计一个过滤器,常用的就是布隆过滤器(可以缓解,为什么是缓解,因为使用过滤器还会造成误判的情况)
4、缓存击穿
击穿是针对一个热点数据,但是某个key是热点数据,请求非常频繁,当这个key突然过期了,大量的请求就会击穿缓存,直接请求数据库,最终导致宕机。
- 数据基本不变:热点数据value基本不更新时,可以设置成永不过期
- 数据更新不频繁:缓存刷新流程耗时较少时,可采用redis、zookeeper等分布式中间件的分布式互斥锁或者本地互斥锁保证少量的请求能请求到数据库并重新更新缓存,其他的流程等锁释放后才可以访问新缓存
六、redis为什么这么快,用了NIO多路复用
1.完全基于内存操作
2.C语言实现,优化过的数据结构,基于几种基础的数据类型,redis做了大量的优化,性能极高
3.使用单线程,无上下文的切换文本
4.基于非阻塞NIO的多路复用机制
七、Redis持久化
-
RDB:快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。
-
优点:
只有一个文件 dump.rdb,方便持久化; 容灾性好,一个文件可以保存到安全的磁盘; 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis的高性能; 相对于数据集大时,比 AOF 的启动效率更高;
缺点:
- 数据安全性低;
- RDB 是间隔一段时间进行持久化,如果在持久化时Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;
-
AOF:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。Redis 默认是快照 RDB 的持久化方式。
-
优点:
数据安全,AOF持久化可以配置 appendfsync 属性,有always属性,每进行一次命令操作就记录到AOF文件中一次; 通过append模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题; AOF机制的rewrite模式。AOF文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall);
缺点:
-
AOF文件比RDB文件大,且恢复速度慢;
-
数据集大的时候,比RDB启动效率低;
RDB和AOF的对比
- AOF文件比RDB更新频率高,优先使用AOF还原数据;
- AOF比RDB更安全也更大;
- RDB性能比AOF好;
- 如果两个都配了优先加载AOF;
-
八、Redis主从复制原理
- 从数据库连接主数据库,发送的是SYNC的请求
- 主数据库接收到SYNC的请求之后,开始执行bgsave命令,并且生成RDB文件
- 主数据库 执行完bgsave命令,向从数据库去发送快照文件,在此期间主数据可以继续执行写操作
- 从数据接收到文件之后,摒弃旧的替换新的文件
- 主数据库向从数据库发送缓存中的写操作
- 从数据库对快照进行一个载入操作,同时接收主数据库发送过来的缓存写操作
- 出现重新连接的操作的时候,会有一个增量复制操作
- 不论什么情况下redis首先进行的是增量同步操作,如果失败了,那么就全量同步
九、Redis哨兵模式
-
哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Master节点和Slave节点,并在被监视的Master节点进入下线状态时,自动将下线Master服务器
属下的某个Slave节点升级为新的Master节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,
并且各个哨兵之间还会进行监控。
sentinel是一种特殊的redis实例,它不存储数据,只对集群进行监控。
哨兵模式的优缺点 优点
哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。 主从可以自动切换,系统更健壮,可用性更高。 缺点
具有主从模式的缺点,每台机器上的数据是一样的,内存的可用性较低。 还要多维护一套哨兵模式,实现起来也变的更加复杂增加维护成本。 Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
Cluster集群模式
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存。因此,在Redis3.0后Cluster集群应运而生,
它实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。
Redis的集群模式本身没有使用一致性hash算法,而是使用slots插槽 。
redis cluster模式采用了无中心节点的方式来实现,每个Master节点都会与其它Master节点保持连接。节点间通过gossip协议交换彼此的信息,同时每个Master节点又有 一个或多个Slave节点;
客户端连接集群时,直接与redis集群的每个Master节点连接,根据hash算法取模将key存储在不同的哈希槽上; 在集群中采用数据分片的方式,将redis集群分为16384个哈希槽。如下图所示,这些哈希槽分别存储于三个Master节点中: Master1负责0
5460号哈希槽 Master2负责546110922号哈希槽 Master3负责10922~16383号哈希槽- 每个节点会保存一份数据分布表,节点会将自己的slot信息发送给其他节点,节点间不停的传递数据分布表;
十、Redis值 I/O多路复用模型实现原理
Redis 的 I/O 多路复用模型有效的解决单线程的服务端,使用不阻塞方式处理多个 client 端请求问题。在看 I/O 多路复用知识之前,我们先来看看 Redis 的客服端怎么跟客服端建立连接的、单线程 socket 服务端为什么会存在 I/O 阻塞。
为什么 Redis 中要使用 I/O 多路复用这种技术呢?因为 Redis 是跑在**「单线程」中的,所有的操作都是按照顺序线性执行的,但是「由于读写操作等待用户输入 或 输出都是阻塞的」,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导,致整个进程无法对其它客户提供服务。而 I/O 多路复用就是为了解决这个问题而出现的。「为了让单线程(进程)的服务端应用同时处理多个客户端的事件,Redis 采用了 IO 多路复用机制。」**
redis集群有多少台
“redis集群规定至少有3台master,因此最少需要6台redis主机。”
redis 6.0 之后引入多线程 为什么?
单线程:
只能使用CPU一个核; 如果删除的键过大(如Set类型有上百万个对象),会导致服务端阻塞好几秒; QPS难再提高。
多线程:
-
优化网络 I/O 模块
-
提高机器内存读写的速度
-
利用多核优势
-
从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:
• 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式 • 使用多线程充分利用多核,典型的实现比如 Memcached。
协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因:
• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核 • 多线程任务可以分摊 Redis 同步 IO 读写负荷
redis过期策略
定期删除+惰性删除
定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
惰性删除 :key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
内存淘汰机制
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。 volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。 volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
redis可以做什么?
1.缓存,毫无疑问这是Redis当今最为人熟知的使用场景。在提升服务器性能方面非常有效;
2.排行榜,如果使用传统的关系型数据库来做这个事儿,非常的麻烦,而利用Redis的SortSet数据结构能够非常方便搞定;
3.计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
怎样实现redis高可用?
高可用有两个含义:一是数据尽量不丢失,二是保证服务尽可能可用。 AOF 和 RDB 数据持久化保证了数据尽量不丢失,那么多节点来保证服务尽可能提供服务。
Redis-cluster没有使用一致性hash,而是引入了哈希槽的概念
一般在实际生产中,服务不会部署成单节点,主要是有三个原因.
- 容易出现单点故障,导致服务不可用
- 单节点处理所有的请求,吞吐量有限
- 单节点容量有限
redis中的keys和scan对比
keys和scan都是用来返回key的
kyes返回所有符合条件的key,比较精准,并且不会重复,数据量比较大的时候时间是比较久的,可能会影响其他线程
scan 是利用游标, 迭代每次返回一部分 key, 并不是全部,速度快
总结:数据量小时使用KEYS,反之使用SCAN
redis热key
在Redis中,我们把访问频率高的Key,称为热Key
产生原因:用户消费的数据远大于生产的数据,如商品秒杀、热点新闻、热点评论等读多写少的场景
可能会造成的影响:缓存击穿
如何解决热Key问题
1、Redis集群扩容:增加分片副本,分摊客户端发过来的读请求;
2、使用二级缓存,即JVM本地缓存,减少Redis的读请求。
Redis限流的三种方式
第一种:基于Redis的setnx的操作
第二种:基于Redis的数据结构zset
第三种:基于Redis的令牌桶算法
Spring
@Component 和 @Bean 的区别
1.作用对象不同:@Component 注解作用于类,而 @Bean 注解作用于方法、
2.@Component 通常是通过路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean 告诉了 Spring 这是某个类的实例,当我们需要用它的时候还给我。
3.@Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,只能通过 @Bean 来实现。
postmapping和RequestMapping的区别
@GetMapping 用于将HTTP GET请求映射到特定处理程序方法的注释。具体来说,@GetMapping是一个作为快捷方式的组合注释 @RequestMapping(method = RequestMethod.GET)。
@PostMapping 用于将HTTP POST请求映射到特定处理程序方法的注释。具体来说,@PostMapping是一个作为快捷方式的组合注释@RequestMapping(method = RequestMethod.POST)。
@RequestMapping: 一般情况下都是用@RequestMapping(method=RequestMethod.),因为@RequestMapping可以直接替代以上两个注解,但是以上两个注解并不能替代@RequestMapping,@RequestMapping相当于以上两个注解的父类!
SpringBean生命周期
推导过程
开始时,我们只有两个流程:对象的实例化和属性填充
负责角色: AutowiredAnnotationBeanPostProcessor
对象实例化
功能描述:根据候选构造器集合中的构造器优先级对beanClass进行实例化。
负责角色:ConstructorResolver
生产环境中 ,有一个接口查询效率比较慢,如何排查?
阿尔萨斯工具类
去本地环境查看, 建索引 多线程异步编排提升效率
偶尔性的慢 服务器
经常性的慢
内存突然上升,如何排查,没有内存溢出?
jstat查看 gc的活动频率,进行分析
List遍历 移除元素5 如何实现?
使用list变为迭代器 使用 remove方法删除
线程
多线程顺序执行
子线程中通过join()方法中指定顺序
主线程中通过join()方法中指定顺序
通过倒数计数器CountDownLatch实现
通过创建单一化线程池newSingleThreadExecutor实现
Java的线程池
Java的线程池是运用场景最多的并发框架 , 几乎所有需要异步或者并发执行任务的程序都可以使用线程池。
合理使用线程池的好处
- 降低资源消耗通过重复利用已经创建的线程降低线程创建的和销毁造成的消耗。
- 提高响应速度。
- 提高线程的可管理性
线程池的工作原理
刚开始创建对象的时候,线程数量为0,任务的不断增加,当线程的数量小于核心线程数时,继续创建对象,当大于时,就无判断是否小于阻塞队列,如果小于就继续创建对象,当大于阻塞队列的时候,就去判断是否小于最大线程,如果小于就继续创建对象,否则就拒绝策略
限流方案
一、接口幂等性
1、根据唯一业务号去更新数据
2、使用Token机制,保证幂等性
二、分布式限流
1、分布式限流的几种维度
1.1 QPS和连接数控制
1.2 传输速率
1.3黑白名单
1.4分布式环境
2、限流方案常用算法
2.1、令牌桶算法
2.2 漏桶算法
3、分布式限流的主流方案
3.1 Guava RateLimiter客户端限流
3.2 基于Nginx的限流
4.基于Redis+Lua的分布式限流
XXL-Job
XXL-JOB是一个轻量级分布式
任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
Spring通知有哪些类型?
前置通知
后置通知
环绕通知
返回后通知
抛出异常后通知
BeanFactory和ApplicationContext有什么区别?
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。
1、 BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
继承MessageSource,因此支持国际化。
资源文件访问,如URL和文件(ResourceLoader)。
载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
提供在监听器中注册bean的事件。
(2)①BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。
③ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。
Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?
Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性。
对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。 有状态的Bean,就是有实例变量的对象,可以保存数据,是非线程安全的。
对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。
Spring基于xml注入bean的几种方式
set()方法注入;
构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
静态工厂注入
实例工厂
Spring的自动装配
在spring中,使用autowire来配置自动装载模式,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象。
(1)在Spring框架xml配置中共有5种自动装配:
no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
byType:通过参数的数据类型进行自动装配。
constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
(2)基于注解的自动装配方式:
使用@Autowired、@Resource注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置。
HashMap负载因子为什么是0.75?
负载因子的作用肯定也是节省时间和空间。
负载因子是1.0
当负载因子是1.0的时候,全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
负载因子是0.5
负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
redis如何实现分布式锁
1.加锁 : 最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名
要保证原子性
2.解锁 : 有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令
在解锁和删除key的时候也要保证一致性
3.锁超时 : 如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。
使用分布式锁要保证满足一下几点:
- 保证互斥性,在任何时刻,要保证只有一个客户端持有锁
- 不可以出现死锁的状态,如果持有锁的这个客户挂了,要保证其他用户可以获取到锁
- 保证上锁和解锁是同一个用户
单例
volatile
volatile是Java提供的一种轻量级的同步机制。
并发编程的3个基本概念
1.原子性 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2.可见性 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.有序性 即程序执行的顺序按照代码的先后顺序执行。
volatile变量的特性
1.保证可见性,不保证原子性
(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
(2)这个写会操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。
(1)重排序操作不会对存在数据依赖关系的操作进行重排序。
(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
3.避免指令重排解决的问题
- 分配对象的内存空间;
- 初始化对象;
- 设置单例指向刚分配的内存地址
4.volatile不适用的场景
1.volatile不适合复合操作
2.解决方法
(1)采用synchronized
(2)采用Lock
5.volatile原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的
- 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
- 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。
ArrayList和LinkedList区别
1、数据结构不同
ArrayList是Array(动态数组)的数据结构,LinkedList是Link(链表)的数据结构。
2、效率不同
当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
解决线程安全的方式有那些?
什么是atomic
ThreadLocal极端情况下会出现什么问题 如何避免?
ConcurrentHashMap JDK1.7和1.8的区别?
synchronized锁升级过程?
cpu飙升的原因
死锁
递归的问题
想要线程有返回值
实现callable接口,返回Future
为什么要用spring
1、 方便解耦,便于开发
2、 spring支持aop编程
3、 声明式事务的支持
IOC和AOP的好处
Spring事务如何使用
在类或者方法上加上@Transactional注解
什么情况下事务不生效
innodb支持事务,myisam不支持事务
遇到非检测异常时,事务不开启,也无法回滚。
- private 方法无法添加事务管理.
- final 方法无法添加事务管理.
- static 方法无法添加事务管理.
@Autowired和@Resource的区别
springmvc的执行流程 spring是如何解决循环依赖的 BeanFactory和FactoryBean的区别 SpringBean的生命周期 Spring事务的传播特性 SpringBoot的底层原理 SpringCloud有哪些组件 Eureka和Zookeeper的区别 Feign的底层原理 Eureka的底层原理 mybaits 为什么通过mapper接口可以连接到数据库 为什么feign可以调用其他对象 动态代理 mybaits #和$的区别 线程池的底层原理 如何知道线程的返回结果 让线程有序执行 Redis的集群模式有哪些?他们之间有什么区别?
JVM调优
查看系统的平均负载 : $ uptime
查看系统的上下文切换情况: vmstat 和 pidstat。vmvmstat 可查看系统总体的指标,pidstat则详细到每一个进程服务的指标
怎么排查 CPU 过高问题
top -o 字段名
查看内存使用情况
- 使用 top 或者 free、vmstat 命令
内存 swap 过高
- swap 和 内存回收的机制
-
- 内存的回收既包括了文件页(内存映射获取磁盘文件的页)又包括了匿名页(进程动态分配的内存)
- 对文件页的回收,可以直接回收缓存,或者把脏页写回磁盘后再回收
- 而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存
IO 过高怎么找问题,怎么调优
iostat -x -k -d 1 1
pidstat -d
Nginx常见的负载均衡
1、负载均衡策略之节点轮询(默认) 节点轮询这个负载均衡策略是Nginx默认的,它表示每个请求按顺序分配到不同的后端服务器, 比如我们配置了十个服务器。那用户发起10次请求的时候,每个服务器都会收到一次这样的一个请求。
2、负载均衡策略之weight 权重配置
权重配置负载均衡策略,这个意思就是我们可以使用weight这一个给服务器配置对应的占比。这一个数字越大,分配的流量就越高。
3、负载均衡策略之ip_hash(固定分发) ip_hash, 这一个的意思就是会根据用户的来源的ip进行哈希,然后根据结果再进行分配到对应的服务器上面去, 这样的话每个用户就可以固定的访问到后端的某一个服务器。
Spring cloud
什么是注册中心?
- 服务提供者(RPC Server) :在启动时,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
- 服务消费者(RPC Client) :在启动时,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。
- 服务注册中心(Registry) :用于保存 RPC Server 的注册信息,当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地 内存中缓存的服务节点列表。
CAP理论
- 一致性(Consistency):所有节点在同一时间具有相同的数据;
- 可用性(Availability) :保证每个请求不管成功或者失败都有响应;
- 分隔容忍(Partition tolerance) :系统中任意信息的丢失或失败不会影响系统的继续运作。
设计模式
常见的工厂模式、代理模式、模板方法模式、责任链模式、单例模式、包装设计模式、策略模式等都是有所了解的
Spring IOC容器可以理解为应用了「工厂模式」(通过ApplicationContext或者BeanFactory去获取对象)
Spring的对象默认都是单例的,所以肯定是用了「单例模式」(源码里对单例的实现是用的DCL来实现单例)
Spring AOP的底层原理就是用了「代理模式」,实现可能是JDK 动态代理,也可能是CGLIB动态代理
Mysql
1. 数据库主从概念、优点、用途
数据库为什么需要主从架构呢?
- 高可用,实时灾备,用于故障切换。比如主库挂了,可以切从库。
- 读写分离,提供查询服务,减少主库压力,提升性能
- 备份数据,避免影响业务。
主从复制原理
- 主数据库有个
bin log二进制文件,纪录了所有增删改SQL语句。(binlog线程) - 从数据库把主数据库的
bin log文件的SQL语句复制到自己的中继日志relay log(io线程) - 从数据库的
relay log重做日志文件,再执行一次这些sql语句。(Sql执行线程)
主从复制的过程
- 主库的更新SQL(update、insert、delete)被写到binlog
- 从库发起连接,连接到主库。
- 此时主库创建一个
binlog dump thread,把bin log的内容发送到从库。 - 从库启动之后,创建一个
I/O线程,读取主库传过来的bin log内容并写入到relay log - 从库还会创建一个SQL线程,从
relay log里面读取内容,从ExecMasterLog_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db
主主、主从、主备的区别
数据库主主:两台都是主数据库,同时对外提供读写操作。客户端访问任意一台。数据存在双向同步。
数据库主从:一台是主数据库,同时对外提供读写操作。一台是从数据库,对外提供读的操作。数据从主库同步到从库。
数据库主备:一台是主数据库,同时对外提供读写操作。一台是备库,只作为备份作用,不对外提供读写,主机挂了它就取而代之。数据从主库同步到备库。
MySQL是怎么保证主从一致的
长链接
主库和从库在同步数据的过程中断怎么办呢,数据不就会丢失了嘛。因此主库与从库之间维持了一个长链接,主库内部有一个线程,专门服务于从库的这个长链接的。
binlog格式
binlog 日志有三种格式,分别是statement,row和mixed。
statement:
例如: delete from t where a > '666' and create_time<'2022-03-01' limit 1; 此时 a 和 create_time
都为索引 binlog 主库使用的为 a索引 从库使用的为create_time索引 主从不一致
如何解决?
使用row row格式的binlog记录的就是要删除的主键ID信息
Mvcc原理
MVCC的主要是通过read view和undo log来实现的
undo log前面也提到了,它会记录修改数据之前的信息,事务中的原子性就是通过undo log来实现的。所以,有undo log可以帮我们找到「版本」的数据
而read view 实际上就是在查询时,InnoDB会生成一个read view,read view 有几个重要的字段,分别是:trx_ids(尚未提交commit的事务版本号集合),up_limit_id(下一次要生成的事务ID值),low_limit_id(尚未提交版本号的事务ID最小值)以及creator_trx_id(当前的事务版本号)
在每行数据有两列隐藏的字段,分别是DB_TRX_ID(记录着当前ID)以及DB_ROLL_PTR(指向上一个版本数据在undo log 里的位置指针)
MVCC其实就是靠「比对版本」来实现读写不阻塞,而版本的数据存在于undo log中。
如何解决死锁?
Jps & Jstack Jps是Jdk自带的一个工具,可以查看到正在运行的Java进程:
死锁的类型:
一般性死锁:这是最经典的死锁方式。指的是多线程环境下每个线程都需要多个资源去执行,但是这些资源又分别被不同的线程占有着,这就造成了一种僵持的状态。
嵌套性死锁:指的就是锁的互相嵌套使用。我们上面故事的死锁类型,其实就属于嵌套性死锁。
重入性死锁:指的是多线程环境下,若当前线程重复调用一个方法则可能因为代码逻辑里的边界情况从而导致死锁。
死锁原理
- 互斥条件
某资源一次只能一个线程访问,该资源只要分配给某个线程,其它线程就无法再访问,直到该线程访问结束。
- 请求与保持条件
线程在已经占有至少一个资源的情况下还可以继续请求占有资源。
- 不可抢占条件
资源若已被其它线程占有,那么想要获取它就只能等待,不能因为你需要该资源就将其抢占。
- 循环等待条件
在竞争环境中存在一个线程等待链,使得每个线程都占有上一个线程所需的至少一种资源。
死锁解除
- 破坏请求与保持条件
请求与保持指线程请求资源的同时必须始终持有资源,所以我们可以在线程开始运行之前,一次性地申请其在整个运行过程中所需的全部资源。直至使用完再释放。
保持加锁顺序
对于多个线程如果需要对方所持有的锁,那么就要尽量按照相同的顺序加锁,这样就能够避免因为各个线程获取锁的顺序混乱导致死锁。
数据库主从延迟的原因与解决方案
哪些情况会导致主从延迟呢?
1、 机器性能差 只需选择主从库一样规格的机器就好。
2、 从库的压力大 可以搞了一主多从的架构,即多接几个从库分摊读的压力。另外,还可以把binlog接入到Hadoop这类系统,让它们提供查询的能力。
3、 大事务也会导致主从延迟
4、 网络延迟
聊聊数据的库高可用方案
双机主备高可用
- 架构描述:两台机器A和B,A为主库,负责读写,B为备库,只备份数据。如果A库发生故障,B库成为主库负责读写。修复故障后,A成为备库,主库B同步数据到备库A
- 优点:一个机器故障了可以自动切换,操作比较简单。
- 缺点:只有一个库在工作,读写压力大,未能实现读写分离,并发也有一定限制
一主一从
- 架构描述: 两台机器A和B,A为主库,负责读写,B为从库,负责读数据。如果A库发生故障,B库成为主库负责读写。修复故障后,A成为从库,主库B同步数据到从库A。
- 优点:从库支持读,分担了主库的压力,提升了并发度。一个机器故障了可以自动切换,操作比较简单。
- 缺点:一台从库,并发支持还是不够,并且一共两台机器,还是存在同时故障的机率,不够高可用。
一主多从
- 架构描述: 一台主库多台从库,A为主库,负责读写,B、C、D为从库,负责读数据。如果A库发生故障,B库成为主库负责读写,C、D负责读。修复故障后,A也成为从库,主库B同步数据到从库A。
- 优点:多个从库支持读,分担了主库的压力,明显提升了读的并发度。
- 缺点:只有台主机写,因此写的并发度不高
MariaDB同步多主机集群
- 架构描述:有代理层实现负载均衡,多个数据库可以同时进行读写操作;各个数据库之间可以通过
Galera Replication方法进行数据同步,每个库理论上数据是完全一致的。 - 优点:读写的并发度都明显提升,可以任意节点读写,可以自动剔除故障节点,具有较高的可靠性。
- 缺点:数据量不支持特别大。要避免大事务卡死,如果集群节点一个变慢,其他节点也会跟着变慢。
数据库中间件
- 架构描述:mycat分片存储,每个分片配置一主多从的集群。
- 优点:解决高并发高数据量的高可用方案
- 缺点:维护成本比较大。
索引是什么?
索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。更通俗的说,索引就相当于目录 。
索引的优点
- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
- 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
索引的缺点
- 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
- 空间方面:索引需要占物理空间。
TODO
MySQL有哪几种索引类型?
1、从存储结构上来划分:BTree索引 、Hash索引,full-index全文索引
2、从应用层次来分:普通索引,唯一索引,复合索引。
- 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
- 唯一索引:索引列的值必须唯一,但允许有空值
- 复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
说一说索引的底层实现?
基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且Hash索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。
B+Tree索引
是B-Tree的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高
为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树?
- B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B(B-)树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对
IO读写次数就降低了。 - 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在
区间查询的情况,所以通常B+树用于数据库索引。
了解索引下推吗?
- 有了索引下推优化,可以在减少回表次数
- 在InnoDB中只针对二级索引有效
怎么查看MySQL语句有没有用到索引?
通过explain
- id:在⼀个⼤的查询语句中每个SELECT关键字都对应⼀个唯⼀的id ,如explain select * from s1 where id = (select id from s1 where name = 'egon1');第一个select的id是1,第二个select的id是2。有时候会出现两个select,但是id却都是1,这是因为优化器把子查询变成了连接查询 。
- select_type:select关键字对应的那个查询的类型,如SIMPLE,PRIMARY,SUBQUERY,DEPENDENT,SNION 。
- table:每个查询对应的表名 。
- type:
type字段比较重要, 它提供了判断查询是否高效的重要依据依据. 通过type字段, 我们判断此次查询是全表扫描还是索引扫描等。如const(主键索引或者唯一二级索引进行等值匹配的情况下),ref(普通的⼆级索引列与常量进⾏等值匹配),index(扫描全表索引的覆盖索引) - key:此字段是 MySQL 在当前查询时所真正使用到的索引。
- rows 也是一个重要的字段. MySQL 查询优化器根据统计信息, 估算 SQL 要查找到结果集需要扫描读取的数据行数. 这个值非常直观显示 SQL 的效率好坏, 原则上 rows 越少越好。
为什么官方建议使用自增长主键作为索引?
结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。
如何创建索引?
在执行CREATE TABLE时创建索引
使用ALTER TABLE命令去增加索引。
创建索引时需要注意什么?
- 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
- 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
- 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。
使用索引查询一定能提高查询的性能吗?
通常通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。这意味着每条记录的I* NSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
- 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%。
- 基于非唯一性索引的检索。
mysql什么情况下用索引?什么情况下不添加或少创建索引?
1、表记录太少
2、经常插入、删除、修改的表
3、数据重复且分布平均的表字段
mysql索引建立规则
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
限流算法
计数器: 计数器是一种最简单限流算法,其原理就是:在一段时间间隔内,对请求进行计数,与阀值进行比较判断是否需要限流,一旦到了时间临界点,将计数器清零。
固定窗口 :它简单地对一个固定的时间窗口内的请求数量进行计数,如果超过请求数量的阈值,将被直接丢弃。
滑动窗口: 滑动窗口把一个固定时间窗口再继续拆分成N个小窗口,然后对每个小窗口分别进行计数,所有小窗口请求之和不能超过我们设定的限流阈值。
漏桶Leaky bucket: 也就是说流量流入的速度是不定的,但是流出的速度是恒定的。 都会以固定的出口流量大小匀速流出,如果请求的流量超过漏桶大小,那么超出的流量将会被丢弃。
令牌桶token bucket: 令牌桶算法是指系统以一定地速度往令牌桶里丢令牌,当一个请求过来的时候,会去令牌桶里申请一个令牌,如果能够获取到令牌,那么请求就可以正常进行,反之被丢弃。
Redis + Lua 分布式限流: 分布式限流最关键的是要将限流服务做成原子化,我们可以借助 Redis 的计数器,Lua 执行的原子性,进行分布式限流,大致的 Lua 脚本代码。
限流对比:
计数器:
- 优点:固定时间段计数,实现简单,适用不太精准的场景;
- 缺点:对边界没有很好处理,导致限流不能精准控制。
滑动窗口:
- 优点:将固定时间段分块,时间比“计数器”复杂,适用于稍微精准的场景;
- 缺点:实现稍微复杂,还是不能彻底解决“计数器”存在的边界问题。
漏桶:
- 优点:可以很好的控制消费频率;
- 缺点:实现稍微复杂,单位时间内,不能多消费,感觉不太灵活。
令牌桶:
- 优点:可以解决“漏桶”不能灵活消费的问题,又能避免过渡消费,强烈推荐;
- 缺点:实现稍微复杂,其它缺点没有想到。
Redis + Lua 分布式限流:
- 优点:支持分布式限流,有效保护下游依赖的服务资源;
- 缺点:依赖 Redis,对边界没有很好处理,导致限流不能精准控制。
垃圾回收
如何确定对象已死
- 引用计数算法:为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加 1,每当对象引用失效时,计数器减 1,但计数器为 0 的时候,就表明该对象没有被引用。
- 可达性分析算法:通过一系列被称之为 “GC Roots” 的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收
垃圾回收算法
2.2.1 标记--清除算法
见名知义,“标记--清除算法” 就是对无效的对象进行标记,然后清除。
2.2.2 复制算法
标记--复制算法就是把 Java 堆分成两块,每次垃圾回收时只使用其中一块,然后把存活的对象全部移动到另一块区域。
2.2.3 标记--整理算法
“标记--整理算法” 算是一种折中的垃圾收集算法,在对象标记的过程,和前面两个执行的是一样步骤。
2.3 垃圾收集器
2.3.1 Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器,是一个单线程工作的收集器。
2.3.2 ParNew 收集器
ParNew 收集器实质上是 Serial 收集器的多线程并行版本。
2.3.3 Parallel Scavenge 收集器
Parallel Scavenge 收集器也是一款新生代收集器,基于标记——复制算法实现,能够并行收集的多线程收集器和 ParNew 非常相似。
2.3.4 Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 “标记-整理算法”。
2.3.5 Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
2.3.6 CMS收集器
CMS 收集器设计的初衷是为了消除 Parallel 收集器和 Serial 收集器 Full gc 周期中的长时间停顿
4. JVM堆内存
4.1 堆内存结构 : Java 堆区可以划分为新生代和老年代,新生代又可以进一步划分为 Eden 区、Survivor 1 区、Survivor 2 区。
4.2 GC类型
- Minor GC/Young GC:针对新生代的垃圾收集;
- Major GC/Old GC:针对老年代的垃圾收集。
- Full GC:针对整个 Java 堆以及方法区的垃圾收集。
电商领域
一、避免重复下单
解决方案就是采用幂等机制,多次请求和一次请求产生的效果是一样的。
利用数据库自身特性 “主键唯一约束”,在插入订单记录时,带上主键值,如果订单重复,记录插入会失败。
库存超卖
通常在扣减库存的场景下使用行级锁,通过数据库引擎本身对记录加锁的控制
设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时 SQL 语句会报错。
账户余额更新,保证事务
数据库的事务隔离级别有:读未提交(RU)、读已提交(RC)、可重复读(RR)、串行化(Serializable)
常用的隔离级别是 RC 和 RR ,因为这两种隔离级别都可以避免脏读。
分布式事务,细想下也很容易理解,就是将一个大事务拆分为多个本地事务,本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。
MySQL读写分离带来的数据不一致问题
部署一个主库实例,客户端请求所有写操作全部写到主库,然后借助 MySQL 自带的 主从同步 功能,做一些简单配置,可以近乎实时的将主库的数据同步给 多个从库实例,主从延迟非常小,一般不超过 1 毫秒。
HashMap
哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值?
-
平方取中法
-
取余数
-
伪随机数法
-
hashCode
当两个对象的hashCode相等时会怎样
hashCode相等产生hash碰撞,hashCode相等会调用equals方法比较内容是否相等,内容如果相等则会进行覆盖,内容如果不等则会连接到链表后方,链表长度超过8且数组长度超过64,会转变成红黑树节点
HashMap的put方法流程
一般用什么作为HashMap的key?
一般用Integer、String这种不可变类当HashMap当key
- 因为String是不可变的,当创建字符串时,它的hashcode被缓存下来,不需要再次计算,相对于其他对象更快
- 因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类很规范的重写了hashCode()以及equals()方法
为什么Map桶中节点个数超过8才转为红黑树?
- 从平均查找长度来看,红黑树的平均查找长度是logn,如果长度为8,则logn=3,而链表的平均查找长度为n/4,长度为8时,n/2=4,所以阈值8能大大提高搜索速度
- 当长度为6时红黑树退化为链表是因为logn=log6约等于2.6,而n/2=6/2=3,两者相差不大,而红黑树节点占用更多的内存空间,所以此时转换最为友好
HashMap为什么线程不安全?
-
多线程下扩容死循环。JDK1.7中的HashMap使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题
-
多线程的put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同的,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。此问题在JDK1.7和JDK1.8中都存在
-
put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题,此问题在JDK1.7和JDK1.8中都存在
HashMap的put过程
Java8开始ConcurrentHashMap,为什么舍弃分段锁
- 加入多个分段锁浪费内存空间。
- 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
- 为了提高 GC 的效率
ConcurrentHashMap(JDK1.8)为什么要使用synchronized而不是如ReentranLock这样的可重入锁?
锁的粒度 首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。 Hash冲突 JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。 扩容 JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。
为什么是synchronized,而不是ReentranLock
- 减少内存开销 假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
- 获得JVM的支持 可重入锁毕竟是API这个级别的,后续的性能优化空间很小。 synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。
ArrayList 和 LinkedList 那个更换空间?
“一般情况下,LinkedList的占用空间更大,因为每个节点要维护指向前后地址的两个节点,但也不是绝对,如果刚好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,因为扩容的原因会浪费将近原来数组一半的容量,不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化”
Hashmap该怎么考虑key应该用什么?
HashMap一般采用String、Integer 等类作为key、因为这些类底层已经重写了hashcode、equals方法,用的是final修饰类在多线程情况下相对安全。
(1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键, 字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使⽤字符串。 (2)因为获取对象的时候要⽤到equals()和hashCode()⽅法,那么键对象正确的重写这两个⽅法是⾮常重要的,这些类已经很规范的覆写了hashCode()以及equals()⽅法。
你还知道哪些hash算法?
MD4 MD5
为什么在解决hash冲突的时候,不直接⽤红⿊树?⽽选择先⽤链表,再转红黑树?
因为红⿊树需要进⾏左旋,右旋,变⾊这些操作来保持平衡,⽽单链表不需要。 当元素⼩于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素⼤于8个的时候,此时需要红⿊树来加快查询速度,但是新增节点的效率变慢了。
因此,如果⼀开始就⽤红⿊树结构,元素太少,新增效率⼜⽐较慢,⽆疑这是浪费性能的。
我不⽤红⿊树,⽤⼆叉查找树可以么?
可以。但是⼆叉查找树在特殊情况下会变成⼀条线性结构(这就跟原来使⽤链表结构⼀样了,造成很深的问题),遍历查找会⾮常慢。
当链表转为红⿊树后,什么时候退化为链表?
为6的时候退转为链表。中间有个差值7可以防⽌链表和树之间频繁的转换。假设⼀下,如果设计成链表个数超过8则链表转换成树结构,链表个数⼩于8则树结构转换成链表,如果⼀个HashMap不停的插⼊、删除元素,链表个数在8左右徘徊,就会频繁的发⽣树转链表、链表转树,效率会很低。
volatile与synchronized的区别
1、volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;volatile只能修饰变量,而synchronized可以修饰方法,代码块。随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用synchronized的比率还是很大的。
2、多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。
3、volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以保证可见性。
4、关键字volatile解决的是变量在多个线程之间的可见性;synchronized关键字解决多个线程之间访问公共资源的同步性。
Synchronized锁的升级过程
synchronized锁升级过程依次为无锁、偏向锁、轻量级锁、重量级锁,部分文章认为synchronized锁不存在降级过程,但在openjdk的hotsopt jdk8u里是有锁降级的机制的。
- 无锁到偏向锁 线程A执行到同步代码块时,检查对象头锁标志位是否为01,再看偏向锁标志位是否为0(即检查对象是否为无锁状态),通过CAS操作尝试修改MarkWord字段,这里CAS操作只尝试一次,失败的话说明发生锁竞争,立即升级为轻量级锁。成功修改后执行同步代码块,执行完毕后不主动释放偏向锁。将Lock Record中的obj设置为null,表示当前线程不在同步代码块中。
- 偏向锁到轻量级锁 线程B执行到同步代码块时,检查到对象头锁标志位为01且偏向锁标志位为1,说明该锁已被线程占有。检查Thread Id是否为当前线程(B线程),是的话就执行同步代码块,否则就进入锁撤销逻辑,jvm会在安全点暂停所有线程,判断持有锁线程的当前状态,分两种情况:
线程已死亡或没在执行同步代码块: JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。进行锁撤销操作,对象由偏向锁变为无锁且不可偏向状态,线程B通过CAS操作尝试修改Thread Id失败升级为轻量级锁,成功则执行同步代码块。 线程正在执行同步代码块: 说明发生了锁竞争,进行锁撤销,将对象头MarkWord置为无锁状态并升级为轻量级锁。 注: 当锁处于偏向锁状态时,只要有多个线程尝试获取同一把锁,该锁都会升级为轻量级锁,区别是升级的时机不同。
- 轻量级锁到重量级锁 线程C执行到同步代码块时,检查到对象头锁标志位00,说明该锁为轻量级锁。通过CAS操作尝试修改MarkWord字段,因为自旋的原因这里尝试多次CAS操作,多次尝试都失败的话将锁升级为重量级锁,线程睡眠等待被唤醒。自旋成功则执行同步代码块,执行完毕后释放锁恢复MarkWord数据,然后检查锁是否升级为重量级锁(线程执行同步代码块的时候可能有其他线程过来抢占锁且不成功,这时锁会升级为重量级锁)。如果升级为重量级锁的话还要执行一步操作 唤醒其他线程。
什么是JMM?
JMM 是Java内存模型( Java Memory Model),简称JMM。它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
2.JMM的三大特性:
1.原子性
一个或多个操作,要么全部执行(执行的过程是不会被任何因素打断的),要么全部不执行。
2.可见性
只要有一个线程对共享变量的值做了修改,其他线程都将马上收到通知,立即获得最新值。
3.有序性
有序性可以总结为:在本线程内观察,所有的操作都是有序的;
3.关于同步的规定:
1.线程解锁前,必须把共享变量的值刷新回主内存。
2.线程加锁前,必须将主内存的最新值读取到自己的工作内存。
3.加锁解锁是同一把锁。
JVM内存结构?堆的内存结构?类加载过程?
JVM内存结构主要由三部分组成,分别是堆、方法区、栈
Java堆中细分为:新生代和老年代,一个新生代分为1个Eden区和2个Survivor区,说明:绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young Garbage Collection,即YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。Survivor区分为so和s1两块内存空间。每次YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代
启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器
延迟任务实现
DelayQueue 实现延迟任务:
DelayQueue 是一个支持延时获取元素的无界阻塞队列,队列中的元素必须实现 Delayed 接口,并重写 getDelay(TimeUnit) 和 compareTo(Delayed) 方法
Redis 实现延迟任务
使用 Redis 实现延迟任务的方法大体可分为两类:通过 zset 数据判断的方式,和通过键空间通知的方式。
使用 Spring 定时任务
如果你使用的是 Spring 或 SpringBoot 的项目的话,可以使用借助 Scheduled 来实现,本文将使用 SpringBoot 项目来演示 Scheduled 的实现,实现我们需要声明开启 Scheduled
SQL 优化
1、创建索引
2、避免索引失效(6种)
3、锁粒度
EXPLAIN 分析 SQL 执行计划
悲观锁:
悲观锁她专一且缺乏安全感了,她的心只属于当前线程,每时每刻都担心着它心爱的数据可能被别的线程修改。因此一个线程拥有(获得)悲观锁后,其他任何线程都不能对数据进行修改啦,只能等待锁被释放才可以执行。
- SQL语句
select ...for update就是悲观锁的一种实现 - 还有Java的synchronized关键字也是悲观锁的一种体现
乐观锁:
乐观锁的很乐观,它认为数据的变动不会太频繁,操作时一般都不会产生并发问题。因此,它不会上锁,只是在更新数据时,再去判断其他线程在这之前有没有对数据进行过修改。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。
B+树和B-树的主要区别?
- B-树内部节点是保存数据的;而B+树内部节点是不保存数据的,只作索引作用,它的叶子节点才保存数据。
- B+树相邻的叶子节点之间是通过链表指针连起来的,B-树却不是。
- 查找过程中,B-树在找到具体的数值以后就结束,而B+树则需要通过索引找到叶子结点中的数据才结束
- B-树中任何一个关键字出现且只出现在一个结点中,而B+树可以出现多次
11.1 一般什么时候考虑JVM调优呢?
- Heap内存(老年代)持续上涨达到设置的最大内存值;
- Full GC 次数频繁;
- GC 停顿时间过长(超过1秒);
- 应用出现OutOfMemory 等内存异常;
- 应用中有使用本地缓存且占用大量内存空间;
- 系统吞吐量与响应性能不高或下降。
11.2 JVM调优的目标
- 延迟:GC低停顿和GC低频率;
- 低内存占用;
- 高吞吐量;
11.3 JVM调优量化目标
- Heap 内存使用率 <= 70%;
- Old generation内存使用率<= 70%;
- avgpause <= 1秒;
- Full gc 次数0 或 avg pause interval >= 24小时 ;
11.4 JVM调优的步骤
- 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
- 确定JVM调优量化目标;
- 确定JVM调优参数(根据历史JVM参数来调整);
- 依次调优内存、延迟、吞吐量等指标;
- 对比观察调优前后的差异;
- 不断的分析和调整,直到找到合适的JVM参数配置;
- 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。