这篇3万字的Java后端面试总结,面试官看了瑟瑟发抖(汇总)

「这篇总结我已经导出成pdf版的了,后台回复"总结"即可获取pdf版本哦~」

HashMap源码

问:HashMap底层原理,为什么线程不安全。

hashmap:
数组+ 链表 + 红黑树
初始长度 = 16
扩容因子 = 0.75
索引确定:
index  = hashCode(key) % length
hashCode(key) 高8位与低8位异或 & (length - 1)

关于线程不安全

HashMap会进行resize操作,在resize操作的时候会造成线程不安全。下面将举两个可能出现线程不安全的地方。

put的时候导致的多线程数据不一致。这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)

问:HashMap与Hashtable的区别

1、继承的父类不同

Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

2、线程安全性不同

javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。

4、key和value是否允许null值

其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。

通过上面的ContainsKey方法和ContainsValue的源码我们可以很明显的看出:

Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

ConcurrentHashMap源码

问:ConcurrentHashMap底层原理,如何保证线程安全的

这里只讨论JDK1.8的ConcurrentHashMap

采用了数组+链表+红黑树的实现方式来设计。

采用Node节点保存key,value及key的hash值。如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    ...
 }

为保证线程安全,采用synchronized+CAS+HashEntry+红黑树

无锁化保证线程安全。

我们看到put方法调用了casTabAt方法。

private static final sun.misc.Unsafe U;


static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}


public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

会后调用的是本地放法。

问:CAS底层原理

引用来源:https://youzhixueyuan.com/concurrenthashmap.html

CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

AQS原理

问:AQS底层以及相关的类

见文章:Java并发编程初探-AQS

问:ThreadLocal底层,软引用和弱引用

引用来源:使用ThreadLocal怕内存泄漏?那你应该来看看这篇文章

线程池

问:线程池组成原理,线程池的拒绝策略

见文章:手写线程池

问:如和理解多线程,高并发

引用来源:https://www.cnblogs.com/cheyunhua/p/10530023.html

高并发可以通过分布式技术去解决,将并发流量分到不同的物理服务器上。但除此之外,还可以有很多其他优化手段:比如使用缓存系统,将所有的,静态内容放到CDN等;还可以使用多线程技术将一台服务器的服务能力最大化。

多线程是指从软件或者硬件上实现多个线程并发执行的技术,它更多的是解决CPU调度多个进程的问题,从而让这些进程看上去是同时执行(实际是交替运行的)。

这几个概念中,多线程解决的问题是最明确的,手段也是比较单一的,基本上遇到的最大问题就是线程安全。在JAVA语言中,需要对JVM内存模型、指令重排等深入了解,才能写出一份高质量的多线程代码。

问:如果有个线程4,要等前面线程1,2,3都执行完才能执行,你要怎么做

示例:

/**
 * Description:倒计数器
 *
 * @author Lvshen
 * @version 1.0
 * @date: 2020/4/16 14:28
 * @since JDK 1.8
 */
public class CountDownLatchDemo {
    static final int COUNT = 20;

    static CountDownLatch cdl = new CountDownLatch(COUNT);

    public static void main(String[] args) throws Exception {
        new Thread(new Teacher(cdl)).start();
        Thread.sleep(1);
        for (int i = 0; i < COUNT; i++) {
            new Thread(new Student(i, cdl)).start();
        }
        synchronized (CountDownLatchDemo.class) {
            CountDownLatchDemo.class.wait();
        }
    }

    static class Teacher implements Runnable {

        CountDownLatch cdl;

        Teacher(CountDownLatch cdl) {
            this.cdl = cdl;
        }

        @Override
        public void run() {
            System.out.println("老师发卷子。。。");
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("老师收卷子。。。");
        }

    }

    static class Student implements Runnable {

        CountDownLatch cdl;
        int num;

        Student(int num, CountDownLatch cdl) {
            this.num = num;
            this.cdl = cdl;
        }

        @Override
        public void run() {
            System.out.println(String.format("学生(%s)写卷子。。。",num));
            //doingLongTime();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(String.format("学生(%s)写卷子。。。",num));
            cdl.countDown();
        }

    }
}

public class ConcurrentTestDemo {

    public static void main(String[] args) {

        //并发数
        int currency = 20;

        //循环屏障
        CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);

        for (int i = 0; i < currency; i++) {
            new Thread(() -> {
                OrderServiceImplWithDisLock orderService = new OrderServiceImplWithDisLock();
                System.out.println(Thread.currentThread().getName() + "====start====");
                //等待一起出发
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                orderService.createOrder();
            }).start();
        }
    }
}

问:i++ 要线程安全,你有几种方法,这几种哪种性能最好

volatile + sychronized/lock.lock , AtomicInteger高并发下lock.lock性能要好,atomicInteger.incrementAndGet()我不知道新能怎么么样,我只知道底层用了CASlock.lock底层也用了CAS

public class VolatileAtomicTest {
    public static volatile int num = 0;

    public synchronized static void increase() {
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });

            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println(num);
    }
}

问:线程间的共享怎么实现

Callable的call方法有返回值;volatile关键字能实现线程变量的可见

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Callable<String> callable = () -> {
        log.info("当前线程:{}", Thread.currentThread().getName());
        return "Lvshen";
    };

    //MyFutureTask<String> myFutureTask = new MyFutureTask(callable);

    FutureTask<String> myFutureTask = new FutureTask<>(callable);
    new Thread(myFutureTask).start();

    System.out.println(String.format("当前线程:[%s],取出的值:[%s]", Thread.currentThread().getName(), myFutureTask.get()));
}

如上代码:Main线程获取到[Thread-0]线程的值。

问:ThreadPoolExecutor参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:当没有任务时,多余核心线程数的线程存活时间

Java锁

问:synchronized与Lock的区别

见文章:Java锁-synchronized底层原理

问:volatile关键字原理,以及原子自增类

见文章:Java关键字——volatile底层原理分析

问:JUC并发包

JDK并发工具类是JDK1.5引入的一大重要的功能,体现在java.util.concurrent包下。java.util.concurrent包主要包含了并发集合类,线程池信号量三组重要工具类,还包括了java.util.concurrent.atomic以及java.util.concurrent.locks两个子包。一般来说,我们称这个包为J.U.C。

JVM

问:Java虚拟机内存划分,GC回收算法。

关于内存划分:

可以看看这篇文章:https://mp.weixin.qq.com/s/fit90VdZUa2pG9lbET0i7w

关于GC回收算法:

见文章:GC回收算法

问:内存溢出如何排查

见文章:https://mp.weixin.qq.com/s/7XGD-Z3wrThv5HyoK3B8AQ

问:虚拟机调优

见视频:https://ke.qq.com/user/index/index.html#/plan/cid=2770807&term_id=102879437

问:类加载

 

算法

问:雪花算法,原理知道吗,有没有缺点。

long id2Long = ((nowTimestamp - baseTimestamp) << TIMESTAMP_LEFT_SHIFT) | workId | sequence;

ip + 端口 + 时间戳 。 跟机器时间有关,如果机器时间回调,会生成重复的ID。

问:说说二叉树,与B+Tree的区别

见文章:MySQL为什么选择B+Tree做索引

问:红黑树和哈希表使用场景

Hash:

hash表使用场景:bitmap的布隆过滤器使用的是hash表。在那些需要一次一次遍历,去寻找元素的问题中,可以将问题转化为根据元素的内容去寻找索引,哈希表在这方面的时间效率是贼高的;在一些字符串词频统计问题、数独问题等问题中,可以利用哈希函数来计算某个元素出现的次数,作为算法的辅助工具;还有些问题,可以利用散列函数的思路,让几个不同的元素获得同样的结果,从而实现一个聚类。

举个用于消息摘要例子,银行的数据库中是不能保存用户密码的原文的,只能保存密码的hash值。在这种应用场景里,对于抗碰撞和抗篡改能力要求极高,对速度的要求在其次。一个设计良好的hash算法,其抗碰撞能力是很高的。

红黑树:

  • epoll的事件管理模块

  • Java中的TreeMap

  • 适用增删比较多的情况

  • AVL适用查询比较多的情况

  • 相对于跳表,红黑树不适用与范围性的查找

MySQL相关

索引优化

问:索引优化,最左原则是什么?原理知不知道;(id name age)组合索引     where id = ,name = , age> 索引失效么。你怎么看explain执行计划。

1、在MySQL中,进行条件过滤时,是按照向右匹配直到遇到范围查询(>,<,between,like)就停止匹配,比如说a = 1 and b = 2 and c > 3 and d = 4 如果建立(a, b, c, d)顺序的索引,d是用不到索引的,如果建立(a, b, d, c)索引就都会用上,其中a,b,d的顺序可以任意调整。

2、= 和 in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立(a, b, c)索引可以任意顺序,MySQL的查询优化器会优化索引可以识别的形式。

 

问:创建索引时,需要考虑哪些因素

主键 自增,要int类型,不要频繁修改。索引不是越多越好,以前公司要求最多5个索引,以常用查询字段建立索引

问:接口慢,怎么优化

  • [ ] arthas查看调用链耗时

  • [ ] 看接口有没有远程调用,远程调用这个网络耗时要考虑进去

  • [x] 调用的远程接口有没有问题,有问题的话,远程接口也需要优化

  • [ ] 方法中的代码有没有问题,比如,循环里面查库了,一个数据多次查库了,全表查询了

  • [x] sql有没有用到索引

  • [ ] 上面的检查都没问题,考虑使用缓存(读多写少用缓存,写多读少用队列)

  • [ ] 还可考虑数据库的主从,读写分离

问:MySQL如果没有定义主键,会创建主键索引吗。有哪几种存储引擎

#存储引擎
Innodb
MyIsam

如果表没有创建主键,如果有唯一键,会用唯一键字段创建主键
如果没有唯一键,则用一个隐式的rowid创建主键索引

问:MySQL回表

普通索引的叶子节点保存的主键的值,通过普通索引查询的值,还需要到主键索引中去查一遍,这就叫回表

问:聚集索引与非聚集索引

  • 聚集索引:叶子节点村的是数据

  • 非聚集索引:叶子节点存的是数据的地址

问:索引分类

主键索引,普通索引,唯一索引,联合索引

问:B+Tree 与Hash的优缺点

  • Hash,单个查询最坏时间复杂度,但是不能进行范围查询

  • B+Tree,可以范围查询,能存更多的数据

使用场景

问:怎么找到最大 age的数值

--索引失效SELECT MAX(`code`) from member
EXPLAIN SELECT * FROM member WHERE code = (SELECT MAX(`code`) from member);

--两个 表的查询都不是失效
EXPLAIN SELECT * FROM member WHERE code = (SELECT `code` FROM member ORDER BY `code` DESC LIMIT 1);

问:MySQL中,char_length() 与length()区别

char_length() : 一般判断中文长度
length() : 一般判断英文长度

问:如何分库分表

来源:https://www.imooc.com/article/301836

垂直分库分表:

垂直分库是基于业务分类的,和我们常听到的微服务治理观念很相似,每一个独立的服务都拥有自己的数据库,需要不同业务的数据需接口调用。而垂直分库也是按照业务分类进行划分,每个业务有独立数据库。

垂直分表是基于数据表的列为依据切分的,是一种大表拆小表的模式。

例如:一个order表有很多字段,把长度较大且访问不频繁的字段,拆分出来创建一个单独的扩展表work_extend进行存储。

拆分前:

order表:

id workNo price describe
int(12) int(2) int(15) varchar(2000)  

拆分后:

order核心表:

id workNo price
int(12) int(2) int(15)  

work_extend表:

id workNo describe
int(12) int(2) varchar(2000)  

水平切分:

水平切分将一张大数据量的表,切分成多个表结构相同,而每个表只占原表一部分数据,然后按不同的条件分散到多个数据库中。

假如一张order表有2000万数据,水平切分后出来四个表,order_1order_2order_3order_4,每张表数据500万,以此类推。

order_1表:

id workNo price describe
int(12) int(2) int(15) varchar(200  

order_2

id workNo price describe
int(12) int(2) int(15) varchar(200  

order_3

id workNo price describe
int(12) int(2) int(15) varchar(200  

order_4

id workNo price describe
int(12) int(2) int(15) varchar(200  

问:如何将10万条数据导入MySQL

// 外层循环,总提交事务次数
    for (int i = 1; i <= 100; i++) {
        suffix = new StringBuffer();
        // 第j次提交步长
        for (int j = 1; j <= 10000; j++) {
            // 构建SQL后缀
            suffix.append("('" + uutil.UUIDUtil.getUUID()+"','"+i*j+"','123456'"+ ",'男'"+",'教师'"+",'www.bbk.com'"+",'XX大学'"+",'"+"2020-08-12 14:43:26"+"','备注'" +"),");
        }
        // 构建完整SQL
        String sql = prefix + suffix.substring(0, suffix.length() - 1);
        // 添加执行SQL
        pst.addBatch(sql);
        // 执行操作
        pst.executeBatch();
        // 提交事务
        conn.commit();
        // 清空上一次添加的数据
        suffix = new StringBuffer();
    }

如上面伪代码,分批次insert即可。

问:怎么查询成绩第二的学生

-- 子查询索引没有失效
EXPLAIN SELECT * FROM member m WHERE m.`code` = 
(SELECT m2.`code` FROM member m2 WHERE m2.`code` < (SELECT m1.`code` FROM member m1 ORDER BY m1.`code` DESC LIMIT 1) ORDER BY m2.`code` DESC LIMIT 1);

-- 使用max()里面的子查询索引会失效
EXPLAIN SELECT * FROM member m WHERE m.`code` = (SELECT MAX(m2.code) FROM member m2 WHERE m2.code < (SELECT MAX(m1.code) FROM member m1));

执行计划

问:MySQL explain执行计划

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL 

EXPLAIN SELECT * from member ORDER BY id;    #index

EXPLAIN SELECT * from member WHERE code < 100 and code <120;  #range

EXPLAIN SELECT * from member WHERE name < '1' and name <'3';  #range

EXPLAIN SELECT * from member WHERE code = '99';  #ref

EXPLAIN SELECT id from member WHERE name = '99';  #ref

EXPLAIN SELECT * from member WHERE id = '1';  #const

事务

问:开启了两个事务,a事务里面作修改未提交,b事务里面做新增会怎样

可以新增,只有修改同一条数据时会被锁住

问:MySQL默认事务隔离级别,以及哪些事务隔离级别

未提交读
已提交读
可重复读 (Mysql默认)
序列化

中间件

Kafka相关

问:怎么保证kafka消息的顺序性,kafka消费端数据不丢失

顺序性:

如上图,分生产者顺序发送,和消费者顺序消费

对于顺序发送,我们需要知道,当数据写入一个partition时,可以保证顺序性,所以如果有一批数据需要保证顺序,那么给这批数据指定一个key即可。

public ListenableFuture<SendResult<K, V>> send(String topic, K key, @Nullable V data) {
        ProducerRecord<K, V> producerRecord = new ProducerRecord(topic, key, data);
        return this.doSend(producerRecord);
    }

如上代码,调用KafkaTemplate的这个send方法。

对于顺序消费,需要将一个partition的数据发送到一个暂存队列中,然后再将这个队列仍给一个线程,这样保证顺序的数据是一个线程获取的。

消费数据不丢失:

消费数据丢失情况可能是,消费者已经拿到数据,将offset提交给了kafka或者zookeeper,但是这个数据还没有实际使用,比如保存到数据库中。这个时候消费者服务挂掉。当消费者服务重启时。导致数据没有存入库中,但offset显示已经消费,消费者再次去消费,拿不到数据了。关于解决办法:将自动提交offset改为手动提交,只有业务正真结束才提交offset。

问:Kafka实现分布式事务

如果服务A调用服务B,那么服务A就是生产者,服务B就是消费者。

如上图,不一定是kafka,消息中间件都可以处理分布式事务问题。在消息中间件处理数据过程中,并不需要处理事务回滚问题。我们需要保证两件事:

1.生产者数据发到kafka一定成功
2.消费者从kafka消费数据一定成功

如何让保证数据百分百发送到kafka?将生产的数据存一份到数据做兜底,如果生产者收不到kafka的回执,采用重试机制,将数据库中的数据发送到kafka中,直到成功,修改数据库中的数据发送状态。

由于这种方式是异步的,生产者和消费者是非耦合的,所以消费者自行从kafka中消费数据。消费成功,消费者会提交偏移量给Zookeeper(新版本是消费者将偏移量提交给kafka了)。

问:kafka为什么快,以及如何选型消息队列

为什么快:

  • 顺序读写

  • NIO多路复用模型,减少系统调用

  • 零拷贝,减少系统调用

选型:

特性 ActiveMQ RabbitMQ RocketMQ Kafka
单机吞吐量 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 10万级,RocketMQ也是可以支撑高吞吐的一种MQ 10万级别,这是kafka最大的优点,就是吞吐量高。    一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic数量对吞吐量的影响     topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降     这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic topic从几十个到几百个的时候,吞吐量会大幅度下降     所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源
时效性 ms级 微秒级,这是rabbitmq的一大特点,延迟是最低的 ms级 延迟在ms级以内
可用性 高,基于主从架构实现高可用性 高,基于主从架构实现高可用性 非常高,分布式架构 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性 有较低的概率丢失数据   经过参数优化配置,可以做到0丢失 经过参数优化配置,消息可以做到0丢失
功能支持 MQ领域的功能极其完备 基于erlang开发,所以并发能力很强,性能极其好,延时很低 MQ功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准
优劣势总结 非常成熟,功能强大,在业内大量的公司以及项目中都有应用     偶尔会有较低概率丢失消息     而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本     而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 erlang语言开发,性能极其好,延时很低;    吞吐量到万级,MQ功能比较完备     而且开源提供的管理界面非常棒,用起来很好用     社区相对比较活跃,几乎每个月都发布几个版本分     在国内一些互联网公司近几年用rabbitmq也比较多一些     但是问题也是显而易见的,RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。    而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。    而且rabbitmq集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和掌控。 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障     日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景     而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控     社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码     还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ挺好的 kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展     同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量     而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略     这个特性天然适合大数据实时计算以及日志收集

总结:ActiveMQ较老的系统会使用,现在社区不活跃,不推荐使用;

RabbitMQ比较稳定,吞吐量万级别,适合小公司,小业务系统使用;

RocketMQ吞吐量10万级别,适合高并发,阿里开源,社区活跃;

Kafka吞吐量10万级别,适合日志采集,大数据实时计算。非常稳定,社区活跃。

Redis相关

问:Redis发布订阅使用场景

见文章:Redis发布订阅

问:Redis分布式锁底层怎么实现

1. setnx + 过期时间 用lua脚本保证原子性

2. 锁持有心跳检测(防止未解锁,锁失效问题)

3. 线程自选获取锁

Redisson框架已有实现

问:怎么处理缓存雪崩,缓存穿透的场景

见文章:Redis进阶

问:限流操作

见文章:用Redis实现接口限流

问:分布式缓存与JVM的缓存区别

JVM缓存如List,Map。在单机内有效。如果集群部署,就会使缓存失效,需要全局的缓存,所以需要使用Redis等缓存中间件。

问:接口幂等如何实现

Github地址:https://github.com/lvshen9/demo-lvshen/tree/master/src/main/java/com/lvshen/demo/autoidempotent

问:Redis分布式锁主从同步问题

RedLock算法解决主从同步问题。

问:Session共享

Redis实现,已经有成熟的API,可集成SpringBoot。

jar包 引入

<!--session redis-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

直接使用,Session已经存入Redis中了。

@RequestMapping(value = "/testSession",method = RequestMethod.GET)
public String testSession(HttpSession session, Model model) {
    List<Member> members = memberService.listMember();
    System.out.println("sessionId------>" + session.getId());
    model.addAttribute("member", JSON.toJSONString(members));
    session.setAttribute("member",JSON.toJSONString(members));
    return "hello world";
}

Zookeeper

问:Zookeeper使用场景

见文章:手写Zookeeper分布式锁

问:Zookeeper底层结构,选举原理,最小集群数

文章来源:https://www.cnblogs.com/wuzhenzhao/p/9983231.html

Zookeeper本质还是一个存储容器,以节点的形式存储数据。

在 ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型。在 ZooKeeper中,节点类型可以分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)三大类,具体在节点创建过程中,通过组合使用,可以生成以下四种组合型节点类型:

  1. 持久节点(PERSISTENT):持久节点是 ZooKeeper中最常见的一种节点类型。所谓持久节点,是指该数据节点被创建后,就会一直存在于 ZooKeeper服务器上,直到有删除操作来主动清除这个节点。

  2. 持久顺序节点(PERSISTENT SEQUENTIAL):持久顺序节点的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中, ZooKeeper会自动为给定节点名加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。

  3. 临时节点(EPHEMERAL):和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。注意,这里提到的是客户端会话失效,而非TCP连接断开。另外, ZooKeeper规定了不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。

  4. 临时顺序节点(EPHEMERAL SEQUENTIAL):临时顺序节点的基本特性和临时节点也是一致的,同样是在临时节点的基础上,添加了顺序的特性。

刚启动时的选举:

Leader 选举会分两个过程启动的时候的 leader 选举、 leader 崩溃的时候的的选举服务器启动时的 leader 选举每个节点启动的时候状态都是 LOOKING,处于观望状态,接下来就开始进行选主流程进行 Leader 选举,至少需要两台机器,我们选取 3 台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器 Server1 启动时,它本身是无法进行和完成 Leader 选举,当第二台服务器 Server2 启动时,这个时候两台机器可以相互通信,每台机器都试图找到 Leader,于是进入 Leader 选举过程。选举过程如下:

(1) 每个 Server 发出一个投票。由于是初始情况,Server1和 Server2 都会将自己作为 Leader 服务器来进行投票,每次投票会包含所推举的服务器的 myid 和 ZXID、epoch,使用(myid, ZXID,epoch)来表示,此时 Server1的投票为(1, 0),Server2 的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自LOOKING状态的服务器。

(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行 PK,PK 规则如下

i. 优先检查 ZXID。ZXID 比较大的服务器优先作为Leader

ii. 如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为 Leader 服务器。

对于 Server1 而言,它的投票是(1, 0),接收 Server2的投票为(2, 0),首先会比较两者的 ZXID,均为 0,再比较 myid,此时 Server2 的 myid 最大,于是更新自己的投票为(2, 0),然后重新投票,对于 Server2 而言,它不需要更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

(4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于 Server1、Server2 而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了 Leader。

(5) 改变服务器状态。一旦确定了 Leader,每个服务器就会更新自己的状态,如果是 Follower,那么就变更为FOLLOWING,如果是 Leader,就变更为 LEADING。

运行时的选举:

当集群中的 leader 服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的Leader 选举,服务器运行期间的 Leader 选举和启动时期的 Leader 选举基本过程是一致的。

(1) 变更状态。Leader 挂后,余下的非 Observer 服务器都会将自己的服务器状态变更为 LOOKING,然后开始进入 Leader 选举过程。

(2) 每个 Server 会发出一个投票。在运行期间,每个服务器上的 ZXID 可能不同,此时假定 Server1 的 ZXID 为123,Server3的ZXID为122;在第一轮投票中,Server1和 Server3 都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票。与启动时过程相同。

(3) 处理投票。与启动时过程相同,此时,Server1 将会成为 Leader。

(4) 统计投票。与启动时过程相同。  (5) 改变服务器的状态。与启动时过程相同

问:ElasticSearch有用怎创建索引的么

这里我们不说原理,我们来说Java API怎么创建索引。

举个例子:

/**
     * 创建索引库
     * 
     * @author lvshen  
     * @date 2020年08月01日
     * 
     * 需求:创建一个索引库为:msg消息队列,类型为:tweet,id为1
     * 索引库的名称必须为小写
     * @throws IOException 
     */
    @Test
    public void addIndex() throws IOException {
        IndexResponse response = client.prepareIndex("msg", "tweet", "1").setSource(XContentFactory.jsonBuilder()
                .startObject().field("userName", "lvshen")
                .field("sendDate", new Date())
                .field("msg", "Lvshen的技术小屋")
                .endObject()).get();
        
        logger.info("索引名称:" + response.getIndex() + "\n类型:" + response.getType()
                    + "\n文档ID:" + response.getId() + "\n当前实例状态:" + response.status());
    }

秒杀设计

问:如何设计一个秒杀系统

这个是面试阿里的时候问的一个问题。问题比较范。要考虑

并发情况下数据库能不能扛住,Mysql 最高并发估计在万级别。如果秒杀用户上千万以上,要考虑分库分表,读写分离,使用缓存,还有使用消息中间件在高峰时期削峰填谷(并发串行化)。也要考虑服务容错,服务降级,以实现系统高可用
考虑线程安全,扣减库存,线程安全问题,防止多扣,可使用数据库状态机思想,但要考虑批量修改时死锁问题。可使用Redis原子性,做库存扣减。

下面总结下需要注意的点:

1.不能超卖(直接的经济损失,平台信誉受损

2.防黑产、黄牛(阿里月饼门):机器的请求速度比人的手速快太多了

3.瞬间爆发的高流量

  • 典型的读多写少的场景(cache缓存)

  • 页面静态化,利用cdn服务器缓存前端文件

  • 按钮置灰3秒、(利用风控规则过滤掉非法用户)

  • 接口层可以做开关限流(一旦抢购结束则直接返回失败)

  • 堆机器,搭建集群利用nginx做负载均衡

  • 热点隔离增加资源有限放流(熔断)多次请求合并为一次

4.尽量把请求拦截在上层,Mysql单机读能力为5k,写能力为3k。redis单机读能力最高可达10w,写能力能达到3-5W。

5.预热,运营人员提前将数据写入Redis。

秒杀流程图:

graph LR

A[秒杀开始] --> B[用户点击秒杀链接]
   B --> C[抢购资格]
   C --> | 否| D>结束并且拉黑]
   C --> | 是 | E[库存是否足够]
   E --> | 是 | F>生成订单]
   E --> | 否 | G>活动结束]
   G --> H{修改Redis的活动状态}

文章推荐:https://mp.weixin.qq.com/s/Q8dWP5c0TJH8fqQdslSTKg

Linux

问:linux中怎么看日志,怎么看进程,怎么看磁盘大小,怎么看内存大小

#看日志
tail -f xx.log
#看进程
jps   / ps -ef | grep xx     /netstat -tnlp | grep xx
#看磁盘大小
du   /  df
#看内存大小
free

更多Linu命令见文章:我在工作中用到的Linux命令

Spring相关

问:Spring事务  A,B 。A调B, A异常,B会回滚么

事务传播类型:

务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

默认传播类型为:PROPAGATION_REQUIRED,上面问题,A,B会合并成一个事务,所以A异常,B会回滚,B异常,A会回滚。

问:Spring中 @Autowired与@Resource区别

  • @Autowired是Spring的注解,Autowired默认先按 byType,如果发现找到多个 bean,则,又按照 byName方式比对,如果还有多个,则报出异常;

  • @Resource 是JDK1.6支持的注解,默认按照名称( ByName)进行装配, 如果没有指定 name属性,当注解写在字段上时,默认取字段名,按照名称查找,如果注解写在 setter方法上默认取属性名进行装配。当找不到与名称匹配的 bean时才按照类型进行装配。

推荐文章:https://mp.weixin.qq.com/s/IglQITCkmx7Lpz60QOW7HA

问:单体服务到微服务的演变史

演变:单体服务 -> SOA -> 微服务

单体服务

概念:所有功能全部打包在一起。应用大部分是一个war包或jar包。我参与网约车最开始架构是:一个乘客项目中有 用户、订单、消息、地图等功能。随着业务发展,功能增多,这个项目会越来越臃肿。

好处:容易开发、测试、部署,适合项目初期试错。

坏处:

随着项目越来越复杂,团队不断扩大。坏处就显现出来了。

  • 复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们在一起。

  • 技术债务:人员流动,不坏不修,因为不敢修。

  • 持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间 功能修改多,越不敢部署,恶性循环。

  • 可靠性差:某个小问题,比如小功能出现OOM,会导致整个应用崩溃。

  • 扩展受限:只能整体扩展,无法按照需要进行扩展,  不能根据计算密集型(派单系统)和IO密集型(文件服务) 进行合适的区分。

  • 阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用spring MVC,想换成spring boot,切换成本很高,因为有可能10万,百万行代码都要改,而微服务可以轻松切换,因为每个服务,功能简单,代码少。

SOA

对单体应用的改进:引入SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在于操作系统进程中。各个服务之间 通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。

所以要引入微服务,是SOA思想的一种具体实践。

微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想

微服务

  • 无严格定义。

  • 微服务是一种架构风格,将单体应用划分为小型的服务单元。

  • 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式;每个服务运行在自己的进程中;服务间采用轻量级的方式进行通信(通常是HTTP API);这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。

那么微服务有哪些特性呢:

独立运行在自己进程中。

一系列独立服务共同构建起整个系统。

一个服务只关注自己的独立业务。

轻量的通信机制RESTful API。

使用不同语言开发。

全自动部署机制

微服务优点

  1. 独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自己的影响。

  2. 易于开发和维护:关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。

  3. 启动块:功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。

  4. 局部修改容易:只需要部署 相应的服务即可,适合敏捷开发。

  5. 技术栈不受限:java,node.js等

  6. 按需伸缩:某个服务受限,可以按需增加内存,cpu等。

  7. 职责专一。专门团队负责专门业务,有利于团队分工。

  8. 代码复用。不需要重复写。底层实现通过接口方式提供。

  9. 便于团队协作:每个团队只需要提供API就行,定义好API后,可以并行开发。

微服务缺点

  1. 分布式固有的复杂性:容错(某个服务宕机),网络延时,调用关系、分布式事务等,都会带来复杂。

  2. 分布式事务的挑战:每个服务有自己的数据库,有点在于不同服务可以选择适合自身业务的数据库。订单用MySQL,评论用Mongodb等。目前最理想解决方案是:柔性事务的最终一致性。

  3. 接口调整成本高:改一个接口,调用方都要改。

  4. 测试难度提升:一个接口改变,所有调用方都得测。自动化测试就变的重要了。API文档的管理也尤为重要。推荐:yapi。

  5. 运维要求高:需要维护 几十 上百个服务。监控变的复杂。并且还要关注多个集群,不像原来单体,一个应用正常运行即可。

  6. 重复工作:比如java的工具类可以在共享common.jar中,但在多语言下行不通,C++无法直接用java的jar包。

什么是刚性事务?

刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

关于如何设计划分服务,我觉得可以学习下DDD领域驱动设计,有很好的指导作用。

问:AOP怎么实现的Redis缓存注解

1.定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizeCache {

    String key();

    String value();

    long expireTimes() default 120L; //默认过期时间120s

    int semaphoreCount() default Integer.MAX_VALUE;  //默认限制线程并发数
}

2.AOP切面编程

@Component
@Aspect
@Slf4j
public class CacheAspect {
    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.lvshen.demo.redis.cache.CustomizeCache)")
    public void cachePointcut() {
    }

    @Around("cachePointcut()")
    public Object doCache(ProceedingJoinPoint point) {
        Object value = null;
        Semaphore semaphore = null;
        MethodSignature signature = (MethodSignature) point.getSignature();

        try {
            //获取方法上注解的类容
            Method method = point.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
            CustomizeCache annotation = method.getAnnotation(CustomizeCache.class);
            String keyEl = annotation.key();
            String prefix = annotation.value();
            long expireTimes = annotation.expireTimes();
            int semaphoreCount = annotation.semaphoreCount();

            //解析SpringEL表达式
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(keyEl);
            StandardEvaluationContext context = new StandardEvaluationContext();

            //添加参数
            Object[] args = point.getArgs();
            DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
            String[] parameterNames = discoverer.getParameterNames(method);
            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i].toString());
            }

            //解析
            String key = prefix + "::" + expression.getValue(context).toString();

            //判断缓存中是否存在
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                log.info("从缓存中读取到值:{}", value);
                return value;
            }

            //自定义组件,如:限流,降级。。。
            //创建限流令牌
            semaphore = new Semaphore(semaphoreCount);
            boolean tryAcquire = semaphore.tryAcquire(3000L, TimeUnit.MILLISECONDS);
            if (!tryAcquire) {
                //log.info("当前线程【{}】获取令牌失败,等带其他线程释放令牌", Thread.currentThread().getName());
                throw new RuntimeException(String.format("当前线程【%s】获取令牌失败,等带其他线程释放令牌", Thread.currentThread().getName()));
            }

            //缓存不存在则执行方法
            value = point.proceed();

            //同步value到缓存
            redisTemplate.opsForValue().set(key, value, expireTimes, TimeUnit.SECONDS);


        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            if (semaphore == null) {
                return value;
            } else {
                semaphore.release();
            }
        }
        return value;
    }
}

3.使用

@CustomizeCache(value = "member", key = "#name")
public List<Member> listByNameSelfCache(String name) {
    return memberMapper.listByName(name);
}

问:Spring Boot注解

@EnableAutoConfiguration:是自动配置的注解;

@Configuration:用于定义配置类;

@ConditionalOnBean(A.class):仅仅在当前上下文中存在A对象时,才会实例化一个Bean;

关于Conditional开头的注解还有很多,有兴趣的可以去Spring官网:https://spring.io/projects/spring-boot

或者SpringBoot中文社区看看:https://springboot.io/

问:Bean的生命周期

简略的说,单列Bean生命随容器存在而存在。非单例的Bean不引用时就会被垃圾回收器回收。

问:AOP原理

AOP的思想是,不去动原来的代码,而是基于原来代码产生代理对象,通过代理的方法,去包装原来的方法,就完成了对以前方法的增强。AOP的底层原理就是动态代理的实现。

关于AOP的使用,比如我之前用AOP思想做的缓存注解等。

切入点:通过一个表达式告诉SpringAOP去哪个地方进行增强。也可以把这个表达式理解为一个查询条件,系统会根据这个查询条件查询到我们要进行增强的代码位置。

连接点:就是SpringAOP通过告诉它的切入点的位置找的的具体的要增强的代码的位置,这个代码位置就是连接点。

切面:切面由一组(增强处理和切入点)共同构成。

目标对象:目标对象就是被增强的目标类。我们也称之为委托类。

AOP代理:代理类就是AOP代理,里面包含了目标对象以及一些增强处理。系统会用AOP代理类代替委托类去执行功能。

织入:织入就是将我们的增强处理增强到指定位置的过程。

具体使用可以看看问题:如何用AOP实现缓存注解的代码。

问:Spring的动态代理

动态代理其实就是Java中的一个方法,这个方法可以实现:动态创建一组指定的接口的实现对象(在运行时,创建实现了指定的一组接口的对象)

分为JDK动态代理和Cglib动态代理

当目标对象实现了接口,默认使用JDK动态代理,也可以强制使用Cglib动态代理。

当目标对象没有实现接口,必须使用Cglib动态代理。

下面是代码:

public interface UserService {
    void addUser(String name, String password);

    void delUser(String name);
}


public class UserServiceImpl implements UserService{
    @Override
    public void addUser(String name, String password) {
        System.out.println("调用addUser()...");
        System.out.println(String.format("参数为:name[%s],password[%s]",name,password));
    }

    @Override
    public void delUser(String name) {
        System.out.println("调用delUser()");
        System.out.println(String.format("参数为:name[%s]",name));
    }
}

JdkProxy

public class JdkProxy  implements InvocationHandler {
    //需要代理的目标对象
    private Object target;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理,监听开始...");
        Object invoke = method.invoke(target, args);
        System.out.println("JDK动态代理,监听结束...");
        return invoke;
    }

    public Object getJdkProxy(Object targetObject) {
        this.target = targetObject;
        //实例化
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
    }
}

测试JdKProxy

@org.junit.Test
public void testJdkProxy() {
    JdkProxy jdkProxy = new JdkProxy();
    UserService userService = (UserService) jdkProxy.getJdkProxy(new UserServiceImpl());

    userService.addUser("lvshen","123456");

}

CglibProxy

public class CglibProxy implements MethodInterceptor {
    private Object target;
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB动态代理,监听开始...");
        Object invoke = method.invoke(target, objects);
        System.out.println("CGLIB动态代理,监听结束...");
        return invoke;
    }

    public Object getCglibProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        //指定父类
        enhancer.setSuperclass(target.getClass());

        enhancer.setCallback(this);
        Object result = enhancer.create();
        return result;
    }
}

测试CglibProxy

public class Test {
    @org.junit.Test
    public void testCglibProxy() {
        CglibProxy cglibProxy = new CglibProxy();
        UserService service = (UserService) cglibProxy.getCglibProxy(new UserServiceImpl());
        service.addUser("zhouzhou","654321");
    }
}

问:Spring与SpringBoot的区别

SpringBoot特点

  • 用来实现微服务;

  • 自动配置

  • 自定义配置

  • 模块化

  • 独立打包 直接运行

  • 内嵌服务器

问:springboot中bootstrap.properties与application.properties的区别

区别:

  • application.properties(application.yml)系统级别的一些参数配置,这些参数一般是不会变动的

  • bootstrap.properties(bootstrap.yml)定义应用级别的配置

SpringBoot 有两种上下文:

  • bootstrap:应用程序的父上下文

  • application:应用程序上下文

bootstrap 加载优先于 applicaton

bootstrap 里面的属性会优先加载,默认也不能被本地相同配置覆盖

应用场景:

  • 使用 Spring Cloud Config Server时,在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息。eg:指定spring.application.name和 spring.cloud.config.server.git.uri

  • 一些固定的不能被覆盖的属性

  • 一些加密/解密的场景

问:applicationContext与beanFactory的区别

两者都能获取bean.

beanFactory:懒加载,调用getBean是才实例化对象

applicationContext:预加载,启用applicationContext就实例化对象了

ApplicationContext 包含 BeanFactory 的所有特性,通常推荐使用前者。但是也有一些限制情形,比如移动应用内存消耗比较严苛,在那些情景中,使用更轻量级的 BeanFactory 是更合理的。然而,在大多数企业级的应用中,ApplicationContext 是你的首选。

    public class HelloWorldApp {
        public static void main(String[] args) {
            XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
            HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
            obj.getMessage();
        }
    }

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
    obj.getMessage();
}
   

MyBatis

问:Mybatis中 #{} 与 ${}的区别

使用#{parameterName}引用参数的时候,Mybatis会把这个参数认为是一个字符串,并自动加上'',例如传入参数是“Smith”,那么在下面SQL中:

Select * from emp where name = #{employeeName}

使用的时候就会转换为:

Select * from emp where name = 'Smith'; 

同时使用${parameterName}的时候在下面SQL中

Select * from emp where name = ${employeeName}

就会直接转换为:

Select * from emp where name = Smith

简单说**#{}是经过预编译的,是安全的**。而**${}**是未经过预编译的,仅仅是取变量的值,是非安全的,存在SQL注入

sql注入问题:

当使用#{}时

DEBUG [http-nio-8080-exec-5] - ==>  Preparing: select * from user where account = ? and password = ?  DEBUG [http-nio-8080-exec-5] - ==> Parameters: 20200801(String), 111111 or account = 'admin' (String) DEBUG [http-nio-8080-exec-5] - <==      Total: 0 返回结果:null

当使用${}时

DEBUG [http-nio-8080-exec-5] - ==>  Preparing: select * from user where account = ? and password = ?  DEBUG [http-nio-8080-exec-5] - ==> Parameters: 201301001(String), 111111 or account = 'admin' (String) DEBUG [http-nio-8080-exec-5] - <==      Total: 0 转换为实际的SQL语句:select * from user where account = '20200801' and password = '111111 or account = 'admin'' 

设计模式

问:介绍下DDD领域驱动设计,是说的什么,里面分为哪些模块

这个说起来比较复杂。这种设计模式更加趋近于现实世界的状态,要求我们写代码时要区分业务代码与非业务代码。

推荐文章:https://www.cnblogs.com/cuiqq/p/10961337.html

问:设计模式分为哪种类

问:充血模型与贫血模型

一、贫血模型

所谓贫血模型,是指Model 中,仅包含状态(属性),不包含行为(方法),采用这种设计时,需要分离出DB层,专门用于数据库操作。

@Data
public class Employee {
        public string Id ;
        public string Name ;
        public string Sex ;
        public DateTime? BirthDay;
      
        /// 直属上级的Id
        public string ParentId ;
 }
//实现方法略    
public class EmpDAO {
        public static bool AddEmployee(Employee emp);
        public static bool UpdateEmployee(Employee emp);
        public static bool DeleteEmployee(Employee emp);
        public static Employee GetEmployeeById(string Id);
}

二、充血模型

Model 中既包括状态,又包括行为,是最符合面向对象的设计方式。

@Data
public class Employee {
        public string Id ;
        public string Name ;
        public string Sex ;
        public DateTime;
  
        /// 直属上级的Id
        public string ParentIdl;
        private Employee _parent;

        public static Employee query(string id){
            Employee emp = new Employee();
            //实现略,仅需填充emp的熟悉即可
            return emp;
        }
    
        //保存对象,实现略
        public bool Save() {
            return true;
        }
        
        // 删除对象,实现略
        public bool Drop(){
            return true;
        }
      
    }

笔试题

问:手写单列模式

见文章:那些能让人秀出花的单列模式

public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton instance = null;

    private LazySimpleSingleton(){
        if (instance != null) {
            throw new RuntimeException("该构造方法禁止获取");
        }
    }

    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            synchronized (LazySimpleSingleton.class) {
                if (instance == null) {
                    instance = new LazySimpleSingleton();
                }
            }
        }
        return instance;
    }
}

问:手写冒泡排序

见文章:只知道冒泡排序?来看看这些排序算法

问:评测题目: 三个线程A、B、C,实现一个程序让线程A打印“A”,线程B打印“B”,线程C打印“C”, 三个线程输出,循环10次“ABC”

见文章:阿里多线程面试题-按线程顺序输出

问:Description:给出有序数组(非递减)和闭区间, 找出数组中在区间之内的元素起始位置和结束位置

  • 输入:

    1. 有序数组[1,1,2,3,4,5,5]

    1. 闭区间[-3,3]

  • 输出:[0,3]

  • 解释:在数组中,前4个元素在区间之内,则起始位置为0,结束位置为3

  • 要求:最坏情况时间复杂度小于O(n)

见文章:腾讯云算法面试题-给定边界输出对应索引下标

问:写一个二分查找算法

public static int getIndex(int[] arr, int key) {
        int mid = arr.length / 2;
        if (arr[mid] == key) {
            return mid;
        }
        int start = 0;
        int end = arr.length - 1;
        while (start <= end) {
            mid = (end - start) / 2 + start;
            if (arr[mid] == key) {
                return mid;
            } else if (arr[mid] > key) {
                end = mid - 1;
            } else {
                start = start + 1;
            }
        }
        //找不到,返回-1
        return -1;
    }

关注公众号:Lvshen_9 。回复"面试",获取更多面试资料

福利

Java进阶之路的思维导图

猜你喜欢

转载自blog.csdn.net/wujialv/article/details/107901171