一些题目

Spring

1. 什么是Spring

Spring框架是一个开源的应用程序框架。旨在降低应用程序开发的复杂度。Spring具有分层体系结构,允许用户自由选择组件,为开发者提供了一个一站式轻量级应用开发平台,提供了功能强大的IOC/AOP等功能。同时它可以集成多个优秀框架比如struts,hibernate,mybatis等

2. Spring有哪些优点

  • (1)控制反转(IOC):Spring使用IOC实现了松耦合,可以将所有对象的创建和依赖关系的维护都交给spring去做

  • (2)AOP编程的支持:Spring提供了面向切面编程,可以方便的实现对应用程序进行权限拦截,运行监控等功能

  • (3)声明式事务的支持:只需要通过简单配置就可以完成对事务的管理,而不需要手动编程

  • (4)MVC框架:Spring的web框架就是一个设计优良的web mvc框架,很好地取代了一些web框架

  • (5)降低J2EE开发难度:Spring对j2ee开发中非常难用的一些API(如JDBC,JavaMail,远程调用等)都提供了封装,使得使用这些API的难度大大降低

3. Spring事务的实现方式

  • 编程式事务管理:编程式事务管理允许你在源代码编程的方式下管理事务,虽然很灵活,但是很难维护,spring推荐使用transactionTemplate

  • 声明式事务管理:这种方式建立在AOP上,本质是在方法执行前后进行拦截,在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。这样就不需要在业务逻辑代码中掺杂事务管理的代码,将用户从复杂的事务处理中解脱出来,用户只需要通过注解或者xml配置管理事务。

对比:

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。

声明式事务使得业务代码不受污染。和编程式事务相比,声明式事务的唯一缺点就是它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用在代码级别,但是可以通过事务管理的代码块独立为方法的方式变通处理。

声明式事务管理也有两种常见的方式:一种是基于tx和aop名称空间的xml配置文件,另一种就是基于@Transaction注解方式。显然基于注解的方式更简单易用

4. Spring是如何管理事务的

Spring事务管理主要有3个接口,Spring事务主要由(PlatformTransactionManager、TransactionDefinition、TransactionStatus)三个共同完成

(1)PlatformTransactionManger:事务管理器,主要用于平台相关事务的管理,主要有3个方法

  • commit 事务提交
  • rollback 事务回滚
  • getTransaction 获取事务状态

(2)TransactionDefinition:事务定义信息,用于定义事务相关的属性,给事务管理器PlatformTransactionManager使用,主要有下面4个方法

  • getIsolationLevel 获取事务隔离级别
  • getPropagationBehavior 获取传播行为
  • getTimeout 获取超时时间
  • isReadOnly 是否只读(保存、更新、删除时属性为false--可读写,查询时为true)
    事务管理器能够根据这个返回值进行优化,这些事务的配置信息,都可以通过配置文件进行配置

(3)TransactionStatus:事务具体运行状态。事务管理过程中,每个时间点事务的状态信息,如它的几个方法

  • hasSvepoint 返回这个事务内部是否包含一个保存点
  • isCompleted 返回该事务是否已经完成,也就是说,是否已经提交或者回滚
  • isNewTransaction 判断当前事务是否是一个新事务

5. Spring事务定义的传播行为(Propagation Behavior),干什么用的

听起来挺高端,其实很简单,既然是传播,那么至少有两种东西,才可以发生传播,单体不存在传播这个行为,即传播行为存在于多个事务中

事务传播行为(propagation behavior):当事务方法被另一个事务方法调用时(logService的addLog()被userService的saveUser()调用),这个事务方法该如何进行(事务传播行为加在被调用的事务方法上!!!如logService的addLog())。比如:方法可能在现有事务(另一个事务方法的事务)中运行,也可能开启一个新事务,并在自己开启的新事务中运行

Spring定义了7种事务传播行为(前两种最常用):

  • ROPAGATION_REQUIRED:支持当前事务,当前事务存在(外层方法上存在transaction注解,且为required),方法将会在该事务中运行,否则就新建一个事务,这是最常见的选择
  • PROPAGATION_REQUIRED_NEW:总是开启一个新的事务(即userservice抛出的异常不会让logservice的addLog回滚,addLog会直接提交),如果当前事务已经存在,则将当前事务挂起
  • PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行
  • PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常
  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起
  • PROPAGATION_NEVER:以非实物方式执行,如果当前存在事务,就抛出异常
  • PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作

PROPAGATION_REQUIRED_NEW和PROPAGATION_NESTED区别:
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全commited 或 rolled back 而不依赖于外部事务,它拥有自己的隔离范围, 自己的锁, 等等.当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.PROPAGATION_REQUIRES_NEW常用于日志记录,或者交易失败仍需要留痕、

另一方面, PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint.如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分,
只有外部事务结束后它才会被提交.

6. Spring中用到了哪些设计模式

Spring框架中使用了大量的设计模式,下面列举比较有代表性的:

  • 代理模式:Spring AOP功能的实现
  • 单例模式:Spring中的bean默认都是单例的
  • 模板方法模式:用来解决代码重复的问题。比如:jdbcTemplate、mongoTemplate 等以 template 结尾的类,它们就使用到了模板模式
  • 工厂模式:Spring使用工厂模式通过beanFactory,applicationC ontext创建对象
  • 适配器模式:Spring AOP的增强或通知使用到了适配器模式,Spring MVC也是使用到了HandlerAdapter适配器模式适配controller
  • 装饰器模式:Spring 中用到的包装器模式在类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责
  • 观察者模式:如applicationListener,事件监听,日志监听
  • 委派模式:不属于23种设计模式,但是在spring中非常常用,一般以delegate结尾的类肯定是委派,还有dispatcher结尾或者开头的,dispatcherServlet中的doDispatcher就是委派

7. SpringMVC工作原理

0e547929eda8419ebe9c7023425322e6.png
  1. 用户发送请求到前端控制器dispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器找到具体的处理器,生成处理器对象以及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapater处理器适配器
  5. HandlerAdapter经过适配器调用具体的处理器(controller,也叫后端控制器)
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
  8. DispatcherSerlvet将modelAndView传递给viewResolver视图解析器
  9. ViewResolver解析后返回具体的view
  10. DispatcherServlet根据view渲染视图(即将模型数据填充至视图中)
  11. DispatcherServlet响应用户

以下组件通常使用框架提供实现:


796324ddbbf14ffb8299d485dbded3f6.png

8. Beanfactory和ApplicationContext区别

Beanfactory:是spring最底层的接口,提供了最简单的容器功能,只提供了
实例化对象和获取对象的功能

ApplicationContext:应用上下文,继承自BeanFactory接口,是spring的更高级的容器,提供了更多有用的功能

  • 国际化(MessageSource)
  • 访问资源,如URL和文件(ResourceLoader)
  • 父子容器,如mvc
  • 消息发送,响应机制(ApplicationEventPublisher)

8.1 两者装载bean的区别:

BeanFactory:beanFactory在启动的时候不会去实例化Bean,从容器中拿bean的时候才会去实例化

ApplicationContext:ApplicationContext在启动的时候就把所有的Bean全部实例化了,它还可以为Bean配置lazy-init=true来让Bean延迟实例化

8.2 我们该用BeanFactory还是ApplicationContext

BeanFactory具有延迟实例化的优点:
应用启动的时候占用资源很少,适合对资源要求较高的应用

ApplicationContext不延迟实例化的优点:

  • 所有的bean在启动的时候都加载,系统运行的速度快
  • 在启动的时候所有的Bean都加载了,我们就能在系统启动的时候,尽早的发现系统中的配置问题
  • 建议web应用,在启动的时候把所有的Bean都加载了(将费时的操作放在系统启动中完成)

9. Beanfactory和FactoryBean区别

FactoryBean和BeanFactory虽然长的很像,但是他们的作用确实完全不像。

FactoryBean是一个Bean,这个Bean不是简单的Bean,而是可以生成某一个类型Bean实例的工厂Bean,它最大的一个作用是:可以让我们自定义Bean的创建过程

实现factoryBean,在getObject方法中创建对象,再通过beanFactory.getBean("factoryBean")的名字就可以获取到我们自己创建的对象

BeanFactory是Spring容器中的一个基本类也是很重要的一个类,在BeanFactory中可以创建和管理Spring容器中的Bean,它对于Bean的创建有一个统一的流程

https://blog.csdn.net/zknxx/article/details/79572387

10. 解释Spirng Bean的生命周期

Spring框架中,一旦将一个Bean纳入Spring IOC容器中,这个Bean的声明周期就会交给Spring管理,一般担当管理角色的是BeanFactory或者ApplicationContext,下面以BeanFactory为例,说明一下Bean的声明周期活动,从启动容器开始,然后执行

image.png

如果使用ApplicationContext来维护一个Bean的生命周期,则基本上与上边的流程相同,只不过在执行BeanNameAware的setBeanName()后,若有Bean类实现了org.springframework.context.ApplicationContextAware接口,则执行其setApplicationContext()方法,然后再进行BeanPostProcessors的processBeforeInitialization()

11. Spring如何解决循环依赖的

image.png

让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。

知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决

12. Spring IOC容器初始化过程

IOC容器初始可以分为3个过程

  1. Resouce资源定位:这个Resouce就是指的BeanDefinition的资源定位。BeanDefinition抽象了对bean的定义,比如bean的信息,依赖关系等,这个过程可以想象成寻找bean的过程
  2. BeanDefinition的载入过程:这个载入过程就是把用户定义好的比如,@bean注解的bean表示为IOC容器内部的数据结构,而这个容器内部的数据结构就是BeanDefinition
  3. 第三个过程是向IOC容器注册这些BeanDeinifiton的过程,该过程将前面的BeanDefinition保存到hashmap中

Resource定位
我们一般使用外部资源描述bean对象,所以IOC容器第一步就是需要定位Resouce外部资源,Resource的定位就是BeanDefinition的资源定位,它是由ResouceLoader通过统一的Resouce接口来完成的,这个Resouce对各种形式的BeanDefinition的使用都提供了统一的接口

载入
第二个过程就是BeanDefinition的载入,BeanDefinitionReader读取,解析Resouce定位的资源,也就是将用户定义的Bean表示成IOC容器的内部数据结构也就是BeanDefinition

注册
第三个过程则是注册,即向IOC容器注册这些BeanDefinition,该过程通过BeanDefinitionRegistery接口实现
在IOC容器内部其实是将第二个过程解析得到的BeanDefinition注入到一个HashMap容器中,IOC是通过这个HashMap来维护这些BeanDefinition的
上面提到的过程不包括Bean的依赖注入实现,Bean的载入和依赖注入不是一个过程,是两个独立的过程,依赖注入是发生在应用第一次通过getBean向容器索要Bean时

经过Resource资源定位,BeanDefinition载入,BeanDefinition注册三个步骤,IOC容器的初始化过程就已经完成了

image.png

13. Spring事务什么时候失效

  1. 数据库引擎不支持事务:比如mysql的myisam引擎不支持,只有innodb支持
  2. 没有被spring管理:比如userServiceImpl上面没有加@service注解
  3. 方法不是public的:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式
  4. 自身调用:
    来看两个示例:
@Service
public class OrderServiceImpl implements OrderService {
 
    public void update(Order order) {
        updateOrder(order);
    }
 
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}

update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?

再来看下面这个例子:

@Service
public class OrderServiceImpl implements OrderService {
 
    @Transactional
    public void update(Order order) {
        updateOrder(order);
    }
 
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(Order order) {
        // update order
    }
 
}

这次在 update 方法上加了 @Transactional,updateOrder 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

这两个例子的答案是:不管用!

因为它们发生了自身调用,即实际调用的是该被代理类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了

解决方式:
(1)将update()和updateOrder()方法放在不同的类里
(2)自己注入自己,用注入的实例调用

@Service
public class OrderServiceImpl implements OrderService {
  @Autowired
  private OrderService orderService; 

    public void update(Order order) {
        orderService.updateOrder(order);
    }
 
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}

(3)获取代理类,利用代理类调用自己类的方法

@Service
public class OrderServiceImpl implements OrderService {
    public void update(Order order) {
        ((OrderService)AopContext.currentProxy()).updateOrder(order);
    }
 
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}
  1. 把异常给try catch了
@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        try {
            // update order
        } catch {
 
        }
    }
}

把异常吃了,然后又不抛出来,事务无法回滚!

  1. 异常类型错误
// @Service
public class OrderServiceImpl implements OrderService {
 
    @Transactional
    public void updateOrder(Order order) {
        try {
            // update order
        } catch {
            throw new Exception("更新错误");
        }
    }
}

这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:@Transactional(rollbackFor = Exception.class)

  1. 数据源没有配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

如上面所示,当前数据源若没有配置事务管理器,那也是白搭!

Redis

1. Redis为什么这么快

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换开销
  • 合理高效的数据结构
  • 采用了非阻塞I/O多路复用机制

2. Redis常用的数据结构以及使用场景

  1. String字符串:Redis最基础的数据结构,其他几种数据结构都是在字符串基础上构建的。
    使用场景:缓存,计数,共享session,分布式锁等
  2. List列表(双向链表):链表,队列,微博关注人时间轴列表等
  3. Hash哈希:键值对结构,存放用户信息等
  4. Set:可以保存多个字符串元素,但是不允许有重复,并且集合中元素无需,无法通过下标获取元素,利用Set的特点以及交集、并集、差集等操作,可以实现去重、共同好友,推荐好友,安全提示功能(https://blog.csdn.net/a7442358/article/details/102621269)
  5. ZSet:访问量排行榜,点击量排行榜等

3. Redis的数据过期策略

Redis 中数据过期策略采用定期删除 + 惰性删除策略

  • 定期删除策略:Redis 启用一个定时器,每隔一段时间遍历内存中所有的数据,判断key是否过期,过期的话就删除。这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有key,非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用
  • 惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间

这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不再是每次扫描全部的 key 了,而是定时随机获取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求。但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制,当内存不够用时,内存淘汰机制就会上场

4. Redis的内存淘汰策略

LRU(最近最少使用:Least Recently Used)
内存淘汰策略基本分为不驱逐,所有键空间和设置了过期时间的键空间
1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)
2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(LRU推荐使用) *********************
3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key,基本没人用。
4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用(因此不太适合)。
5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。
6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。

5. Redis的缓存穿透,缓存击穿,缓存雪崩问题和解决方案

缓存穿透:访问一个本来不存在的数据,肯定在缓存中找不到,这将导致每次请求这个数据都会从数据库查询,可能导致数据库挂掉

  1. 在接口做校验
  2. 在数据库中查询返回为空,但是仍然将这个空结果缓存,但是缓存的时间较短
  3. 布隆过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。

缓存击穿:某个key在刚过期的时候,大量访问该key的请求过来,查询缓存发现没有,大量请求就走了数据库,可能导致数据库挂掉

  1. 使用互斥锁,在缓存中没有获取到结果的时候,不直接从mysql取,而是尝试加互斥锁mutex lock,如果加成功,就去数据库读取结果, 并将结果写回到缓存,让互斥锁key过期,没加成功的,就等待例如50ms,然后再次尝试去缓存中获取该数据 (通用做法)
  2. 设置key永不过期

缓存雪崩:多个key在保存的时候设置了相同的过期时间,导致同一时间有大量key同时过期,请求全部转发到数据库,使数据库瞬时压力过大奔溃

  1. 将缓存的过期时间分散开。如在原有的过期时间的基础上,添加一个随机值,比如1~5分钟随机,这样每个key的过期时间重复的几率就会减少,就很难引发集体失效的情况

6. Redis的持久化机制

Redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的持久化策略有两种:

RDB: redis database 生成的文件名:dump.rdb,redis默认的持久化方式
定义:当符合一定条件的时候,redis会自动将内存中的所有数据进行快照然后存储到磁盘上。就像拍照一样,将这一瞬间所有东西保存下来,redis再次启动的时候,会读取rdb快照文件,将数据从磁盘加载到内存,1GB快照文件加载内存时间大概为20~30s
过程:当条件满足,redis需要执行RDB的时候,服务器会执行如下操作
1. redis调用系统函数fork,派生出一个子进程进行持久化,用fork目的是复制一个与当前进程一样的进程(变量,环境变量,程序计数器)都和原进程一直,但是是新的进程,并作为原进程的子进程
2. 子进程将这一时间的所有数据存储到一个临时RDB文件(持久化就是将内存中数据写入临时文件)
3. 子进程完成临时RDB文件的写入以后,redis就用这个新的临时RDB文件替换原来的RDB文件,并删除旧的RDB文件
Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的

Redis的AOF和RDB会不会造成阻塞
Redis持久化之RDB原理

优点:  
    1. 生成RDB文件的时候,redis主进程会fork一个子进程进行所有保存工作,主进程不需要进行任何磁盘IO操作
    2. 在恢复大数据集的时候速度比AOF快  
缺点:  
    1. 每次会单独创建一个子进程进行持久化,在fork一个子进程时,内存的数据也被复制了,即占用内存大小是原来的两倍
    2. 因为快照是每隔一段时间作一次的,因此如果redis意外down掉,则会丢失最后一次快照后的所有更改  

AOF: Append only File 生成的文件名:appendonly.aof
出现是为了弥补RDB数据不一致性的问题
采用日志的形式记录每个 写操作 ,并追加到文件中,重启的时候会根据日志文件内容将写指令从前到后执行一遍

优点:  
    1. 因为每次操作都会被记录,因此即使发生故障停机,也最多只会丢失一秒钟的数据
    2. redis在AOF文件体积很大的时候,会执行aof重写,以降低数据大小  
缺点:  
    1. 对于相同的数据集来说,AOF文件的体积大于RDB,因为RDB只记录操作结果,AOF记录的每次操作过程
    2. 恢复速度更慢,因为RDB只需要读入,AOF需要先读再写入

7. Redis和memcached的区别

  1. 存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性
  2. 数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,而redis支持五种数据类型
  3. 用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
  4. value的大小:redis可以达到1GB,而memcache只有1MB

8. Redis的主从复制、哨兵、RedisCluster

主从复制
原理:从服务器向主服务发送SYNC命令,主服务器收到SYNC命令以后,开始执行bgsave命令生成RDB快照文件,并使用缓冲池记录此后执行的所有写命令。
当RDB快照文件生成以后,主服务器向所有从服务器放RDB快照文件,并继续记录发送过程中的写命令。从服务器收到RDB文件后,加载该文件然后执行来自主服务器的缓冲区的写命令(从服务器初始化完成)。此后,主服务器每执行一次写请求,就会向从服务器发送相同的命令,从服务器接收并执行该命令(从服务器初始化完成后的操作)
优点

  1. 读写分离:从节点可以提升主节点的读能力,有效应对大并发读的场景。默认情况,master节点可读可写,slave只可读,slave可以是多个
  2. 主节点同步从节点的时候,是非阻塞的,不会影响用户的写入和读取,从节点也是非阻塞的完成数据同步,如果这时候有读取的请求,则会返回同步之前的数据

缺点

  1. 主机宕机会导致写入失败,需要等待重启或者人工干预,手动设置其他某个从服务器为新的主服务器,会造人一段时间内服务的不可用
  2. 主机宕机了,有部分数据未同步到从服务器,某个从服务器设置为新的主服务器以后出现数据不一致的问题
  3. 集群容量一旦到达上限,在线扩容就十分麻烦

哨兵模式
Redis Sentinel
主从复制模式有很多优点,但是有个缺点就是在主服务器down掉以后,需要由人工干预去确定新的从服务器为主服务器,比较麻烦。因此redis在2.8版本引入了稳定版本的redis哨兵机制(2.6版本引入了但是不稳定),由该机制来实现从服务器自动转换为主服务器的自动化的故障迁移Automatic failover

假设当前有1主服务器,2个从服务器,3个sentinel,sentinel之间互相监控,sentinel还监控master,slave,这样构成6+9=15条监控线

流程
1.每个sentinel进程会每隔一段时间(如1秒)向其他sentinel进程,master,slave服务器发送消息以确定如果对方还活着

  1. 如果对方在规定的时间(down-after-milliseconds配置的时间)没有答复,则该sentinel就会标记该实例为主观下线(subjective down)SDOWN
  2. 如果一个master服务器被标记为SDOWN主观下线状态,不会立即将某个从服务器设置为主服务器,而是由把他标记为下线状态的sentinel进程通过向其他sentinel间歇性(一秒)发送" is-master-down-by-addr "指令并获取响应信息来等待足够数量的sentinel进程在指定的时间内认定master主观下线,则该master主服务器会被标记为客观下线(Objective down)ODOWN
  3. 所有的sentinel进程进行投票选举,选举出一个sentinel leader,并由该sentinel leader来进行这一次的automatic failover
  4. 该sentinel leader通过诸如过滤故障主机,挑选slave优先级最低的服务器/寻找偏移量最大的节点/run id最小的从服务器成为新的master服务器
  5. master和slave服务器切换以后,master和slave的redis.conf,sentinel.conf都会发生相应的改变

注意:“客观下线”条件只适用于主服务器:对于任何其他类型的 Redis 实例, Sentinel(哨兵)进程在将它们判断为下线前不需要进行协商,所以Slave从服务器或者其他 Sentinel(哨兵)进程永远不会达到“客观下线”条件

优点:拥有主从复制的所有优点,同时还实现了atomatic failover自动故障迁移
缺点:1.集群容量一旦到达上限,在线扩容就十分麻烦 2.实现哨兵模式较为复杂

Redis Cluster
Redis3.0以后引入,官方推荐模式
原理:所有redis节点彼此相互连接,内部使用二进制协议提高通信速度,节点的失效是通过集群中超过半数的节点检测到其失效才算失效,redis-cluster将所有的网络节点映射到0~16383共16384个卡槽slot上,当需要在redis集群中放置一个key的时候,采用CRC16(key)求出的结果对16384取模,以圈定范围,将key放在某个slot槽中

流程:
(1)如果有3个redis节点A,B,C分布在3台不同服务器上或者同一台服务器的3个端口上
(2)节点A负责0 - 5460卡槽,节点B负责5461 - 10922卡槽,节点C负责10923 ~ 16383卡槽
(3)客户端连接node1,发送get某个key的请求,则node1同样会对这个key进行一次CRC16(key)然后取模操作
(4)节点A判断该取模后的结果是否位于节点A管理的卡槽范围内,在则直接获取返回,否则内部跳转到实际管理该卡槽的节点

优点:
(1)采用无中心架构,数据按照slot分布在多个节点上
(2)集群中每个节点都是平等的关系,节点间数据共享,每个节点都保存了各自的数据和集群的状态
(3)可扩展性良好,可线性扩展到1000多个节点,节点可冬天添加或删除

缺点:
(1)客户端实现复杂
(2)节点可能会因为某些原因发生阻塞(阻塞时间大于cluster-node-timeout)而被判断下线,这种failover是没必要的
(3)批量操作限制,目前只支持具有相同slot值的key执行批量操作
(4)key事务操作受限,只支持多key在同一节点事务操作,不支持多key分布在不同节点的事务操作

9. Redis的zset底层数据结构

https://www.cnblogs.com/tong-yuan/p/skiplist.html
有序链表自身不能使用二分查找,数组可以,我们需要实现有序链表的二分查找,用到了跳表
如链表元素1,3,6,7,11,我们可以将1,6提取出来创建成一级索引,效率会快很多,我们还可以在一级索引基础上提取出来二级索引
zset底层数据结构是跳表skipList,是链表加多级索引的结构,就叫做跳表。跳表本质上是一种可以进行二分查找的有序链表。跳表在原有的有序链表的基础上添加了多级索引,通过索引实现快速查找,跳表不仅能提供搜索性能,还可以提高插入和删除的性能

多线程

1. 什么是CAS操作以及ABA问题

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。如 Intel 处理器,比较并交换通过指令的 cmpxchg 系列实现

CAS最底层使用如下指令实现
ock cmpxchg指令 lock compex change
cmpxchg是cpu的汇编指令,不是原子的。即拿到0改为1往回比较原来的是否为0的过程是2步,第一步比较是否依然为0,第二步如果依然为0就改成1。这两步骤之间可能依然被人打断,如在判断为0以后,第二步之前被别人改成2,你再判断就不相等了
前面之所以加lock指令意思就是当cpu对指定的内存进行cas操作的时候,不允许任何其他cpu打断我。
多颗cpu下,某个cpu访问内存中的某块区域进行cas操作的时候,直接会将内存总线锁住,其他cpu一定访问不了这块内存。单CPU的话是不用加lock的,肯定是打断不了自己的

ABA问题:
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性

2. Synchronized和Lock的区别

  1. 首先synchronized是java内置关键字在jvm层面,Lock是个java类
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁
  3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
  4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了
  5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可
  6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

3. AQS简介

AQS:全称AbstractQueueSynchronizer,抽象队列同步器,这个类在java并发包下,它是一个底层同步工具类,比如CountDownLatch,SammphoreReentrantLock,ReentrantReadWriteLock等等都是基于AQS

AQS内部有3个属性,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列

AQS是自旋锁,在等待唤醒的时候,经常会使用自旋的方式,不停地尝试获取锁,直到被其他线程获取成功

AQS有两个队列,同步队列和条件队列。同步队列依赖一个双向链表来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中。通过signal或signalAll将条件队列中的节点转移到同步队列
其他过程参见本博客AQS文

4. 为什么要使用线程池

  1. 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下。

5. Executors提供了几种线程池

  1. newFixedThreadPool:生成一个固定大小的线程池,最大线程数与核心线程数相等,keepAliveTime设置为0(因为这里没用,即使不为0,线程池也不会回收corePoolSize内的线程),任务队列使用LinkedBlockingQueue无界队列
  2. newSingleThreadExecutor:生成一个线程的固定线程池,这个和上面的一样,只要设置线程数=1即可,
  3. newCachedThreadPool:创建一个可缓存的(核心线程数为0,最大线程数为Integer.MAX_VALUE,keepAliveTime为60秒,任务队列采用SynchronousQueue[ˈsɪŋkrənəs])的线程池,如果线程池的大小超过了处理任务所需要的线程,则会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以添加新线程来处理任务。并且因为核心线程数为0,因此,在提交任务的时候,会直接提交到队列中
    附录:SynchronousQueue作为阻塞队列的时候,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止,反之亦然。在SynchronousQueue内部没有任何存放元素的能力。所以类似peek操作或者迭代器操作也是无效的,元素只能通过put类操作或者take类操作才有效。通常队列的第一个元素是当前第一个等待的线程。如果没有线程阻塞在该队列则poll会返回null。从Collection的视角来看SynchronousQueue表现为一个空的集合。
  4. newScheduledThreadPool:创建一个定长的线程池,支持定时以及周期性的任务执行,使用延时队列。https://blog.csdn.net/liuchangjie0112/article/details/90698401

6. 核心线程池ThreadPoolExecutor内部参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
  1. corePoolSize:指定了线程池中的线程数量
  2. maximumPoolSize:指定了线程池中的最大线程数量
  3. keepAliveTime:线程池维护线程所允许的空闲时间
  4. unit: keepAliveTime 的单位。
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  7. handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。

corePoolSize 到 maximumPoolSize 之间的线程会被回收,当然 corePoolSize 的线程也可以通过设置而得到回收(allowCoreThreadTimeOut(true))

7. 说说线程池中的线程创建时机

  1. 如果当前线程池线程数少于核心线程数,那么在提交任务的时候会新创建一个线程,并由该线程来执行任务
  2. 如果当前线程数已经到达核心线程数,那么就将添加的任务放入队列中,等待线程池中的线程从队列中获取任务
  3. 如果队列已满,那么创建新的线程来执行任务,需要保证线程池中的线程数不能超过最大线程数,如果超过了最大线程数,就会执行拒绝策略

8. 线程池任务执行过程中发生异常怎么处理

如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它

9. 线程池的拒绝策略

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃等待队列中最早的任务,然后将这个新任务添加进去
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

10. 线程池的线程数量怎么确定

  1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1
  2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1
  3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

11. 线程池都有哪几种工作队列

  1. ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择
  2. LinkedBlockingQueue:底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列
  3. SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式
  4. PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值

举例 ArrayBlockingQueue 实现并发同步的原理:原理就是读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程

MQ

1.Kafka为什么这么快

所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。对 Linux 操作系统而言,零拷贝技术依赖于底层的 sendfile() 方法实现。对应于 Java 语言,FileChannal.transferTo() 方法的底层实现就是 sendfile() 方法。

所谓零拷贝,就是减少上下文切换,不经由用户空间,直接在内核空间把数据发往网卡接口。

操作系统中文件读取的正常发送状态
操作系统底层提供的sendfile(transferTo()函数),零拷贝:

JDK

“+”运算符进行字符串连接和 StringBuffer/StringBuilder对象的 append 方法连接字符串的区别

虽然在代码中使用了"+"拼接字符串,但系统在编译时仍然将"+"转换成StringBuilder。因此,我们可以得出结论,在 Java 中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder

如果从运行结果来看,那么”+"和 StringBuilder 是完全等效的。但如果从运行效率和资源消耗方面看,那它们将存在很大的区别。如使用循环来连接字符串,每执行一次循环,就会创建一个 StringBuilder 对象虽然 Java 有垃圾回收器,但这个回收器的工作时间是不定的。如果不断产生这样的垃圾,那么仍然会占用大量的资源。

解决这个问题的方法就是在程序中直接使用 StringBuilder 来连接字符串,创建 StringBuilder 的代码被放在了 for 语句外。虽然这样处理在源程序中看起来复杂,但却换来了更高的效率,同时消耗的资源也更少了。

在使用 StringBuilder 时要注意,尽量不要"+"和 StringBuilder 混着用,否则会创建更多的 StringBuilder 对象

注意:jdk1.4无stringbuilder的时候,运算符+拼接字符串采用的是stringbuffer

“+”运算符进行字符串连接和 StringBuffer/StringBuilder对象的 append 方法连接字符串的区别
Java 中 String 与 StringBuffer 和 StringBuilder 的区别

你可能感兴趣的:(一些题目)