流程
- 算法题
- 简历上项目用到技术、流程、遇到问题
- HR
准备
- 常考的题型和回答思路
- 刷100算法题,理解其思想,不要死记
- 最近一家公司所负责的业务和项目:
- 项目背景、演进之路,有哪个阶段,每个阶段主要做什么
- 项目中技术选型,使用一些工具和框架的调研,为啥选这个
- 项目的亮点:最牛的事,复杂的需求方案设计、性能优化、线上问题处理、项目重构等
- 架构设计:平台化思想。如DDD领域驱动设计
- 项目管理:如何高效的协调各个团队工作,使用那些方法来保障项目按时交付。当遇到困难时,如何应对等
知识点大纲
GC和JVM(强弱引用);数据库;是双重检查的单例模式;什么是亮点技术点?
技术点:快照、MAP
开始
微服务
微服务架构就是将单体的应用程序分成多个应用程序,这多个应用程序就成为微服务,每个微服务运行在自己的进程中,并使用轻量级的机制通信。这些服务围绕业务能力来划分,并通过自动化部署机制来独立部署。这些服务可以使用不同的编程语言,不同数据库,以保证最低限度的集中式管理。
微服务特点
- 解耦 – 系统内的服务很大程度上是分离的。因此,整个应用程序可以轻松构建,更改和扩展
- 组件化 – 微服务被视为可以轻松更换和升级的独立组件
- 业务能力 – 微服务非常简单,专注于单一功能
- 自治 – 开发人员和团队可以彼此独立工作,从而提高速度
- 持续交付 – 通过软件创建,测试和批准的系统自动化,允许频繁发布软件
- 责任 – 微服务不关注应用程序作为项目。相反,他们将应用程序视为他们负责的产品
- 分散治理 – 重点是使用正确的工具来做正确的工作。这意味着没有标准化模式或任何技术模式。开发人员可以自由选择最有用的工具来解决他们的问题
- 敏捷 – 微服务支持敏捷开发。任何新功能都可以快速开发并再次丢弃
Springboot ⚫
静态代理和动态代理
代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。
⚫ 静态代理:由程序员创建或是由特定工具生成,在代码编译时就确定了被代理的类是哪 一个是静态代理。静态代理通常只代理一个类;
⚫ 动态代理:在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口 下的多个实现类;
实现步骤:
a.实现 InvocationHandler 接口创建自己的调用处理器;
b.给 Proxy 类提供 ClassLoader 和代理接口类型数组创建动态代理类;
c.利用反射机制得到动态代理类的构造函数;
d.利用动态代理类的构造函数创建动态代理类对象;
@Transactional
它允许为事务设置传播、隔离、超时、只读和回滚条件。 还可以指定事务管理器。
- Spring 创建一个代理,或者操作类字节码,来管理事务的创建、提交和回滚。 在代理的情况下,Spring 会忽略内部方法调用中的 @Transactional。
- 优先级:接口–》超类–》类–》接口方法–》超类方法–》类方法(从低到高)。必须是public
- 失效场景:非public;同一个类中方法调用(代理原理引起);异常被catch“吃了”
传播性–【活动事务的复用】(没有创建、没有不用、没有异常、不用)
- REQUIRED:默认值。@Transactional(propagation = Propagation.REQUIRED)
- SUPPORTS:Spring 首先检查是否存在活动事务。 如果存在事务,则将使用现有事务。 如果没有事务,则以非事务方式执行。
- MANDATORY:如果存在活动事务,则将使用它。 如果没有活动事务,则 Spring 会抛出异常
- NEVER:不用
- NOT_SUPPORTED:如果当前存在事务,首先Spring将其挂起,然后在没有事务的情况下执行业务逻辑。
- REQUIRES_NEW:如果当前事务存在,Spring 将挂起它,然后创建一个新事务
- NESTED:Spring 检查事务是否存在,如果存在,则标记一个保存点。 这意味着如果业务逻辑执行抛出异常,那么事务将回滚到这个保存点。 如果没有活动事务,它的工作方式类似于 REQUIRED。JpaTransactionManager 仅支持 JDBC 连接的 NESTED
if (isExistingTransaction()) {if (isValidateExistingTransaction()) {validateExisitingAndThrowExceptionIfNotValid();}return existing;
}
return createNewTransaction(); /* if后面是核心*/
//----------------------------------------------------------if(isExistingTransaction()){略}
return emptyTransaction; /*SUPPORTS*/if(isExistingTransaction()){略}
throw IllegalTransactionStateException; /*MANDATORY*/
//--------------------------------------------------if (isExistingTransaction()) {throw IllegalTransactionStateException;
}
return emptyTransaction; /*NEVER*/
事务隔离
隔离是常见的 ACID 属性之一:原子性、一致性、隔离性和持久性。
- 脏读:读取并发事务未提交的变化
- 不可重复读:如果并发事务更新同一行并提交,则在重新读取行时获得不同的值
- 幻读:如果另一个事务添加或删除范围内的某些行并提交,则在重新执行范围查询后获取不同的行
- DEFAULT默认:当 Spring 创建新事务时,隔离级别将是RDBMS 的默认隔离。 因此,如果更改数据库,应该小心。在正常流程中,隔离仅在创建新事务时适用。 因此,如果出于某种原因不想让一个方法在不同的隔离中执行,必须将 TransactionManager::setValidateExistingTransaction 设置为 true。
- READ_UNCOMMITTED:最低的隔离级别。受到三个副作用影响。@Transactional(isolation = Isolation.READ_UNCOMMITTED)
- READ_COMMITTED:可防止脏读。但是如果一个事务提交了它的更改,可能会通过重新查询而改变结果。(Postgres、SQL Server 和 Oracle 的默认级别)
- REPEATABLE_READ: 可防止脏读和不可重复读。它是防止丢失更新所需的最低级别。 当两个或多个并发事务读取和更新同一行时,就会发生更新丢失。 REPEATABLE_READ 根本不允许同时访问一行。 因此丢失的更新不会发生。
- SERIALIZABLE:一组可串行化事务的并发执行与串行执行的结果相同
REPEATABLE_READ原理:当前记录的行锁被其他事务占用的话,就需要进入锁等待。
@Autowired 和 @Resource
- Resource 是 JDK 提供的,而 Autowired 是 Spring 提供的
- Resource 不允许找不到 bean 的情况,而 Autowired 允许(@Autowired(required = false))
- 指定 name 的方式不一样,@Resource(name = “baseDao”),@Autowired()@Qualifier(“baseDao”)
- Resource 默认通过 name 查找(当找不到与名称匹配的 bean 时才按照类型进行装配。但是需要注意的是,如果 name 属性一旦指定,就只会按照名称进行装配),而 Autowired 默认通过 type 查找。
Spring Bean的生命周期
- 根据配置情况调用 Bean 构造方法或工厂方法实例化 Bean。
- 利用依赖注入完成 Bean 中所有属性值的配置注入。
- 如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值(实际不建议使用,就是spring通过map管理bean)
- 如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。
- 如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。
- 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。
- 如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。
- 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。
- 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。
- 如果在 中指定了该 Bean 的作用范围为 scope=“singleton”,则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 中指定了该 Bean 的作用范围为 scope=“prototype”,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。
- 如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。
Aware子接口 描述 BeanNameAware 获取容器中 Bean 的名称 BeanFactoryAware 获取当前 BeanFactory ,这样可以调用容器的服务 ApplicationContextAware 同上 MessageSourceAware 获取 Message Source 相关文本信息 ApplicationEventPublisherAware 发布事件 ResourceLoaderAware 获取资源加载器,这样获取外部资源文件
Spring Bean的作用域
⚫ 单例 singleton : bean 在每个 Spring IOC 容器中只有一个实例。
⚫ 原型 prototype:一个 bean 的定义可以有多个实例。
⚫ request:每次 http 请求都会创建一个 bean。
⚫ session:在一个 HTTP Session 中,一个 bean 定义对应一个实例。
⚫ globalsession
⚫ application
Spring AOP原理
【类】:代理对象实现了某个接口用JDK Proxy创建;【接口】:没有实现接口对象用Cglib生成被代理对象的子类。
代理就是创建一个和impl实现类平级的一个对象,这个对象是一个代理对象,实现和impl相同的功能,aop的横向机制原理。
能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring MVC 的工作流程
⚫ 用户向服务端发送一次请求,这个请求会先到前端控制器 DispatcherServlet。
⚫ DispatcherServlet 接收到请求后会调用 HandlerMapping 处理器映射器。由此得知,该请求该由哪个 Controller 来处理(并未调用 Controller,只是得知)
⚫ DispatcherServlet 调用 HandlerAdapter 处理器适配器,告诉处理器适配器应该要去执行哪个 Controller
⚫ HandlerAdapter 处理器适配器去执行 Controller 并得到 ModelAndView(数据和视图),并层层返回给 DispatcherServlet
⚫ DispatcherServlet 将 ModelAndView 交给 ViewResolver 视图解析器解析,然后返回真正的视图。
⚫ DispatcherServlet 将模型数据填充到视图中
⚫ DispatcherServlet 将结果响应给用户
Spring Boot启动流程以及源码
- 创建 SpringApplication 实例。SpringApplication 是 Spring Boot 启动的入口类,它是一个工厂类,用于创建 Spring 应用上下文。在创建 SpringApplication 实例时,会进行一些基本的配置,如设置主程序类、设置 Web 应用类型等。
- 加载配置文件。在创建 SpringApplication 实例时,会加载配置文件,包括 application.properties 和 application.yml 等文件。配置文件中可以设置一些基本配置信息,如应用名称、端口号等。
- 初始化 SpringApplication 实例。在创建 SpringApplication 实例后,需要进行一些初始化操作,如添加配置、设置属性等。
- 执行 SpringApplication 实例的 run() 方法。在执行 run() 方法时,Spring Boot 会根据配置情况,创建应用上下文、注册 Bean 等。其中,SpringApplication.run() 方法是 Spring Boot 启动的核心方法,它会根据 Web 应用类型,创建不同的应用上下文(普通应用或web应用)
SpringApplication(初始化启动类属性变量、推动web应用类型、加载类路径META-INF下的spring.factories文件实例初始化器和监听器、推断主启动类main) --> ConfigurableApplicationContext
SpringBoot定时任务@Scheduled
使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。
使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
Spring 框架中用到了哪些设计模式
- 工厂设计模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建bean 对象。
- 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
- 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller。
设计原则
⚫ 遵循单一职责原则
⚫ 开放-封闭原则
⚫ 里氏代换原则(LSP)
⚫ 依赖倒置原则
⚫ 接口隔离原则(Interface Segregation Principle)
⚫ 迪米特法则(Law of Demeter)
内存调优方面 ?
排查 OOM 的经验;写代码的时候也会注意内存性能;了解 JVM 结构和 GC 流程的知识;dump
RPC
基础
⚫ 需要有非常高效的网络通信,比如一般选择 Netty 作为网络通信框架;
⚫ 需要有比较高效的序列化框架,比如谷歌的 Protobuf 序列化框架;
⚫ 可靠的寻址方式(主要是提供服务的发现),比如可以使用 Zookeeper 来注册服务等等;
⚫ 如果是带会话(状态)的 RPC 调用,还需要有会话和状态保持的功能;
JAVA ⚫
编码
- utf-8:变长编码技术,不同类型的字符可以由1~6个字节组成。int型占4个字节
- utf-16:世界上所有语言都可通过这本字典Unicode 来相互翻译,而 UTF-16 定义了 Unicode 字符在计算机中存取方法,用两个字节来表 示 Unicode 转化格式。不论什么字符都可用两字节表示,即 16bit。
- GB2312:双字节编码,总编码范围是 A1-A7,A1-A9 是符号区,包含 682 个字符,B0-B7是 汉字区,包含 6763 个汉字;
- GBK 为了扩展 GB2312,加入了更多的汉字,编码范围是 8140~FEFE,有 23940 个码位,能 表示 21003 个汉字
- ASCII 码:总共 128 个,用一个字节的低 7 位表示,0〜31 控制字符如换回车删除等;32~126 是打印字符,可通过键盘输入并显示出来;
- ISO-8859-1,用来扩展 ASCII 编码,256 个字符,涵盖了大多数西欧语言字符。
I/O模型有哪几种
阻塞,非阻塞,复用,信号驱动,异步。
I/O多路复用实现方式select、poll、epoll 区别
- select:它仅仅知道了,有 I/O 事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以 select 具有 O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
- poll:poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.。
- epoll:epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以我们说 epoll 实际上是事件驱动(每个事件关联上 fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了 O(1)),通过红黑树和双链表数据结构,并结合回调机制,造就了 epoll 的高效,epoll_create(),epoll_ctl()和 epoll_wait()系统调用
浏览器打开一个链接的时候,计算机做了哪些工作步骤
域名解析–> 发起 TCP 的 3 次握手 –> 建立 TCP 连接后发起 http 请求 –> 服务器响应 http请求–>浏览器得到 html 代码【加载】 –> 浏览器解析 html 代码,并请求 html 代码中的资源(如js、css、图片等)【解析】 –> 浏览器对页面进行渲染呈现给用户;【初始化】
线程生命周期,切换
线程的生命周期包含 5 个阶段,包括:新建、就绪、运行、阻塞、销毁。
⚫ 新建(NEW):就是刚使用 new 方法,new 出来的线程;
⚫ 就绪(RUNNABLE):就是调用的线程的 start()方法后,这时候线程处于等待 CPU 分配资源阶段,谁先抢的 CPU 资源,谁开始执行;
⚫ 运行(RUNNING):当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run 方法定义了线程的操作和功能;
⚫ 阻塞(BLOCKED):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如 sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用 notify 或者 notifyAll()方法。唤醒的线程不会立刻执行run 方法,它们要再次等待 CPU 分配资源进入运行状态;
⚫ Waiting(无限等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting 状态。进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 方法或者 notifyAll 方法时才能够被唤醒。
⚫ 销毁(TERMINATED):如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
线程&进程
操作系统中可以拥有多个进程,一个进程里可以拥有多个线程,线程在进程内执行
- 容易创建新线程。创建新进程需要重复父进程
- 线程可以控制同一进程的其他线程。进程无法控制兄弟进程,只能控制其子进程
- 进程拥有自己的内存空间。线程使用进程的内存空间,且要和该进程的其他线程共享这个空间;而不是在进程中给每个线程单独划分一点空间。
- (同一进程中的)线程在共享内存空间中运行,而进程在不同的内存空间中运行
- 线程可以使用 wait(),notify(),notifyAll()等方法直接与其他线程(同一进程)通信;而,进程需要使用“进程间通信”(IPC)来与操作系统中的其他进程通信。
join 和 deamon 的区别
- join:当子线程调用 join 时,主线程会被阻塞,当子线程结束后,主线程才能继续执行。
- deamon:当子进程被设置为守护进程时,主进程结束,不管子进程是否执行完毕,都会随着主进程的结束而结束。
关于创建
守护进程
fork两次的原因如下:为了创建一个新的会话组,防止获得终端
第一次fork前:在开始运行且fork之前拥有一个会话两个进程组:shell拥有一个进程组,你运行的程序拥有一个进程组,且他们都是组长进程;shell还是会话首进程
第一次fork后:父进程的退出带来的变化是告知shell命令已完成;新创建的子进程不可能是组长进程(因为父进程在它之前创建)。这为接下来调用setsid做准备(因为调用setsid()的进程不能是组长进程)
第二次fork原因:在调用setsid()后,原来的子进程现在在一个新会话里面,新会话里有一个进程组并且它就是组长进程。而且如果原来有终端现在也分离了。然而问题就处在这里,组长进程是有可能获得终端的,为了防止它以后获得终端,所以再次fork来消除潜在问题
父进程宕掉,子进程情况
如果父进程是会话首进程(如shell),那么父进程退出后,子进程也会退出;反之如果父进程不是会话首进程,那么父进程退出后,子进程不会退出,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程。
一个公司创业,首先是一个人,然后慢慢加了几个人,做大以后有了部门,每个部门各司其职。这里公司就是一个会话,创业者就是会话首进程,公司名就是会话id;各个部门就是进程组,各个部门的名字就是组id;员工就是进程,工号就是进程id(当员工辞职后工号会留下来给新任员工,对比进程id)
孤儿进程和僵尸进程的区别
孤儿进程
:父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程(father died)。子进程的资源由 init 进程(进程号 PID = 1)回收。
僵尸进程
:子进程退出了,但是父进程没有用 wait 或 waitpid 去获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵死进程。
内核态与用户态(❤️ ω ❤️)
假设没有这种内核态和用户态之分,程序随随便便就能访问硬件资源,比如说分配内存,程序能随意的读写所有的内存空间,如果程序员一不小心将不适当的内容写到了不该写的地方,就很可能导致系统崩溃。用户程序是不可信的,不管程序员是有意的还是无意的,都很容易将系统干到崩溃。
正因为如此,Intel 就发明了 ring0-ring3 这些访问控制级别来保护硬件资源,ring0 的就是我们所说的内核级别,要想使用硬件资源就必须获取相应的权限(设置 PSW 寄存器,这个操作只能由操作系统设置)。操作系统对内核级别的指令进行封装,统一管理硬件资源,然后向用户程序提供系统服务,用户程序进行系统调用后,操作系统执行一系列的检查验证,确保这次调用是安全的,再进行相应的资源访问操作。**内核态能有效保护硬件资源的安全。
进程间的几种通讯方式
- 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信.
- 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
管道是只读不回;消息队列是写 、读,然后写、读(2个内存地址);共享内存是一个内存地址。
名词解释:
管道
:所谓的管道,就是内核里面的一串缓存。管道传输的数据是无格式的流且大小受限。创建的子进程会复制父进程的文件描述符,可以各自的fd读写同一个管道文件实现进程通信。在shell里通过「|」匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,那么在我们编写 shell 脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销。
命名管道
:因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。(管道因为匿名在,没有实体,也就没有管道文件)
- 只读且阻塞方式 open(const char *pathname, O_RDONLY); 对应写 open(…, O_WRONLY);
- 只读且非阻塞方式 open(const char *pathname, O_RDONLY | O_NONBLOCK);
共享内存
:在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。(PCB进程控制块–>Addr Space地址空间–>页表–>物理地址)对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。
信号
:信号就是由用户、系统或进程发送给目标进程的信息,以通知目标进程中某个状态的改变或是异常。信号产生分为硬中段和软中段(如系统异常,指令和状态变化-定时器)。信号处理分为忽略【kill、stop指令不能忽略】、捕捉【处理信号】和默认动作【默认处理方式,一般杀死】。
信号量
:(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。信号量是一种特殊的变量,对信号量的访问必须是原子操作,信号量的操作只有两种:P操作(-1,申请资源)和V操作(+1,释放资源)
select 和 epoll (了解一下)
- select:支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的 BLOCK 或 NONBLOCK 操作。当应用程序通过设备驱动访问该设备时(默认为 BLOCK 操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。 - epoll:epoll 由三个系统调用组成,分别是 epoll_create,epoll_ctl 和 epoll_wait。epoll_create 用于创建和初始化一些内部使用的数据结构;epoll_ctl 用于添加,删除或者修改指定的 fd 及其期待的事件,epoll_wait 就是用于等待任何先前指定的 fd 事件
Java的异常体系
Java 中 Throwable 是所有异常和错误的超类,两个直接子类是 Error(错误)和 Exception
- Error是程序无法处理的错误,由JVM产生和抛出。如OOM、ThreadDeath等,一般会终止程序
- Exception是程序本身可以处理的异常,分为运行异常(NullPoint、IndexOutOf),非运行异常IO、SQL、FileNotFount以及自定义的Exception
final、finally、finallize?finally 是在 return 之前执行还是之后?finally 块里的代码一定会执行吗?
- final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
- finally 是 Java 保证重点代码一定要被执行的一种机制。可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
- finalize 是基础类 java.lang.Object 的一个方法,设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为deprecated。
finally 块的语句在 try 或 catch 中的 return 语句执行之后返回之前执行且 finally 里的修改语句可能影响也可能不影响 try 或 catch 中 return 已经确定的返回值,若 finally 里也有 return语句则覆盖 try 或 catch 中的 return 语句直接返回。
finally 块里的代码不一定会执行。比如:
⚫ try 语句没有被执行到,如在 try 语句之前就返回了,这样 finally 语句就不会执行,这也说明了 finally 语句被执行的必要而非充分条件是:相应的 try 语句一定被执行到。
⚫ 在 try 块中有 System.exit(0**
解析与分派(❤️ ω ❤️)
解析
指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。
分派
可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。
Java中实现多态的机制
多态
是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用 修改源程序,就可以让引用变量绑定到各种不同的类实现上。Java 实现多态有三个必要条件: 继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能 够具备调用父类方法和子类的方法。
Java反射
在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能 称为 java 语言的反射机制。
Class.forName(“全限类名”) //创建对象
对象.getClass(); //获取class//获取构造器对象,通过构造器 new 出一个对象
Clazz.getConstructor([String.class]);
Con.newInstance([参数]);//通过 class 对象创建一个实例对象 == new Class()
Cls.newInstance()//通过 class 对象获得一个属性对象
Field c=cls.getFields(); //获得某个类的所有的公共(public)的字段,包括父类中的字段。
Field c=cls.getDeclaredFields(); //获得某个类的所有声明的字段,即包括 public、private和 proteced,但是不包括父类的声明字段//通过 class 对象获得一个方法对象
Cls.getMethod(“方法名”,class……parameaType); //(只能获取公共的)
Cls.getDeclareMethod(“方法名”); //(获取任意修饰的方法,不能执行私有)
M.setAccessible(true);//(让私有的方法可以执行)//让方法执行
Method.invoke(obj 实例对象,obj 可变参数); //-----(是有返回值的)
Java注解(❤️ ω ❤️)
注解是通过@interface
关键字来进行定义的,形式和接口差不多,只是前面多了一个@
public @interface TestAnnotation{ }
要使注解能正常工作,还需要使用元注解(5种),它是可以注解到注解上的注解。
- @Retention说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
- @Document 注解中的元素包含到javadoc中去。
- @Target 限定注解的应用场景,ElementType.FIELD 给属性进行注解;ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD 可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE 可以给一个类型进行注解,如类、接口、枚举。
- @Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用 的话,该子类就可继承超类的注解;
注解的作用:
⚫ 提供信息给编译器:编译器可利用注解来探测错误和警告信息
⚫ 编译阶段:软件工具可以利用注解信息来生成代码、html 文档或做其它相应处理;
⚫ 运行阶段:程序运行时可利用注解提取代码注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象
泛型原理
Java 中的泛型有 3 种形式,泛型方法,泛型类,泛型接口。Java 通过在编译时类型擦除的方式来实现泛型。擦除时使用 Object 或者界定类型替代泛型,同时在要调用具体类型方法或者成员变量的时候插入强转代码,为了保证多态特性,Java 编译器还会为泛型类的子类生成桥接方法。类型信息在编译阶段被擦除之后,程序在运行期间无法获取类型参数所对应的具体类型。
泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。
为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在 一定程度无视类型参数 T,直接从 T 所在的类开始向上 T 的父类去擦除,如调用泛型方法, 传入类型参数 T 进入方法内部,若没在声明时做类似publicTmethodName(T extends Fathert){},Java 就进行了向上类型的擦除,直接把参数 t当做 Object 类来处理,而不是传进去的 T。 即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的 T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如 newT(),或者 T.play()(play 为某子类的方法而不是擦除后的类的方法)
擦除是向上类型,所以子类的方法不能调用。如果入参没有extends就当Object处理。
String【注意new String()】
String 类是 final 型,固 String 类不能被继承,它的成员方法也都默认为 final 方法。因此任何改变都会生成新的String对象。
String类是通过char数组来保存字符串的,String对equals方法进行了重定,比较的是值相等。
String a=“test”; String b=“test”; String c=newString(“test”);
a、b 和字面上的 test 都是指向 JVM 字符串常量池中的"test"对象,他们指向同一个对象。
new 关键字一定会产生一个对象 test,该对象存储在堆中。在堆中的 test 应该是引用字符串常量池中的 test。c=="test"是false,c是堆中句柄池对象,而“test”是堆中常量池中对象。(运行时都在方法区)
常用集合 【List、Vector、Map、Set】
- Vector: 底层数据结构是数组,查询快,增删慢,线程安全,效率低,默认长度为 10,超过会 100%延长,变成 20,浪费空间
- List:ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成本较高。LinkedList:使用链表实现,无需扩容。
- Set:HashSet:底层数据结构是哈希表(无序,唯一),通过 hashcode()和 equals()保证元素唯一。LinkedHashSet: 底层数据结构是链表和哈希表(FIFO 插入有序,唯一),由链表保证元素有序,由哈希表保证元素唯一。TreeSet:底层数据结构是红黑树(唯一,有序),通过自然排序和比较器排序保证元素有序,根据比较返回值是否是 0 来保证元素唯一性。
- Map:HashMap :空间换时间,哈希冲突不大的情况下查找数据性能很高。LinkedHashMap 基本特点:继承自 HashMap,对 Entry 集合添加了一个双向链表(before、after用于维护Entry插入的先后顺序;next用于维护HashMap各个桶中Entry的连接顺序)。TreeMap 是有序的。
Lambda
lambda 表达式,也被称为闭包。lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑。简洁、不易维护、可读性差。
作用域
- 访问局部变量:我们可以直接在 lambda 表达式中访问外部的局部变量:但是和匿名对象不同的是,这里的变量可以不用声明为 final,该代码同样正确,不过这里的变量必须不可被后面的代码修改(即隐性的具有 final 的语义)
- 访问字段和静态变量:与局部变量相比,我们对 lambda 表达式中的实例字段和静态变量都有读写访问权限。该行为和匿名对象是一致的。
虚拟内存
当每个进程创建的时候,内核会为每个进程分配虚拟内存,这个时候数据和代码还在磁盘上,当运行到对应的程序时,进程去寻找页表,如果发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中并更新页表,下次再访问该虚拟地址时就能命中了。
⚫ 虚拟内存地址空间是连续的,没有碎片。
⚫ 虚拟内存的最大空间就是 cup 的最大寻址空间,不受内存大小的限制,能提供比内存更大的地址空间
栈会溢出吗?什么时候溢出?方法区会溢出吗?
栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError 异常,方法递归调用产生这种结果。
方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,如果动态生成大量的 Class 文件,也会产生内存溢出。常见的场景还有:大量 JSP 或动态产生JSP 文件的应用(JSP 第一次运行时需要编译为 java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的类加载器加载也会视为不同的类)
java 程序诊断
⚫ top 找出进程 CPU 比较高 PID
⚫ top -Hp PID 打印 该 PID 进程下哪条线程的 CPU 占用比较高 tid
⚫ printf “%x\n” tid 将该 id 进行 16 进制转换 tidhex
⚫ jstack PID |grep tidhex 打印线程的堆栈信息
自带工具
⚫ jstat:虚拟机进程状况工具
⚫ jinfo:Java 配置信息工具
⚫ jmap:Java 内存映像工具
⚫ jhat:虚拟机堆转储快照分析工具
⚫ jstack:Java 堆栈跟踪工具
⚫ JConsole: Java 监视与管理控制台
⚫ VisualVM: 多合一故障处理工具
JVM
JVM 类加载机制分为五个部分:加载Loading,验证Verification,准备Preparation,解析Resolution,初始化Initialization。 类再加上 使用Using 、卸载Unloading。
Java 虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述该类的二进制流”这个动作放到 Java 虚拟机外部去实现。比便让程序应用自己决定如何取获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。
⚫ 方法区(method):被所有的线程共享。方法区包含所有的类信息和静态变量。(运行时常量池)
⚫ 堆(heap):被所有的线程共享,存放对象实例以及数组,Java 堆是 GC 的主要区域。
⚫ 栈(stack):每个线程包含一个栈区,栈中保存一些局部变量等。(本地局部变量、操作数栈、动态链接、返回地址)
⚫ 程序计数器:是当前线程执行的字节码的行指示器。
⚫ 本地方法栈
CMS,G1垃圾回收中三色标记
三色(黑、灰、白)标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The World),从而达到清除 JVM 内存垃圾的目的。
- 黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象);
- 灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC 需要从此对象中去寻找垃圾);
- 白色:表示对象没有被垃圾收集器访问过,即表示不可达
CMS
漏标问题
:增量更新
在应对漏标问题时,CMS 使用了增量更新(Increment Update)方法来做,在一个未被标记的对象(白色对象)被重新引用后,引用它的对象若为黑色则要变成灰色,在下次二次标记时让GC 线程继续标记它的属性对象(但还是存在漏标的问题)
内存碎片
:使用Mark-Sweep-Compact算法,减少垃圾碎片。压缩、阈值
CMS 采用了 Mark-Sweep 算法,最后会产生许多内存碎片,当到一定数量时,CMS 无法清理这些碎片了,CMS 会让 Serial Old 垃圾处理器来清理这些垃圾碎片,而 Serial Old 垃圾处理器是单线程操作进行清理垃圾的,效率很低。所以使用 CMS 就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行 Serial Old GC 时,效率过低。
调优参数(配套使用):
-XX:+UseCMSCompactAtFullCollection 开启 CMS 的压缩
-XX:CMSFullGCsBeforeCompaction 默认为 0,指经过多少次 CMS FullGC 才进行压缩当 JVM 认为内存不够,再使用 CMS 进行并发清理内存可能会发生 OOM 的问题,而不得不进行 Serial Old GC,Serial Old 是单线程垃圾回收,效率低。
解决方案:降低触发 CMS GC 的阈值,让浮动垃圾不那么容易占满老年代
调优参数:
-XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就
进行 CMS GC
G1
漏标问题
:SATB
- 在开始标记的时候生成一个快照图标记存活对象
- 在一个引用断开后,要将此引用推到 GC 的堆栈里,保证白色对象(垃圾)还能被 GC 线程扫描到(在**write barrier(写屏障)**里把所有旧的引用所指向的对象都变成非白的)
- 配合 Rset,去扫描哪些 Region 引用到当前的白色对象,若没有引用到当前对象,则回收
Full GC
:且 JDK10 之前的 Full GC,为单线程的。加大内存;提高CPU性能,增加速度小于回收速度;降低阈值,让 Mixed GC 提早发生(默认 45%)
TCP/UDP
应用层 传输层 互连网络层 网络接口层
区别
TCP 基于连接,消耗资源多,保证数据顺序与正确性;UDP 基于无连接,消耗小,结构简单,无顺序丢包。
TCP原理
因为 TCP 是全双工,每个方向都必须进行单独关闭。关闭连接时,当 Server 端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client 端,”你发的 FIN 报文我收到了”。只有等到 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四步握手。
Hash算法解决冲突
- 开放寻址法:
- 链地址法:
- 再哈希法:
- 公共溢出区:
HashMap的底层数据结构
线程不安全:多个线程同时操作;两个线程遇到12的倍数,会出现old转移new时候值存储异常。
扰动函数
:右移16位正好为32bit的一半,hashcode低位与高位异或。是为了混合原始哈希吗的高位和低位,来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,使高位的信息也被保留下来。return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
扩容
:【空参】实例化map内部数组为null,第一次调用put方法,开始第一次初始化扩容,长度16;【有参】不小于指定容量2的幂数,将值赋给阈值threshold。第一次put,阈值给容量,然后阈值=容量*负载因子。以后就是容量和阈值为原来2倍。
jdk1.8之前采用数组和链表结合在一起使用的链表散列。通过(n-1)&hash分桶,一个桶里用拉链法解决冲突。hash采用扰动函数处理,减少碰撞。
jdk1.8以后当链表长度大于阈值(默认8),会调用treeifyBin()方法,根据map数组来决定是否转换为红黑树。只有数组长度大于或者等于64的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是执行resize()对数组扩容。扩展后 Node 对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
ConcurrentHashMap 的存储结构 (HashTable)
- java7使用的分段锁,每一段(Segment)上同时只有一个线程可以操作,每个段( HashEntry 数组 + 链表)类似HashMap数据的结构,可以扩容。但是Segment的个数一旦初始化就不能改变,默认16个。
- java8使用Synchronized锁加CAS的机制。Node 数组 + 链表 / 红黑树结构类似HashMap。冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。对每个数组元素加锁
HashTable:使用Synchronized锁实现线程安全,读操作不加锁,由于HashEntry的value变量是volatile
ArrayList 和 LinkedList 的区别
需要频繁读取集合中的元素时,更推荐使用 Arrayist,而在增删操作较多时,更推荐使用 LinkedList。
- 数据结构实现:ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成本较高。LinkedList:使用链表实现,无需扩容。数组静态分配内存,链表动态分配内存;数组是事先定义长度,当数据少的时候,造成内存浪费。数组需要内存连续。
- 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
- 增加和删除效率:在非首尾的增删操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。
- 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
- 当数组内存过大时会出现什么问题(堆内存溢出),链表增删过多会出现的什么问题(大量内存碎片)
多台服务器同时对一个数据定时任务,怎么处理
对于一个定时任务,如果当前任务已经被某一个服务器处理后,另外一个服务器就不需要执行这个任务了。总结:就是在阈值时间没有执行就执行。任务ID分布式锁。
- 在定时任务里加锁机制,等某台服务器获取权限,其他服务器将不再执行此次定时任务。
- 在数据库的创建定时任务控制表 job_controller,创建 updated_by 字段,用来存放执行代码的服务器生成的序列号。创建 updateTime 字段,用于记录标记更新 update_by 的时间戳,也可以理解为上一次任务执行的时间戳。
- 在代码层面,在执行任务的时候,首先生成一个序列号,然后将序列号存储在当前任务的记录上。然后再从数据库里查询当前记录的序列号,在做标记前,首先检查当前任务的上一次执行时间离当前时间超过阈值(自己定义),如果超过则表明还没有其他节点执行该任务,然后为 task 保存标签和当前运行时间。当然如果上一次运行时间为空的情况下,也是允许标记的,如果数据库中的序列号与当前节点生成序列号相匹配,则执行任务的具体逻辑,反之,则什么都不做处理。
多线程
线程的生命周期开销非常高;消耗过多CPU资源,线程切换。
安全性问题(并发);活跃性问题(死锁);性能问题(线程切换)
分布式锁
- 基于数据库实现分布式锁
- 基于缓存实现分布式锁
- 基于ZK实现分布式锁
Redis缓存分布式锁实现
- set px nx
- 守护线程,进行 renew
- Redis 分布式锁实现: 先拿 setnx 来争抢锁,抢到之后,再用 expire(过期)给锁加一个过期时间防止锁忘记了释放。
- 如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样:set
指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用的!
ThreadLocal(线程本地变量)
ThreadLocal 是一个解决线程并发问题的一个类,用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。例如,由于 JDBC 的连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时,就不是线程安全的。通过将 JDBC 的连接对象保存到 ThreadLocal 中,每个线程都会拥有属于自己的连接对象副本。
实现原理:
⚫ 每个 Thread 线程内部都有一个 ThreadLocalMap;以线程作为 key,泛型作为 value,可以理解为线程级别的缓存。每一个线程都会获得一个单独的 map。
⚫ 提供了 set 和 get 等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 方法总是返回由当前执行线程在调用 set 时设置的最新值。
应用场景:
⚫ JDBC 连接⚫ Session 管理⚫ Spring 事务管理⚫ 调用链,参数传递⚫ AOP
多线程同步
使用 synchronized 关键字⚫ wait 和 notify⚫ 使用特殊域变量 volatile 实现线程同步⚫ 使用重入锁实现线程同步⚫ 使用局部变量来实现线程同步⚫ 使用阻塞队列实现线程同步⚫ 使用原子变量实现线程同步
创建线程三个方法
⚫ 通过继承 Thread 类创建线程类。并且运行其 start()方法
⚫ 实现 Runnable 接口创建线程类。运行 Thread 对象的 start()方法
⚫ 通过 Callable 和 Future 接口创建线程。运行 Thread 对象的 start()方法
获取多线程返回值
⚫ 主线程等待。⚫ 使用 Thread 的 join 阻塞当前线程等待。⚫ 实现 Callable 接口(通过 FutureTask 或线程池的 Future)。
乐观锁和悲观锁
悲观锁
:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁
:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,MVCC快照读,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
互斥锁(mutex)和自旋锁(spinlock)场景
在多核机器中,如果锁住的“事务”很简单,占用很少的时间,就应该使用 spinlock,这个时候 spinlock 的代价比 mutex 会小很多。”事务”很快执行完毕,自旋的消耗远远小于陷入sleep 和 wake 的消耗。如果锁住“事务”粒度较大,就应该使用 mutex,因为如果用spinlock,那么在“事务”执行过程中自旋很长时间还不如使得线程 sleep。
自旋锁会独占cpu时间片,所以单核不能使用。
可重入锁(ReentrantLock)
⚫ synchronized 是关键字,ReentrantLock 是 API 接口
⚫ Lock 需要手动加锁,手动释放锁
⚫ synchronized 不可中断,ReentrantLock 可中断、可超时
⚫ synchronized 是非公平锁,ReentrantLock 公平、非公平皆可
⚫ ReentrantLock 支持 Condition,多条件
//不可重入锁:其实调用outer 的线程已经获取了 lock 锁,但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为不可重入可重入就意味着
public class UnReentrant{Lock lock = new Lock();public void outer(){lock.lock();inner();lock.unlock();}public void inner(){lock.lock();//do somethinglock.unlock();}
}
线程可以进入任何一个它已经拥有的锁所同步着的代码块。synchronized、ReentrantLock 都是可重入的锁。
线程池参数以及大小
⚫ corePoolSize 核心线程大小:最小可以同时运行的线程数量。
⚫ maximumPoolSize 线程池最大线程数量:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
⚫ keepAliveTime 空闲线程存活时间。
⚫ unit 空间线程存活时间单位。
⚫ workQueue 工作队列:达到核心线程数的话,信任就会被存放在队列中。
⚫ threadFactory 线程工厂。
⚫ handler 拒绝策略。
⚫ 如果当前线程数<corePoolSize,如果是则创建新的线程执行该任务 。
⚫ 如果当前线程数>=corePoolSize,则将任务存入 BlockingQueue 。
⚫ 如果阻塞队列已满,且当前线程数<maximumPoolSize,则新建线程执行该任务。
⚫ 如果阻塞队列已满,且当前线程数>=maximumPoolSize,则抛出异常 。
⚫ RejectedExecutionException,告诉调用者无法再接受任务了(默认是丢弃任务并抛出异常)
注意:4个拒绝策略:默认;丢弃不抛出异常;丢弃最前面任务,重新提交被拒绝任务;callerRun由调用线程处理该任务。如果给用户发消息超出队列,建议用callerRun或者消息队列
。
协程
:是用户态的线程,是对普通线程更细粒度的划分。它是在用户态运行的,由用户自行调度,所以也就避免了频繁的上下文切换问题。java不如GO语音成熟。
- CPU密集型任务(n+1):1的目的是替补,有其他异常不至于CPU处于空闲状态。
- I/O密集型任务(200起步):((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目。虽然I/O线程数量增多,会造成非常频繁的上下文切换,进而影响效率。但在互联网应用中,它却是一个优秀的解决方案
排查OOM的问题
- 增加参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录。
- jstat查看监控JVM的内存和GC情况,先观察问题大概出在上面区域。
- 使用MAT工具载入dump文件,分析大对象占用情况。比如hashmap缓存可用软引用。
缓存穿透、缓存击穿和缓存雪崩
缓存穿透(多个key)
大量并发查询不存在的KEY,同时给缓存和数据库带来压力(ddos攻击、数据被误删)。
Bloom 过滤或 RoaingBitmap 判断 KEY 是否存在,如果布隆过滤器中没有查到这个数据,就不去数据库中查。在处理请求前增加恶意请求检查,如果检测到是恶意攻击,则拒绝进行服务。
缓存击穿(单一key)
某个 KEY 失效的时候,正好有大量并发请求访问这个 KEY。
KEY 的更新操作添加全局互斥锁。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存雪崩
当某一时刻发生大规模的缓存失效的情况,导致大量的请求无法获取数据,从而将流量压力传导到数据库上,导致数据库压力过大甚至宕机(大量的数据同一个时间失效)。
- 更新策略在时间上做到比较平均(随机值)。
- 使用的热数据尽量分散到不同的机器上。多台机器做主从复制或者多副本,实现高可用。
- 实现熔断限流机制,对系统进行负载能力控制。对于非核心功能的业务,拒绝其请求,只允许核心功能业务访问数据库获取数据。
- 服务降级:提供默认返回值,或简单的提示信息。
LRU-最近最少使用策略
- 使用双向链表实现的队列,队列的最大容量为缓存的大小。在使用过程中,把最近使用的页面移动到队列头,最近没有使用的页面将被放在队列尾的位置。
- 使用一个哈希表,把页号作为键,把缓存在队列中的节点的地址作为值,只需要把这个页对应的节点移动到队列的前面。
堆内存
用于分配对象的存储空间。-Xmx最大、-Xms最小、-Xmn、-XXjava8以后无效(meta空间无限)
数据库 ⚫
SQL注入攻击
者把 SQL 命令插入到 Web 表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的 SQL 命令。
在利用表单输入的内容构造 SQL 命令之前,把所有输入内容过滤一番就可以了。
对SQL慢查询优化❤️
- 分析语句,是否加载了不必要的字段/数据。(尽量缩小数据量)
- 分析SQL执行计划,思考可能的优化点,是否命中索引等。
- like以%开头索引无效,当like以&结尾,索引有效
- or语句前后均为索引时,索引生效
- 组合索引,左匹配规则 。尽量按照最左前缀原则来使用联合索引,并且将区分度高的列放在前面
- 数据类型出现隐式转换,如varchar不加单引号的数字。
- 索引列上使用IS NULL。索引不索引空值的,所以索引列避免null
- 索引列使用not、<>、!=不会使用索引。
- 索引字段进行计算操作,函数操作时不会使用索引。5.7及以上可以建立函数或运算的索引
- 当全表扫描比索引快的时候。数据少,直接一次聚簇查询,不用多次查询索引再聚簇。
- 复杂的语句,查看有没有嵌套子查询。
- 如果数据量太大,考虑分表
- 利用缓存,减少查询次数
- sp_lock,sp_who,活动的用户查看,原因是读写竞争资源。
limit 1000000 加载很慢处理
- 如果 id 是连续的,可以这样,返回上次查询的最大记录(偏移量),再往下 limit: \qquad 【select id,name from employee where id>1000000 limit 10】
- 在业务允许的情况下限制页数:有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。
- order by + 索引(id 为索引):select id,name from employee order by id limit 1000000,10
- 利用延迟关联或者子查询优化超多分页场景:SELECT a.* FROM employee a, (select id from employee where 条件 LIMIT 1000000,10 ) b where a.id=b.id(节约反查询其它字段数据丢弃时间)
Mysql索引分类
- 单列索引:普通索引、唯一索引、主键索引
- 组合索引:多个字段,遵循最左前缀集合。
- 全文索引:在 MyISAM 引擎上才能使用,只能在 CHAR,VARCHAR,TEXT 类型字段上使用全文索引
- 空间索引:空间索引是对空间数据类型的字段建立的索引,MySQL 中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用 SPATIAL 关键字。要求,引擎为 MyISAM,创建空间索引的列,必须将其声明为 NOT NULL。
什么是散列表? select * 和 select 1?
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
有时候为了提高效率,只是为了测试下某个表中是否存在记录,就用 1 来代替。
索引分类区别
⚫ 索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
⚫ 普通索引(由关键字 KEY 或 INDEX 定义的索引)的唯一任务是加快对数据的访问速度。
⚫ 普通索引允许被索引的数据列包含重复的值。如果能确定某个数据列将只包含彼此各不相同的值,在为这个数据列创建索引的时候就应该用关键字 UNIQUE 把它定义为一个唯一索引。也就是说,唯一索引可以保证数据记录的唯一性。
⚫ 主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字 PRIMARY KEY 来创建。
⚫ 索引可以覆盖多个数据列,如像 INDEX(columnA, columnB)索引,这就是联合索引。
⚫ 索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执行这些写操作时,还要操作索引文件。
Mysql事务的特性
- 原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行
- 一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态。
- 隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务。
- 持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。
Mysql聚簇索引、非聚簇索引的区别
聚簇索引InnoDB
数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一个聚簇索引,因为一个表的物理顺序只有一种情况,所以,对应的聚簇索引只能有一个。 聚簇索引的叶子节点就是数据节点,既存储索引值,又在叶子节点存储行数据。
Innodb 创建表后生成的文件有:
frm:创建表的语句
idb:表里面的数据+索引文件
非聚集索引MyISAM
逻辑顺序与磁盘上行的物理存储顺序不同。非聚簇 索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。索引命中后,需要回表查 询。
Myisam 创建表后生成的文件有:
frm:创建表的语句 MYD:表里面的数据文件(myisam data)
MYI:表里面的索引文件(myisam index)
innodb 的次索引指向对主键的引用 (聚簇索引)
myisam 的次索引和主索引都指向物理行 (非聚簇索引)
Mysql默认InnoDB引擎,B+树(范围查询)
聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。聚簇索引的叶子节点就是数据节点,既存储索引值,又在叶子节点存储行数据。
哈希虽然能够提供 O(1) 的单数据行操作性能,但是对于范围查询和排序却无法很好地支持,最终导致全表扫描;B 树能够在非叶节子点中存储数据,但是这也导致在查询连续数据时可能会带来更多的随机 I/O,而 B+树的所有叶节点可以通过指针相互连接,能够减少顺序遍历时产生的额外随机 I/O;B+树的叶子节点是数据阶段用了一个双向链表串联起来,便于范围查找。
Mysql的锁 【表、行、页】
- 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
- 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
- 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
MVCC(多版本并发控制)
- 脏读:读取并发事务未提交的变化
- 不可重复读:如果并发事务更新同一行并提交,则在重新读取行时获得不同的值。RR是可以重复读
- 幻读:如果另一个事务添加或删除范围内的某些行并提交,则在重新执行范围查询后获取不同的行。RR可以解决幻读。
当前读
:select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读。它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。 【悲观锁】
快照读
:不加锁的 select 操作就是快照读,即不加锁的非阻塞读。快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。【乐观锁】
MVCC(Multi-Version Concurrency Control),多版本并发控制,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并发性能的一种机制。在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。(写-写必须使用锁)
- 事务版本号:为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
- 表的隐藏列(3个):TRX_ID事务ID占6个字节;ROLL_PTR回滚指针,指向上一个版本(存储rollback segment里)占7个字节;ROW_ID隐藏主键;实际还有一个删除flag字段
- undo log:insert undo log-在回滚需要,事务提交成功被立即丢弃;update undo log-不仅在回滚,在快照读时也需要。所以不能随便删除,只有在快照或回滚不涉及该日志,对应日志才被purge线程统一清除。
- read view:就是事务进行快照读操作的时候生产的读视图。遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果不可见,用回滚指针取上一个版本,直到满足条件就是当前事务可见的最新老版本。
undo 原理:
- 在事务 1 修改该行(记录)数据时,数据库会先对该行加排他锁
- 然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本
- 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID, 我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
- 事务提交后,释放锁
read view流程
例:4个事务,1,3在活跃存在trx_list中,4已经提交,考察对象事务2执行快照读
先从undo log取出改行最新数据TRX_ID为4,去跟Read View的 up_limit_id 比较,4是否小于up_limit_id(1),不符合条件;继续判断4是否大于等于 low_limit_id (5),也不符合,最后判断4是否在活跃表trx_list中,不在符合可见条件,所以4对于事务2快照读是可见的。
RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR(默认)隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View
Mybatis 如何将对象转换成 SQL
SQL 绑定是在加载 Mybatis 配置文件,然后扫描到哪个 mapper 子节点,再加载 mapper 映射文件,扫描里面的 SQL 节点,然后封装成对象(MappedStatement,在这个对象的SqlSource 封装着 sql 语句)。所有的配置信息保存在 Configuration 类,最后动态代理执行的时候,取出来封装 sql 的对象,执行 sql。
主从复制,及延迟的原因
主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的binlog 日志拷贝到自己本地,写入一个 relay 中继日志中接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL。
主从延迟:主库的从库太多;从库硬件配置比主库差;慢sql语句过多;主从库之间网络延迟;主库读写压力大。
Redis
redis可以处理2^32
Redis 本质上是一个 Key-Value 类型的内存数据库,很像 Memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。
因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作。
Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个value 的最大限制是 1GB,不像 Memcached 只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能。
比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性 能消息队列服务,用他的 Set可以做高性能的 tag 系统等等。另外 Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一 个功能加强版的 Memcached 来用。
Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
数据结构
- String:简单的key-value类型, 二进制安全的,可以包含任何数据(如jpg或序列化对象),最大512M。需要计数的场景,如访问次数、点赞与转发量等
- list: 链表。发布于订阅或者说消息队列、慢查询。
- hash:hashMap(数组+链表)。对象数据的存储。如用户信息。
- set:HashSet。数据不重复,且交集、并集等
- sorted set:增加了一个权重参数score,元素能够按score进行有序排列。需要排序,如直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息等。
- bitmap:存储连续的二进制数字,一个bit位来表示某个元素对应的值或者状态。需要保存状态信息(如签到、登录)并进一步分析的场景。如签到情况、活跃用户情况、用户行为统计。
数据结构压缩列表和跳跃表
- 压缩表(ziplist):本质上就是一个字节数组,是 Redis 为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。
- 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均 O(logN)、最坏 O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
Redis 中的数据都是热点数据
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(驱逐):禁止驱逐数据
主从同步
redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
全量同步
master 服务器会开启一个后台进程用于将 redis 中的数据生成一个 rdb 文件,与此同时,服务器会缓存所有接收到的来自客户端的写命令(包含增、删、改),当后台保存进程处理完毕后,会将该 rdb 文件传递给 slave 服务器,而 slave 服务器会将 rdb 文件保存在磁盘并通过读取该文件将数据加载到内存,在此之后 master 服务器会将在此期间缓存的命令通过 redis 传输协议发送给 slave 服务器,然后 slave 服务器将这些命令依次作用于自己本地的数据集上最终达到数据的一致性。
增量同步
部分同步的实现依赖于在 master 服务器内存中给每个 slave 服务器维护了一份同步日志和同步标识,每个 slave 服务器在跟 master 服务器进行同步时都会携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后,slave 服务器隔断时间(默认1s)主动尝试和 master 服务器进行连接,如果从服务器携带的偏移量标识还在 master 服务器上的同步备份日志中,那么就从 slave 发送的偏移量开始继续上次的同步操作,如果 slave发送的偏移量已经不再 master 的同步备份日志中(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内 master 服务器接收到大量的写操作),则必须进行一次全量更新。在部分同步过程中,master 会将本地记录的同步备份日志中记录的指令依次发送给 slave 服务器从而达到数据一致。
持久化RDB和AOF优缺点
RDB
RDB 持久化方式,是将 Redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
优点:
⚫ RDB 是一个非常紧凑(有压缩)的文件,它保存了某个时间点的数据,非常适用于数据的备份。
⚫ RDB 作为一个非常紧凑(有压缩)的文件,可以很方便传送到另一个远端数据中心 ,非常适用于灾难恢复。
⚫ RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 redis的性能。
⚫ 与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些。
RDB 缺点:
⚫ Redis 意外宕机时,会丢失部分数据。
⚫ 当 Redis 数据量比较大时,fork 的过程是非常耗时的,fork 子进程时是会阻塞的,在这期间 Redis 是不能响应客户端的请求的。
AOF
AOF 方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍。
优点:
⚫ 使用 AOF 会让你的 Redis 更加持久化。
⚫ AOF 文件是一个只进行追加的日志文件,不需要在写入时读取文件。
⚫ Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写 。
⚫ AOF 文件可读性高,分析容易。
缺点:
⚫ 对于相同的数据来说,AOF 文件大小通常要大于 RDB 文件。
⚫ 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。
混合持久化方式(保证数据不丢失)
Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
在使用 RDB 进行快照时会通过子进程的方式进行实现
- 通过 fork 创建的子进程能够获得和父进程完全相同的内存空间,父进程对内存的修改对于子进程是不可见的,两者不会相互影响; 进程间沟通可以用管道。
- 通过 fork 创建子进程时不会立刻触发大量内存的拷贝,内存在被修改时会以页为单位进行拷贝,这也就避免了大量拷贝内存而带来的性能问题;
Redis数据不丢失 ❤️
使用 AOF 和 RDB 结合的方式:RDB 做镜像全量持久化,AOF 做增量持久化。因为 RDB 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。
Redis 集群模式:
master 节点持久化
如果采用了主从架构,那么建议必须开启 master node 的持久化!不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master宕机重启的时候数据是空的,然后可能一经过复制,salve node 数据也丢了,master 就会将空的数据集同步到 slave 上去,所有 slave 的数据全部清空。
Redis 断点续传
从 redis 2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
主备切换的过程,可能会导致数据丢失
解决异步复制和脑裂导致的数据丢失
redis.conf 中
min-slaves-to-write 1
min-slaves-max-lag 10
要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master就不会再接收任何请求了
上面两个配置可以减少异步复制和脑裂导致的数据丢失。
消息队列MQ
消息队列使用场景
- 应用耦合:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败;
- 异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间;
- 限流削峰:广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;
- 消息驱动的系统:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,消费者(可能有多个)负责对消息进行处理;
RabbitMQ保证消息不丢失 ❤️
生产者开启事务(同步,性能差)或者开启confirm模式(异步,性能好);exchange持久化,queue持久化、消息持久化。消费者关闭自动ACK。
Kafka
Kafka 是一个分布式流式处理平台。整个架构中包含三个角色:生产者(Producer)、代理(Broker)、消费者(Consumer)。
Kafka 给 Producer 和 Consumer 提供注册的接口,数据从 Producer 发送到 Broker,Broker 承担一个中间缓存和分发的作用,负责分发注册到系统中的 Consumer。
topic和patition
Partition
:Kafka 可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡和水平扩展。Partition 的引入就是解决水平扩展问题的一个方案。
topic
:producer 只需要关心消息发往哪个 topic,而 consumer 只关心自己订阅哪个 topic,并不关心每条消息存于整个集群的哪个 broker。
kafka偏移量保证顺序
消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。发送消息的时候指定 key/Partition。
kafka信息完整性 ❤️
生产者:回调函数
Kafka 生产者(Producer) 使用 send方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样也让它变为了同步操作,可以采用为其添加回调函数的形式。
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(result ->logger.info("生产者成功发送消息到 topic:{} partition:{}的消息", result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage())
);
Producer 的 retries(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了。
消费者:手动提交,会出现重复消费(幂等,发送每条数据增加一个唯一标识 ID)。
当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。
解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。
- 生产者发送每条数据的时候,里面加一个全局唯一的 id,消费到了之后,先根据这个 id去比如 Redis 里查一下,之前消费过吗,如果没有消费过,就处理,然后这个 id 写Redis。如果消费过就别处理了。
- 基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
Broker:只能降低
leader挂掉了,follower没有完全同步leader中数据。
当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
Kafka、RabbitMQ、RocketMQ(阿里) 之间的区别
性能:
消息中间件的性能主要衡量吞吐量,Kafka 的吞吐量比 RabbitMQ 要高出 1~2 个数量级,RabbitMQ 的单机 QPS 在万级别,Kafka 的单机 QPS 能够达到百万级别。RocketMQ 单机写入 TPS 单实例约 7 万条/秒,单机部署 3 个 Broker,可以跑到最高 12 万条/秒,消息大小10 个字节,Kafka 如果开启幂等、事务等功能,性能也会有所降低。
数据可靠性
Kafka 与 RabbitMQ 都具备多副本机制,数据可靠性较高。RocketMQ 支持异步实时刷盘,同步刷盘,同步 Replication,异步 Replication。
服务可用性
Kafka 采用集群部署,分区与多副本的设计,使得单节点宕机对服务无影响,且支持消息容量的线性提升。RabbitMQ 支持集群部署,集群节点数量有多种规格。RocketMQ 是分布式架构,可用性高。
功能
Kafka 与 RabbitMQ 都是比较主流的两款消息中间件,具备消息传递的基本功能,但在一些特殊的功能方面存在差异,RocketMQ 在阿里集团内部有大量的应用在使用。
算法
常见排序算法
1. 冒泡排序 o( n 2 n^2 n2) 【稳定】 o(1)
通过重复走完数组的所有元素,通过两两比较,直到没有数可以交换的时候结束这个数,再到下个数,直到整个数组排好顺序。
2. 插入排序 o( n 2 n^2 n2) 【稳定】 o(1)
每次从未排好序的数据堆中拿出一个数,插入到已排好序的数据队列的正确位置。
3. 希尔排序 o( n 1.3 n^{1.3} n1.3) 【不稳定】 o(1)插入排序改进版本
是插入排序的改进版。它将数组按照一定的步长分成若干个子序列,对每个子序列进行插入排序,然后逐步缩小步长,最后对整个数组进行插入排序
4. 选择排序 o( n 2 n^2 n2) 【不稳定】o(1)
每次从未排好序的数据堆中找到最小的数,插入到已排好序的数据队列的头部。
5. 快速排序 o( N ∗ l o g N N*logN N∗logN) 【不稳定】o(logn)
以数据堆中的一个数为标准,将数据堆分为小于等于和大于该数的两堆,对于分割后的两堆数再分别利用上述方法进行分割,以此类推,直到堆中只有一个数为止。
6. 堆排序 O(N*logN) 【不稳定】o(1)
将数据堆中的数两两组队排序,对于排序好的这些子堆再两两组队排序,以此类推,直到只剩下一个堆。
7. 归并排序 O(N*logN) 【稳定】o(n)
基于堆的排序算法,分为最大堆和最小堆。排序分为两个过程堆的构造和堆的排序。
8. 基数排序 o(d(n+r)) 【稳定】o®
9. 计数排序 o(n+m) 【稳定】 o(m)
利用数组下标来确定元素的正确位置(下标是值,数组值是出现次数)。
适用于一定范围内的整数排序。数组长度=max-min+1;最小值做偏移量。
10. 桶排序 o(n) 【稳定】 o(n) 计数排序补充区间
计数排序补充,用每一个桶代表一个区间范围,里面承载一个或多个元素。
桶长度=一般等于原元素长度;区间跨度=(max-min)/(桶数量-1).元素放入桶num=(int)((array[i]-min)*(bucketNum-1)/d)。类似hashmap数组+链
1 亿个数据取出最大前 100 个有什么方法
快速排序
:最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为 O(nlogn),如快速排序。- 局部
淘汰法-插入
:该方法与排序方法类似,用一个容器保存前 10000 个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的 10000 个数还小,那么容器内这个 10000 个数就是最大 10000 个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这 1 亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为 O(n+m^2),其中 m 为容器的大小,即 10000。 分治法-桶
:将 1 亿个数据分成 100 份,每份 100 万个数据,找到每份数据中最大的 10000个,最后在剩下的 10010000 个数据里面找出最大的 10000 个。如果 100 万数据选择足够理想,那么可以过滤掉 1 亿数据里面 99%的数据。100 万个数据里面查找最大的10000 个数据的方法如下:用快速排序的方法,将数据分为 2 堆,如果大的那堆个数 N大于 10000 个,继续对大堆快速排序一次分成 2 堆,如果大的那堆个数 N 大于 10000个,继续对大堆快速排序一次分成 2 堆,如果大堆个数 N 小于 10000 个,就在小的那堆里面快速排序一次,找第 10000-n 大的数字;递归以上过程,就可以找到第 1w 大的数。参考上面的找出第 1w 大数字,就可以类似的方法找到前 10000 大数字了。此种方法需要每次的内存空间为 10^64=4MB,一共需要 101 次这样的比较。最小堆法
:首先读入前 10000 个数来创建大小为 10000 的最小堆,建堆的时间复杂度为 O(mlogm)(m 为数组的大小即为 10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至 1 亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有 10000 个数字。该算法的时间复杂度为 O(nmlogm),空间复杂度是 10000(常数)Hash 法
:如果这 1 亿个书里面有很多重复的数,先通过 Hash 法,把这 1 亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的 10000 个数。
z = x + y z = x + y z=x+y ; a b a \qquad b ab
a 1 a_1 a1 ; a 1 a^1 a1 ; a 11 = b 1 2 a_{11} = b^{\frac{1}{2}} a11=b21 ; z = x ⋅ y z = x \cdot y z=x⋅y ; z = x × y z = x \times y z=x×y; z = x ÷ y z = x \div y z=x÷y