2021年 我的java面试题

面试 同时被 2 个专栏收录
1 篇文章 0 订阅
1 篇文章 0 订阅

一、Java 基础

1、Object类常用方法有那些?

Equals
Hashcode
toString
wait
notify
clone
getClass

2、重载和重写的区别

        重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载。

        重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类 

型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)

3、equals与==的区别

        == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,

即是否是指相同一个对象。比较的是真正意义上的指针操作。

        1、比较的是操作符两端的操作数是否是同一个对象。

        2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。

        3、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如:

int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。

equals:

        equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类

的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而

Object中的equals方法返回的却是==的判断

4、String StringBuffffer 和 StringBuilder 的区别是什么?

        String是只读字符串,它并不是基本数据类型,而是一个对象。

        StringBuffer与StringBuilder都是用来进行字符串操作的。

        StringBuffer是线程安全的,Stringbuilder是非线程安全的。所以Stringbuilder比

stringbuffer效率更高,StringBuffer的方法大多都加了synchronized关键字
 

5、Java的四种引用,强弱软虚

    强引用
    
        强引用是平常中使用最多的引用,强引用在程序
内存不足(OOM)的时候也不会被回收,使用方式:

String str = new String("str");

​​​​​​​
    软引用
    
        软引用在程序内存不足时,会被回收,使用方式:

// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));

        可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
    


    弱引用
    
        弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:

WeakReference<String>wrf=newWeakReference<String>(str)

         可用场景:Java源码中的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
        


    虚引用
         虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue,使用
    例子:

PhantomReference<String>prf=newPhantomReference<String>(new String("str"),newReferenceQueue<>());

         可用场景: 对象销毁前的一些操作,比如说资源释放等。** Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用( SoftReference 等)。

6、拷贝和浅拷贝的区别是什么?

        浅拷贝:

                被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指

向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象.

        深拷贝:

                被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向

被复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的对象

都复制了一遍.


7、反射的实现方式:


        第一步:获取Class对象,有4中方法:

                1)Class.forName(“类的路径”);

                2)类名.class

                3)对象名.getClass()

                4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象


 

二、集合框架

1、hashMap的解读

   hashMap 是一个以键值对形式存储的一个集合类。他在jdk1.7 和 jdk1.8 之间,他的实现策略有所不同,其中比较重要的两个区别就是数据结构 头插尾插

   在JDK1.7的时候,hashMap 采用的数据结构是数组加链表。但是到了JDK1.8 之后就是数组加链表加红黑树了。加入红黑树是为了提高他的查询效率。

   还有一点就是在JDK1.7之前,当我们遇到哈希碰撞,需要在链表上添加数据的时候,采用的是头插法;但是到了JDK1.8的时候,改用了尾插法。因为头插法在多线程的情况下,会导致一些问题。比如说,他会形成循环链表,因为他本身就是线程不安全的,当有多个线程线程同时走到扩容的方法时候,多个线程同时扩容,会产生一个死的 循环的 链表,循环链表的坏处就是,当我新插入一个新的节点的时候,他就会永远找不到尾结点。永远找不到尾结点就跟死循环一样,把CPU卡死,耗尽CPU性能,所以为了解决这个问题,在JDK1.8之后,改为了尾插法。

   接下来就以JDK1.8,聊聊 hashMap的原理。

   首先我们在创建hashMap的时候,根据阿里开发手册要求,让我们传入一个它的初始化容量。就是在我们预知的前提下,我们预知将来可能要插入多少条数据的情况下,我们最好能传入一个初始化容量,而且这个容量最好是一个2的次幂。

   当然,如果不传,这个初始化容量就是16,或者你传入了15,最后通过它底层的 tableSizeFor 方法,通过一系列的 与运算,也会得到一个距离15最近的一个2的次幂,也就是16。

   然后,我们在往hashMap里面添加数据的时候,就会产生两个问题。一个就是扩容的问题,还一个是树化的问题。

   关于扩容,在 hashMap 里面有一个成员变量,叫做加载因子,默认是 0.75。当我们插入的节点数量 >= 容量 * 加载因子。也就是默认的 >= 16 * 0.75 = 12。也就是当我们插入的数据大于等于12的时候,他就会进行一个扩容。

   关于树化,在源码里面也有一个成员变量,叫树化的最小容量,默认是64,也就是当这个数组容量不足64的时候,会优先选择扩容而不是树化。只有数组容量大于了 64,并且它的链表长度 >= 8,这时候才会进行树化。还有一个成员变量是 树的阈值,就是当这个树化结构 小于 6 的时候,他又会回到链表结构了。

     源码分析

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 再 new 一个 hashMap的时候可以传入一个初始化容量,
 * 这个再阿里巴巴的开发手册里也是要求我们去传入这个容量的。
 * 这个初始容量就是我们数组的初始默认大小(他一定是2的幂次方)
 * 因为他源码里面的一个tableSizeFor这个方法,他就会通过一系列的
 * 位移运算,返回一个最接近2的次幂的一个数,用这个作为数组容量
 * 
 * 
 * 
 * 
 */
 
// 默认的初始化容量, 1 左移 4 位  也就是 1 * 2^4 = 16 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 

// 最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的加载因子,在数组扩容的时候,当数组达到某个阈值的时候,才会扩容
// 为什么是0.75?这是他们经过大量的计算得出的最佳结果,他在当初始容量
// 乘以加载因子,当大于这个数的时候,才回去扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 树化的一个阈值,就是在链表达到这个阈值的时候,才会树化,生成红黑树
// 为什么是8,因为根据泊松分布原则,到了第八个节点的时候,他的树化概率就
// 会非常的低了,因为树化节点大小是普通节点的两倍,所以我们要避免树化、
static final int TREEIFY_THRESHOLD = 8;

// 树化的一个阈值,当树形结构低于6的时候,转为链表结构
static final int UNTREEIFY_THRESHOLD = 6;

// 最小的树化容量默认是64
static final int MIN_TREEIFY_CAPACITY = 64;

   

三、扩容的时机

1、数组的size大于等于 初始容量*加载因子 16*0.75=12;

2、扩容的大小 <<1 左移了一位 就是乘以2倍

四、树化的时机

1、数组容量大于等于 64

2、链表的长度大于等于 8

五、树化的过程

1、把普通节点Node转换为树形节点treeNode

2、调用treeify进行树化

六、头插与尾插

1、JDK1.7采用的头插法

        JDK1.8采用的尾插法 数据结构:加入红黑树

2、头插法的优点:

        他的查找效率会更好,它只要进行一次hash,找到它的头结点,然后让新节点next指向头结点,把这个新节点直接赋值给table就可以的。

头插法的缺点:

        它在多线程的情况下,有可能会产生一个循环链表,因为他本身就不是线程安全的,当有多个线程同时走到扩容的方法的时候,多个线程同时扩容,会产生一个死的循环的链表,循环链表的坏处就是,当我新插入一个新的节点的时候,他就会永远找不到尾结点,永远找不到尾结点就跟死循环一样,把我们CPU直接卡死。

尾插法需要遍历,直到找到最后一个尾结点,才能插入。

补充:

线程安全的 concurrenthashmap1.7和1.8的区别

        JDK1.7:

        原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。

        JDK1.8:

        抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

        put() 方法通过 volition保证可见性,在 synchronized 同步代码块中 做 CAS 比较并交换!
 

  

2、线程安全的集合类

2.1 List 不安全

        解决方案:

public static void main(String[] args) {
        /**
         * 解决方案;
         * 1、List<String> list = new Vector<>();
         * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3、List<String> list = new CopyOnWriteArrayList<>();
         */
        // CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
        // 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
        // 在写入的时候避免覆盖,造成数据问题!
        // 读写分离
        // CopyOnWriteArrayList 比 Vector Nb 在哪里?
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

2.2、set 不安全

public static void main(String[] args) {
        // Set<String> set = new HashSet<>();
        // Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <=30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }

hashSet 底层是什么?

add set 本质就是 map key是无法重复的!
 

2.3、map 不安全

public static void main(String[] args) {
        // map 是这样用的吗? 不是,工作中不用 HashMap
        // 默认等价于什么? new HashMap<>(16,0.75);
        // Map<String, String> map = new HashMap<>();
        // 唯一的一个家庭作业:研究ConcurrentHashMap的原理
        Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(
                        0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }

2.4 copyOnWriteArrayList 的思想和原理

原理:

   当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

源码分析:


public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
 
    /** The lock protecting all mutators */
    transient final ReentrantLock lock = new ReentrantLock();
 
    /** The array, accessed only via getArray/setArray. */
    // 存储数据的array数组,注意此处是用volatile修饰的
    private volatile transient Object[] array;



    /**
     * Appends the specified element to the end of this list.
     * 往list尾部添加指定元素
	 *
	 * @param e element to be appended to this list
	 * @return <tt>true</tt> (as specified by {@link Collection#add})
	*/
	public boolean add(E e) {
		final ReentrantLock lock = this.lock;
		// 加锁
		lock.lock();
		try {
			// 获取成员变量array[]
			Object[] elements = getArray();
			int len = elements.length;
			// 原数组拷贝给新数组(即将添加一个元素,所以 len + 1)
			Object[] newElements = Arrays.copyOf(elements, len + 1);
			newElements[len] = e;
			// 新数组替换原数组
			setArray(newElements);
			return true;
		} finally {
			// 解锁
			lock.unlock();
		}
	}
}




   因为CopyOnWriteArrayList在add/remove操作时,不会修改原数组,所以读操作不会存在线程安全问题。这其实就是读写分离的思想,只有写入的时候才加锁,复制副本来进行修改。CopyOnWriteArrayList也叫写时复制容器。

优缺点:
CopyOnWriteArrayList的优点主要有两个:

  1. 线程安全
  2. 大大的提高了“读”操作的并发度(相比于Vector)

缺点也很明显:

  1. 每次“写”操作都会开辟新的数组,浪费空间
  2. 无法保证实时性,因为“读”和“写”不在同一个数组,且“读”操作没有加互斥锁,所以不能保证强一致性,只能保证最终一致性
  3. add/remove操作效率低,既要加锁,还要拷贝数组

所以CopyOnWriteArrayList比较适合读多写少的场景。

三、多线程、高并发

1、实现多线程的几种方法

        1、继承 Thread 类 ;

        2、实现 Runnable 接口;  

        3、实现 Callable 接口 + FutureTask,它是有返回值的,可以抛异常,实现是 call() 方法

        4、基于线程池 创建

2、继承 Thread 类 代码

// Lock三部曲
// 1、 new ReentrantLock(); 英 /riːˈentrənt/   软安串得
// 2、 lock.lock(); // 加锁
// 3、 finally =>  lock.unlock(); // 解锁  英 /ˈfaɪnəli/  

new Thread(()->{
    // 线程调用资源类,这里写你的业务代码
    },"A").start();
    
    
public class MyThread extends Thread {
    public void run() {
        System.out.println("MyThread.run()");
    }
}

MyThread myThread1 = new MyThread();
myThread1.start();    

3、实现 Runnable 接口 代码

// 如果自己的类已经 extends 另一个类,就无法直接 extends Thread,
// 此时,可以实现一个 Runnable 接口。
Runnable 接口。
public class MyThread extends OtherClass implements Runnable {
    public void run() {
        System.out.println("MyThread.run()");
    }
}

//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用
target.run()
public void run() {
    if (target != null) {
        target.run();
    }
}

4、ExecutorService、Callable、Future 有返回值的线程 代码

// 有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行
// Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
// 返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

public class Demo01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int taskSize = 10;
        //创建一个线程池
        ExecutorService pool =  Executors.newFixedThreadPool(taskSize);
        // 创建多个有返回值的任务
        List<Future> list = new ArrayList<>();
        for (int i = 0; i < taskSize; i++) {
            Callable callable= new MyCallable();
            // 执行任务并获取 Future 对象
            Future future = pool.submit(callable);
            list.add(future);
        }
        // 关闭线程池
        pool.shutdown();
        // 获取所有并发任务的运行结果
        for (Future future : list) {
            // 从 Future 对象上获取任务的返回值,并输出到控制台
            System.out.println("res = " + future.get().toString());
        }
    }
}

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

5、基于线程池的方式 代码

        线程池:三大方法、7大参数、4种拒绝策略

线程池的好处:

        1、降低资源的消耗

        2、提高响应的速度

        3、方便管理。

线程复用、可以控制最大并发数、管理线程

 5.1、线程池:三大方法

// 单个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建一个固定的线程池的大小
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 可伸缩的,遇强则强,遇弱则弱
ExecutorService threadPool = Executors.newCachedThreadPool(); 

// Executors 工具类、3大方法
public class Demo01 {
    public static void main(String[] args) {
         // 单个线程
         ExecutorService threadPool = Executors.newSingleThreadExecutor();
         // 创建一个固定的线程池的大小
         ExecutorService threadPool = Executors.newFixedThreadPool(5);
         // 可伸缩的,遇强则强,遇弱则弱
         ExecutorService threadPool = Executors.newCachedThreadPool(); 
        
        try {
            for (int i = 0; i < 100; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

5.2、线程池:七大参数

  1. int corePoolSize, // 1、核心线程池大小
  2. int maximumPoolSize, // 2、最大核心线程池大小
  3. long keepAliveTime, // 3、超时了没有人调用就会释放
  4. TimeUnit unit, // 4、超时单位
  5. BlockingQueue<Runnable> workQueue, // 5、阻塞队列
  6. ThreadFactory threadFactory, // 6、线程工厂:创建线程的,一般不用动
  7. RejectedExecutionHandler handle // 7、拒绝策略

源码分析:

// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
}
// 创建 固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads){
        return new ThreadPoolExecutor(5,5,
        0L,TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());
}
// 创建 缓存的线程池
public static ExecutorService newCachedThreadPool(){
        return new ThreadPoolExecutor(0,Integer.MAX_VALUE,
        60L,TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());
}

//本质ThreadPoolExecutor()
public ThreadPoolExecutor(
        int corePoolSize,    // 1、核心线程池大小
        int maximumPoolSize, // 2、最大核心线程池大小
        long keepAliveTime,  // 3、超时了没有人调用就会释放
        TimeUnit unit,       // 4、超时单位
        BlockingQueue<Runnable> workQueue, // 5、阻塞队列
        ThreadFactory threadFactory, // 6、线程工厂:创建线程的,一般不用动
        RejectedExecutionHandler handle // 7、拒绝策略) {
        if(corePoolSize< 0||
        maximumPoolSize<=0||
        maximumPoolSize<corePoolSize ||
        keepAliveTime< 0)
        throw new IllegalArgumentException();
        if(workQueue==null||threadFactory==null||handler==null)
        throw new NullPointerException();
        this.acc=System.getSecurityManager()==null?
        null:
        AccessController.getContext();
        this.corePoolSize=corePoolSize;
        this.maximumPoolSize=maximumPoolSize;
        this.workQueue=workQueue;
        this.keepAliveTime=unit.toNanos(keepAliveTime);
        this.threadFactory=threadFactory;
        this.handler=handler;
}

5.3、线程池:四种拒绝策略

// 队列满了,还有进来的,不处理这个,直接抛出异常 (线程池默认的策略)
new ThreadPoolExecutor.AbortPolicy() 
// 谁调用的再回到那个线程,由调用线程处理该任务
new ThreadPoolExecutor.CallerRunsPolicy() 
//队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardPolicy() 
//队列满了,尝试去和最早的竞争,也不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() 

5.4、线程池:小结与扩展

        根据阿里开发手册的强制要求:线程池不予许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式,来规避资源耗尽的风险。

        因为使用 FixedThreadPool 和 SingleThreadPool,他允许的请求队列长度为 Integer.MAX_VALUE,他的值约为 21亿。这可能会堆积大量的请求,从而导致 OOM,内存就爆了。

        因为使用 CachedThreadPool,他允许创建的线程数量为 Integer.MAX_VALUE,值也约为 21 亿,这可能会创建大量的线程,从而导致 OOM,内存也爆掉了。

        所以我们正常工作中用的都是 ThreadPoolExecutor,这种手动创建线程池的方式,就是写一个线程池的配置类,然后加上 @Configuration 这个注解,手写一个 ThreadPoolTaskExecutor 类。后面要用 这个类就 @Autowired一下就行。

5.5、工作中的 线程池配置类

@Configuration
public class ThreadPoolConfig
{
    // 核心线程池大小
    private int corePoolSize = 50;

    // 最大可创建的线程数
    private int maxPoolSize = 200;

    // 最大线程到底该如何定义
    // 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
    // 2、IO 密集型 > 判断你程序中十分耗IO的线程,
    // 程序 15个大型任务 io十分占用资源!
    // 获取CPU的核数
    int cpu_number = Runtime.getRuntime().availableProcessors()
    System.out.println(cpu_number);

    // 队列最大长度
    private int queueCapacity = 1000;

    // 线程池维护线程所允许的空闲时间
    private int keepAliveSeconds = 300;

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(maxPoolSize);
        executor.setCorePoolSize(corePoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //队列满了,尝试去和最早的竞争,也不会抛出异常!
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        return executor;
    }
}

// 调用
@Autowired
ThreadPoolConfig threadPoolConfig;

@Test
void te() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = threadPoolConfig.threadPoolTaskExecutor();
    for (int i = 0; i < 50; i++) {
        final int j = i;
        threadPoolTaskExecutor.execute(()->{
            System.out.println(Thread.currentThread().getName()+" --j = "+j+" OK");
        });
    }
}

第二种配置类

@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
       return new ThreadPoolExecutor(pool.getCoreSize(),
                pool.getMaxSize(),pool.getKeepAliveTime(),
                TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

    }
}



@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}


gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10

6、如何停止一个正在运行的线程

        1、使用 interrupt (英 /ˌɪntəˈrʌpt/  英特 rua 普特)方法来中断线程

        2、使用 抛异常 停止线程

7、Lock 和 synchronized 有以下几点不同:

        1、Synchronized 内置的Java关键字, Lock 是一个Java类

        2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁

        3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁

        4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;

        5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);

        6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

8、notify() 和 notifyAll() 有什么区别?

        1、notify 随机唤醒某一个wait线程,notifyAll会唤醒全部wait的线程

        2、所以 notify 可能会造成死锁,而notifyAll 不会

9、sleep()和 wait() 有什么区别?

        1、sleep() 属于 Thread类中的静态方法 ;而 wait() 属于object类中的成员方法。

        2、sleep() 是线程类 Thread 的方法,他不涉及线程通信,调用的时候会暂停这个线程的指定时间,但是监控依然保持着。所以不会释放锁,只要到时间自动恢复;而 wait()是 Object类的方法,他是用于线程间的通信,调用这个wait的方法,会放弃这个对象锁,进入等待队列。等待调用notify或者notifyAll唤醒线程,他才会进入到对象锁定池,准备获得对象锁进入运行状态。

        3、wait、notify、notifyAll,他们只能在同步方法或者同步代码块中使用;而sleep() 可以再任何地方使用。

        4、sleep()方法必须要捕获一个中断异常(Interrupted Exception 英 /ɪkˈsepʃn/  一可 塞普省);而 wait()、notify、notifyAll不需要捕获异常。

        5、wait() 方法必须要配合 while循环使用,不能使用 if 判断,这会导致虚假唤醒。

while (number!=0){ //0
    // 等待
    this.wait();
}

        5.1、如果需要精准唤醒,就需要 JUC包下的Condition(译:条件 英 /kənˈdɪʃn/ 肯第省),

他可以精准的通知和唤醒线程

// Reentrant 译:可重入 英 /riːˈentrənt/ 瑞 安穿特
private Lock lock = new ReentrantLock();
// 创建多个 condition 做唤醒、通知
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
    // 在 while 循环里 condition1 等待
    while (number!=1){
        // 等待
        condition1.await();
    }
    // 业务代码 
    // signal (译:信号通知   英 /ˈsɪɡnəl/  C哥老)
    // 唤醒 2号 condition,做到精准通知
    condition2.signal();
    

10、volatile 是什么?

volatile 是 java虚拟机提供的一种轻量级的同步框架:

作用:

        1、保证可见性 (主内存和线程内存)

        2、不保证原子性 (因为没有锁,并发下会同时执行)

        3、禁止指令重排序        (内存屏障)

        

        谈到可见性就要谈到 JMM。JMM 就是Java内存模型,他本身就是一种抽象的概,实际上并不存在。他描述的就是一种规范或规则。

在 JMM 关于同步的规定:

        1、加锁 和 解锁 都必须是同一把锁

        2、线程加锁前,必须读取主内存的最新值,到自己的工作内存中

        3、线程解锁前,必须把共享变量的值刷新会主内存中

Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
 

11、锁的对象


        1、对于普通同步方法,锁是当前实例对象。
        2、对于静态同步方法,锁是当前类的 Class 对象。
        3、对于同步方法块,锁是 Synchonized 括号里配置的对象

12、线程池的好处

        1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

        2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

        3、提高线程的可管理型。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

13、线程的声明周期(状态)

源码分析:       六大状态

public enum State {
	// 新生
	NEW
	// 运行
	RUNNABLE,
	// 阻塞
	BLOCKED,
	// 等待,死死地等
	WAITING,
	// 超时等待
	TIMED_WAITING,
	// 终止
	TERMINATED;
}

14、start 与 run 区别

        1. start() 方法来启动线程,真正实现了多线程运行。这时无需等待 run方法体代码执行完毕,可以直接继续执行下面的代码。
        2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
        3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

15、start()方法 与 run()方法?


        当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。

        但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行

16、异步编排

  1、创建异步对象

        CompletableFuture 提供了四个静态方法来创建一个异步操作。

        1、 runXxxx 都是没有返回结果的, supply(英 /səˈplaɪ/  涩普赖)Xxx 都是可以获取返回结果
        2、 可以传入自定义的线程池, 否则就用默认的线程池;

 2、 线程串行化方法

         注意:方法不以 Async 结尾, 意味着 Action 使用相同的线程执行, 而 Async 可能会使用其他线程执行(如果是使用相同的线程池, 也可能会被同一个线程选中执行)

1、thenApply(英 /əˈplaɪ/  额普莱尔) 方法: 当一个线程依赖另一个线程时, 获取上一个任务返回的结果, 并返回当前任务的返回值。
2、
thenAccept(英 /əkˈsept/  额可赛普特)方法: 消费处理结果。 接收任务的处理结果, 并消费处理, 无返回结果。
3、thenRun 方法: 只要上面的任务执行完成, 就开始执行 thenRun, 只是处理完任务后, 执行thenRun 的后续操作带有 Async 默认是异步执行的。 同之前。以上都要前置任务成功完成。Function<? super T,? extends U>
T: 上一个任务返回结果的类型
U: 当前任务的返回值类型

  

@Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo  = new SkuItemVo();
        
        // supplyAsync 有返回值
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1、sku基本信息获取  pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);

        // thenAcceptAsync 拿到上面的结果 ,没有返回值
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //3、获取spu的销售属性组合。
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);

        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
            //4、获取spu的介绍  pms_spu_info_desc
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);
        }, executor);

        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {
            //5、获取spu的规格参数信息。
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);



        //2、sku的图片信息  pms_sku_images
        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);

        //3、查询当前sku是否参与秒杀优惠
        CompletableFuture<Void> secKillFuture = CompletableFuture.runAsync(() -> {
            R seckillInfo = seckillFeignService.getSkuSeckillInfo(skuId);
            if (seckillInfo.getCode() == 0) {
                SeckillInfoVo seckillInfoVo = seckillInfo.getData(new TypeReference<SeckillInfoVo>() {
                });
                skuItemVo.setSeckillInfo(seckillInfoVo);
            }
        }, executor);



        //等到所有任务都完成
        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,secKillFuture).get();


        return skuItemVo;
    }

17、深入理解 CAS

CAS  = compare(英 /kəmˈpeə(r)/ 康木 牌儿)AndSet : 比较并交换!


CAS : 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环!(自旋锁循环)

缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
 

18、原子引用,解决ABA 问题

        因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A

        从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

核心:atomicStampedReference.getStamp()  获取版本号

atomicStampedReference.compareAndSet(1, 2,
        atomicStampedReference.getStamp(),
        atomicStampedReference.getStamp() + 1);
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
    // 正常在业务操作,这里面比较的都是一个个对象
    static AtomicStampedReference<Integer> atomicStampedReference = new
            AtomicStampedReference<>(1, 1);

    // CAS compareAndSet : 比较并交换!
    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);
            System.out.println("a2=>" + atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1));
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();
        // 乐观锁的原理相同!
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }

19、ReadWriteLock 读写锁

        为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。


写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!


Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 体 的 实 现ReentrantReadWriteLock。

package com.nz.springbootrabbitmq.test;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCache myCache = new MyCache();
        MyCacheLock myCacheLock = new MyCacheLock();
        // 写入
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCacheLock.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }
        // 读取
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCacheLock.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

// 加锁的
class MyCacheLock {
    private volatile Map<String, Object> map = new HashMap<>();
    // 读写锁: 更加细粒度的控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock lock = new ReentrantLock();

    // 存,写入的时候,只希望同时只有一个线程写
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 取,读,所有人都可以读!
    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

/**
 * 自定义缓存
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    // 存,写
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入OK");
    }

    // 取,读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取OK");
    }
}

20、深入理解 synchronized

        synchronized 是一个重量级锁,他在升级到 重量级锁之前,还有一个 锁升级的四个过程。

        1、无锁

        2、偏向锁

        3、轻量级锁

                1、自旋锁(默认是10次)

                2、适应性自旋锁(JDK1.6改进,假如上次自旋成功,那么你自旋的次数就会增加)

        4、重量级锁

synchronized 在 升级锁的过程中,就是,当我们创建一个对象的时候,这个对象 有一个 对象头,头里面有一个 mark word,他里面存储了一些数据,比如是否是 偏向锁,还有他的锁的标志位。如果 偏向锁的标志位是 0 ,说明这个对象还没有被加上 偏向锁。反之亦然。

        偏向锁:

                当一个线程获得了一个锁,并且 不存在多个线程 去竞争这个锁的情况下,这个锁就是偏向的。

        轻量级锁:

                轻量级锁就是 由 偏向锁升级来的。轻量级锁他的竞争方式就是在这个 mark word 中,使用 cas 算法 去 竞争这个锁,也就是之前说的 自旋锁和 自适应自旋锁。

        重量级锁:

                如果 轻量级锁 正 他自旋的这段期间,还没有获取到这个锁,那么就会 升级到 重量级锁。重量级锁 他是依赖 对象内部的 这个 monitor (英 /ˈmɒnɪtə(r)/   摸你特),而这个 monitor又 依赖于 操作系统的 互斥锁来实现。所以 重量级锁 也叫 互斥锁。

总结:

        偏向锁 和 轻量级锁 都是 乐观锁,重量级锁是悲观锁。

大概流程:

        当一个对象刚开始实例化的时候,是没有任何线程来访问他的,他就是属于一个 无锁的状态。

        第一个线程开始访问他的时候,他就是获得了一个偏向锁,一旦有第二个线程开始访问这个对象,因为 偏向锁是不会主动释放的,所以 第二个线程 就可以看到 这个对象是一个 偏向锁的状态。这个时候就表明了,在这个对象上,出现了 锁竞争的问题。

        要竞争锁的这个线程需要检查,原来持有 对象锁的 线程是否存活。如果不存活,就可以将对象变成无锁状态,并重新进行偏向锁的持有。

        如果存活,那就马上执行那个线程的操作栈,检查这个对象的使用情况。

        如果不存在 这个对象的使用了,那么就可以把这个对象 恢复成无锁状态,然后重新进行偏向锁的竞争。

        如果存在 这个对象的使用,且仍然需要持有这个偏向锁,那么这个时候 偏向锁就会升级为轻量级锁。

        然后这个 轻量级锁就会 进行 他的 自旋的操作,如果超过了一定的 自旋次数,还没有获取到这个锁,这个时候 又回 升级到 重量级锁了。

        重量级锁就是 除了拥有这个线程以外的其他线程,都是阻塞的状态,就为了防止这个 CPU的一个 空轮询的问题。而且 阻塞 和 唤醒的这个过程,都需要操作系统来帮忙,都是非常耗时的。所以重量级锁 他的 开销是非常大的。

        

21、深入理解 AQS

        

        

        

        

        

四、Spring 框架

1、spring 核心与原理

1、spring 七大核心模块

                1、spring core 核心模块

                2、spring aop 面向切面编程

                3、spring rom 和 mybatis 整合的模块

                4、DAO,自己能产生连接数据库的模块

                5、spring web,跟第三方 web 框架 整合的模块

                6、spring context,扫描模块

                7、spring mvc 模块

     

  2、IOC

                IOC,控制反转,是一种设计思想,他的作用就是,将原本在程序中手动创建对象的控制权,交由给 Spring 框架来管理。IOC 容器实际上就是一个 Map,存放的是各种对象。

                他将对象之间的相互依赖关系,交给 IOC 容器来管理,并由 IOC 容器完成对象的注入。

                DI,也叫 依赖注入,实现方式有三种:

                1、setter (结构)方法注入

                2、inferface(接口)注入

                3、constructor(构造器)注入

      

  3、AOP

        AOP,面向切面编程,可以通过 @Aspect 注解实现, 他的通知有五种类型:

                1、before:前置通知,在一个方法执行前被调用。

                2、after: 在方法执行之后调用的通知,无论方法执行是否成功。

                3、after-returning: 仅当方法成功完成后执行的通知。

                4、after-throwing: 在方法抛出异常退出时执行的通知。

                5、around: 在方法执行之前和之后调用的通知。

4、spring 的 五种事务隔离级别

        在TransactionDefinition接口中定义了五个表示隔离级别的常量:

                ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。

                ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。

                ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

                ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

                ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

5、spring 的 八种事务传播行为

        在TransactionDefinition接口中定义了八个表示事务传播行为的常量

支持当前事务的情况:

        PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

        PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

        PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。

不支持当前事务的情况:

        PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。

        PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。

        PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:

        PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。

6、spring 中的 循环依赖 怎么解决?

        通过一个三级缓存去做的,具体不清楚

2、Spring MVC 的执行流程

1、 用户发送请求至前端控制器DispatcherServlet。 

2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、 Controller执行完成返回ModelAndView。

7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

9、 ViewReslover解析后返回具体View。

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、 DispatcherServlet响应用户

自己的话描述:

         1、首先用户发起请求,到前段控制器,也就是 DispatcherServlet,它相当于一个转发器,负责接收请求,响应结果。也就是相当于 mvc 模式中的 c。

        2、然后,DispatcherServlet 接收到请求,再去调用 HandlerMapping 也就是 处理器映射器。他就是根据 用户请求的 rul 找到对应的 Handler,就是处理器。并且在 SpringMVC 中,提供了 不同的映射器实现不同的映射方式。例如:通过配置文件,也就是 xml 文件 ;还有 实现接口方式 和 注解方式。

        3、然后,这个 处理器映射器 根据相应的映射方式(xml 配置、或者注解等),找到这个具体的 Handler 处理器。生成 处理器对象,及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet 。 

        4、DispatcherServlet  再去调用 HandlerAdapter 处理器适配器。(Adapter 英 /əˈdæptə(r) :额大普特/ ),它的作用就是按照特定规则,也就是 HandlerAdapter 的要求执行 Handler。并且这个 HandlerAdapter 也用到了一个 适配器模式,他可以通过扩展适配器 对 更多类型的 处理器 进行执行。

        5、然后,这个 HandlerAdapter 经过适配后,再去调用具体的 Handler 处理器,这就是我们所写的 Controller 层的代码了,前四部都是 SpringMVC帮我做好的。而我们这层 Controller 就叫 后端控制器,DispatcherServlet 叫做 前段控制器

        6、然后,这个 Controller 执行完后,就开始放回一个 ModelAndView。

        7、HandlerAdapter 再将 Controller 的执行结果,就是这个 ModelAndView 返回给 DispatcherServlet

        8、DispatcherServlet 再将这个 ModelAndView 传给 ViewReslover 视图解析器。(Reslover 英[rɪˈzɒlvə] :瑞子over)。他的作用就是根据 逻辑视图名 解析成 真正的视图 View。

        9、ViewReslover 解析后,返回具体的 View。

        10、 DispatcherServlet 根据 View 进行 渲染视图(就是将 Model 里面的数据 填充到 View里面)

        11、最后就是 DispatcherServlet  将整个封装好的页面 响应给用户了,这就是一个完整的 SpringMVC 工作流程了。

3、mybatis 用过哪些标签 ?

4、mybatis里的 where 标签 和 sql查询语句里的 where有什么区别

 

5、@autowrite 和 @resource 的区别?分别怎么注入的

        @Autowired 默认是 byType,是按照 类型装配 注入的,如果想按照名称的话就要用另一个注解 @Qualifier 一起使用。

        @Resource 默认是 byName,是按照 参数名称 来配置注入的。只有找不到这个名称相匹配的bean的时候,才会去按照 类型去装配注入。

五、MySQL 面试题

1、MySQL 的优化

        1、版本优化

        在其性能上,MySQL 8.0 的速度要比 MySQL 5.7 快 2 倍。MySQL 8.0 在以下方面带来了更好的性能:读/写工作负载IO 密集型工作负载、以及高竞争("hot spot"热点竞争问题)工作负载。

        2、配置文件优化

                1、数据库连接优化:

                        设置允许同时访问 MySQL 的最大连接数,默认是151,最多是2000。

                        max_connections=1000

                2、内存优化

                        如果你有大量的相同的查询,且很少修改表的话,就应该把缓存查询大小设置大一点,这样他就不会重复查询了 。

                        query_cache_size = 64M 

               3、日志优化

                        开启慢查询日志,设置慢查询时间,3秒、5秒的都可以。

                        slow_query_log = 1  ;  long_query_time = 3

                4、存储引擎优化

                        由于MySQL5.7过后,他的默认存储引擎就是 InnoDB了。所以 InnoDB的缓存大小也要设置,这样他就会先走内存,而不会走磁盘了,大大的提高了查询效率。大小最好是物理内存的60%-80%

                        innodb_buffer_pool_size=107M

                        或者,当你选用 MyISAM 存储引擎的时候,也需要调整他的索引缓存大小。因为他索引 和 数据 的存储是分开的。大小最好不要超过你可用内存的 30%。

                        key_buffer_size = 64M

        

        详细配置信息如下:


# MySQL配置优化

# 客户端设置
[client]
# port参数表示的是MySQL数据库的端口,默认的端口是3306
# ,如果你需要更改端口号的话,就可以通过在这里修改
port=3306


[mysql]
# 参数是客户端默认的字符集,如果你希望它支持中文
# ,可以设置成gbk或者utf8。
default-character-set=gb2312


# SERVER SECTION
# 服务端设置

# ----------------------------------------------------------------------
[mysqld]
# 基础信息


#Mysql服务的唯一编号 每个mysql服务Id需唯一
server-id = 1

# port参数也是表示数据库的端口。
port=3306

# --------------安装目录相关-------------------------------------------



# 参数表示MySQL的安装路径。
basedir="E:/Java/Mysql/"

# 参数表示MySQL数据文件的存储位置,也是数据库表的存放位置。
datadir="C:/ProgramData/MySQL/MySQL Server 5.5/Data/"

# 临时目录 比如load data infile会用到,一般都是使用/tmp
tmpdir  = /tmp

# 设置socke文件地址
socket  = /tmp/mysql.sock

# ----------事务隔离级别,默认为可重复读(REPEATABLE-READ)----------



# 隔离级别可选项目:READ-UNCOMMITTED  READ-COMMITTED  REPEATABLE-READ  SERIALIZABLE
transaction_isolation = REPEATABLE-READ

# ----------------数据库引擎与字符集相关设置----------------------------



# mysql 5.1 之后,默认引擎就是InnoDB了
default-storage-engine=INNODB

# 内存临时表默认引擎,默认InnoDB
default_tmp_storage_engine = InnoDB

# mysql 5.7新增特性,磁盘临时表默认引擎,默认InnoDB
internal_tmp_disk_storage_engine = InnoDB


#数据库默认字符集,主流字符集支持一些特殊表情符号(特殊表情符占用4个字节)
character-set-server=utf8

#数据库字符集对应一些排序等规则,注意要和character-set-server对应
collation-server = utf8_general_ci

# 设置client连接mysql时的字符集,防止乱码
# init_connect='SET NAMES utf8'

# 是否对sql语句大小写敏感,默认值为0,1表示不敏感
lower_case_table_names = 1

# ----------------数据库连接相关设置----------------------------------



# 表示SQL模式的参数,通过这个参数可以设置检验SQL语句的严格程度
sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"

# 表示允许同时访问MySQL服务器的最大连接数
# ,其中一个连接是保留的,留给管理员专用的。
max_connections=1000
# 优化策略:默认值为:151,最多2000。
#			如果服务器的并发连接请求量比较大,建议调高。

# MySQL默认的wait_timeout  值为8个小时, interactive_timeout参数需要同时配置才能生效
# MySQL连接闲置超过一定时间后(单位:秒,此处为1800秒)将会被强行关闭
interactive_timeout = 1800 
wait_timeout = 1800 

# 在MySQL暂时停止响应新请求之前的短时间内多少个请求可以被存在堆栈中 
# 官方建议back_log = 50 + (max_connections / 5),封顶数为900
back_log = 900

# ----------------数据库数据交换设置----------------------------------



# 该参数限制服务器端,接受的数据包大小,如果有BLOB子段
# ,建议增大此值,避免写入或者更新出错。有BLOB子段,建议改为1024M
max_allowed_packet = 128M

# ----------------内存,cache与buffer设置-----------------------------



# 内存临时表的最大值,默认16M,此处设置成128M
tmp_table_size=64M

# 用户创建的内存表的大小,默认16M,往往和tmp_table_size一起设置,限制用户临师表大小。
# 超限的话,MySQL就会自动地把它转化为基于磁盘的MyISAM表,存储在指定的tmpdir目录下
# ,增大IO压力,建议内存大,增大该数值。
max_heap_table_size = 64M

# 这个系统变量控制着查询缓存工能的开启的关闭,0时表示关闭
# ,1时表示打开,2表示只要select 中明确指定SQL_CACHE才缓存。
# 		看业务场景决定是否使用缓存,不使用,下面就不用配置了。
query_cache_type = 0 

# 默认值1M,
#		优点:是查询缓冲可以极大的提高服务器速度
#			, 如果你有大量的相同的查询并且很少修改表。
#		缺点:在你表经常变化的情况下或者如果你的查询原文每次都不同
#			,查询缓冲也许引起性能下降而不是性能提升。
query_cache_size = 64M 

# 只有小于此设定值的结果才会被缓冲,保护查询缓冲
#		,防止一个极大的结果集将其他所有的查询结果都覆盖。
query_cache_limit = 2M

# 每个被缓存的结果集要占用的最小内存,默认值4kb,一般不怎么调整。
# 如果Qcache_free_blocks值过大,可能是query_cache_min_res_unit值过大,应该调小些
# query_cache_min_res_unit的估计值:(query_cache_size - Qcache_free_memory) / Qcache_queries_in_cache
query_cache_min_res_unit = 4kb

# 在一个事务中binlog为了记录SQL状态所持有的cache大小
# 如果你经常使用大的,多声明的事务,你可以增加此值来获取更大的性能.
# 所有从事务来的状态都将被缓冲在binlog缓冲中然后在提交后一次性写入到binlog中
# 如果事务比此值大, 会使用磁盘上的临时文件来替代.
# 此缓冲在每个连接的事务第一次更新状态时被创建
binlog_cache_size = 1M




# -------------------------日志文件相关设置------------------------------
# 日志文件相关设置,一般只开启三种日志,错误日志,慢查询日志,二进制日志。普通查询日志不开启。



# 普通查询日志,默认值off,不开启
general_log = 0

# 普通查询日志存放地址
general_log_file = /usr/local/mysql-5.7.21/log/mysql-general.log

# 全局动态变量,默认3,范围:1~3
# 表示错误日志记录的信息,1:只记录error信息;2:记录error和warnings信息;3:记录error、warnings和普通的notes信息。
log_error_verbosity = 2
# 错误日志文件地址
log_error = /usr/local/mysql-5.7.21/log/mysql-error.log

# 开启慢查询
slow_query_log = 1

# 开启慢查询时间,此处为1秒,达到此值才记录数据
long_query_time = 3

# 检索行数达到此数值,才记录慢查询日志中
min_examined_row_limit = 100

# mysql 5.6.5新增,用来表示每分钟允许记录到slow log的且未使用索引的SQL语句次数,默认值为0,不限制。
log_throttle_queries_not_using_indexes = 0

# 慢查询日志文件地址
slow_query_log_file = /usr/local/mysql-5.7.21/log/mysql-slow.log

# 开启记录没有使用索引查询语句
log-queries-not-using-indexes = 1

# 开启二进制日志
log_bin = /usr/local/mysql-5.7.21/log/mysql-bin.log
# mysql清除过期日志的时间,默认值0,不自动清理,而是使用滚动循环的方式。
expire_logs_days = 0
# 如果二进制日志写入的内容超出给定值,日志就会发生滚动。你不能将该变量设置为大于1GB或小于4096字节。 默认值是1GB。
max_binlog_size = 1000M
# binlog的格式也有三种:STATEMENT,ROW,MIXED。mysql 5.7.7后,默认值从 MIXED 改为 ROW
# 关于binlog日志格式问题,请查阅网络资料
binlog_format = row
# 默认值N=1,使binlog在每N次binlog写入后与硬盘同步,ps:1最慢
# sync_binlog = 1 



# -----INNODB Specific options -->  INNODB 的特定选项  -----




# 表示缓存的大小,InnoDB使用一个缓冲池类保存索引和原始数据。
innodb_buffer_pool_size=107M
# 优化策略:这个值越大越好,他能保证在大多数的读取操作时候
# 			,读取的是内存数据,而非磁盘。典型的值是5-6GB(8GB内存)
#			,物理内存的60%-80%

# 表示日志文件的大小。
innodb_log_file_size=54M
# 优化策略:这是确保写操作快速而可靠,并且在崩溃时恢复。
# 			,一般情况下设置为 512MB,或者 1G,
#			,如需频繁写入数据的话,最好是设置为 4G。


# 表示附加的内存池,用来存储InnoDB表的内容。
innodb_additional_mem_pool_size=16M

# 是设置提交日志的时机,若设置为1
# ,InnoDB会在每次提交后将事务日志写到磁盘上。
innodb_flush_log_at_trx_commit=1

# 表示用来存储日志数据的缓存区的大小。
# 此参数确定些日志文件所用的内存大小,以M为单位。缓冲区更大能提高性能
# ,但意外的故障将会丢失数据。MySQL开发人员建议设置为1-8M之间
innodb_log_buffer_size=2M


# 表示在InnoDB存储引擎允许的线程最大数。
innodb_thread_concurrency=18



# -----MyISAM Specific options -->  MyISAM 的特定选项  -----



# 指定索引缓冲区的大小, 为MYISAM数据表开启供线程共享的索引缓存,对INNODB引擎无效。相当影响MyISAM的性能。
# 不要将其设置大于你可用内存的30%,因为一部分内存同样被OS用来缓冲行数据
# 甚至在你并不使用MyISAM 表的情况下, 你也需要仍旧设置起 8-64M 内存由于它同样会被内部临时磁盘表使用.
# 默认值 8M,建议值:对于内存在4GB左右的服务器该参数可设置为256M或384M。注意:该参数值设置的过大反而会是服务器整体效率降低!
key_buffer_size = 64M


# 表示MyISAM表全表扫描的缓存大小。
# 为每个扫描MyISAM的线程分配参数设置的内存大小缓冲区。 
# 默认值128kb,建议值:16G内存建议1M,4G:128kb或者256kb吧
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为 128kb*连接数;
# 极端情况128kb*maxconnectiosns,会超级大,所以要考虑日常平均连接数。
# 一般不需要太关心该数值,稍微增大就可以了,
read_buffer_size = 262144 


# 表示将排序好的数据存入该缓存中。
# 支持任何存储引擎
# MySQL的随机读缓冲区大小,适当增大,可以提高性能。
# 默认值256kb;建议值:得参考连接数,16G内存,有人推荐8M
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为128kb*连接数;
# 极端情况128kb*maxconnectiosns,会超级大,所以要考虑日常平均连接数。
read_rnd_buffer_size=256K


# order by或group by时用到 
# 支持所有引擎,innodb和myisam有自己的innodb_sort_buffer_size和myisam_sort_buffer_size设置
# 默认值256kb;建议值:得参考连接数,16G内存,有人推荐8M.
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为 1M*连接数;
# 极端情况1M*maxconnectiosns,会超级大。所以要考虑日常平均连接数。
sort_buffer_size = 1M

# 表示MySQL重建索引时所允许的最大临时文件的大小。
# myisam_max_sort_file_size=100G

# 表示重建索引时的缓存大小。
# myisam_sort_buffer_size=69M


# 表示保留客户端线程的缓存。
# 当客户端断开之后,服务器处理此客户的线程将会缓存起来以响应下一个客户而不是销毁。可重用,减小了系统开销。
# 默认值为9,建议值:两种取值方式,方式一,根据物理内存,1G  —> 8;2G  —> 16; 3G  —> 32; >3G  —> 64;
# 方式二,根据show status like  'threads%',查看Threads_connected值。
thread_cache_size = 16


        3、建库、建表优化

                首先尽量满足三个范式:

                        第一范式:对属性的原子性约束;

                        第二范式:对记录的唯一性约束;

                        第三范式:对字段冗余性的约束;

        存储引擎根据业务来,一般默认的innoDB就够了。

        这里主要说一下建表时候的 索引优化:

                        1、为常用作为搜索条件的字段建立索引

                        2、为经常拿来 排序、分组、联合查询的字段 简历索引

                        3、如果有需要,可以添加唯一性索引,可以更加快速的确定某条记录,如税号、身份证等等。

                        4、限制索引的数目,索引越多,更新表也就越慢,因为他同时还要维护索引。

                        5、使用索引,要符合 最左前缀匹配原则,就想爬楼梯,一层层爬,不能越级

                        6、删除很少使用 或 不再使用的 索引

        4、SQL 语句的优化

                1、查询 SQL 尽量不用 select * ,而是具体 select字段

                2、如果知道查询的结果只有一条,建议用 limit 1,这样只要找到对应的一条记录后,就不会继续扫描了。

                3、优化 like 语句, % 放前面会索引失效。

                4、联合查询,优先使用 inner join ,如果 左表结果小就用 left join

                5、使用索引查询,符合 最左前缀匹配原则,不要用 or,或者 函数计算,!=,等等

                6、合理使用 exist 和 in。用 in 先执行()括号内的语句,再执行外部语句。

                        exist 先执行外部语句,在执行内部语句

                        使用时,遵从 小表驱动大表 原则;里面表数据少就用 in,反之亦然

                7、尽量用 union all(英 /ˈjuːniən/ 优尼恩 )替换 union ;

                8、执行 sql 前,先用 explain 分析下你的 sql,主要看 索引这块。

                9、查看数据库锁的状态,使用 show status like 'innodb_row_lock%' 命令

        5、大表的优化

                当我们表中的数据量过大时的优化

                1、限定数据的范围,如控制一个月的范围,或者一个礼拜。

                2、主从复制、读写分离,主库负责写,从库负责读

                3、分表分库

2、MyISAM 和 InnoDB 的区别有哪些?

     1、InnoDB 支持事务,MyISAM 不支持事务

        InnoDB 支持 ACID 的事务,支持事务的四种 隔离级别;

     2、InnoDB 支持外键,MyISAM 不支持外键。对一个包含外键的 InnoDB 表转为 MyISAM 表 会失败。

     3、InnoDB 是 聚集索引,数据文件 和 索引 绑在一起的。而 MyISAM 是 非聚集索引,文件是分开的。

     4、InnoDB 不保存表的具体行数,而 MyISAM 用一个变量保存了 整个表的行数。

     5、InnoDB 不支持全文索引,而 MyISAM 支持全文索引。

     6、InnoDB 最小的锁粒度是行锁,而 MyISAM 是表锁

3、哈希索引、平衡二叉树、B树、 B+树 的 区别

        哈希索引:因为他存储的都是 哈希值,都是无序的,所以就不能进行范围查找,就像 UUID一样,不可能作为 主键一样。

        平衡二叉树:他的缺点就是,随着数据量的增加,他的树的高度也会变得很高,而且树越高,他的查找速度也越慢。然后,他还有个 致命的缺点,就是回旋查找:假如我需要一个范围查找,我插入了十个数,1到10,我需要查找大于5的树,但我这个 5 的 数据节点位于最下面一层,我要找大于5 就需要回到上层节点,做回旋查询。而且数据量越大,如一万,大于5的话,会更慢

        B树:每个节点可以存储一个或两个 (key 和 value【引用地址】),这样的话,他的树的高度就会变得矮一些了。但是 还是存在 回旋查找的 问题

        B+树:沿用了 B树的 特点,一个节点可以存储 两个值,减少 树的高度。并且 他通过 叶子结点的 单向链表 并且 有序的 解决了 回旋查找 问题

        所有 非叶子节点,只保存 key,不保存 data,叶子节点保存所有信息。B+树 的 叶子节点有一条链相连,指向下一个叶子节点

4、为什么说B+比B树更适合

    为什么说B+比B树更适合实际应用中操作系统的文件索引和数据库索引?

        1、B+树 的磁盘读写代价低:因为 B+树的内部节点,不存值,只有key,所以它比 B树容量更小,一次性读入内存的关键字就越多,相对 IO 读写次数就降低了。

        2、B+树 的查询效率更加稳定:因为查询关键字的路径长度是相同的,所以每一个数据的查询效率都相当。

        3、B+树更加便于遍历:因为他所有非叶子结点只有索引,只需要扫一遍叶子节点就可以了。但是B树的  分支节点同样存储着数据,要查找具体数据,就要进行一次中序遍历查找,所以B+树更加适合在区间范围

        4、B+树更适合基于范围的查找:由于 B树在提高 IO 性能的同时 并没有解决 yi元素遍历效率低下的问题,所以才有了 B+树。因为 B+树 只需要遍历一次 叶子节点就可以实现整棵树的遍历。这在数据库中范围查找是非常频繁的。

5、索引有哪些?

        索引的目的:加快 检索表中数据的方法,相当于书籍的索引。

四种索引:

        1、主键索引(primary)

        2、唯一索引(unique)

        3、普通索引(index)

        4、全文索引(fulltext)

        索引并非是越多越好,创建索引也需要耗费资源,一是增加了数据库的存储空间,二是在插入和删除时要花费较多的时间维护索引

6、数据库事务

        事务(TRANSACTION)是作为单个逻辑工作单元执行的一系列操作, 这些操作作为一个整体

一起向系统提交,要么都执行、要么都不执行 。

        事务是一个不可分割的工作逻辑单元事务必须具备以下四个属性,简称 ACID 属性:

        原子性(Atomicity)

                定义

                1. 事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。

                实现原理:undo log

                undo log 他是一种 事务日志。当然 MySQL 里面有很多种日志,如 二进制日志、错误日志、查询日志、慢查询日志 等等吧。

                在 InnoDB 存储引擎中 还提供了 两种 事务日志,一种就是 刚说的 undo log(回滚日志),redo log (重做日志),其中 redo log 是用于保证 事务的 持久性,而 undo log 则是保证了事务的 原子性 和 隔离性 的 基础。

                当 事务 对 数据库 进行 修改时候,InnoDB 会生成对应的 undo log,当你事务执行失败,或者 执行了 rollback 回滚操作,InnoDB 就会 根据 undo log 里面的内容作出相反的工作,比如说 执行了 insert 语句,然后发生了 回滚 ,他就会执行 delete 操作,就是把 数据给改回去。

        一致性(Consistency)

                2. 当事务完成时,数据必须处于一致状态。

        隔离性(Isolation)

                3. 对数据进行修改的所有并发事务是彼此隔离的, 这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。

        实现原理:mvcc 

                MVCC 就是 多版本并发控制,他的主要工作就是 提高数据库的读写性能。MVCC主要是处理 读请求的。这个读就是快照读,就是普通的 select 请求。而当前读 就是一种悲观锁。比如说 update、insert,delete就是一种 排它锁。所以我们的 MVCC 都是 基于这个 快照读的,他的目的就是 提高这个 读写的时候,能够不用去 竞争锁,提高性能。

                在这个 MVCC 里面 有几个常见的概念,第一个就是 undo log,第二个是 版本链,第三个是 readView。

                在我们插入 数据的时候,或者修改数据,他会有两个隐藏字段,一个就是 事务ID,他是自增的,还有个是 回滚指针。这个 回滚指针就是用来 回滚到上一个版本用的。所以 这个 版本链是由 undo log ,还有这个 回滚指针把他们连接起来的

                第三个概念就是 readView。他就是一个 快照,他的作用是 让你在 版本链 里面选择 某一条记录。 他在代码里面就是一个对象,他有四个参数。

                版本链。        

        永久性(Durability)

                4. 事务完成后,它对数据库修改被永久保持,事务日志能够保持事务的永久性

                实现原理:undo log

                        在 InnoDB 作为 存储引擎的情况下,数据 是存放在 磁盘中的,假如 MySQL 宕机了,在 重启的时候,会读取 redo log 中的数据,对数据库进行恢复。 因为 redo log 是 预写式日志,所有修改 先写入日志,在更新到 Buffer Poll,所以 他保证了 数据 不会 因为 MySQL 宕机而丢失,保证了 持久性要求。

7、并发事务带来哪些问题?

        在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个

用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

        1、脏读(Dirty read):当一个 事务(T1) 正在访问数据,并且对数据 进行了修改,而这种修改还没提交到数据库中。这时候,另一个并发事务(T2)也访问了 这个数据,然后使用了T1修改后的数据。因为这个数据是还没有提交的,所以 T2 这个事务读取到的就是 T1 修改的“脏数据”,依据“脏数据”所做的操作可能是不正确的。

        2、丢失修改(Lost to modify):指的是一个并发事务(T1)读取一个数据的时候,修改了这个数据,这时另一个并发事务(T2),也访问了这个数据,又修改了一次这个数据。这样 第一次事务(T1)修改的结果就丢失了。因此叫 丢失修改。

        3、不可重复读(Unrepeatableread):指在一个并发事务(T1) 多次 读取一个数据时,这个事务还未结束时候,这时另一个并发事务(T2)也进来了,它在第一个事务多次读取数据的中间,修改了这个数据。这就 导致了 第一个事务(T1),多次读取的数据的结果 也能不一样了。因此称为不可重复读

        4、幻读(Phantom read):幻读 和 不可重复读类似。它发生在一个并发事务(T1),读取 多行数据 ,接着 另一个 并发事务(T2)又进来了,在第一个事务读取多行数据的时候,插入或删除了 某条记录。这就导致了 T1 事务 会查询到一些 原本不存在的记录,就好像幻觉一样,因此称为幻读。

8、事务隔离级别有哪些?MySQL的默认隔离级别是?

        1、读未提交(read-uncommitted)

                最低的隔离级别允许读取尚未提交的数据变更,可能会导致脏读、幻读、或不可重复读。

        2、读已提交(read-committed)

                允许读取并发事务已经提交的数据,可以阻止脏读,但幻读和不可重复读还会有。

        3、可重复读(repeatable-read)

                同一字段的多次 读取结果都是一致的,除非数据是被本身事务自己所修改。可以阻止脏读和不可重复读,但幻读还会有。

        4、可串行化(serializable)

                最高的隔离级别:完全服从 ACID 的隔离级别。所有的事务 依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读,幻读,不可重复读。

9、$ 和 # 的区别?

        {}是预编译处理,${}是字符串替换,在mybatis中使用#{}可以防止sql注入,提高系统安全性。

六、Redis 缓存

1、Redis 为什么单线程还这么快?

        redis 是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最
高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案。

2、redis-key 命令

127.0.0.1:6379> keys * # 查看所有的key
(empty list or set)

127.0.0.1:6379> set name kuangshen # set key
OK

#	EXISTS 英 /ɪɡˈzɪsts/  一刻Z 此四
127.0.0.1:6379> EXISTS name # 判断当前的key是否存在  
(integer) 1

127.0.0.1:6379> EXISTS name1
(integer) 0

127.0.0.1:6379> move name 1 # 移除当前的key
(integer) 1
#	EXPIRE	英 /ɪkˈspaɪə(r)/  伊克斯 败儿
127.0.0.1:6379> EXPIRE name 10 # 设置key的过期时间,单位是秒
(integer) 1

127.0.0.1:6379> ttl name # 查看当前key的剩余时间
(integer) 4

127.0.0.1:6379> type name # 查看当前key的一个类型!
string

3、String 类型

127.0.0.1:6379> set key1 v1 # 设置值
OK
127.0.0.1:6379> get key1 # 获得值
"v1"
127.0.0.1:6379> keys * # 获得所有的key
1) "key1"
127.0.0.1:6379> EXISTS key1 # 判断某一个key是否存在
(integer) 1
127.0.0.1:6379> APPEND key1 "hello" # 追加字符串,如果当前key不存在,就相当于setkey
(integer) 7
127.0.0.1:6379> STRLEN key1 # 获取字符串的长度!
(integer) 7


############# 增量 ######################
127.0.0.1:6379> set views 0 # 初始浏览量为0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 自增1 浏览量变为1
(integer) 1
127.0.0.1:6379> decr views # 自减1 浏览量-1
(integer) 1
127.0.0.1:6379> INCRBY views 10 # 可以设置步长,指定增量!
(integer) 9
127.0.0.1:6379> DECRBY views 5 # 可以设置步长,指定减量!
(integer) 5


################# 字符串范围 range############
127.0.0.1:6379> set key1 "hello,kuangshen" # 设置 key1 的值
OK
127.0.0.1:6379> get key1
"hello,kuangshen"
127.0.0.1:6379> GETRANGE key1 0 3 # 截取字符串 [0,3]
"hell"
127.0.0.1:6379> GETRANGE key1 0 -1 # 获取全部的字符串 和 get key是一样的
"hello,kuangshen"


################### 替换!################
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 1 xx # 替换指定位置开始的字符串!
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"


################## setex  与  setnx #############
# setex (set with expire) # 设置过期时间
# setnx (set if not exist) # 不存在在设置 (在分布式锁中会常常使用!)
127.0.0.1:6379> setex key3 30 "hello" # 设置key3 的值为 hello,30秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 26
127.0.0.1:6379> setnx mykey "redis" # 如果mykey 不存在,创建mykey
(integer) 1
127.0.0.1:6379> setnx mykey "MongoDB" # 如果mykey存在,创建失败!
(integer) 0


############ 批量设置与取值 #####################
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k1"
2) "k2"
3) "k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx 是一个原子性的操作,要么一起成功,要么一起失败!
(integer) 0
127.0.0.1:6379> get k4
(nil)


################# 对象 #####################
# set user:1 {name:zhangsan,age:3} # 设置一个user:1 对象 值为 json字符来保存一个对象!
# 这里的key是一个巧妙的设计: user:{id}:{filed} , 如此设计在Redis中是完全OK了!
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"


############### getset ###################
# getset  先get然后在set
127.0.0.1:6379> getset db redis # 如果不存在值,则返回 nil
(nil)
127.0.0.1:6379> get db
"redis
127.0.0.1:6379> getset db mongodb # 如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"

4、list 

        在redis里面,我们可以把list玩成 ,栈、队列、阻塞队列!
        所有的list命令都是用
L 开头的,Redis不区分大小命令

127.0.0.1:6379> LPUSH list one # 将一个值或者多个值,插入到列表头部 (左)
(integer) 1

127.0.0.1:6379> LRANGE list 0 -1 # 获取list中值!
1) "three"
2) "two"
3) "one"

127.0.0.1:6379> Lpop list # 移除list的第一个元素
"three"

127.0.0.1:6379> Lpop list # 移除list的第一个元素
"three"

127.0.0.1:6379> Lpop list # 移除list的第一个元素
"three"

127.0.0.1:6379> lrem list 1 one # 移除list集合中指定个数的value,精确匹配
(integer) 1

##########################################################################
# lset 将列表中指定下标的值替换为另外一个值,更新操作
127.0.0.1:6379> EXISTS list # 判断这个列表是否存在
(integer) 0

##########################################################################
# linsert # 将某个具体的value插入到列把你中某个元素的前面或者后面!
127.0.0.1:6379> Rpush mylist "hello"

5、Set(集合)

##########################################################################
127.0.0.1:6379> sadd myset "hello" # set集合中添加匀速

127.0.0.1:6379> SMEMBERS myset # 查看指定set的所有值
1) "hello"
2) "lovekuangshen"
3) "kuangshen"

127.0.0.1:6379> SISMEMBER myset hello # 判断某一个值是不是在set集合中!
(integer) 1

127.0.0.1:6379> srem myset hello # 移除set集合中的指定元素
(integer) 1

6、Hash(哈希)

##########################################################################
127.0.0.1:6379> hset myhash field1 kuangshen # set一个具体 key-vlaue
(integer) 1
127.0.0.1:6379> hget myhash field1 # 获取一个字段值
"kuangshen"
127.0.0.1:6379> hmset myhash field1 hello field2 world # set多个 key-vlaue
OK
127.0.0.1:6379> hmget myhash field1 field2 # 获取多个字段值
1) "hello"
2) "world"

127.0.0.1:6379> hmset myhash field1 hello field2 world # set多个 key-vlaue
OK
127.0.0.1:6379> hmget myhash field1 field2 # 获取多个字段值
1) "hello"
2) "world"

7、Zset(有序集合)

127.0.0.1:6379> zadd myset 1 one # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 添加多个值
(integer) 2
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"

##########################################################################
排序如何实现
127.0.0.1:6379> zadd salary 2500 xiaohong # 添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 kaungshen
(integer) 1
# ZRANGEBYSCORE key min max
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示全部的用户 从小到大!
1) "kaungshen"
2) "xiaohong"
3) "zhangsan"

127.0.0.1:6379> ZRANGEBYSCORE salary -inf 2500 withscores # 显示工资小于2500员工的升
序排序!
1) "kaungshen"
2) "500"
3) "xiaohong"
4) "2500"

8、三种特殊类型

  1、Geospatial 地理位置

        这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!

 2、Hyperloglog

        Redis Hyperloglog 基数统计的算法!
        作用: 统计 页面访问人数,传统的可以用set,但是这样浪费了空间去存储这些用户id,我们的目的就是统计,所以可以用 Hyperloglog ,他占用的内存很小。但是有 0.81的错误率,可以用于一些允许容错的地方。

  3、Bitmap

        用于统计用户信息,活跃,不活跃! 登录 、 未登录! 打卡,365打卡! 两个状态的,都可以使用。他也是一种数据结构! 操作二进制位来进行记录,就只有0 和 1 两个状态!所以在一些状态位,可以用它来表示,在线多少人,不在线多少之类的。
 

9、redis 事务

        Redis单条命令式保存原子性的,但是事务不保证原子性!第三方

        redis的事务:

                开启事务(multi)

                命令入队(......)                

                执行事务(exec)/ 取消事务 (discard)
 

10、redis 配置文件

        1、配置文件 对 unit单位 大小写不敏感

        2、快照 rdb

# 如果900s内,如果至少有一个1 key进行了修改,我们及进行持久化操作
save 900 1
# 如果300s内,如果至少10 key进行了修改,我们及进行持久化操作
save 300 10
# 如果60s内,如果至少10000 key进行了修改,我们及进行持久化操作
save 60 10000

stop-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作!
rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些cpu资源!
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验!
dir ./ # rdb 文件保存的目录!

        3、SECURITY 安全

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass # 获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置redis的密码
OK
127.0.0.1:6379> config get requirepass # 发现所有的命令都没有权限了
(error) NOAUTH Authentication required.
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # 使用密码进行登录!
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456

        4、限制客户端数量

                内存上限后的处理策略,共有6种,默认是只删除过期时间

maxclients 10000 # 设置能连接上redis的最大客户端的数量
maxmemory <bytes> # redis 配置最大的内存容量

maxmemory-policy noeviction # 内存到达上限之后的处理策略
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误

        5、APPEND ONLY 模式 aof配置 三种执行策略

                默认: 每隔一秒同步一次

# 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendonly no 

# 持久化的文件的名字
appendfilename "appendonly.aof"

# 每次修改都会 sync。消耗性能
# appendfsync always 

# 每秒执行一次 sync,可能会丢失这1s的数据!
appendfsync everysec 

# 不执行 sync,这个时候操作系统自己同步数据,速度最快!
# appendfsync no 

11、Redis持久化

  1、RDB

        Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是 RDB,一般情况下不需要修改这个配置!

触发机制

1、save的规则满足的情况下,会自动触发rdb规则

2、执行 flushall 命令,也会触发我们的rdb规则!

3、退出redis,也会产生 rdb 文件! 备份就自动生成一个 dump.rdb

恢复rdb文件!

1、只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!

2、查看需要存在的位置

127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin" # 如果在这个目录下存在 dump.rdb 文件,启动就会自动恢复其中的数据

优点:

        1、适合大规模的数据恢复!

        2、对数据的完整性要不高!

缺点:

        1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了!

         2、fork进程的时候,会占用一定的内容空间!

  2、AOF

        以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

优点:

        1、每一次修改都同步,文件的完整会更加好! ​

        2、每秒同步一次,可能会丢失一秒的数据 ​ 3、从不同步,效率最高的! ​

缺点:

        1、相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢! ​

        2、Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化

扩展:

        1、RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储

        2、AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

        3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化

        4、同时开启两种持久化方式

  •   在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

    • RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段

  • 5、性能建议

    • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这条规则。

    • 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。

    • 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

12、主从复制

        复制3个配置文件,然后修改对应的信息

                1、端口
                2、pid 名字
                3、log文件名字
                4、dump.rdb 名字

        修改完毕之后,启动我们的3个redis服务器,可以通过进程信息查看!  

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host 6379 找谁当自己的老大!
OK
127.0.0.1:6380> info replication
# Replication
role:slave # 当前角色是从机
master_host:127.0.0.1 # 可以的看到主机的信息
master_port:6379


# 在主机中查看!
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1 # 多了从机的配置
slave0:ip=127.0.0.1,port=6380,state=online,offset=42,lag=1 # 多了从机的配置
master_replid:a81be8dd257636b2d3e7a9f595e69d73ff03774e

主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!

复制原理  

        1、Slave(从) 启动成功连接到 master(主) 后会发送一个sync同步命令

        2、Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

        全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

        增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中看到! 
 

13、哨兵模式

        1、新建一个哨兵配置文件 sentinel.conf

# sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

        后面的这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!

        2、启动哨兵!

[root@kuangshen bin]# redis-sentinel kconfig/sentinel.conf

哨兵模式

优点:
    1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
    2、 主从可以切换,故障可以转移,系统的可用性就会更好
    3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!

缺点:
    1、Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
    2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

哨兵模式的全部配置!  

# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供
#密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
#这个数字越小,完成failover所需的时间就越长,
#但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
#可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那
#里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,
#slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知
#相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),
#将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信
#息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配
#置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无
#法正常启动成功。
#通知脚本
# shell编程
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置!

14、缓存失效问题

  1、缓存穿透

                是指查询一个一定不存在的数据

                在流量大时, 可能 DB 就挂掉了

        解决

                缓存空结果、 并且设置短的过期时间· 

                布隆过滤器

布隆过滤器 是什么?

        布隆过滤器其实是一个很长的二进制的一个向量,相当于一个很长的数组,里面存放的二进制数,就是 0 和 1 ,0 表示 不存在 某个数据,1 表示 存在 某个数据。

布隆过滤器的 用途?

        1、解决 Redis 的 缓存穿透问题

        2、在爬虫时,对爬虫网址过滤,已经存在布隆过滤器中的网址,不再爬取

        3、垃圾邮件过滤,对每个发送邮件的地址,存入布隆过滤器的黑名单中,存在,就为垃圾

布隆过滤器的 原理?

存入过程

        布隆过滤器 他其实就是一个二进制数据的集合,他在把数据加到这个集合前,首先会进行 N次哈希计算函数,计算它的哈希值,并且把这个哈希值映射到这个二级制的数组下标中,将这个下标对应的值,0 改为 1,表示已存在。

查询过程

        查询过程 和 存入过程 差不多,也是经过 N 次哈希计算函数,算出哈希值,找到这个哈希值对应的下标,看下标对应的值 是否为 1,为 1 就是存在,反之亦然。

删除过程

        这是他的一个缺点之一,下面再说

布隆过滤器的 优缺点?

优势:

        1、因为存储的是 二进制数据,所以他占用的空间很小

        2、他的插入和查询速度是很快的

        3、保密性很好,因为存的都是 二进制数据

缺点:

        因为 添加数据 是要经过 哈希运算的,所以可能会出现 最后的 哈希运算 的结果 是相同的。

        1、存在误

        因为最后的哈希运算值 可能是一样的,所以会出现误判。

        2、删除困难

        在删除的时候,可能会 一个哈希值 代表了 两个数据,你先删除 其中一个数据代表的哈希值,但是会同时 删除 另一个相同 哈希值的数据。

布隆过滤器的 实现?

1、导入 redisson 依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.13.4</version>
</dependency>

2、java 代码

public class RedissonBloomFilter {

  public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    config.useSingleServer().setPassword("1234");
    //构造Redisson
    RedissonClient redisson = Redisson.create(config);

    RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
    //初始化布隆过滤器:预计元素为100000000L,偏差率为3%
    bloomFilter.tryInit(100000000L,0.03);
    //将号码10086插入到布隆过滤器中
    bloomFilter.add("10086");

    //判断下面号码是否在布隆过滤器中
    //输出false
    System.out.println(bloomFilter.contains("123456"));
    //输出true
    System.out.println(bloomFilter.contains("10086"));
  }
}

        

2、缓存雪崩

        是指在我们设置缓存时采用了相同的过期时间, 导致缓存在某一时刻同时失效, 请求全部转发到 DB, DB 瞬时压力过重雪崩  

 解决 :

   原有的失效时间基础上增加一个随机值

   数据预热  事先访问一遍

   限流降级
 

 3、缓存击穿

        热点的key 过期了,突然大并发访问来了,击穿缓存,查询数据库 

解决:

         解锁,分布式锁,读写锁

         热点数据永不过期
 

15、缓存数据一致性

        问题
                双写模式

                失效模式

        解决

                使用 canal ,感知到MySQL的更新去更新数据库

                分布式读写锁
 


 

七、RabbitMQ 消息中间件

  1、为什么要使用 RabbitMQ?

        1、在分布式系统下具备异步处理,流量削峰,应用解耦,负载均衡
        2、拥有持久化的机制,进程消息,队列中的信息也可以保存下来。
        3、实现消费者和生产者之间的解耦。
        4、对于高并发场景下,利用消息队列可以使得
同步访问变为串行访问达到一定量的限流,利于数据库的操作。
        5.可以使用消息队列达到异步下单的效果,排队中,后台进行逻辑下单。

   2、使用 rabbitmq 的场景

        1、服务间异步通信
        2、顺序消费
        3、定时任务
        4、请求削峰
        5、异常报错

  3、rabbitmq 如何保证消息不丢失

                前提: 交换机 或者 队列 挂了

        发布确认机制 confirm 

                1、在配置文件当中需要添加
                        spring.rabbitmq.publisher-confirm-type=correlated

                        (英 /'kɒrəleɪtɪd/ 靠累儿铁特 )  触发回调方法

spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
ing.rabbitmq.publisher-confirm-type=correlated

                2、添加配置类

                        声明 确认交换机、确认队列,绑定关系

                3、添加回调接口  监听交换机是否工作

/**
 *   回调接口
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        // 注入
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
        /**
         * true:
         *      交换机无法将消息进行路由时,会将该消息返回给生产者
         * false:
         *      如果发现消息无法进行路由,则直接丢弃
         */
        rabbitTemplate.setMandatory(true);
    }

    /**
     * @param correlationData    交换机不管是否收到 消息 的一个回调方法
     * @param ack       交换机是否 收到消息
     * @param cause     原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
      String id = correlationData !=null?correlationData.getId():"";
        if (ack) {
            log.info("交换机 已经收到 id 为:{} 的消息",id);
        } else {
            log.info("交换机 还未收到 id 为:{} 的消息,由于原因:{}",id,cause);
        }
    }


    @Override
    public void returnedMessage(ReturnedMessage returned) {

        log.info("消息:{},被交换机{},退回,退回原因:{},路由Key:{}",
                returned.getMessage().getBody(),
                returned.getExchange(),
                returned.getReplyText(),
                returned.getRoutingKey());

    }
}

        总结:

        要保证消息不丢失,首先保证交换机能否正常工作,所以需要手写一个 回调配置类,发送消息的时候 绑定一个 唯一ID,可以用 redis 的 setnx 来保证唯一性。回调接口类 通过 实现 RabbitTemplate.ConfirmCallback 接口,实现一个 confirm 方法,他有三个参数,一个消息相关数据的(CorrelationData)、一个boolean ack、和 String cause。 只要去判断 那个 Boolean 类型的 ack。如果为 true 则说明 交换机收到了消息,反之,可以日志打印 错误原因。

        然后,还要实现一个 RabbitTemplate.ReturnCallback 接口,这是 当消息无法路由的时候的一个回调方法,相当于监听队列的。并且可以设置 rabbitTemplate.setMandatory(true) ,为 true 的时候,代表 交换机无法将消息路由时,会将该消息返回给生产者。反之,直接丢弃消息。

        另外还有一个方案,就是采用 备份交换机,就是在去声明一个备份交换机,然后在原来的交换机上,去添加一个参数,声明他有一个备份交换机。参数:alternate-exchange。这样当你路由key发送失败的话,会走那个备份交换机发送消息。

// 声明交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
    Map<String, Object> arguments = new HashMap<>(2);
    // 添加参数 设置该交换机 的 备份交换机
    arguments.put("alternate-exchange",BACKUP_EXCHANGE);
    return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE)
            .withArguments(arguments).durable(true).build();
}

        同时还要做好 容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机
制,可
记录到数据库,采用定期扫描重发的方式

        在消费者端,一定要采用手动应答(手动ack)模式,失败或者没来得及处理,就noAck并重
新入队

 

  4、rabbitmq 如何保证消息不重复消费

        消费者 每次 消费消息时用该 全局唯一id 先判断该消息是否已消费过,这个全局 唯一ID 推荐使用 redis 的 setnx 命令,他天然具有幂等性。从而实现不重复消费。

        或者在rabbitMQ的每一个消息都有 redelivered 字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的,但是这种不太严谨
 

  5、rabbitmq 如何解决消息积压问题

        1、设置消息过期时间,超时进入到死信队列,然后再慢慢处理死信队列中消息

        2、上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

  6、不公平分发

        设置参数 channel.basicQos(1);

  7、死信队列

     来源:

        1、消息 TTL 过期
        2、队列达到最大长度(队列满了,无法再添加数据到 mq 中)
        3、消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.

  8、RabbitMQ解决分布式事务

        经典案例,以目前流行的外卖为例,用户下单后,调用订单服务,订单服务调用派单系统通知送外卖人员送单,这时候订单系统与派单系统采用MQ异步通讯。

RabbitMQ解决分布式事务原理

答案:柔性事务-可靠消息+最终一致性方案( 异步确保型)

        需要保证以下三要素:

        a、确保生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制

        b、确保消费者能够正确消费消息,采用手动ACK模式(注意重试、幂等性问题)

        c、如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。(如果第一个事务中出错,补单消费者会在重新执行一次第一个事务,例如第一个事务是添加订单表,如果失败在补单的时候重新生成订单记录,由于订单号唯一,所以不会重复)。每一个发送的消息都在数据库做好记录。 定期将失败的消息再次发送一遍
 

八、ElasticSearch 搜索引擎

  1、elasticsearch 的倒排索引是什么

        传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。

        而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。

九、Zookeeper + Dubbo 微服务

十、Linux 面试题

1、绝对路径用什么符号表示?

        1、绝对路径: 如/etc/init.d

        2、当前目录和上层目录: ./ ../

        3、主目录: ~/

        4、切换目录: cd
 

2、列举几个常用的Linux命令

        1、列出文件列表:ls【参数 -a -l】

        2、创建目录和移除目录:mkdir rmdir

        3、创建空文件:touch
        
        4、文件权限 chmod 777 file

        5、用于显示文件后几行内容:tail,例如: tail -n 1000:显示最后1000行
        
        6、打包:tar -xvf
        
        7、打包并压缩:tar -zcvf

        8、查找字符串:grep

        9、显示当前所在目录:pwd

        10、编辑器:vim vi

        11、复制文件  cp -r
        
        12、删除文件  rm -r
        
        13、"?"可替 代单个字符
        
        14、"*"可替 代多个字符
        
        15、后台运行:在命令后加 “&”
        
        16、查看后台任务: job -l
        
        17、把后台任务调到前台执行 fg
        
        18、把停下的后台任务在后台执行起来 bg
        
        19、查看磁盘使用空间: df -hl

3、查看日志

         Linux查看日志的命令有多种: tail、cat、tac、head、echo等,本文只介绍几种常用的方法。

    1、tail
        最常用的一种查看方式
        命令格式: tail[必要参数][选择参数][文件]
        -f  循环读取
        -q 不显示处理信息
        -v 显示详细的处理信息
        -c<数目> 显示的字节数
        -n<行数> 显示行数
        -q, --quiet, --silent 从不输出给出文件名的首部
        -s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒
        例如:

#查询日志尾部最后10行的日志;
tail -n 10 test.log 

#查询10行之后的所有日志;
tail -n +10 test.log 

#循环实时查看最后1000行记录(最常用的)
tail -fn 10 test.log 


        一般还会配合着grep搜索用,例如 :

tail -fn 1000 test.log | grep '关键字'


        如果一次性查询的数据量太大,可以进行翻页查看,例如 :

 # 可以进行多屏显示(ctrl + f 或者 空格键可以快捷键)

 tail -n 4700 aa.log |more -1000 

2、head
        跟tail是相反的head是看前多少行日志

# 查询日志文件中的头10行日志;
head -n 10 test.log 

# 查询日志文件除了最后10行的其他所有日志;
head -n -10 test.log 

 head其他参数参考tail

4、搜索文件用什么命令? 格式是怎么样的

        find <指定目录> <指定条件> <指定动作>

        whereis 加参数与文件名

        locate 只加文件名

        find 直接搜索磁盘,较慢。

        find / -name "string*
        
 

十一、前端 技术栈

1、ajax 原理

        要完整实现一个AJAX异步调用和局部刷新,通常需要以下几个步骤:

                1、创建XMLHttpRequest对象,即创建一个异步调用对象.

                2、创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息.

                3、设置响应HTTP请求状态变化的函数.

                4、发送HTTP请求.

                5、获取异步调用返回的数据.

                6、使用JavaScript和DOM实现局部刷新.

2、cookie 和 session 的区别

1、存储位置不同

cookie的数据信息存放在客户端浏览器上。

session的数据信息存放在服务器上。

2、存储容量不同

单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie。

对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制。

3、存储方式不同

cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。

session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。

4、隐私策略不同

cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的。

session存储在服务器上,对客户端是透明对,不存在敏感信息泄漏的风险。

5、有效期上不同

开发可以通过设置cookie的属性,达到使cookie长期有效的效果。

session依赖于名为JSESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session不能达到长期有效的效果。

6、服务器压力不同

cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。

session是保管在服务器端的,每个用户都会产生一个session。假如并发访问的用户十分多,会产生十分多的session,耗费大量的内存。

7、浏览器支持不同

假如客户端浏览器不支持cookie:

cookie是需要客户端浏览器支持的,假如客户端禁用了cookie,或者不支持cookie,则会话跟踪会失效。关于WAP上的应用,常规的cookie就派不上用场了。

运用session需要使用URL地址重写的方式。一切用到session程序的URL都要进行URL地址重写,否则session会话跟踪还会失效。

假如客户端支持cookie:

cookie既能够设为本浏览器窗口以及子窗口内有效,也能够设为一切窗口内有效。

session只能在本窗口以及子窗口内有效。

8、跨域支持上不同

cookie支持跨域名访问。

session不支持跨域名访问。

3、tcp 和 udp 的区别

        1、TCP 是面向连接的,UDP 是面向无连接的

        2、UDP程序结构较简单

        3、TCP 是面向字节流的,UDP 是基于数据报的

        4、TCP 保证数据正确性,UDP 可能丢包

        5、TCP 保证数据顺序,UDP 不保证

十二、其他技术栈

1、nginx

        说明: 在 http{},里面的 upstream{} 里面配置你的 负载均衡,填写多台服务器的ip,并且可以选择 分配方式,一共四种:

        1、轮询(默认),或者指定 weight(权重)

        2、ip_hash:每个请求,按照 ip 的 hash 结果分配,这样就每个访客固定访问一个后端服务器,可以解决 session 问题。

        3、fair:按照 后端服务器的响应时间来分配请求,响应时间短的优先分配

        4、url_hash:按照 访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个服务器,后端服务器为缓存时,比较有效。

        然后,在下面的 server{},里面就可以配置你的 域名,就是反向代理,还可以配置一些 静态资源的路径之类的。

配置文件详解

######Nginx配置文件nginx.conf中文详解#####

#定义Nginx运行的用户和用户组
user www www;

#nginx进程数,建议设置为等于CPU总核心数。
worker_processes 8;
 
#全局错误日志定义类型,[ debug | info | notice | warn | error | crit ]
error_log /usr/local/nginx/logs/error.log info;

#进程pid文件
pid /usr/local/nginx/logs/nginx.pid;

#指定进程可以打开的最大描述符:数目
#工作模式与连接数上限
#这个指令是指当一个nginx进程打开的最多文件描述符数目,理论值应该是最多打开文件数(ulimit -n)与nginx进程数相除,但是nginx分配请求并不是那么均匀,所以最好与ulimit -n 的值保持一致。
#现在在linux 2.6内核下开启文件打开数为65535,worker_rlimit_nofile就相应应该填写65535。
#这是因为nginx调度时分配请求到进程并不是那么的均衡,所以假如填写10240,总并发量达到3-4万时就有进程可能超过10240了,这时会返回502错误。
worker_rlimit_nofile 65535;


events
{
    #参考事件模型,use [ kqueue | rtsig | epoll | /dev/poll | select | poll ]; epoll模型
    #是Linux 2.6以上版本内核中的高性能网络I/O模型,linux建议epoll,如果跑在FreeBSD上面,就用kqueue模型。
    #补充说明:
    #与apache相类,nginx针对不同的操作系统,有不同的事件模型
    #A)标准事件模型
    #Select、poll属于标准事件模型,如果当前系统不存在更有效的方法,nginx会选择select或poll
    #B)高效事件模型
    #Kqueue:使用于FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS X.使用双处理器的MacOS X系统使用kqueue可能会造成内核崩溃。
    #Epoll:使用于Linux内核2.6版本及以后的系统。
    #/dev/poll:使用于Solaris 7 11/99+,HP/UX 11.22+ (eventport),IRIX 6.5.15+ 和 Tru64 UNIX 5.1A+。
    #Eventport:使用于Solaris 10。 为了防止出现内核崩溃的问题, 有必要安装安全补丁。
    use epoll;

    #单个进程最大连接数(最大连接数=连接数*进程数)
    #根据硬件调整,和前面工作进程配合起来用,尽量大,但是别把cpu跑到100%就行。每个进程允许的最多连接数,理论上每台nginx服务器的最大连接数为。
    worker_connections 65535;

    #keepalive超时时间。
    keepalive_timeout 60;

    #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求头的大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。
    #分页大小可以用命令getconf PAGESIZE 取得。
    #[root@web001 ~]# getconf PAGESIZE
    #4096
    #但也有client_header_buffer_size超过4k的情况,但是client_header_buffer_size该值必须设置为“系统分页大小”的整倍数。
    client_header_buffer_size 4k;

    #这个将为打开文件指定缓存,默认是没有启用的,max指定缓存数量,建议和打开文件数一致,inactive是指经过多长时间文件没被请求后删除缓存。
    open_file_cache max=65535 inactive=60s;

    #这个是指多长时间检查一次缓存的有效信息。
    #语法:open_file_cache_valid time 默认值:open_file_cache_valid 60 使用字段:http, server, location 这个指令指定了何时需要检查open_file_cache中缓存项目的有效信息.
    open_file_cache_valid 80s;

    #open_file_cache指令中的inactive参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive时间内一次没被使用,它将被移除。
    #语法:open_file_cache_min_uses number 默认值:open_file_cache_min_uses 1 使用字段:http, server, location  这个指令指定了在open_file_cache指令无效的参数中一定的时间范围内可以使用的最小文件数,如果使用更大的值,文件描述符在cache中总是打开状态.
    open_file_cache_min_uses 1;
    
    #语法:open_file_cache_errors on | off 默认值:open_file_cache_errors off 使用字段:http, server, location 这个指令指定是否在搜索一个文件是记录cache错误.
    open_file_cache_errors on;
}
 
 
 
#设定http服务器,利用它的反向代理功能提供负载均衡支持
http
{
    #文件扩展名与文件类型映射表
    include mime.types;

    #默认文件类型
    default_type application/octet-stream;

    #默认编码
    #charset utf-8;

    #服务器名字的hash表大小
    #保存服务器名字的hash表是由指令server_names_hash_max_size 和server_names_hash_bucket_size所控制的。参数hash bucket size总是等于hash表的大小,并且是一路处理器缓存大小的倍数。在减少了在内存中的存取次数后,使在处理器中加速查找hash表键值成为可能。如果hash bucket size等于一路处理器缓存的大小,那么在查找键的时候,最坏的情况下在内存中查找的次数为2。第一次是确定存储单元的地址,第二次是在存储单元中查找键 值。因此,如果Nginx给出需要增大hash max size 或 hash bucket size的提示,那么首要的是增大前一个参数的大小.
    server_names_hash_bucket_size 128;

    #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求的头部大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。分页大小可以用命令getconf PAGESIZE取得。
    client_header_buffer_size 32k;

    #客户请求头缓冲大小。nginx默认会用client_header_buffer_size这个buffer来读取header值,如果header过大,它会使用large_client_header_buffers来读取。
    large_client_header_buffers 4 64k;

    #设定通过nginx上传文件的大小
    client_max_body_size 8m;

    #开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。注意:如果图片显示不正常把这个改成off。
    #sendfile指令指定 nginx 是否调用sendfile 函数(zero copy 方式)来输出文件,对于普通应用,必须设为on。如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络IO处理速度,降低系统uptime。
    sendfile on;

    #开启目录列表访问,合适下载服务器,默认关闭。
    autoindex on;

    #此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
    tcp_nopush on;
     
    tcp_nodelay on;

    #长连接超时时间,单位是秒
    keepalive_timeout 120;

    #FastCGI相关参数是为了改善网站的性能:减少资源占用,提高访问速度。下面参数看字面意思都能理解。
    fastcgi_connect_timeout 300;
    fastcgi_send_timeout 300;
    fastcgi_read_timeout 300;
    fastcgi_buffer_size 64k;
    fastcgi_buffers 4 64k;
    fastcgi_busy_buffers_size 128k;
    fastcgi_temp_file_write_size 128k;

    #gzip模块设置
    gzip on; #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0;    #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;    #压缩等级
    gzip_types text/plain application/x-javascript text/css application/xml;    #压缩类型,默认就已经包含textml,所以下面就不用再写了,写上去也不会有问题,但是会有一个warn。
    gzip_vary on;

    #开启限制IP连接数的时候需要使用
    #limit_zone crawler $binary_remote_addr 10m;



    #负载均衡配置
    upstream piao.jd.com {
     
        #upstream的负载均衡,weight是权重,可以根据机器配置定义权重。weigth参数表示权值,权值越高被分配到的几率越大。
        server 192.168.80.121:80 weight=3;
        server 192.168.80.122:80 weight=2;
        server 192.168.80.123:80 weight=3;

        #nginx的upstream目前支持4种方式的分配
        #1、轮询(默认)
        #每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
        #2、weight
        #指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
        #例如:
        #upstream bakend {
        #    server 192.168.0.14 weight=10;
        #    server 192.168.0.15 weight=10;
        #}
        #2、ip_hash
        #每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
        #例如:
        #upstream bakend {
        #    ip_hash;
        #    server 192.168.0.14:88;
        #    server 192.168.0.15:80;
        #}
        #3、fair(第三方)
        #按后端服务器的响应时间来分配请求,响应时间短的优先分配。
        #upstream backend {
        #    server server1;
        #    server server2;
        #    fair;
        #}
        #4、url_hash(第三方)
        #按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
        #例:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法
        #upstream backend {
        #    server squid1:3128;
        #    server squid2:3128;
        #    hash $request_uri;
        #    hash_method crc32;
        #}

        #tips:
        #upstream bakend{#定义负载均衡设备的Ip及设备状态}{
        #    ip_hash;
        #    server 127.0.0.1:9090 down;
        #    server 127.0.0.1:8080 weight=2;
        #    server 127.0.0.1:6060;
        #    server 127.0.0.1:7070 backup;
        #}
        #在需要使用负载均衡的server中增加 proxy_pass http://bakend/;

        #每个设备的状态设置为:
        #1.down表示单前的server暂时不参与负载
        #2.weight为weight越大,负载的权重就越大。
        #3.max_fails:允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream模块定义的错误
        #4.fail_timeout:max_fails次失败后,暂停的时间。
        #5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。

        #nginx支持同时设置多组的负载均衡,用来给不用的server来使用。
        #client_body_in_file_only设置为On 可以讲client post过来的数据记录到文件中用来做debug
        #client_body_temp_path设置记录文件的目录 可以设置最多3层目录
        #location对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡
    }
     
     
     
    #虚拟主机的配置
    server
    {
        #监听端口
        listen 80;

        #域名可以有多个,用空格隔开
        server_name www.jd.com jd.com;
        index index.html index.htm index.php;
        root /data/www/jd;

        #对******进行负载均衡
        location ~ .*.(php|php5)?$
        {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            include fastcgi.conf;
        }
         
        #图片缓存时间设置
        location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires 10d;
        }
         
        #JS和CSS缓存时间设置
        location ~ .*.(js|css)?$
        {
            expires 1h;
        }
         
        #日志格式设定
        #$remote_addr与$http_x_forwarded_for用以记录客户端的ip地址;
        #$remote_user:用来记录客户端用户名称;
        #$time_local: 用来记录访问时间与时区;
        #$request: 用来记录请求的url与http协议;
        #$status: 用来记录请求状态;成功是200,
        #$body_bytes_sent :记录发送给客户端文件主体内容大小;
        #$http_referer:用来记录从那个页面链接访问过来的;
        #$http_user_agent:记录客户浏览器的相关信息;
        #通常web服务器放在反向代理的后面,这样就不能获取到客户的IP地址了,通过$remote_add拿到的IP地址是反向代理服务器的iP地址。反向代理服务器在转发请求的http头信息中,可以增加x_forwarded_for信息,用以记录原有客户端的IP地址和原来客户端的请求的服务器地址。
        log_format access '$remote_addr - $remote_user [$time_local] "$request" '
        '$status $body_bytes_sent "$http_referer" '
        '"$http_user_agent" $http_x_forwarded_for';
         
        #定义本虚拟主机的访问日志
        access_log  /usr/local/nginx/logs/host.access.log  main;
        access_log  /usr/local/nginx/logs/host.access.404.log  log404;
         
        #对 "/" 启用反向代理
        location / {
            proxy_pass http://127.0.0.1:88;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;
             
            #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
             
            #以下是一些反向代理的配置,可选。
            proxy_set_header Host $host;

            #允许客户端请求的最大单文件字节数
            client_max_body_size 10m;

            #缓冲区代理缓冲用户端请求的最大字节数,
            #如果把它设置为比较大的数值,例如256k,那么,无论使用firefox还是IE浏览器,来提交任意小于256k的图片,都很正常。如果注释该指令,使用默认的client_body_buffer_size设置,也就是操作系统页面大小的两倍,8k或者16k,问题就出现了。
            #无论使用firefox4.0还是IE8.0,提交一个比较大,200k左右的图片,都返回500 Internal Server Error错误
            client_body_buffer_size 128k;

            #表示使nginx阻止HTTP应答代码为400或者更高的应答。
            proxy_intercept_errors on;

            #后端服务器连接的超时时间_发起握手等候响应超时时间
            #nginx跟后端服务器连接超时时间(代理连接超时)
            proxy_connect_timeout 90;

            #后端服务器数据回传时间(代理发送超时)
            #后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
            proxy_send_timeout 90;

            #连接成功后,后端服务器响应时间(代理接收超时)
            #连接成功后_等候后端服务器响应时间_其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间)
            proxy_read_timeout 90;

            #设置代理服务器(nginx)保存用户头信息的缓冲区大小
            #设置从被代理服务器读取的第一部分应答的缓冲区大小,通常情况下这部分应答中包含一个小的应答头,默认情况下这个值的大小为指令proxy_buffers中指定的一个缓冲区的大小,不过可以将其设置为更小
            proxy_buffer_size 4k;

            #proxy_buffers缓冲区,网页平均在32k以下的设置
            #设置用于读取应答(来自被代理服务器)的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k
            proxy_buffers 4 32k;

            #高负荷下缓冲大小(proxy_buffers*2)
            proxy_busy_buffers_size 64k;

            #设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长
            #设定缓存文件夹大小,大于这个值,将从upstream服务器传
            proxy_temp_file_write_size 64k;
        }
         
         
        #设定查看Nginx状态的地址
        location /NginxStatus {
            stub_status on;
            access_log on;
            auth_basic "NginxStatus";
            auth_basic_user_file confpasswd;
            #htpasswd文件的内容可以用apache提供的htpasswd工具来产生。
        }
         
        #本地动静分离反向代理配置
        #所有jsp的页面均交由tomcat或resin处理
        location ~ .(jsp|jspx|do)?$ {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:8080;
        }
         
        #所有静态文件由nginx直接读取不经过tomcat或resin
        location ~ .*.(htm|html|gif|jpg|jpeg|png|bmp|swf|ioc|rar|zip|txt|flv|mid|doc|ppt|
        pdf|xls|mp3|wma)$
        {
            expires 15d; 
        }
         
        location ~ .*.(js|css)?$
        {
            expires 1h;
        }
    }
}
######Nginx配置文件nginx.conf中文详解#####

十三、SpringCloud Alibaba

SpringCloud Alibaba - Nacos: 注册中心(服务发现/注册)

SpringCloud Alibaba - Nacos: 配置中心(动态配置管理)

SpringCloud - Ribbon: 负载均衡

SpringCloud - Feign: 声明式 HTTP 客户端(调用远程服务)

SpringCloud Alibaba - Sentinel: 服务容错(限流、 降级、 熔断)

SpringCloud - Gateway: API 网关(webflux 编程模式)

SpringCloud - Sleuth: 调用链监控

SpringCloud Alibaba - Seata: 原 Fescar, 即分布式事务解决方案

 

十四、VUE

  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页

打赏作者

别訆我小内脏

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值