面试实战

xx-job 分片

  1. 单机多任务:自定义业务规则,配置多个xxl任务,每个任务指定不同的参数,但使用相同的jobhanlder:
  2. 多机分片:配置多个机器地址,采用多机器取模的方式,来为不同的机器指定各自服务,路由策略选择:分片广播
    分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍

StringUtils类中isEmpty与isBlank的区别:

  1. StringUtils.isEmpty(String str) 判断某字符串是否为空,为空的标准是 str==null 或 str.length()==0
  2. StringUtils.isBlank(String str) 判断某字符串是否为空或长度为 0 或由空白符 (whitespace) 构成

arraylist vector 区别:

Vector线程安全的,而ArrayList不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。

redis处理大批量的数据: redis分片

Redis的分片(Sharding或者Partitioning)技术是指将数据分散到多个Redis实例中的方法,分片之后,每个redis拥有一部分原数据集的子集。在数据量非常大时,这种技术能够将数据量分散到若干主机的redis实例上,进而减轻单台redis实例的压力。分片技术能够以更易扩展的方式使用多台计算机的存储能力(这里主要指内存的存储能力)和计算能力:

redis利用集群的方式+槽位完成,分片的数据的定位和管理维护。

1.客户端分片:启动多个redis数据库节点,由客户端决定每个键交由哪个节点存储,下次客户端读取该键时直接到该节点读取。这样可以实现将整个数据分布存储在N个数据库节点中,每个节点只存放总数据量的1/N
2.通过代理服务器实现数据分片
客户端直接与代理联系,代理计算集群节点信息,并把请求发送到对应的集群节点。降低了客户端的复杂度,需要proxy收集集群节点信息。

预分片技术:具体来说是在节点部署初期,就提前考虑日后的存储规模,建立足够多的实例(如128个节点),初期时数据很少,所以每个节点存储的数据也非常少。由于节点轻量的特性,数据之外的内存开销并不大,这使得只需要很少的服务器即可运行这些实例。

通过一致性哈希分片算法(treeMap)来实现数据分片
增加虚拟节点

为什么要重写hashcode和equals方法?什么时候要重写hashcode和equals方法?

使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()。

一个对象有好多个字段,当我们需要比较这个对象里面的其中几个字段条件是否一致,从而判断对象是否一致的时候就需要重写

Object的equals方法在没有重写的情况下,其源码是使用"=="进行比较两对象的,比较的是地址值;
string 里面重写了 hashcode和equals方法,所以equal比较的是两对象的内容是否一致

自己动手写一个注解的实现

通过aop切面拦截判断某个方法上是否有自己定义的注解

@Target(value= {ElementType.TYPE, ElementType.METHOD})
@Retention(value= RetentionPolicy.RUNTIME)
public @interface Log {

    /** 方法名 */
    AspectEnum methodName() default AspectEnum.insert;

    /** 操作名称 */
    String operateName();
}
@Slf4j
@Aspect
@Component
public class LogAspect {

    @Autowired
    private RedisClient redisClient;
    @Reference
    private GlobalUsersServiceApi globalUsersServiceApi;
    @Reference
    private GlobalOperateLogServiceApi globalOperateLogServiceApi;

    @Pointcut("@annotation(com.nuonuo.nnjf.annotation.Log)")
    public void methodPointcut() {
    }

    @Before("methodPointcut()")
    public void before(JoinPoint joinPoint) {
        try{
            //从获取RequestAttributes中获取HttpServletRequest的信息
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = (HttpServletRequest) requestAttributes.
                    resolveReference(RequestAttributes.REFERENCE_REQUEST);

            //获取存入缓存的用户信息
            String userId = (String) redisClient
                    .get(String.format("%s:%s", CookieUtil.getUserCookieValue(request), RedisConstants.GLOBAL_SESSION_KEY));
            GlobalUsers globalUser = globalUsersServiceApi.getGlobalUser(userId);

            //基于formData方式提交
            String requestParam = getRequestParam(request.getParameterMap());
            if(StringUtils.isBlank(requestParam)){
                //基于json方式提交
                requestParam = JsonUtil.toFastJsonString(joinPoint.getArgs());
            }

            GlobalOperateLog globalOperateLog = new GlobalOperateLog()
                    .setId(UuidUtils.getOptimizedUUID()).setUserId(userId)
                    .setCreateTime(new Date()).setRemoteHost(request.getRemoteHost())
                    .setRemoteAddr(AddressUtils.getIpAddr(request)).setOperateName(globalUser.getCRealname())
                    .setOperateType(globalUser.getCUsertype());

            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Log annotation = method.getAnnotation(Log.class);
            String operateName = annotation.operateName();
            StringBuilder stringBuilder = new StringBuilder().append("用户(")
                    .append(globalUser.getCUsername()).append(")正在通过条件为:")
                    .append(requestParam).append(operateName);
            String action = stringBuilder.toString();
            globalOperateLog.setContent(action);
            globalOperateLogServiceApi.saveGlobalOperateLog(globalOperateLog);
        }catch (Exception e){
            log.error("aop add log error,{}", e.getMessage());
        }
    }


    /**
     * @param map 获取请求参数
     * @return String
     */
    private String getRequestParam(Map<String, String[]> map) {
        JSONObject jsonObject = new JSONObject();
        if(null != map && map.size() == 0){
            return null;
        }
        for (String key : map.keySet()) {
            jsonObject.put(key, map.get(key));
        }
        return jsonObject.toJSONString();
    }
}

redis和数据库的一致性

先删除缓存,然后改数据库,然后重新设置缓存

对,有并发的情况下要加锁,然后面试官给我了个不加锁的,叫延时删除的一个方法

就是设置完数据库后过个几百毫秒再删除缓存

索引b+数的底层原理

1.中间节点每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

jmm内存模型和怎么工作的

Java 内存模型(JMM)是JAVA虚拟机规范定义的一组规则或规范。它试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

它由主内存和工作内存组成:
所有的变量都 存储在主内存中,每个线程还有自己的工作内存
线程间通信必须要经过主内存。
线程之间如何通信、同步。java并发采用的是共享内存模型

缓存一致性问题:读写时要根据协议来进行操作,这类协议有MSI、MESI、MOSI、Synapse、Firefly及DragonProtocol,等等:
指令重排序问题:volatile

原子性、可见性、有序性、happen-before规则

JMM内存模型三大特性

1、原子性
使用 synchronized 互斥锁来保证操作的原子性
2、可见性
volatile,会强制将该变量自己和当时其他变量的状态都刷出缓存。
synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
3、有序性
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 ->最终执行的命令

重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

简述 Java 的 happen before 原则

如果两个操作之间具有happen-before关系,那么前一个操作的结果就会对后面的一个操作可见。

1.程序的顺序性规则
2.volatile规则
3.传递性规则
4.锁规则
5.线程启动规则
6.线程中断规则
7.线程终结规则

实现一个阻塞队列

数组+重入锁+条件队列

    static class BlockQueue<T> {
        private Object queue[];
        private int front;
        private int rear;
        private int maxSize;

        final private Lock lock = new ReentrantLock();
        Condition full = lock.newCondition();
        Condition empty = lock.newCondition();

        public BlockQueue(int maxSize) {
            this.front = 0;
            this.rear = 0;
            this.maxSize = maxSize;
            this.queue = new Object[maxSize];
        }
         /**
         * 阻塞 入队方法在这
         * @param element
         */
         
        /**
         * 阻塞出队方法在这
         */
    }

入队方法


        /**
         * 阻塞 入队
         * @param element
         */
        public void put(T element) throws InterruptedException {
            lock.lock();
            try{
                while ( (rear + 1) % maxSize == front ) {
                    System.out.println("Queue is full");
                    full.await();
                }
                queue[rear] = element;
                rear = (rear + 1) % maxSize;
                empty.signal();
            } finally {
                lock.unlock();
            }
        }

出队方法

       /**
         * 阻塞出队
         */
        public T take() throws InterruptedException{
            lock.lock();
            try{
                while( rear == front ){
                    System.out.println("Queue is empty");
                    empty.await();
                }
                Object element = queue[front];
                queue[front] = null;
                front = (front+1)%maxSize;
                full.signal();
                return (T) element;
            }finally {
                lock.unlock();
            }
        }

二叉树深度优先遍历和广度优先遍历

深度优先搜索二叉树是先访问根结点,然后遍历左子树接着是遍历右子树,因此我们可以利用堆栈的先进后出的特点,
现将右子树压栈,再将左子树压栈,这样左子树就位于栈顶,可以保证结点的左子树先与右子树被遍历。

广度优先搜索(Breadth First Search),又叫宽度优先搜索或横向优先搜索,是从根结点开始沿着树的宽度搜索遍历,上面二叉树的遍历顺序为:ABCDEFG.
可以利用队列实现广度优先搜索。

dubbo超时是消费端还是服务端?如何解决

dubbo的超时是针对消费端的,由于是一种NIO模式,消费端发起请求后得到一个ResponseFuture,然后消费端一直轮询这个ResponseFuture直至超时或者收到服务端的返回结果

超时在哪设置?
消费端
1.全局控制
2.接口控制
3.方法控制
服务端
1.全局控制
2.接口控制
3.方法控制

超时的实现原理是什么?

dubbo默认采用了netty做为网络组件,它属于一种NIO的模式。消费端发起远程请求后,线程不会阻塞等待服务端的返回,而是马上得到一个ResponseFuture,消费端通过不断的轮询机制判断结果是否有返回。因为是通过轮询,轮询有个需要特别注要的就是避免死循环,所以为了解决这个问题就引入了超时机制,只在一定时间范围内做轮询,如果超时时间就返回超时异常。

超时解决的是什么问题?

对调用的服务设置超时时间,是为了避免因为某种原因导致线程被长时间占用,最终出现线程池用完返回拒绝服务的异常

Dubbo的服务请求失败怎么处理

dubbo启动时默认有重试机制和超时机制。
超时机制的规则是如果在一定的时间内,provider没有返回,则认为本次调用失败,
重试机制在出现调用失败时,会再次调用。如果在配置的调用次数内都失败,则认为此次请求异常,抛出异常。

Dubbo重连机制会不会造成错误

dubbo在调用服务不成功时,默认会重试2次。
Dubbo的路由机制,会把超时的请求路由到其他机器上,而不是本机尝试,所以 dubbo的重试机器也能一定程度的保证服务的质量。
但是如果不合理的配置重试次数,当失败时会进行重试多次,这样在某个时间点出现性能问题,调用方再连续重复调用,
系统请求变为正常值的retries倍,系统压力会大增,容易引起服务雪崩,需要根据业务情况规划好如何进行异常处理,何时进行重试。

dubbo的SPI是如何理解

SPI 服务发现机制,本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。
配置文件的路径在META-INF/dubbo路径下

dubbo在原有的spi基础上主要有以下的改变,
①配置文件采用键值对配置的方式,使用起来更加灵活和简单
②增强了原本SPI的功能,使得SPI具备ioc和aop的功能,

redis脑裂怎么处理

修改redis.conf文件中的参数

min-replicas-to-write 3
min-replicas-max-lag 10

第一个参数表示 slave 连接到master的最少数量
第二个参数表示 slave连接到master的最大延迟时间

配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
redis中的异步复制情况下的数据丢失问题也能使用这两个参数

ThreadLocal 本地使用

ThreadLocal 线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。

获取线程的当前时间:线程追踪链

    private static ThreadLocal<SimpleDateFormat> sdfLocal = new ThreadLocal<SimpleDateFormat>();
    
      private static String currentTimeStr() {
        Timestamp ts = new Timestamp(System.currentTimeMillis());
        SimpleDateFormat sdf = getSimpleDateFormat(null);
        return sdf.format(ts);
    }


    private static SimpleDateFormat getSimpleDateFormat(TimeZone tz) {
        SimpleDateFormat sdf = sdfLocal.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            if (tz != null) {
                sdf.setTimeZone(tz);
            }
            sdfLocal.set(sdf);
        }
        return sdf;
    }

zk节点宕机如何处理

zk节点宕机:

  1. 如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数
    据是有多个副本的,数据并不会丢失;
  2. 如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。

redis节点宕机如何处理

redis节点宕机:Redis提供了哨兵机制通过sentinel模式启动redis后,自动监控master/slave的运行状态,基本原理是:心跳机制+投票裁决。

分布式集群下如何做到唯一序列号

1、利用数据库递增,全数据库唯一。
2、雪花算法(推特、百度、美团)
3、号段模式(滴滴TinyId实现)

如何做一个分布式锁

1)基于数据库实现分布式锁
2)基于缓存redis实现分布式锁 (设置超时时间、redission、 set 、redlock 、watch dog)
3)基于Zookeeper实现分布式锁(临时顺序节点)

dubbo是哪个版本的 dubbo原理 dubbo为什么用二进制

2.7.4.1
0. 服务容器负责启动,加载,运行服务提供者。
1.服务提供者(生产者)在启动时,向注册中心注册自己提供的服务。
2.服务消费者在启动时,向注册中心订阅自己所需的服务。
3.注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
4.服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
5.服务消费者和提供者,在内存中累计调用次数和调用时间,(异步通知)定时每分钟发送一次统计数据到监控中心

为了 序列化,便于网络之间的传输

Dubbo提供了4种负载均衡机制:

1.权重随机算法:RandomLoadBalance
2.加权轮询算法:RoundRobinLoadBalance
3.一致性哈希算法:ConsistentHashLoadBalance
4.最少活跃调用数算法:LeastActiveLoadBalance

dubbo支持的通信协议

①dubbo://
②hessian://
③rmi://
④http://
⑤webservice://
⑥thrift://
⑦memcached://
⑧redis://

Java创建普通对象的流程

Java普通对象的创建:
1)new指令
2)分配内存
3)初始化
4)对象的初始设置
5) 初始化 方法

生命周期:加载-验证-准备-解析-初始化-使用-卸载

对象怎么样才会到老年代中

1.大对象直接进入老年代
2.长期存活的对象进入老年代
3.动态年龄判断并进入老年代
4. 在一次安全Minor GC 中,仍然存活的对象不能在另一个Survivor 完全容纳,则会通过担保机制进入老年代。

hashmap为什么会在1.8加上红黑树,有什么好处

1.加快检索速度 lng (O)

synchronized 关键字底层是如何实现的?它与 Lock 相比优缺点分别是什么?

synchronized 是通过竞争 monitor(监视器锁) 来实现了 ,monitorenter、monitorexit
1.synchronized 用完不需要手动解锁,Lock需要unlock
2.synchronized 、lock默认都是非公平锁,lock可以手动改成公平锁
3.lock可以指定唤醒哪些线程,synchronized 只能随机或者全部唤醒线程
4.lock是个接口,synchronized是关键字

synchronized和ReentrantLock的区别

1.synchronized 用完不需要手动解锁,ReentrantLock需要unlock
2.公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以手动改成公平锁
3.绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象
4. ReentrantLock 只能修饰代码块,而 synchronized 可以用于修饰方法、修饰代码块等;
5. ReentrantLock 可以知道是否成功获得了锁,而 synchronized 却不行。

ReentrantLock原理?

ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。

同步代码块synchronized与volatile的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
  2. synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  3. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  4. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  5. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  6. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

redis持久化(RDB、AOF)

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。
①、优势
(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。。
(2)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
②、劣势
快照持久化期间修改的数据不会被保存,可能丢失数据。

AOF工作机制,redis将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。
4、优点
(1)AOF可以更好的保护数据不丢失
(2)适合做灾难性的误删除的紧急恢复。
5、缺点
对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大,恢复速度满

redis缓存雪崩,缓存穿透

缓存雪崩: 缓存雪崩是指 设置缓存时采用了相同的过期时间,导致缓存在某一个时刻同时失效, 或者缓存服务器宕机宕机导致缓存全面失效,请求全部转发到了DB层面,DB由于瞬间压力增大而导致崩溃。缓存失效导致的雪崩效应对底层系统的冲击是很大的。
解决方式:

  1. 对缓存的访问,如果发现从缓存中取不到值,那么通过加锁(互斥锁)或者队列的方式保证缓存的单进程操作,从而避免失效时并并发请求全部落到底层的存储系统上;但是这种方式会带来性能上的损耗
  2. 将缓存失效的时间分散,降低每一个缓存过期时间的重复率
  3. 如果是因为缓存服务器故障导致的问题,一方面需要保证缓存服务器的高可用、另一方面,应用程序中可以采用多级缓存
  4. 服务降级

缓存穿透: 缓存穿透是指缓存和数据库中都没有的数据
缓存击穿(缓存雪崩): 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期)
缓存穿透:
  缓存穿透是指查询一个根本不存在的数据,缓存和数据源都不会命中。
解决方式

  1. 如果查询数据库也为空,直接设置一个默认值null存放到缓存
  2. 根据缓存数据Key的设计规则,将不符合规则的key进行过滤采用布隆过滤器, 将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。

zookeeper跟Eureka有什么区别

zookeeper 是cp, Eureka 是ap

分布式的CAP原则

c:一致性
a:可用性
p:分区容错性

设计场景使用rabbitMq解决,对比与其他mq的不同

1.异步处理
2.流量削峰
3.应用解耦

支持持久化,可靠性、灵活的路由、集群、事务、高可用的队列

消息中间件如何解决消息丢失问题(MQ系统的数据如何保证不丢失)(保障可靠性)

RabbitMQ :
1)生产者弄丢了数据
开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

2)RabbitMQ 弄丢了数据
开启RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。

持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。

3)消费端弄丢了数据
关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把

面试实战_第1张图片

消息幂等性(重复消费问题)

基于数据库的乐观锁(传递版本号,修改的时候进行比对)

rabbitMq集群

镜像队列(haproxy 负载均衡)

消费者限流

1.listener-container配置prefetch属性设置消费端一次拉取多少信息
2.消费者开启手动确认ack,
消费者每次从mq拉来一条信息,手动确认消费处理完后,才开始拉下一条

消息什么情况下会变成死信(死信队列)

1.队列消息长度达到限制
2.消费者拒收消息,并且不把消息重新放入原目标队列
3.原队列存在过期设置,消息达到超时时间未被消费

延迟队列

消息进入队列后,不会立即被消费,只有达到指定时间后,才会被消费。 (TTL+死信队列)

rabbitMq所有组件,rabbitMq的工作模式,rabbitMq基于什么语言开发

Broker:它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输,
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息的载体,每个消息都会被投到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来.
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离。
Producer:消息生产者,就是投递消息的程序.
Consumer:消息消费者,就是接受消息的程序.
Channel:消息通道,在客户端的每个连接里,可建立多个channel.

1.Work模式
2.订阅模式
3.路由模式
4.通配符模式
5.RPC模式

Erlang

rabbitMq信息传递路径

Producer——》Broker——》Exchange——》Queue——》Consumer

rabbitMq消息的可靠投递*

(生产者端)
1.confirm确认模式, Producer——》Exchange,返回一个confirmCallback
2.return 退回模式,Exchange——》Queue失败返回一个returnCallback
(消费者端)
ACK :自动确认、手动确认、根据异常情况确认
nack拒绝签收的时候

聊一下Virtual Hosts

虚拟主机
虚拟主机有一个名字。当客户端连接到RabbitMQ的时候,客户端指定一个虚拟主机的名字来连接到它。如果认证成功,并且用户有权限访问该虚拟主机,则连接建立成功。连接到虚拟主机以后,可以操作这个虚拟主机下的exchanges, queues, bindings等。

虚拟主机的限制有两个:最大连接数和最大队列数。

Mq解决重复消费

消费端处理消息的业务逻辑保持幂等性。
1.做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
2.做更新操作,在业务端加一张表,用来存放消息是否执行成功,消费端收到业务端的表记录已经处理消息的 id,每次一个消息进来之前先判断该消息是否执行过,如果执行过就放弃,如果没有执行就开始执行消息,消息执行完成之后存入这个消息的 id
3.以redis为例,给消息分配一个全局id,只要消费过该消息,将以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

Redis默认端口号

6379

B树和B+树的区别,B+树叶子结点是双向链表吗

B树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点;
B-树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键字范围的子结点;
所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;

双向链表

类加载过程,类加载器,双亲委派机制,为什么双亲委派

加载、验证、准备、解析、验证、初始化

从上到下,分为启动类加载器、扩展类加载器、应用程序类加载器、自定义加载器。。

双亲委派模式:
类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,
如果父类加载器可以完成类加载任务,就成功返回;
只有父类加载器无法完成此加载任务时,才自己去加载

防止内存中出现多份同样的字节码

arrayList里有1000个数,选出重复数字(map, bitmap)

map, bitmap

HashMap如何做到快速定位key值

1.确定数组index:hashcode % table.length-1 与运算,快速取模;如果数组有值,循环列表比对key
2.链表查询O(n),红黑树查询O(lng(n))

如何设计分库分表和数据迁移的,思路与具体实施流程

取模 1、中间变量 = hashcode%(库数量*每个库的表数量);
2、库序号 = 取整(中间变量/每个库的表数量);
3、表序号 = 中间变量%每个库的表数量;

有没有异步执行任务的场景,为什么,如何异步执行的

1.发送短信验证码
2.购买成功或者抽奖成功短信通知
3.订单流程处理

1.启动线程池
2.mq

请求怎么保证多个节点只有一个响应

nignx做负载均衡

dubbo服务A请求服务B通过什么方式,调用数据传递过程

dubbo协议
http 三次握手

dubbo服务间请求如何获取服务器地址

基于zookeeper获取服务提供者地址列表的方法

有没有看过openFeign源码,流程是怎样的

1).ApplicationService 向Eureka Server 注册服务。
2).Application Client从Eureka Server中发现服务信息。
3).在Application Client中调用OpenFeign接口中方法
4).Application Client中OpenFeign通过应用程序名调用Application Service

Factorybean有了解吗,你使用或者了解的哪些技术是基于这个实现的**

FactoryBean用来修饰普通bean,一旦实现了要重写两个方法,最终要调get0bject方法中的实例,get0bject可以动态生产实例bean。

Spring如何定制化功能扩展(spi)

有没有了解过spi,全称是什么

服务发现机制

@Component 和 @Bean 的区别**

@Compent 作用就相当于 XML配置
@Bean 需要在配置类中使用,即类上需要加上@Configuration注解
两者都可以通过@Autowired装配

如果你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component注解的,因此就不能使用自动化装配的方案了,但是我们可以使用@Bean,当然也可以使用XML配置。

String 是如何实现的?它有哪些重要的方法?

String 内部实际存储结构为 char 数组

1.多构造方法:String 为参数的构造方法、char[] 为参数构造方法、StringBuffer 为参数的构造方法、StringBuilder 为参数的构造方法
2.equals() :先通过 instanceof 判断是否为 String 类型,如果不是则会直接返回 false,当判断参数为 String 类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回 true,否则则返回 false。
3. compareTo() :循环对比所有的字符,当两个字符串中有任意一个字符不相同时,则 return char1-char2。小于return -1,相同return0,大于return1
4.indexOf():查询字符串首次出现的下标位置
5.lastIndexOf():查询字符串最后出现的下标位置
6.contains():查询字符串中是否包含另一个字符串
7.toLowerCase():把字符串全部转换成小写
8.toUpperCase():把字符串全部转换成大写
9.length():查询字符串的长度
10.trim():去掉字符串首尾空格
11.replace():替换字符串中的某些字符
12.split():把字符串分割并返回字符串数组
13.join():把字符串数组转为字符串

compareTo() 方法和 equals()区别

1.equals() 可以接收一个 Object 类型的参数,而 compareTo() 只能接收一个 String 类型的参数;
2.equals() 返回值为 Boolean,而 compareTo() 的返回值则为 int。

为什么 String 类型要用 final 修饰?

1.它能够缓存结果,当你在传参时不需要会修改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。
2.安全,当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题

final 修饰的第一个好处是安全;第二个好处是高效

== 和 equals 的区别是什么?

== 对于基本数据类型来说,是用于比较 “值”是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的。
Object 中的 equals() 方法其实就是 ==,而 String 重写了 equals() 方法把它修改成比较两个字符串的值是否相等。

String 和 StringBuilder、StringBuffer 有什么区别?

String 类型是不可变的,所以在字符串拼接的时候如果使用 String 的话性能会很低
StringBuffer使用了 synchronized 来保证线程安全
StringBuilder是不安全的

String 的 intern() 方法有什么含义?

intern:返回字符串对象的规范化表示形式,取字符串常量池的引用。

String 类型在 JVM(Java 虚拟机)中是如何存储的?编译器对 String 做了哪些优化?

String 常见的创建方式有两种,new String() 的方式和直接赋值的方式,
直接赋值的方式会先去字符串常量池中查找是否已经有此值,如果有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;
而 new String() 的方式一定会先在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串

s1 拼接了多个字符串,但对比的结果却是 true

哈希冲突解决方式

1, 开放定址法:
2, 再哈希法:
3, 链地址法:
4, 建立公共溢出区:

HashMap 底层是如何实现的?在 JDK 1.8 中它都做了哪些优化?

在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的,
JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构
红黑树具有快速增删改查的特点,这样就可以有效的解决链表过长时操作比较慢的问题。

什么是加载因子?加载因子为什么是 0.75?

加载因子也叫扩容因子或负载因子,用来判断什么时候进行扩容的

出于容量和性能之间平衡的结果:
当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash 冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;

而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。

JDK 1.8 HashMap 扩容时做了哪些优化?

1.JDK 1.8 在扩容时并没有像 JDK 1.7 那样,重新计算每个元素的哈希值,而是通过高位运算(e.hash & oldCap)来确定元素是否需要移动,使用 e.hash & oldCap 得到的结果,高一位为 0,当结果为 0 时表示元素在扩容时位置不会发生任何变化,当结果为 1 时,表示元素在扩容时位置发生了变化,新的下标位置等于原下标位置 + 原数组长度

2.尾插法

当有哈希冲突时,HashMap 是如何查找并确认元素的?

当哈希冲突时我们需要通过判断 key 值是否相等,才能确认此元素是不是我们想要的元素。

HashMap 源码中有哪些重要的方法?

查询(get)、新增(put)和数据扩容(resize)。

HashMap 是如何导致死循环的?

JDK 1.7 resize() 在并发环境下
发生死循环的原因是 JDK 1.7 链表插入方式为首部倒序插入,
这个问题在 JDK 1.8 得到了改善,变成了尾部正序插入。

线程的状态有哪些?它是如何工作的?

1.NEW :新建状态,线程被创建出来,但尚未启动时的线程状态
2.RUNNABLE:就绪状态,表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配CPU资源
3.BLOCKED:堵塞等待锁的线程状态,表示堵塞状态的线程正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法;
4.WAITING:等待状态,一个处于等待状态的线程正在等待另一个线程线程执行某个特定的动作,比如,一个线程调用了 Object.wait() 方法,那它就在等待另一个线程调用 Object.notify() 或 Object.notifyAll() 方法
5.TIME_WAITING:计时等待状态,和等待状态(WAITING)类似,它只是多了超时时间,比如调用了有超时时间设置的方法 Object.wait(long timeout) 和 Thread.join(long timeout) 等这些方法时,它才会进入此状态;
6.TERMINATED:终止状态,表示线程已经执行完成。
面试实战_第2张图片

线程的优先级有什么用?该如何设置?

线程的优先级可以理解为线程抢占 CPU 时间片的概率,优先级越高的线程优先执行的概率就越大,但并不能保证优先级高的线程一定先执行。

通过 Thread.setPriority() 来设置优先级

线程的常用方法有哪些?

1.join():在一个线程中调用 other.join() ,这时候当前线程会让出执行权给 other 线程,直到 other 线程执行完或者过了超时时间之后再继续执行当前线程
2.yield():表示给线程调度器一个当前线程愿意出让 CPU 使用权的暗示,但是线程调度器可能会忽略这个暗示。
3.sleep()

sleep()和yield()方法的区别

sleep()和yield()都是Thread类中的静态方法,都会使得当前处于运行状态的线程放弃CPU,但是两者的区别还是有比较大的:
1.sleep使当前线程暂停一段时间,而且是不考虑其它线程的优先级的,而且不释放资源锁;yeild只会让位给优先级一样或者比它优先级高的线程,而且不能由用户指定暂停多长时间
2:当线程执行了sleep方法之后,线程将转入到睡眠状态,直到时间结束,而执行yield方法,直接转入到就绪状态。这些对线程的生命周期会造成影响的。
3:sleep方法需要抛出或者捕获异常,因为线程在睡眠中可能被打断,而yield方法则没异常。

sleep()和wait()方法的区别

1、sleep是线程中的方法,但是wait是Object中的方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

BLOCKED(阻塞等待)和 WAITING(等待)有什么区别?**

BLOCKED 可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源;

而 WAITING 则是因为自身调用了 Object.wait() 或着是 Thread.join() 又或者是 LockSupport.park() 而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒,比如Object.wait() 而进入 WAITING 状态之后,则需要等待另一个线程执行 Object.notify() 或 Object.notifyAll() 才能被唤醒。

start() 方法和 run() 方法有什么区别?**

1.start() 方法属于 Thread 自身的方法,并且使用了 synchronized 来保证线程安全
2.run() 方法为 Runnable 的抽象方法,必须由调用类重写此方法,重写的 run() 方法其实就是此线程要执行的业务方法
3.start() 方法可以开启线程,让线程从 NEW 状态转换成 RUNNABLE 状态,而 run() 方法只是一个普通的方法。
4.它们可调用的次数不同,start() 方法不能被多次调用,否则会抛出 java.lang.IllegalStateException;而 run() 方法可以进行多次调用,因为它只是一个普通的方法而已

两个线程交替打印

1.使用 Lock
2.wait 和notifyAll、synchronized
3. Lock/Condition
4. Semaphore(信号标)
join只能顺序打印一次,不能多次


    private static int times ;
    private static Semaphore semaphoreA = new Semaphore(1);
    private static Semaphore semaphoreB = new Semaphore(0);
    private static Semaphore semaphoreC = new Semaphore(0);

    public ThreadTest(int times) {
        this.times = times;
    }


    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest(10);
        new Thread(()->{
            threadTest.print("a",semaphoreA,semaphoreB);
        }).start();
        new Thread(()->{
            threadTest.print("b",semaphoreB,semaphoreC);
        }).start();
        new Thread(()->{
            threadTest.print("c",semaphoreC,semaphoreA);
        }).start();
    }
    private void print(String name, Semaphore current, Semaphore next) {
        for(int i=0;i<times;i++){
            try {
                // A获取信号执行,A信号量减1,当A为0时将无法继续获得该信号量
                current.acquire();
                System.out.println(name);
                // B释放信号,B信号量加1(初始为0),此时可以获取B信号量
                next.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        }
    }

线程池参数

ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue workQueue);
corePoolSize:核心池的大小
maximumPoolSize:线程池最大线程数
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
unit:参数keepAliveTime的时间单位
workQueue:一个阻塞队列,

当新提交一个任务时:
(1)如果poolSize (2)如果poolSize=corePoolSize,新任务会被放入阻塞队列等待。
(3)如果阻塞队列的容量达到上限,且这时poolSize (4)如果阻塞队列满了,且poolSize=maximumPoolSize,那么线程池已经达到极限,会根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常

ArrayBlockingQueue:是一个有界缓存等待队列,可以指定缓存队列的大小。当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
LinkedBlockingQueue:LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。
SynchronousQueue:没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。

常用的线程池:

  1. public static ExecutorService newSingleThreadExecutor() 创建仅有一个线程工作的线程池
  2. public static ExecutorService newCachedThreadPool() 创建一个缓存线程池
  3. public static ExecutorService newFixedThreadPool(int nThreads) 创建指定大小的线程池
  4. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个支持定时以及周期性执行任务的缓存线程池。
  5. public static ScheduledExecutorService newSingleThreadScheduledExecutor() 创建一个支持定时以及周期性执行任务的单线程的线程池

ThreadPoolExecutor 的执行方法有几种?它们有什么区别?

execute() VS submit()
1.submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。
2.execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法

什么是线程的拒绝策略?

当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。

拒绝策略的分类有哪些?

拒绝策略
1.ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出异常。
2.ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

如何自定义拒绝策略?

自定义拒绝策略只需要新建一个(实现) RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法即可

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
        new RejectedExecutionHandler() {  // 添加自定义拒绝策略
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                // 业务处理方法
                System.out.println("执行自定义拒绝策略");
            }
        });

for (int i = 0; i < 6; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName());
    });
}

ThreadPoolExecutor 能不能实现扩展?如何实现扩展?

ThreadPoolExecutor 的扩展主要是通过重写它的 beforeExecute() 和 afterExecute() 方法实现的

 static class MyThreadPoolExecutor extends ThreadPoolExecutor {
        private final ThreadLocal<Long> localTime = new ThreadLocal<>();
        public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        }
        /**
         * 开始执行之前
         * @param t 线程
         * @param r 任务
         */
        @Override
        protected void beforeExecute(Thread t, Runnable r) {
            Long sTime = System.nanoTime(); // 开始时间 (单位:纳秒)
            localTime.set(sTime);
            System.out.println(String.format("%s | before | time=%s",
                    t.getName(), sTime));
            super.beforeExecute(t, r);
        }
        /**
         * 执行完成之后
         * @param r 任务
         * @param t 抛出的异常
         */
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            Long eTime = System.nanoTime(); // 结束时间 (单位:纳秒)
            Long totalTime = eTime - localTime.get(); // 执行总时间
            System.out.println(String.format("%s | after | time=%s | 耗时:%s 毫秒",
                    Thread.currentThread().getName(), eTime, (totalTime / 1000000.0)));
            super.afterExecute(r, t);
        }
    }

synchronized底层指令:monitorenter和monitorexit

synchronized是基于JVM内置锁实现,通过内部对象Object Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。
注意:只有synchronized锁升级为重量级锁时才会用到Object Monitor(监视器锁)

synchronized关键字被编译成字节码后会被翻译成monitorentermonitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。

公平锁 VS 非公平锁
JDK 1.6 时锁做了哪些优化?
1.自适应自旋锁
2.锁升级:偏向锁到轻量级锁再到重量级锁

什么是乐观锁和悲观锁?它们的应用都有哪些?乐观锁有什么问题?**

悲观锁指的是数据对外界的修改采取保守策略,它认为线程很容易会把数据修改掉,因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用。(synchronized )
乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测。(CAS)
JDK 在 1.5 时提供了 AtomicStampedReference 类也可以解决 ABA 的问题

什么是可重入锁?用代码如何实现?它的实现原理是什么?

可重入锁也叫递归锁,指的是同一个线程,如果外面的函数拥有此锁之后,内层的函数也可以继续获取该锁。在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁。

public class LockExample {
    public static void main(String[] args) {
        reentrantA(); // 可重入锁
    }
    /**
     * 可重入锁 A 方法
     */
    private synchronized static void reentrantA() {
        System.out.println(Thread.currentThread().getName() + ":执行 reentrantA");
        reentrantB();
    }

    /**
     * 可重入锁 B 方法
     */
    private synchronized static void reentrantB() {
        System.out.println(Thread.currentThread().getName() + ":执行 reentrantB");
    }
}

可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为 0,当被线程占用和重入时分别加 1,当锁被释放时计数器减 1,直到减到 0 时表示此锁为空闲状态。

什么是共享锁和独占锁?

只能被单线程持有的锁叫独占锁,可以被多线程持有的锁叫共享锁。
独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 synchronized 就是独占锁,
而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。

什么是浅克隆和深克隆?如何实现克隆?

浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的。
简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象

深克隆(Deep Clone)是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象,

在 Java 语言中要实现克隆则需要实现 Cloneable 接口,并重写 Object 类中的 clone() 方法

在 java.lang.Object 中对 clone() 方法的约定有哪些?

1.对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象;
2.对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回 true,因为克隆对象与原对象的类型是一样的;
3.对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。

Arrays.copyOf() 是深克隆还是浅克隆?

从结果可以看出,我们在修改克隆对象的第一个元素之后,原型对象的第一个元素也跟着被修改了,这说明 Arrays.copyOf() 其实是一个浅克隆

深克隆的实现方式有几种?

1.所有对象都实现克隆方法;(都实现 Cloneable 的接口)
2.通过构造方法实现深克隆;(如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新 new 一个对象)
3.使用 JDK 自带的字节流实现深克隆;
4.使用第三方工具实现深克隆,比如 Apache Commons Lang;(Apache Commons Lang 来实现深克隆 SerializationUtils.clone(p1))
5.使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等。(gson.fromJson())

Java 中的克隆为什么要设计成,既要实现空接口 Cloneable,还要重写 Object 的 clone() 方法?

为了一个重要但不常用的克隆功能,使用实现接口的方式是那时最合理的方案了,而且在 Java 语言中一个类可以实现多个接口。
因为 clone() 方法语义的特殊性,因此最好能有 JVM 的直接支持,既然要 JVM 直接支持,就要找一个 API 来把这个方法暴露出来才行,最直接的做法就是把它放入到一个所有类的基类 Object 中,这样所有类就可以很方便地调用到了。

如何实现动态代理?JDK 代理和 CGLib 代理有什么区别?

1.JDK 代理是 Java 语言自带的功能,无需通过加载第三方类实现;
2.Java 对 JDK 代理提供了稳定的支持,并且会持续的升级和更新 JDK 代理,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
3.JDK Proxy 是通过拦截器加反射的方式实现的;
4.JDK Proxy 只能代理继承接口的类;jdk代理的类必须是接口
5.JDK Proxy 实现和调用起来比较简单;
6.CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;
7.CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。CGlib代理的类必须可以被继承

你对 JDK Proxy 和 CGLib 的掌握程度。

JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可

Lombok 是通过反射实现的吗?

Lombok 的实现和反射没有任何关系,它是通过编译期 自定义注解处理器来实现的

动态代理和静态代理有什么区别?

静态代理:事先需要写好代理类,每个业务类都要对应一个代理类

动态代理的使用场景有哪些?

RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等

如何实现本地缓存和分布式缓存?

本地缓存可以使用 EhCache 和 Google 的 Guava 来实现,
而分布式缓存可以使用** Redis 或 Memcached** 来实现。
spring 内置 cache 属于本地缓存的一种,它的本质是使用 map 进行数据存储的。

如何自己手动实现一个缓存系统?

数据类型:ConcurrentHashMap
缓存过期:定时删除、惰性删除、定期删除
缓存淘汰:LRU
先来说一下自定义缓存的实现思路,
1.首先需要定义一个存放缓存值的实体类,这个类里包含了缓存的相关信息,比如缓存的 key 和 value,缓存的存入时间、最后使用时间和命中次数(预留字段,用于支持 LFU 缓存淘汰),
2.再使用 ConcurrentHashMap 保存缓存的 key 和 value 对象(缓存值的实体类),
3.新增一个无限循环的线程用于检测并删除过期的缓存
4.然后再新增一个缓存操作的工具类,用于添加和删除缓存,
5.最后再缓存启动时,开启一个无限循环的线程用于检测并删除过期的缓存

消息队列的使用场景

1.商品秒杀
2.系统解耦
3.日志记录
我们可以通过 JDK 提供的 Queue 来实现自定义消息队列,使用 DelayQueue 实现延迟消息队列。

介绍一个你熟悉的消息中间件?

rabbitMQ:生产者、消费者和代理。
1.支持持久化,RabbitMQ 支持磁盘持久化功能,保证了消息不会丢失;
2.高并发,RabbitMQ 使用了 Erlang 开发语言
3.支持分布式集群,正是因为 Erlang 语言实现的,因此 RabbitMQ 集群部署也非常简单,只需要启动每个节点并使用 --link 把节点加入到集群中即可,并且 RabbitMQ 支持自动选主和自动容灾;
4.支持多种语言,比如 Java、.NET、PHP、Python、JavaScript、Ruby、Go 等;
5.支持消息确认,支持消息消费确认(ack)保证了每条消息可以被正常消费;
6.它支持很多插件,比如网页控制台消息管理插件、消息延迟插件等,RabbitMQ 的插件很多并且使用都很方便。

RabbitMQ 的消息类型,分为以下四种:
1.direct(默认类型)模式,此模式为一对一的发送方式,也就是一条消息只会发送给一个消费者;
2.headers 模式,允许你匹配消息的 header 而非路由键(RoutingKey),除此之外 headers 和 direct 的使用完全一致,但因为 headers 匹配的性能很差,几乎不会被用到;
3.fanout 模式,为多播的方式,会把一个消息分发给所有的订阅者;
4.topic 模式,为主题订阅模式,允许使用通配符(#、*)匹配一个或者多个消息,我可以使用“cn.mq.#”匹配到多个前缀是“cn.mq.xxx”的消息,比如可以匹配到“cn.mq.rabbit”、“cn.mq.kafka”等消息。

如何手写一个消息队列和延迟消息队列?

实现自定义延迟队列需要实现 Delayed 接口,重写 getDelay() 方法

Spring 中单例bean是线程安全的吗?

不安全,没有同步处理, 当bean有状态的时候不安全,使用ThreadLocal
1.作用域设置成原型、request
2.bean不设置状态

Spring Bean 的作用域有哪些?它的注册方式有几种?

Bean 的作用域一共有 5 个。
(1)singleton 作用域:表示在 Spring 容器中只有一个 Bean 实例,以单例的形式存在,是默认的 Bean 作用域。
(2)prototype 作用域:原型作用域,每次调用 Bean 时都会创建一个新实例,也就是说每次调用 getBean() 方法时,相当于执行了 new Bean()。
(3)request 作用域:每次 Http 请求时都会创建一个新的 Bean,该作用域仅适应于 WebApplicationContext 环境。
(4)session 作用域:同一个 Http Session 共享一个 Bean 对象,不同的 Session 拥有不同的 Bean 对象,仅适用于 WebApplicationContext 环境。
(5)application 作用域:全局的 Web 作用域,类似于 Servlet 中的 Application。

Bean 的注册方式有三种:
1.XML 配置文件的注册方式
2.Java 注解的注册方式
3.Java API 的注册方式

什么是同名 Bean?它是如何产生的?应该如何避免?**

同一个 Spring 配置文件中 Bean 的 id 和 name 是不能够重复的,否则 Spring 容器启动时会报错。但**如果 Spring 加载了多个配置文件的话,可能会出现同名 Bean 的问题。**同名 Bean 指的是多个 Bean 有相同的 name 或者 id。

Spring 对待同名 Bean 的处理规则是使用最后面的 Bean 覆盖前面的 Bean,所以我们在定义 Bean 时,尽量使用长命名非重复的方式来定义,避免产生同名 Bean 的问题。

聊一聊 Spring bean的生命周期。

面试实战_第3张图片
1.bean实例化
2.注入对象属性
3.执行aware(beanNameAware、beanFactoryAware、applicationContextAware)
4.beanpostProcessor前置处理
5.初始化bean、初始化方法
6.beanpostProcessor后置处理
7.注册回调方法
8.bean使用
9.执行销毁方法

Spring 循环依赖怎么解决

1.懒加载注解
2.三级缓存

Spring IoC 有哪些优势?

ioc:控制反转,用依赖注入(反射机制),将实例的初始化交给spring容器来管理,ioc的反射机制允许我们不重新编译代码,因为它的对象是动态生成的。(工厂模式)

ioC 的优点有以下几个:
1.使用更方便,拿来即用,无需显式的创建和销毁的过程;
2.可以很容易提供众多服务,比如事务管理、消息服务等;
3.提供了单例模式的支持;
4.提供了 AOP 抽象,利用它很容易实现权限拦截、运行期监控等功能;
5.更符合面向对象的设计法则;
6.低侵入式设计,代码的污染极低,降低了业务对象替换的复杂性。

IoC 的注入方式有哪些?

IoC 的注入方式有三种:构造方法注入、Setter 注入和接口注入。

谈一谈你对 AOP 的理解。

aop是面向切面编程,是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,
便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
用于权限、日志 (适配器模式)

Spring AOP 目前提供了三种配置方式:
1.基于 Java API 的方式;
2.基于 @AspectJ(Java)注解的方式;
3.基于 XML 标签的方式。

Spring 中的动态代理是通过什么方式实现的?

Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK 代理,在没有实现接口时就会使用 CGLib代理

spring 事务什么失效

1.发生自调用,不要用this,用代理类
2.方法不是public
3.数据不支持事务
4.没有被spring

MyBatis中#{}和${}的区别

#{} 占位符,即sql 预编译,能防止sql 注入,#{} 对应的变量自动加上单引号
${} 拼接符,即 sql 拼接,不能防止sql 注入, ${} 对应的变量不会加上单引号’

  1. 能用 #{} 的地方就用 #{},不用或少用 ${}
  2. 表名作参数时,必须用 ${}。如:select * from ${tableName}
  3. order by 时,必须用 ${}。如:select * from t_user order by ${columnName}
  4. 使用 ${} 时,要注意何时加或不加单引号,即 和 ′ {} 和 ' {}’

mybatis分页**

  1. 关键字limit来实现
  2. 利用interceptor拦截器来拼接sql
  3. PageHelpe(使用Interceptor拦截器方式)

Mybatis源码、Mybatis处理流程**

sqlSessionFactoryBuilder——》parse解析
Configuration——》build
SqlSessionFactory——》openSession
SqlSession——》query
Executor——》newStatementHander
StatementHandler——》handleResultSets
ResultSetHandler

mybatis二级缓存

①、一级缓存是SqlSession级别的缓存。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

②、二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

Spring 和 Spring Boot 有什么区别?Spring Boot 的优点有哪些?

Spring Boot 本质上是 Spring 框架的延伸和扩展,它的诞生是为了简化 Spring 框架初始搭建以及开发的过程,使用它可以不再依赖 Spring 应用程序中的 XML 配置,为更快、更高效的开发 Spring 提供更加有力的支持。

Spring Boot 特性一:更快速的构建能力(Starters )
Spring Boot 特性二:起步依赖
Spring Boot 特性三:内嵌容器支持(默认Tomcat)
Spring Boot 特性四:Actuator 监控

Spring Boot 的启动流程是怎么样的? starter

1.创建并启动计时监控类
2.声明应用上下文对象和异常报告集合
3.设置系统属性 headless 的值
4.创建所有 Spring 运行监听器并发布应用启动事件
5.初始化默认应用的参数类
6.准备环境
7.创建 Banner 的打印类
8.创建应用上下文
9.实例化异常报告器
10.准备应用上下文
11.刷新应用上下文
12.应用上下文刷新之后的事件处理
13.停止计时监控类
14.输出日志信息
15.发布应用上下文启动完成事件
16.执行所有 Runner 运行器
17.发布应用上下文就绪事件
18.返回应用上下文对象

Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。

Spring Boot 自动配置原理是什么?

@SpringBootApplication 里面 @EnableAutoConfiguration注解的自动配置功能 里面的@Import 会读取jar包类路径下的 META-INF 目录下的 spring.factories 文件,获得每个框架定义的需要自动配置的配置类,把它们添加在spring容器中

MQ 常见的使用场景有哪些?你都用过哪些 MQ 中间件?**

1.实现削峰填谷(商品秒杀以及产品抢购)
2.使用 MQ 实现消息通讯
3.使用 MQ 实现日志系统

MQ 的特点是什么?引入 MQ 中间件会带来哪些问题?

特点:
1.先进先出
2.发布、订阅工作模式:
3.持久化。
4.分布式:
5.消息确认

问题:
1.增加了系统的运行风险
2.增加了系统的复杂度,需要考虑消息丢失、消息重复消费、消息的顺序消费等问题

常见的 MQ 中间件的优缺点分析。

1.Redis :业务不复杂的场景下可以尝试性的使用 Redis 提供的消息队列。
2.RabbitMQ
3.Kafka

Kafka VS RabbitMQ

Kafka 和 RabbitMQ 都支持分布式集群部署,并且都支持数据持久化和消息消费确认等 MQ 的核心功能
1.Kafka 支持消息回溯,它可以根据 Offset(消息偏移量)、TimeStamp(时间戳)等维度进行消息回溯,而 RabbitMQ 并不支持消息回溯;
2.Kafka 的消息消费是基于拉取数据的模式,也就是消费者主动向服务器端发送拉取消息请求,而 RabbitMQ 支持拉取数据模式和主动推送数据的模式,也就说 RabbitMQ 服务器会主动把消息推送给订阅的消费者;
3.在相同配置下,Kafka 的吞吐量通常会比 RabbitMQ 高一到两个级别
4.Kafka 支持幂等性了;而 RabbitMQ 是没有幂等性功能支持的;
5.RabbitMQ 支持多租户的功能,也就是常说的 Virtual Host(vhost),每一个 vhost 相当于一个独立的小型 RabbitMQ 服务器,它们拥有自己独立的交换器、消息队列及绑定关系等,并且拥有自己独立权限,而且多个 vhost 之间是绝对隔离的,但 Kafka 并不支持多租户的功能。

MySQL 是如何运行的?sql在mysql的执行是怎么样的

sql等执行过程分为两类,
一类对于查询等过程如下:权限校验—》查询缓存—》分析器(词法分析)—》优化器—》权限校验—》执行器—》引擎
对于更新等语句执行流程如下:分析器(词法分析)----》权限校验----》执行器—》引擎—》redo log prepare—》binlog—》redo log commit。(两阶段提交)

MySQL 它有哪些引擎?如何选择数据库的引擎?

  1. InnoDB :支持事务功能、外键 、行锁 适合修改、删除
  2. MyISAM:不支持事务,拥有较高的插入和查询的速度
  3. MEMORY :内存型的数据库引擎,重启数据库之后,所有数据都会丢失,临时表

查询缓存在什么问题?

当我们好不容易缓存了很多查询语句之后,任何一条对此表的更新操作都会把和这个表关联的所有查询缓存全部清空,那么在更新频率相对较高的业务中,查询缓存功能完全是一个鸡肋。
因此,在 MySQL 8.0 的版本中已经完全移除了此功能

InnoDB 自增索引的持久化问题。

在一个自增表里面一共有 5 条数据,id 从 1 到 5,删除了最后两条数据,也就是 id 为 4 和 5 的数据,之后重启的 MySQL 服务器,又新增了一条数据,请问新增的数据 id 为几?

MySQL 8.0 之后 InnoDB 会把索引持久化到日志中,重启服务之后自增索引是不会丢失的,因此答案是 6

主键索引和唯一索引的区别

主键索引一定是唯一索引,唯一索引不一定是主键索引,
唯一性索引列允许空值,而主键列不允许为空值。
一个表最多只能创建一个主键,但可以创建多个唯一索引。
主键可以被其他表引用为外键,而唯一索引不能。

什么时候用不到索引

索引原则都建立在最左匹配原则

  1. 索引列上使用函数(replace\substr\concat\sum count avg),表达式
  2. 字符串不加引号,出现隐式转换
  3. like条件中前面带%
  4. 负向查询,例如not like

MySQL 的优化方案有哪些?

MySQL 数据库常见的优化手段分为三个层面:SQL 和索引优化、数据库结构优化、系统硬件优化

1.SQL 和索引优化、explain是检查哪几个字段

  1. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
  2. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
  3. 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
  4. 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
  5. in 和 not in 也要慎用,否则会导致全表扫描,
  6. like条件中前面带%
  7. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
  8. 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
  9. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
    10.索引并不是越多越好,一个表的索引数最好不要超过6个
    11.查询具体的字段而非全部字段,任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
    12.大量数据的插入,不要用循环一条条新增,改成用批量新增
    13.适当增加冗余字段,增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所有可以适当的增加冗余字段,以减少多张表的关联查询,这是以空间换时间的优化策略。

explain :type key

id:选择标识符
select_type:表示查询的类型。
table:输出结果集的表
partitions:匹配的分区
type:表示表的连接类型
possible_keys:表示查询时,可能使用的索引
key:表示实际使用的索引
key_len:索引字段的长度
ref:列与索引的比较
rows:扫描出的行数(估算的行数)
filtered:按表条件过滤的行百分比
Extra:执行情况的描述和说明

2.数据库结构优化
① 最小数据长度:应该将表的字段设置的尽可能小,比如身份证号,可以设置为 char(18) 就不要设置为 varchar(18)。
② 使用最简单数据类型:能使用 int 类型就不要使用 varchar 类型,因为 int 类型比 varchar 类型的查询效率更高。
③ 尽量少定义 text 类型:如果必须要使用 text 定义字段,可以把此字段分离成子表,需要查询此字段时使用联合查询,这样可以提高主表的查询效率。
④ 适当分表、分库策略

3.硬件优化
① 磁盘 变小变多
磁盘应该尽量使用有高性能读写能力的磁盘,比如固态硬盘,这样就可以减少 I/O 运行的时间,从而提高了 MySQL 整体的运行效率。
② 网络 变大
保证网络带宽的通畅(低延迟)以及够大的网络带宽是 MySQL 正常运行的基本条件,如果条件允许的话也可以设置多个网卡,以提高网络高峰期 MySQL 服务器的运行效率。
③ 内存 变大
MySQL 服务器的内存越大,那么存储和缓存的信息也就越多,而内存的性能是非常高的,从而提高了整个 MySQL 的运行效率。

联合索引需要注意什么问题?

正确使用联合索引:要复合最左匹配

如何排查慢查询?

慢查询通常的排查手段是先使用慢查询日志功能,查询出比较慢的 SQL 语句,然后再通过 explain 来查询 SQL 语句的执行计划最后分析并定位出问题的根源,再进行处理。
1.修改 MySQL 配置文件, 配置my.cnf 中的“slow_query_log=1”
2.explain :type key,当 type 为 all 时,则表示全表扫描,因此效率会比较低,

mysql大量数据的批量处理、大量数据的查询

插入使用 批量新增,
删除使用批量删除,
查询使用索引,分页时候通过 传入最后一个id,例如: id= ?limit 20

mysql一致性,永久性还有隔离性的实现原理

A: 原子性 (Atomicity)原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。 (undo log)
C:一致性 (Consistency) (redo log)
I:隔离性 (Isolation) 隔离性是指多个事务并发执行的时候,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。(MVCC 锁)
D:持久性 (Durability) 持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

mysql innodb引擎 insert 数据时 默认会加什么锁

innodb引擎的insert,update,delete操作都会给操作数据加上排他锁(行级锁)

mysql 的innodb引擎 四种隔离级别**

1.读未提交:也就是一个事务还没有提交时,它做的变更就能被其他事务看到。:会出现脏读、不可重复读、幻读、
2.读已提交:指的是一个事务只有提交了之后,其他事务才能看得到它的变更。造成不可重复读、幻读
3.可重复读:此方式为默认的隔离级别,它是指一个事务在执行过程中(从开始到结束)看到的数据都是一致的,在这个过程中未提交的变更对其他事务也是不可见的。但会出现幻读
4.串行化:是指对同一行记录的读、写都会添加读锁和写锁,后面访问的事务必须等前一个事务执行完成之后才能继续执行,所以这种事务的执行效率很低。

脏读、不可重复读和幻读

脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。
不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。(可以间隙锁解决幻读)

关系型数据库和文档型数据库有什么区别?

1.关系型数据库都会支持的 ACID 特性,也就是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability
2.关系型数据库支持 事务和持久化
3.关系型数据库一般遵循三范式设计思想
4.文档型数据库:MongoDB ,它提供了更高效的读/写性能以及可自动容灾的数据库集群,还有灵活的数据库结构,从而给系统的数据库存储带来了更多可能 性。

非关系型数据库和文档型数据库有什么区别?

非关系型数据包含了文档型数据库
非关系型数据通常包含 3 种数据库类型:
1.文档型数据库:MongoDB
2.键值型数据库: Redis 和 Memcached
3.全文搜索型:ElasticSearch 和 Solr

MongoDB 支持事务吗?

MongoDB 在 4.0 之前是不支持事务的, MongoDB 4.0 之中正式添加了事务的功能

Redis 是如何处理过期数据的?

惰性删除是指 Redis 服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则正常返回值信息给客户端。
定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。

当内存不够用时 Redis 又是如何处理的?

当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略

redis淘汰策略
在 4.0 版本之前 Redis 的内存淘汰策略有以下 6 种。
1)voltile-lru:在所有设置了过期时间的键值中,淘汰最久未使用的键值。
2)volatile-ttl:在所有设置了过期时间的键值中,淘汰快过期的键值。
3)volatile-random:在所有设置了过期时间的键值中,随机淘汰任意键值。
4)allkeys-lru:在整个键值中,淘汰最久未使用的键值。
5)allkeys-random:在整个键值中,随机淘汰任意键值。
6)no-enviction(驱逐):不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,(Redis 默认策略)
而在 Redis 4.0 版本中又新增了 2 种淘汰策略:
7)volatile-lfu,在所有设置了过期时间的键值中,淘汰最少使用的键值;
8)allkeys-lfu,:在整个键值中,淘汰最少使用的键值。

Redis 内存淘汰算法

LRU 淘汰算法(最近最少使用):基于链表结构实现的,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要进行内存淘汰时,只需要删除链表尾部的元素即可。
LFU 淘汰算法(最不常用的):根据总访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”

单机锁有哪些?它为什么不能在分布式环境下使用?

1.悲观锁,是数据对外界的修改采取保守策略,它认为线程很容易把数据修改掉,因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用,典型应用是 synchronized;
2.乐观锁,和悲观锁的概念恰好相反,乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测,典型应用是 ReadWriteLock 读写锁;
3.可重入锁,也叫递归锁,指的是同一个线程在外面的函数获取了锁之后,那么内层的函数也可以继续获得此锁,在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁;
4.独占锁和共享锁,只能被单线程持有的锁叫做独占锁,可以被多线程持有的锁叫共享锁,独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 ReentrantLock 就是独占锁;而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。

在分布式系统中,每次请求可能会被分配在不同的服务器上,而单机锁是在单台服务器上生效的。如果是多台服务器就会导致请求分发到不同的服务器,从而导致锁代码不能生效,因此会造成很多异常的问题,那么单机锁就不能应用在分布式系统中了。

redis分布式锁是怎么样的、分布式锁的缺点、怎么解决

原理:
使用setnx命令

  1. setnx(key,1)当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁,当其他线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
  2. del(key)释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
  3. setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放,expire(key, 30)

缺点:
1. setnx和expire的非原子性 ,解决:Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令。
2. 超时后使用del 导致误删其他线程的锁,解决:可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
3. 过期时间设置的太短,程序还没有执行完,出现并发的可能性,解决:让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”,守护线程会执行expire指令,给锁续期。当线程A执行完任务,会显式关掉守护线程。

面试实战_第4张图片
zookeeper使用临时节点加锁, 删除或者断开的时候会删锁,实现简单,但是添加、删除节点性能较低;
redis使用setnx加锁,基于CAS,情况复杂,但是并发性能高

Redis 是如何实现分布式锁的?可能会遇到什么问题?

1.使用 Redis 实现分布式锁主要需要使用 setnx 方法,返回值为 1 的话,则表示创建锁成功,否则就是失败。
2.使用 expire 来设置键值的过期时间
3.释放锁使用 del 删除即可

1.setnx lock true 和 expire lock 30 命令是非原子的,也就是一个执行完另一个才能执行( 用新版本set 命令)
2.删了不是自己的锁,设置时候自己线程id,删之前先判断(把判断和删除放到一个原子单元中去执行,因此需要借助 Lua 脚本来执行)
3.锁超时

分布式锁超时的话会有什么问题?如何解决?

1.把执行耗时的方法从锁中剔除,减少锁中代码的执行时间,保证锁在超时之前,代码一定可以执行完;
2.把锁的超时时间设置的长一些,正常情况下我们在使用完锁之后,会调用删除的方法手动删除锁,因此可以把超时时间设置的稍微长一些。
3.守护线程 锁延长时间
4.redission 里面又watch dog机制

Redis 中如何实现的消息队列?实现的方式有几种?

1.使用 List 类型实现(持久化、不支持重复消费、不支持消息确认)lpush、lpop
2.使用 ZSet 类型实现(持久化、不支持重复消费、不支持消息确认)
3.Publisher(发布者)和 Subscriber(订阅者)来实现消息队列(无法持久化保存消息、订阅者离线重连之后就不能消费之前的历史消息、不支持消费者确认机制)
4.使用 Stream 的 xadd 和 xrange,提供了 xack 手动确认消息消费的命令

在 Java 代码中使用 List 实现消息队列会有什么问题?应该如何解决?

lpush、lpop
如果消息的空闲时间比较长,一直没有新任务,而 while 循环不会因此停止,它会一直执行循环的动作,这样就会白白浪费了系统的资源。

通过阻塞读:使用 brpop 替代 rpop 来读取最后一条消息,当队列没有数据时,它会进入休眠状态,当有数据进入队列之后,它才会“苏醒”过来执行读取任务

在程序中如何使用 Stream 来实现消息队列?

xadd 和 xrange

Redis 是如何实现高可用的?

1.数据持久化
2.主从数据同步(主从复制)
3.Redis 哨兵模式(Sentinel)
4.Redis 集群(Cluster)

数据持久化有几种方式?

AOF 方式和 RDB 方式。
RDB(Redis DataBase,快照方式)是将某一个时刻的内存数据,以二进制的方式写入磁盘。
AOF(Append Only File,文件追加方式)是指将所有的操作命令,以文本的形式追加到文件中。

RDB 具备更快速的数据重启恢复能力,并且占用更小的磁盘空间,但有数据丢失的风险;
而 AOF 文件的可读性更高,但却占用了更大的空间,且重启之后的恢复速度更慢

Redis 主从同步有几种模式?

主从模式和从从模式
主从模式就是一个主节点和多个一级从节点
而从从模式是指一级从节点下面还可以拥有更多的从节点

什么是 Redis 哨兵模式?它解决了什么问题?

就是当 Redis 的主节点宕机之后,必须人工介入手动恢复

哨兵的工作原理是每个哨兵会以每秒钟 1 次的频率,向已知的主服务器和从服务器,发送一个 PING 命令。如果最后一次有效回复 PING 命令的时间,超过了配置的最大下线时间(Down-After-Milliseconds)时,默认是 30s,那么这个实例会被哨兵标记为主观下线。

如果一个主服务器被标记为主观下线,那么正在监视这个主服务器的所有哨兵节点,要以每秒 1 次的频率确认主服务器是否进入了主观下线的状态。如果有足够数量(quorum 配置值)的哨兵证实该主服务器为主观下线,那么这个主服务器被标记为客观下线。此时所有的哨兵会按照规则(协商)自动选出新的主节点服务器,并自动完成主服务器的自动切换功能,而整个过程都是无须人工干预的。

*Redis 集群的优势是什么?

Redis 集群除了拥有主从模式 + 哨兵模式的所有功能之外,
还提供了多个主从节点的集群功能,实现了真正意义上的分布式集群服务
Redis 集群可以实现数据分片服务

当我们有 N 个主节点时,可以把 16384 个槽位平均分配到 N 台主服务器上。当有键值存储时,Redis 会使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位,再把此键值存储在对应的服务器上,读取操作也是同样的道理,这样我们就实现了数据分片的功能。

redis脑裂怎么处理

修改redis.conf文件中的参数

min-replicas-to-write 3
min-replicas-max-lag 10

第一个参数表示连接到master的最少slave数量
第二个参数表示slave连接到master的最大延迟时间

Java 中可作为 GC Roots 的对象有哪些?

1.所有被同步锁持有的对象,比如被 synchronize 持有的对象;
2.字符串常量池里的引用(String Table);
3.类型为引用类型的静态变量;
4.虚拟机栈中引用对象;
5.本地方法栈中的引用对象。

说一下死亡对象的判断细节?

校验的内容就是此对象是否重写了 finalize() 方法
任何对象的 finalize() 方法都只会被系统调用一次。

生产环境如何排除和优化 JVM?

jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jinfo:查询虚拟机参数配置工具
jmap:堆快照生成工具
jhat :堆快照分析功能
jstack:查询虚拟机当前的线程快照信息

除了比较实用的命令行工具之外,有没有方便一点的排查工具?

JConsole 和 JVisualVM

JVM 常见的调优手段有哪些?

1.-Xmx,设置最大堆内存大小;
2.-Xms,设置初始堆内存大小;
3.-XX:MaxNewSize,设置新生代的最大内存;
4.-XX:MaxTenuringThreshold,设置新生代对象经过一定的次数晋升到老生代;
5.-XX:PretrnureSizeThreshold,设置大对象的值,超过这个值的对象会直接进入老生代;
6.-XX:NewRatio,设置分代垃圾回收器新生代和老生代内存占比;
7.-XX:SurvivorRatio,设置新生代 Eden、Form Survivor、To Survivor 占比。

单例其他实现方式

懒汉、饿汉、双重检测锁
还可以使用静态内部类 和枚举类来实现单例。

public class Singleton {
    // 枚举类型是线程安全的,并且只会装载一次
    private enum SingletonEnum {
        INSTANCE;
        // 声明单例对象
        private final Singleton instance;
        // 实例化
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    //类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}

8. 设计模式

适配器模式:完成从 一个接口 到 另一个接口 的 转换,这个负责转换的就是 适配器
装饰器模式:在原来的基础上装饰的更漂亮,即包裹起原来的,在此基础上加上一些额外的功能)
代理模式 :为其他对象提供一种代理以控制对这个对象的访问。

MyBatis 使用了哪些设计模式?在源码中是如何体现的?

1.工厂模式: SqlSessionFactory
2.建造者模式(Builder):SqlSessionFactoryBuilder
3.单例模式:ErrorContext
4.适配器模式:Log
5.代理模式:MapperProxyFactory
6.模板方法模式:BaseExecutor
7.装饰器模式:Cache

Spring使用了哪些设计模式?在源码中是如何体现的?

1.工厂模式:BeanFactory
2.单例模式:典型应用场景是 Spring 中 Bean 实例
3.适配器模式:HandlerAdatper
4.代理模式:AOP
5.观察者模式:listener的实现
6.策略模式:资源访问Resource接口
7.装饰器模式:名中含有Wrapper
8.模板方法模式:JDBC

软件中的六大设计原则是什么?

  1. 单一职责原则
  2. 开闭原则
  3. 里氏替换原则
  4. 依赖倒置原则
  5. 接口隔离原则
  6. 迪米特法则

红黑树和二叉树有什么区别?

二叉树(Binary Tree)是指每个节点最多只有两个分支的树结构,即不存在分支大于 2 的节点
红黑树(Red Black Tree)是一种自平衡二叉查找树
可以看出使用平衡二叉树可以有效的减少二叉树的深度,从而提高了查询的效率。

你知道哪些算法?讲一下它的内部实现过程?

快速排序:
1、先从数列中取出一个数作为基准数
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3、再对左右区间重复第二步,直到各区间只有一个数
概括来说为 挖坑填数+分治法

插入排序:
1.从数组的第二个数据开始往前比较,即一开始用第二个数和他前面的一个比较,如果 符合条件(比前面的大或者小,自定义),则让他们交换位置。
2.然后再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较,
3.重复步骤二,一直到数据全都排完。

冒泡排序:
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

什么是幂等性?如何保证接口的幂等性?如何避免重复请求提交的问题?

用于表示任意多次请求均与一次请求执行的结果相同,也就是说对于一个接口而言,无论调用了多少次,最终得到的结果都是一样的。

1.前端拦截:按键置灰
2.使用数据库实现幂等性:通过悲观锁来实现幂等性、通过唯一索引来实现幂等性、通过乐观锁来实现幂等性
3.使用 JVM 锁实现幂等性:Lock 或者是 synchronized 来实现幂等性
4.使用分布式锁实现幂等性:Redis 或者 ZooKeeper 来实现分布式锁

幂等性的关键步骤
实现幂等性的关键步骤分为以下三个:
1.每个请求操作必须有唯一的 ID,而这个 ID 就是用来表示此业务是否被执行过的关键凭证,例如,订单支付业务的请求,就要使用订单的 ID 作为幂等性验证的 Key;
2.每次执行业务之前必须要先判断此业务是否已经被处理过;
3.第一次业务处理完成之后,要把此业务处理的状态进行保存,比如存储到 Redis 中或者是数据库中,这样才能防止业务被重复处理。

为什么 TCP 需要三次握手?

原因一:防止重复连接
原因二:同步初始化序列化

IO缓冲区(buffer)的原理及作用

缓冲区就是内存里的一块区域,把数据先存内存里,然后一次性写入,类似于数据库的批量操作,这样大大提高高了数据的读写速率。

zookeeper 节点类型

1.临时节点
2.带顺序的临时节点
3.持久节点
4.带顺序的持久节点

http和https的区别

1.HTTPS 协议需要到 CA 申请证书
2.HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
3.HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4.HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全

HTTP为什么不安全

1.因为HTTP协议使用的是明文传输,通信过程是完全开放的
2.HTTP协议没有用户和网站的身份验证机制

RPC和HTTP区别

1.传输协议:RPC,可以基于TCP协议,也可以基于HTTP协议,HTTP,基于HTTP协议
2.传输效率:RPC,使用自定义的TCP协议,可以让请求报文体积更小,
3.性能消耗:主要在于序列化和反序列化的耗时RPC,可以基于thrift实现高效的二进制传输,HTTP,大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能
4.负载均衡:RPC,基本都自带了负载均衡策略,HTTP,需要配置Nginx,HAProxy来实现
5.服务治理:RPC,能做到自动通知,不影响上游,HTTP,需要事先通知,修改Nginx/HAProxy配置

sql在mybatis的执行过程

sqlSessionFactoryBuilder——》parse(解析)
Configuration——》build(生成)
SqlSessionFactory——》openSession(创建)
SqlSession——》query(执行)
Executor——》newStatementHander
StatementHandler——》handleResultSets
ResultSetHandler

锁优化*

无锁 、偏向锁(threadID、锁标识)、轻量级锁(CAS)、重量级锁(monitorenter、monitorexit)

工作理财业务流程和项目

产品列表(定时任务更新我们本地数据库)——产品详情——用户开二类户——旷世活体验证——填写银行卡信息——卫城四要素验证——产品购买——查看我的产品——查看回款计划——债权转让申请——债权转让协议——债权转让撤回—— 查看转让产品列表——购买债权转让产品

工作票据业务流程和项目

上传票据——ocr识别票据信息(存票据信息)——发起询价——返回询价结果(询价结果存询价记录表)——点交易,判断认证,客户认证(存用户认证表)——申请订单(判断用户是否认证成功,创建订单,状态为处理中)—处理中(报价结果通知)—反显报价信息(修改订单状态为待确认)——订单报价确认(修改订单状态为待签约)——订单合同签署(修改订单状态为待背书)—处理中页面(合同签署结果通知)—显示网银质押背书信息——票据质押背书确认(修改订单记录,状态为待签收)—处理中页面(放款通知)—银行放款(修改订单记录,状态为成功)

订单取消通知(修改订单记录,状态为已取消)

票据,包括汇票、银行本票和支票

0 处理中:调用订单申请接口已成功,但报价结果通知接口未返回该笔终态时,生成一笔处理中的订单,并显示“去交易”按钮,点击跳转至交易申请超时页面。
7 待确认:报价结果通知接口返回成功,但客户未成功进行报价确认,则该笔订单状态更新为待确认,并显示“去交易”按钮,点击跳转至报价确认页面。
8 待签约:客户已成功进行报价确认,但未成功调用合同确认接口,则更新这笔订单状态为待签约,并显示“去交易”按钮,点击跳转至合同签署页面。
1 待背书:合同签署结果通知接口返回成功,但客户未进行背书确认,则更新这笔订单状态为待背书,并显示“去交易”按钮,点击跳转至网银背书页面。
3 待签收:客户已完成背书确认,但未收到放款通知接口返回的终态时,更新这笔状态为待签收,并显示“去交易”按钮,点击跳转至交易等待页面。
4 成功:在接收到放款通知接口返回的状态为成功时,更新这笔订单状态为成功,只展示“查看合同”按钮,点击可下载合同至本地。
5失败:在报价结果通知接口、合同签署结果通知接口或放款通知接口返回失败时,均更新这笔订单状态为失败,若是签署结果通知接口返回签约失败,则展示“重新签约”按钮,其余失败状态不展示任何按钮。
6已取消:在订单取消通知接口接收到对方返回的订单信息后,更新这笔订单状态为已取消,不展示任何按钮。

项目中遇到什么问题 怎么解决 为什么要这样做?项目中有什么难点 ?为什么这么设计?还可以优化吗?

问题:申请订单的时候,先会创建一个处理中状态的订单,然后去调外部接口,外部接口接口返回后进行用户金额的处理,但是对方接口返回时好时坏,老是超时异常,
导致
1.前端页面响应很慢,用户会再次点击,数据库出现重复订单
2.超时异常,我们这边算失败状态,但是第三方是成功的订单,导致用户实际扣了钱,但是显示的订单状态、用户金额不对

解决:
1.前端点击提交后,按钮置灰,页面出现加载状态图标
2.加分布式锁(产品ID+用户ID),设置超时时间3分钟
3.数据库对订单编号加唯一索引
4.返回结果改成异步,先会直接返回给用户处理中页面,启动线程异步去查询状态;在处理中页面轮询前端3秒一次调接口,前端会轮询查询状态接口60s,没有返回就会到用户订单页面
5.此外,如果1分钟还没有处理完的数据,有一个xx-job定时任务一分钟一次查询状态,进行处理

优化:超时异常时候,可以用消息中间件rabbitMQ进行处理,通过消息中间件去查询订单状态,处理业务,就不需要启动线程、定时任务去处理了;但是考虑程序的复杂性稳定性就没加

dubbo调用服务 两台机器有一台出问题,
1.dubbo admin 里面 这个接口路由下 动态配置里设置diasable,每次重启发布要重新设置
2.在dubbo.xml配置group分组名字,在调用的接口上也配置了group

项目中学到了什么?有什么收获?

1.分布式dubbo的服务拆分、调用
2.redis、mysql、zookeeper集群 知识
3. 数据库的优化(sql、表结构)
4.相关的金融知识
5.dubbo服务禁用

项目中几个亮点

1.分布式锁
2.分布式框架dubbo 业务拆分
3.分库分表
4.redis、mysql、zookeeper集群
5.数据库的优化(sql、表结构)

什么时候会full gc

1.调用 System.gc 时,系统建议执行 Full GC,但是不必然执行。
2.老年代空间不足。
3.方法区空间不足。
4.通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存。
6.由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

如何自己实现消息队列

1.信号量
2.线程池 生产者‘消费者模式

内存泄露是什么?有什么危害?怎么解决?

内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

1.频繁的gc
2.导致系统奔溃

1.对于不再需要使用的对象,显示的将其赋值为null
2.用完及时清除释放

mysql 内关联、左右链接、 union 、 union all

union :两个结果集合并为一个,取唯一值,记录没有重复
union all:两个结果集合并为一个,取到得是所有值,记录可能有重复

你可能感兴趣的:(JAVA)