自我介绍讲一个项目的点，因为用到了中间件平台的数据同步，于是开始鞭打数据同步。。如果同步的时候，插入了新数据怎么处理？binlog有什么用？binlog的数据格式有哪些？如何监听binlog？mysqldump 之后需要进行什么处理？（这个问的太细了，细到我都不知道面试官是什么意思？）唯一索引和联合索引有什么区别？联合索引可以是唯一索引吗？那mysql索引结构是什么样的？一个索引的建立过程是什么样的？如果我对age字段建立索引，建立的过程是什么样的？为什么走索引加快了？为什么age可以建立索引？sex字段就不行？为什么sex建立索引还是会扫全表？如果我订单表达到一定规模之后mysql单表是撑不住了，怎么办？具体你会怎么分库分表？分库分表如果进行条件查询？同步ES？不使用其他组件，单单是mysql怎么操作，所有表遍历找过去吗？索引会不会失效？ok，redis源码有了解吗？他的线程模型是什么样的？redis有哪些存储日志的形式？同步还是异步？那AOF具体是怎么存储日志的？AOF不断的写日志不是会有很多的io操作吗？怎么避免？RDB是怎么进行操作？那如果数据库和redis需要缓存一致性怎么解决？那我不考虑最终一致性，我要强一致性，怎么解决？ok，网络这块我也问问。http和https的区别是什么？https是怎么建立连接的？先建立什么？再建立什么？具体是怎么建立的ssl tls加密？http的请求头和响应头一般有什么信息，有什么用？ok，页的概念你清楚吗？为什么需要内存对齐？页碎是什么？go里面怎么样会发生死锁？死锁的场景具体有哪些？内存泄漏有哪些场景？怎么排查？goruntine泄漏的场景有哪些？怎么排查？进程、线程、协程有什么区别？协程能被 kill 掉吗？那协程应该怎么处理？那context一般有什么信息？有什么用途？那如果我要 clone 一个context，子context 和 父context 是一摸一样吗？为什么？singleflight 是什么？什么时候用的？如果这个goruntine超时怎么办？doChan方法具体是怎么实现的？为什么会有饥饿模式？什么时候会让出时间片？IO密集型和计算密集型的区别？这段代码会发生什么？为什么？具体是怎么溢出的？var a uint = 1 var b uint = 2 fmt.Println(a-b) 说出以下输出结果？为什么？func TestSlicePrint(t *testing.T) { a := []byte("AAAA/BBBBB") index := bytes.IndexByte(a, '/') b := a[:index] c := a[index+1:] b = append(b, "CCC"...) fmt.Println(string(a)) fmt.Println(string(b)) fmt.Println(string(c)) }这个锁是什么用的？这段代码有什么问题？func TestNumPrint(t *testing.T) { wg := sync.WaitGroup{} lock := sync.Mutex{} a := 0 b := 10 for i := 0; i < 5; i++ { go func() { if a > b { fmt.Println("done") return } lock.Lock() defer lock.Unlock() a++ fmt.Printf("i: %d \n", i) }() } wg.Wait() }最后手撕一题：整数反转。
进程进程是资源分配的最小单位，每个进程都有自己的独立内存空间，进程由进程控制块、程序段和数据段组成。进程控制块（PCB）保存进程运行期间相关的数据，是进程存在的唯一标志。程序段是能被进程调度程序调度到CPU 运行的程序的代码段。数据段用来存储程序运行期间的相关数据。进程是应用程序运行的载体，可看做是正在执行的程序。程序本身是没有生命周期的，只是存在于磁盘上的一些指令集合，但程序一旦被运行起来就是进程。启动后的进程，会依赖操作系统的调度完成生命周期的转换。由于每个进程都有独立的代码和数据空间，所以进程间的切换会有较大的开销。线程线程是CPU任务调度和执行的最小单位，一个进程中可以包含多个线程，这些线程可以并发运行，且共享进程提供的相同的代码和数据空间，每个线程都有自己独立的运行栈和程序计数器，线程间切换的开销小。协程协程，又称微线程，是一种用户态的轻量级线程，一个线程可以有多个协程。协程的调度完全由用户控制（也就是在用户态执行）。协程拥有自己的寄存器上下文和栈。协程调度切换时，将寄存器上下文和栈保存到线程的堆区，在切回来的时候，恢复先前保存的寄存器上下文和栈，直接操作栈则基本没有内核切换的开销，可以不加锁的访问全局变量，所以协程的上下文切换非常快。线程在进程的内部，它不能独立执行，必须依存于进程中，由进程提供多个线程的执行控制，进程和线程都可以并发的执行。协程抽象于线程之上，线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行。与线程相比，协程占用资源小，由用户调度，切换开销小，同一个线程中的协程不需要使用锁，执行效率更高。
一、什么是设计模式设计模式，是一套被反复使用，多数人知晓的，经过分类编目的，代码设计经验的总结。使用设计模式是为了可重复代码、让代码更容易被他人理解、保证代码可靠性、程序重用性。二、为什么要学习设计模式看代码源码：如果你不懂色剂模式去看源码，你就会很迷茫，你会寸步难行看看前辈的代码：你去个公司难道都是新项目让你接手？很有可能是解盘的，前辈的开发难道不用设计模式？编写自己的理想中的好代码：我个人反正是这样的，对于我自己开发的项目我会很认真，我对它比我对女朋友还好，把项目当初自己儿子一样三、设计模式分类创建型模式：单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式结构型模式：适配器模式、桥接模式、组合模式、装饰模式、外观模式、亨元模式、代理模式行为型模式：访问者模式、模板模式、策略模式、状态模式、观察着模式、备忘录模式、中介者模式、迭代器模式、解释器模式、命令模式、责任链模式四、设计模式的六大原则开放封闭原则（Open Close Principle）原则思想：尽量通过扩展软件实体来解决需求变化，而不是通过修改已有代码来完成变化描述：一个软件产品在生命周期内，都会发生变化，既然变化是一个既定的事实，我们就应该在设计的时候尽量适应这些变换，以提高稳定性和灵活性。优点：单一原则告诉我们，每个类都有自己负责的职责，里氏替换原则不能破坏继承关系体系里氏代换原则（Liskov Substiution Principle）原则是想：是用的基类可以在任何地方使用继承的子类，完美替换基类。大致意思是：子类可以扩展父类的功能，但不能改变父类原有的功能。子类可以实现父类的抽象方法，但不能覆盖父类的抽象方法优点：增加程序健壮型，即使增加了子类，原有的子类还可以继续运行，互不影响。依赖倒转原则（Dependence Inversion Principle）一览倒置原则的核心思想是面向接口编程。依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中，尽量引用层次高的抽象类这个时开放封闭原则的基础，具体内容是：对接口编程，依赖于抽象而不依赖于具体接口隔离原则（Interface Segregation Principle）这个原则的意思是：使用多个隔离的接口，比使用单个接口要好。还是一个降低类之间的耦合度的意思，从这儿我们看出，其实设计模式就是一个软件的设计思想，从大型软件架构出发，为了升级和维护方便，所以上文中多次出现：降低依赖，降低耦合。例如：支付类的接口和订单类的接口，需要把着俩类别的接口变成两个隔离的接口迪米特法则（最少知道法则）（Demeter Principle）原则思想：一个对象应当对其他对象有尽可能少的了解，简称类的解耦大概意思就是一个类尽量减少对其他类的依赖，原则是低耦合，高内聚，只有使各个模块间的耦合尽量的低，才能提高代码的复用率优点：低耦合、高内聚单一职责原则（Principle of single responsibility）原则思想：一个方法只负责一件事情。描述：单一职责原则很简单，一个方法一个类只负责一个职责，各个职责的程序改动，不影响其他程序，这是常识，几乎所有程序员都会遵循这个原则。优点：降低类和类的耦合，提高可读性，增加可维护性，降低可变性的风险。五、单例模式1. 什么是单例模式保证一个类只有一个实例，并且提供一个访问全局访问点2. 那些地方用到了单例模式网站的计数器，一半也是采用单例模式实现，否则难以同步应用程序的日志应用，一般都是单例模式实现，只有一个实例操作才好，否则内容不好追加显示。多线程的线程池的设计一般也是采用单例模式，因为线程池要方便对池中的线程进行控制Windows的（任务管理器）就是很典型的单例模式，他不能打开两个Windows的（回收站）也是典型的单例模式。在整个系统运行过程中，回收站纸维护一个实例。3. 单例优缺点优点：单例模式中，活动的单例只有一个实例，对单例类的所有实体话得到的都是相同一个实例。这样就防止其他对象对自己实例话，确保所有对象都访问一个实例单例模式具有一定伸缩性，类自己来控制实例话进程，类就在改变实例话进程上又相应的伸缩性提供了一个对唯一实例的受控访问。由于在系统内存中只存在一个对象，因此可以节约系统资源，当需要频繁创建和销毁的对象是单例模式无疑可以提高系统的性能允许可变数目的实例避免共享资源的多重占用缺点：不适用于变化的对象，如果同一类型的对象总是要在不同的用例场景发生变化，单利就会引起数据的错误，不能保存彼此的状态。由于单例模式中没有抽象层，因此单例类的扩展有很大的困难。单例类的职责过重，在一定程度上违背单一职责原则滥用单例带来的一些负面问题，如果为了节省资源将数据库连接池对象设计位单例类，可能会导致共享连接池对象的程序过多而出现连接池溢出；如果实体化的对象长时间不被利用，系统会认为是垃圾而被回收，这将导致对象状态丢失。4.单例模式使用注意事项：使用时不能反射模式创建单例，否则会有实例化一个新的对象使用懒单例模式时注意线程安全问题饿单例模式和懒单例模式构造方法都是私有的，因而是不能被继承的，有些单例模式可以被继承（如登记模式）5.单例防止反射漏洞攻击private static boolean flag = false; private Sigleton(){ if(flag == false){ flag = !flag }else{ throw new RuntimException(”单例模式被侵犯”); } } public static void main(string[] args){ }6.如何选择单例创建方法如果不需要延迟加载单例，可以使用枚举或者饿汉式，如果需要延迟加载，可以使用静态内部类或者懒汉式，相对来说静态内部类好与懒汉式。最好使用饿汉式7.单例创建方式（主要使用懒汉式和饿汉式）饿汉式：类初始化时，会立即加载该对象，线程天生安全，调用效率高。懒汉式：类初始化时，不会初始化该对象，真正需要对象的时候才会创建该对象，具备懒加载功能静态内部方式：结合了懒汉式和饿汉式各自优点，真正需要对象的时候才会加载，加载类是线程安全的枚举单例：使用枚举实现单例模式。优点：实现简单、调用效率高，枚举本身就是单例，由jvm从根本上提供保障！避免通过反射和反序列化的漏洞，缺点没有加载延迟双重检测锁方式（应为jvm本质重排序的原因，可能会初始化多次，不推荐使用）1.饿汉式饿汉式：类初始化时，会立即加载该对象，线程天生安全，调用效率高package com.lijie; //饿汉式 public class Demo1{ //类初始化时，会立即加载该对象，线程安全，调用效率高 private static Demo1 demo1 = new Demo1(); private Demo1(){ System.out.println(”私有Demo1构造参数初始化”)； } public static Demo1 getInstance(){ return demo1; } public static void main(String[] args){ Demo1 s1 = Demo1.getInstance(); Demo1 s2 = Demo1.getInstance(); System.out.println(s1 == s2) } }2.懒汉式懒汉式：类初始化时，不会初始化该对象，真正需要使用的时候才会创建该对象，具备懒加载功能。package com.lijie; //懒汉式 public class Demo2{ private static Demo2 demo2; private Demo2(){ System.out.println(”私有Demo2构造参数初始化”); } public synchronized static Demo2 getInstance(){ if(demo2 == null){ demo2 == new Demo2(); } return demo2; } public static void mian(String[] args){ Demo2 s1 = Demo2.getInstance(); Demo2 s2 = Demo2.getInstance(); System.out.println(s1 == s2) } }3.静态内部类静态内部方式：结合了懒汉式和饿汉式各自的优点，真正需要对象时才会加载，加载时线程安全的。package com.lijie; //静态内部类方式 public class Demo3{ private Demo3(){ System.out.println(“私有Demo3构造参数初始化”); } public static class SingletonClassIntance(){ private static final Demo3 DEMO_3 = new Demo3(); } public static Demo3 getInstance(){ return SingletonClassInstance.DEMO_3; } public static void main(String[] args){ Demo3 s1 = Demo3.getInstance(); Demo3 s2 = Demo3.getInstance(); System.out.println(s1 == s2) } }4.枚举单例式枚举单例：使用枚举实现单例模式。优点：实现简单、调用高效，枚举本身就是单例，有jvm从根本上提供保障！避免通过反射和反序列化漏洞，缺点没有延迟加载。package com.lijie; //使用枚举实现单例模式。优点：实现简单、调用高效，枚举本身就是单例，有jvm从根本上提供保障！避免通过反射和反序列化漏洞，缺点没有延迟加载 public static Demo4{ public static Demo4 getInstance(){ retrun Demo.INSTANCE.getInstance(); } public static void main(String[] args){ Demo4 s1 = Demo4.getInstance(); Demo4 s2 = Demo4.getInstance(); System.out.println(s1 == s2); } //定义枚举 private static enum Demo{ INSTACE; private Demo4 demo4; private Demo(){ System.out.println("枚举Demo私有构造参数"); demo4 = new Demo4(); } public Demo4 getInstance(){ return demo4; } } }5.双重检测锁方式双重检测锁方式（因为jvm本质重排序的原因，可能会多次初始化，不推荐使用）package com.lijie; //双重检查锁方式 public class Demo5{ private static Demo5 demo5; private Demo5(){ System.out.println("私有demo4构造参数初始化"); } public static Demo5 getInstance(){ if(demo5 == null){ synchronized(Demo5.class){ if(demo5 == null){ demo5 = new Demo5(); } } } return demo5; } public static void main(String[] args){ Demo5 s1 = Demo5.getInstance(); Demo5 s2 = Demo5.getInstance(); System.out.println(s1 == s2); } }六、工厂模式1.什么是工厂模式他提供了一种创建对象的最佳方式。在工厂模式中，我们创建对象时不会对客户端暴露逻辑，并且是通过一个共同的接口来指向新创建的对象。实现了创建者和调用者分离，工厂模式分为简单工厂、工厂方法、抽象工厂模式2.工厂模式好处工厂模式是我们最常用的实例化对象模式了，是用工厂方法代替new操作的一种模式。利用工厂模式可以降低程序的耦合性，为后期的维护修改提供了很大的便利。将选择实现类、创建对象统一管理和控制。从而将调用这跟我们的实现类解耦。3.为什么要学习工厂设计模式不知道你们面试题问到过源码没有，你知道Spring的源码吗，MyBatis的源码吗，等等等 如果想学习很多框架的源码，或者你想自己开发自己的框架，就必须先掌握设计模式（工厂设计模式用的是非常广泛的）4.Spring开发的工厂设计模式1.Spring IOC看过Spring源码就知道，在Spring IOC容器创建bean的过程是使用了工厂设计模式Spring中无论是通过xml配置还是通过配置类还是注解进行创建bean，大部分都是通过简单工厂 来进行创建的。当容器拿到了beanName和class类型后，动态的通过反射创建具体的某个对象，最后将创建的对 象放到Map中。2.为什么Spring IOC要使用工厂设计模式创建Bean呢在实际开发中，如果我们A对象调用B，B调用C，C调用D的话我们程序的耦合性就会变高。（耦合 大致分为类与类之间的依赖，方法与方法之间的依赖。）在很久以前的三层架构编程时，都是控制层调用业务层，业务层调用数据访问层时，都是是直接 new对象，耦合性大大提升，代码重复量很高，对象满天飞为了避免这种情况，Spring使用工厂模式编程，写一个工厂，由工厂创建Bean，以后我们如果要 对象就直接管工厂要就可以，剩下的事情不归我们管了。Spring IOC容器的工厂中有个静态的 Map集合，是为了让工厂符合单例设计模式，即每个对象只生产一次，生产出对象后就存入到 Map集合中，保证了实例不会重复影响程序效率。5.工厂模式分类工厂模式分为简单工厂、工厂方法、抽象工厂模式简单工厂：用来生产同一等级结构中的任意产品。（不支持扩展增加产品）工厂方法：用来生产同一等级结构中的固定产品。（支持扩展增加产品）抽象工厂：用来生产不同产品族的全部产品。（不支持扩展增加产品：支持增加产品族）5.1 简单工厂模式什么是简单工厂模式​ 简单工厂模式相当于是一个工厂中有各种产品，创建在一个类中，客户无需知道具体产品的名称，只需要知道产品类所对应的参数即可。但是工厂的职责过重，而当类型过多是不利于系统的扩展维护。代码演示：创建工厂package com.lijie; public interface Car{ public void run(); }创建工厂的产品（奥迪）package com.lijie; public class AoDi implements Car{ public void run(){ Sysmtem.out.println("奥迪..") } }创建工厂的产品（宝马）package com.lijie; public class Bmw implements Car{ public void run(){ Sysmtem.out.println("宝马..") } }创建核心工厂类，有他决定具体调用哪个产品package com.lijie; public class CarFactory{ public static Car createCar(string name){ if("".equals(name)){ return null; } if(name.equals("奥迪")){ return new AoDi(); } if(name.equals("宝马")){ return new Bmw(); } return null; } }演示创建工厂的具体实例package com.lijie; public class Client01{ public static void main(String[] args){ Car aodi = CarFactory.createCar("奥迪"); Car bmw = CarFactory.createCar("宝马"); aodi.run(); bmw.run(); } }单工厂的优点/缺点优点：简单工厂模式能够根据外界给定的信息，觉得究竟应该创建那个具体类的对象，明确区分了各自的职责和权力，有利于整个软件体系结构的优化。缺点：很明显工厂类集中了所有实例的创建逻辑，容易违反GRASPR的高内聚的责任分配原则5.2 工厂方法模式什么是工厂方法模式​ 工厂方法模式Factory Method，又称多态性工厂模式。在工厂方法模式中，核心的工厂类不再负有左右的产品创建，而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂角色，仅负责给出具体的工厂子类必须实现的接口，而不接触哪一个产品应当被实例化这种细节代码演示：创建工厂package com.lijie; public interface Car{ public void run(); }创建工厂方法调用接口(所有的产品需要new出来必须继承他来实现方法)package com.lijie; public interface CarFactory{ Car createCar(); }创建工厂的产品（奥迪）package com.lijie; public class AoDi implements Car{ public void run(){ Sysmtem.out.println("奥迪..") } }创建工厂的产品（宝马）package com.lijie; public class Bmw implements Car{ public void run(){ Sysmtem.out.println("宝马..") } }创建工厂方法调用接口的实例（奥迪）package com.lijie; public class AoDiFactory implements CarFactory{ public Car createCar(){ return new AoDi(); } }创建工厂方法调用接口的实例（宝马）package com.lijie; public class BmwFactory implements CarFactory{ public Car createCar(){ return new Bmw(); } }演示创建工厂的具体事例package com.lijie; public class Client{ public static void main(String[] args){ Car aodi = new AodiFactory().createCar(); Car bmw = new BmwFactory().createCar(); aodi.run(); bmw.run(); } }5.3 抽象工厂模式什么是抽象工厂模式​ 抽象工厂简单地说是工厂的工厂，抽象工厂可以创建具体工厂，有具体工厂来产生具体产品。代码演示：创建第一个字工厂，及实现类package com.lijie; //汽车 public interface Car{ void run(); } class CarA implements Car{ public void run(){ System.out.println("宝马"); } } class CarB implements Car{ public void run(){ System.out.println("奥迪"); } }创建第二子工厂，及实现类package com.lijie; //发动机 public interface Engine{ void run(); } class EngineA implements Engine{ public void run(){ System.out.println("EngineA"); } } class EngineB implements Engine{ public void run(){ System.out.println("EngineB") } }创建一个总工厂，及实现类（由总工厂的实现类觉得调用哪个工厂的实例）package com.lijie; public interface TotalFactory{ //创建汽车 Car createCar(); //创建发动机 Engine createEngine(); } class TotalFactoryReally implements TotalFactory(){ public Engine create Engine(){ return new EngineA(); } public Car createCar(){ return new CarA(); } }运行测试package com.lijie; public class Test{ public static void main(String[] args){ TotalFactory totalFactory2 = new TotalFactoryReally(); Car car = totalFactory2.createCar(); car.run(); TotalFactory totalFactory2 = new TotalFactoryReally(); Engine engine = totalFactory.createEngine(); engine.run(); } }七、代理模式1.什么是代理模式通过代理控制对象的访问，可以在这个对象调用方法之前、调用方法之后去处理/添加新功能（也就是AO和P微实现）代理在原有代码乃至原业务流程都不修改的情况下，直接在业务流程中切入新代码，增加新功能，这也和Spring的面相切面编程很类似2.代理模式应用场景​ Spring AOP、日志打印、异常处理、事务控制、权限控制等3.代理的分类静态代理（静态定义代理类）动态代理（动态生成代理类，也称为jdk自带动态代理）Cglib、javaassist（字节码操作库）4.三种代理的区别静态代理：简单代理模式，是动态代理的理论基础。常见使用在代理模式jdk动态代理：使用反射完成代理。需要有顶层接口才能使用，常见是mybatis的mapper文件代理cglib动态代理：也是使用反射完成代理，可以直接代理类（jdk动态代理不行），使用字节码技术，不能对final类进行继承。（需要导入jar包）5.用代码演示三种代理5.1 静态代理什么是静态代理​ 由程序员创建或工具生成代理类的源码，再编译代理类。所谓静态也就是在程序员运行前就已经纯在代理类的字节码文件，代理类和委托的关系在运行前就确定了。代码演示：​ 我有这样一段代码：（如何在不能修改UserDao接口类的情况下开事务和关闭事务呢）package com.lijiel; //接口类 public class UserDao{ public void save(){ System.out.println("保存数据方法"); } }package com.lijie; //运行测试类 public class Test{ public static void main(String[] args){ UserDao userDao = new UserDao(); userDao.save(); } }修改代码，添加代理类package com.lijie; //代理类 public class UserDaoProxy extends UserDao{ private UserDao userDao; public UserDaoProxy(UserDao userDao){ this.userDao = userDao; } public void save(){ System.out.println("开启事务..."); userDao.save(); System.out.println("关闭事务..."); } }//添加完静态代理的测试类 public class Test{ public static void main(String[] args){ UserDao userDao = new UserDao(); UserDaoProxy userDaoProxy = new UserDaoProxy(userDao); userDaoProxy.save(); } }缺点：每个需要代理的对象都需要自己重复编写代码，很不舒服。优点：但是可以面相实际对象或者接口的方式实现代理5.2 动态代理什么是动态代理动态代理也叫做，JDK代理，接口代理动态代理的对象，是利用JDK的API，动态的在内存中构建代理对象（是根据被代理的接口来动态生成代理类的class文件，并加载运行的过程），这就叫动态代理package com.lijie; //接口 public interface UserDao{ void save(); }package com.lijie; //接口实现类 public class UserDaoImpl implements UserDao{ public void save(){ System.out.println("数据保存方法"); } }下面是代理类，可重复使用，不想静态代理那样要自己重复编写代理package com.lijie; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; //每次生成动态代理对象是，实现了InvocationHandler接口的调用处理对象 public class InvocationHandlerImpl implements InvocationHadler{ //这其实业务实现类对象，用来调用具体的业务方法 private Object tartget; //通过构造函数传入目标对象 public InvocationHandlerImpl(Object target){ this.target = target; } //动态代理实际运行的代理方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ System.out.println("调用开始处理"); //下面invoke()方法是一反射的方式来创建对象，第一个参数是要创建的对象，第二是构成方法参数，第二个参数来决定创建对象使用哪个构造方法 Object result = method.invoke(target, args); System.out.println("调用处理结束"); return result; } }利用动态代理使用代理方法package com.lijie; import java.lang.reflect.Proxy; public class Test{ public static void main(String[] args){ //被代理对象 UserDao userDaoImpl = new UserDaoImpl(); InvocationHandlerImpl invocationHanderImpl = new InvocationHandlerImpl(userDaoImpl); //类加载器 ClassLoader loader = userDaoImpl.getClass().getClassLoader(); Class<?>[] interfaces = userDaoImpl.getClass().getInterfaces(); //主要装载器、一组接口及调用处理动态代理实例 UserDao newProxyInstance = (UserDao) Proxy.newProxyInstance(loader,interfaces,invocationHandlerImpl); newProxyInstance.save(); } }缺点：必须是面向接口，目标业务必须实现接口优点：不用关心代理类，只需要在运行在运行阶段才指定代理哪一个对象5.3 CGLIB动态代理CGLIB动态代理原理：利用asm开源包，对代理类的class文件加载进来，通过修改其字节码生成子类来处理。什么是CGLIB动态代理CGLIB动态代理和jdk代理一样，使用反射完成代理，不同的是他可以直接代理类（jdk动态代理不行，他必须目标业务必须实现接口），CGLIB动态代理底层使用字节码技术，CGLIB动态代理不能对final类进行继承。（CGLIB动态代理需要导入jar包）代码演示：package com.lijie; //接口 public interface UserDao{ void save(); }package com.lijie; //接口实现类 public class UserDaoImpl implements UserDao{ public void save(){ System.out.println("保存数据方法"); } }package com.lijie; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MehtodProxy; import java.lang.reflect.Method; //代理主要类 public class CglibProxy implements MethodInterceptor{ private Object targetObject; //这里的目标类型为Object，则可以接受任意一种参数作为被代理类，实现动态代理 public Object getInstance(Object target){ //设置需要创建的子类 this.targetObject = target; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(this); return enhancer.create(); } //代理实际方法 public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{ System.out.println("开启事务"); Object result = proxy.invoke(targetObject,args); System.out.println("关闭事务"); //返回代理对象 return result; } }package com.lijie; //测试CGLIB动态代理 public class Test{ public static void mian(String[] args){ CglibProxy cglibProxy = new CglibProxy(); UserDao userDao = (UserDao) cglibProxy.getInstance(new UserDaoImpl()); UserDao.save(); } }八、建造者模式1.什么是建造者模式建造者模式：是将一个复杂的对象的构建与他的表示分离，是的同样的构建过程可以创建不同的方式进行创建。工厂类模式是提供的是创建单个类的产品而建造者模式则是将各种产品集中起来进行管理，用来具有不同属性的产品建造者模式通常包括下面几个角色：uilder:给出一个抽象接口，以规范产品对象的各个组成部分的建造。这个接口规定要实现复杂对象的那些部分的创建，并不涉及具体的对象的创建ConcreteBuilder：实现Builder接口，针对不同的商业逻辑，具体化复杂对象的各个部分的创建，创建完成后，提供产品实例。Director：调用具体建造者来创建复杂对象的各个部分，在指导者中不涉及具体产品信息，只负责保证对象各部分完整创建或按某种顺序创建。Product：要创建的复杂对象。2.建造者模式的使用场景使用场景：需要生成的对象具有复杂的内部结构。需要生成的对象内部具有属性本身互相依赖。与工厂模式的区别是：建造者模式更加关注零件装配的顺序。JAVA中的SpringBuilder就是建造者模式创建的，他把一个单个字符的char数组组合起来Spring不是建造者模式，他提供的操作因该是对于字符串本身的一些操作，而不是创建或改变一个字符串。3.代码案例建立一个装备对象Armspackage com.lijie; //装备类 public class Arms{ //头盔 private String helment; //铠甲 private String armor; //武器 private String weapon; //省略get和set方法 }创建Builder接口（各一个抽象接口，以规范产品对象的各个组成部分的建造，这个接口只是规范）package com.lijie; public interface PersonBilder{ void builderHelmetMurder(); void builderArmorMurder(); void buildrWeaponMurder(); void builderHelemetYangLong(); void builderArmorYangLong(); void builderWeaponYangLong(); Arms builderArms();//组装 }创建builder实现类（这个类主要实现复杂对象创建的哪些部分需要什么属性）package com.lijie; public class ArmsBuilder implements PersonBuilder { private Arms arms; //创建一个Arms实例，用于调用set方法 public ArmsBuilder(){ arms = new Arms(); } public void builderHeletMurder(){ arms.setHelmet("夺命头盔") } public void builderArmorMurder() { arms.setArmor("夺命铠甲"); } public void builderWeaponMurder() { arms.setWeapon("夺命宝刀"); } public void builderHelmetYanLong() { arms.setHelmet("炎龙头盔"); } public void builderArmorYanLong() { arms.setArmor("炎龙铠甲"); } public void builderWeaponYanLong() { arms.setWeapon("炎龙宝刀"); } public Arms BuilderArms() { return arms; } }Director（调用具体建造者来创建复杂对象的各个部分，在指导者中不涉及具体产品的信息，只负 责保证对象各部分完整创建或按某种顺序创建）package com.lijie; public class PersonDirector { //组装 public Arms constructPerson(PersonBuilder pb) { pb.builderHelmetYanLong(); pb.builderArmorMurder(); pb.builderWeaponMurder(); return pb.BuilderArms(); } //这里进行测试 public static void main(String[] args) { PersonDirector pb = new PersonDirector(); Arms arms = pb.constructPerson(new ArmsBuilder()); System.out.println(arms.getHelmet()); System.out.println(arms.getArmor()); System.out.println(arms.getWeapon()); } }九、模板方法模式1.什么是模板方法模式模板方法模式：定义一个操作中的算法骨架（父类），而将一些步骤延迟到子类中。模板方法使的子类可以不改变一个算法的结构来重定义该算法的2.什么时候使用模板方法实现一些操作是，整体步骤很固定，但是呢，就是其中一小部分需要改变，这时候可以使用模板方法模式，将容易变的部分抽象出来，提供子类实现。3.实际开发中应用场景哪里用到了模板方法其中很多框架中都有用到了模板方法例如：数据库访问的封装、Junit单元测试、servlet中关于doGet/doPost方法的调用等等4.现实生活中的模板方法例如：去餐厅吃饭，餐厅给我们提供一个模板就是：看菜单，点菜，吃饭，付款，走人（这里的“点菜和付款”是不确定的由子类来完成的，其他的则是一个模板）5.代码实现模板方法模式先定义一个模板。把模板中的点菜和付款，让子类来实现。package com.lijie; //模板方法 public abstract class RestaurantTemplate{ // 看菜单 public void menu(){ System.out.println("看菜单"); } // 点菜 abstract void spotMenu(); // 吃饭 public void havingDinner(){ System.out.println("吃饭"); } // 付款 abstract void payment(); // 走人 public void GoR(){ System.out.println("走人"); } // 模板通用结构 public void process(){ menu(); spotmenu(); havingDinner(); payment(); GoR(); } }具体的模板子类package com.lijie; public class RestaurantGinsengImpl extends RestaurantTemplate{ void spotMenu(){ System.out.println("人参"); } void payment(){ System.out.prinln("5块"); } } public class RestaurantLobsterImpl extends RestaurantTemplate { void spotMenu() { System.out.println("龙虾"); } void payment() { System.out.println("50块"); } }客户端测试package com.lijie; public class Client{ public static void main(String[] args){ RestaurantGinsengImpl restaurantGinsengImpl = new RestaurantGinsengImpl(); restaurantGinsengImpl.process(); } }十、外观模式1.什么是外观模式外观模式：也叫门面模式，隐藏系统的复杂性，并向客户端提供了一个客户端可以访问系统的接口他向现有的系统添加一个接口，用这个接口来隐藏实际的系统的复杂性。使用外观模式，他外部看起来就是一个接口，其实他的内部有很多复杂的接口已经被实现2.外观模式例子用户注册完之后，需要调用阿里短信接口、邮件接口、微信推送接口创建阿里短信接口package com.lijie; //阿里短信消息 public interface AliSmsService{ void sendSms(); } public class AliSmsServiceImpl implements AliSmsService { public void sendSms() { System.out.println("阿里短信消息"); } } 创建邮件接口package com.lijie; //发送邮件消息 public interface EmailSmsService{ void sendSms(); } public class EamilSmsServiceImpl implements EamilSmsService{ public void sendSms() { System.out.println("发送邮件消息"); } }创建微信推送package com.lijie; //微信消息推送 public interface WeiXinSmsService { void sendSms(); } public class WeiXinSmsServiceImpl implements WeiXinSmsService { public void sendSms() { System.out.println("发送微信消息推送"); } }创建门面（门面看起来很简单，复杂的东西被门面给封装好了）package com.lijie; public class Computer{ AliSmsService aliSmsService; EamilSmsService eamilSmsService; WeiXinSmsService weiXinSmsService; public Computer() { aliSmsService = new AliSmsServiceImpl(); eamilSmsService = new EamilSmsServiceImpl(); weiXinSmsService = new WeiXinSmsServiceImpl(); } //只需要调用它 public void sendMsg() { aliSmsService.sendSms(); eamilSmsService.sendSms(); weiXinSmsService.sendSms(); } }启动测试package com.lijie; public class Client { public static void main(String[] args) { //普通模式需要这样 AliSmsService aliSmsService = new AliSmsServiceImpl(); EamilSmsService eamilSmsService = new EamilSmsServiceImpl(); WeiXinSmsService weiXinSmsService = new WeiXinSmsServiceImpl(); aliSmsService.sendSms(); eamilSmsService.sendSms(); weiXinSmsService.sendSms(); //利用外观模式简化方法 new Computer().sendMsg(); } }十一、原型模式1.什么是原型模式原型设计模式简单来说就是克隆原先表明了有一个样板实例，这个原型是可以定制的。原先模式多用于创建复杂的或构造耗时的实例，应为这种情况下，复制一个已经纯在的实例可以使重新运行更高效。2.原型模式的应用场景类初始化需要消耗非常多的资源，这个资源包括数据、硬件资源等。这时我们就可以通过拷贝避免这些消耗。通过new产生的一个对象需要非常繁琐的数据准备或者权限，这时可以使用原型模式。一个对象需要提供给其他对象访问，而且各个调用者可能都需要修改其值是，可以考虑使用原先模式拷贝多个对象供调用者使用，即保护性拷贝。3.原型模式的使用方法实现Cloneable接口。在java语言有一个Cloneable接口，他的作用只有一个，就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中，只有实现了这个接口的类才可以被拷贝，否则在运行时会抛出CloneNotSupportedException异常。重写Object类中的clone方法。java中，所有类的父类都是Object类，Object类中有一个Clone方法，作用是返回一个对象的拷贝，但是其作用域是protected类型的，一般的类无法调用，因此Prototype类需要将clone方法的作用域改为public类型。3.1 原型模式分为浅复制和深复制浅复制：只是拷贝了基本类型的数据，而引用类型数据，只是拷贝了一份引用地址。深复制：在计算机中开辟了一块新的内存地址用于存放复制的对象。4.代码演示创建User类package com.lijie; import java.util.ArrayList; public class User implements Cloneable{ private String name; private String password; private ArrayList<String> phones; protected User clone(){ try { User user = (User) super.clone(); //默认引用类型为浅复制，这是设置了深复制 user.phones = (ArrayList<String>) this.phones.clone();//设置深复制 return user; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } //省略所有属性Git Set方法...... }测试复制package com.lijie; import java.util.ArrayList; public class Client { public static void main(String[] args) { //创建User原型对象 User user = new User(); user.setName("李三"); user.setPassword("123456"); ArrayList<String> phones = new ArrayList<>(); phones.add("17674553302"); user.setPhones(phones); //copy一个user对象,并且对象的属性 User user2 = user.clone(); user2.setPassword("654321"); //查看俩个对象是否是一个 System.out.println(user == user2); //查看属性内容 System.out.println(user.getName() + " | " + user2.getName()); System.out.println(user.getPassword() + " | " + user2.getPassword()); //查看对于引用类型拷贝 System.out.println(user.getPhones() == user2.getPhones()); } }十二、策略模式1.什么是策略模式定义了一系列的算法 或 逻辑 或 相同意义的操作，并将每一个算法、逻辑、操作封装起来，而且使它们还可以相互替换。（其实策略模式Java中用的非常非常广泛）我觉得主要是为了 简化 if...else 所带来的复杂和难以维护。2.策略模式应用场景​ 策略模式的用意是针对一组算法或逻辑，将每一个算法或逻辑封装到具有共同接口的独立的类中，从而使得它们之间可以相互替换。例如：我要做一个不同会员打折力度不同的三种策略，初级会员，中级会员，高级会员（三种不同的计算）。例如：我要一个支付模块，我要有微信支付、支付宝支付、银联支付等3.策略模式的优点和缺点优点： 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性非常良好。缺点： 1、策略类会增多。 2、所有策略类都需要对外暴露。4.代码演示模拟支付模块有微信支付、支付宝支付、银联支付定义抽象的公共方法package com.lijie; //策略模式 定义抽象方法 所有支持公共接口 abstract class PayStrategy { // 支付逻辑方法 abstract void algorithmInterface(); }定义实现微信支付package com.lijie; class PayStrategyA extends PayStrategy { void algorithmInterface() { System.out.println("微信支付"); } }定义实现支付宝支付package com.lijie; class PayStrategyB extends PayStrategy { void algorithmInterface() { System.out.println("支付宝支付"); } }定义实现银联支付package com.lijie; class PayStrategyC extends PayStrategy { void algorithmInterface() { System.out.println("银联支付"); } }定义下文维护算法策略package com.lijie;// 使用上下文维护算法策略 class Context { PayStrategy strategy; public Context(PayStrategy strategy) { this.strategy = strategy; } public void algorithmInterface() { strategy.algorithmInterface(); } }运行测试package com.lijie; class ClientTestStrategy { public static void main(String[] args) { Context context; //使用支付逻辑A context = new Context(new PayStrategyA()); context.algorithmInterface(); //使用支付逻辑B context = new Context(new PayStrategyB()); context.algorithmInterface(); //使用支付逻辑C context = new Context(new PayStrategyC()); context.algorithmInterface(); } }十三、观察者模式1.什么是观察者模式先讲什么是行为性模型，行为型模式关注的是系统中对象之间的相互交互，解决系统在运行时对象之间的相互通信和协作，进一步明确对象的职责。观察者模式，是一种行为性模型，又叫发布-订阅模式，他定义对象之间一种一对多的依赖关系，使得当一个对象改变状态，则所有依赖于它的对象都会得到通知并自动更新。2.模式的职责​ 观察者模式主要用于1对N的通知。当一个对象的状态变化时，他需要及时告知一系列对象，令他们做出响应。实现有两种方式：推：每次都会把通知以广播的方式发送给所有观察者，所有的观察者只能被动接收。拉：观察者只要知道有情况即可，至于什么时候获取内容，获取什么内容，都可以自主决定。3.观察者模式应用场景关联行为场景，需要注意的是，关联行为是可拆分的，而不是“组合”关系。事件多级触发场景。跨系统的消息交换场景，如消息队列、事件总线的处理机制。4.代码实现观察者模式定义抽象观察者，每一个实现该接口的实现类都是具体观察者。package com.lijie; //观察者的接口，用来存放观察者共有方法 public interface Observer { // 观察者方法 void update(int state); }定义具体观察者package com.lijie; // 具体观察者 public class ObserverImpl implements Observer { // 具体观察者的属性 private int myState; public void update(int state) { myState=state; System.out.println("收到消息,myState值改为："+state); } public int getMyState() { return myState; } }定义主题。主题定义观察者数组，并实现增、删及通知操作。package com.lijie; import java.util.Vector; //定义主题，以及定义观察者数组，并实现增、删及通知操作。 public class Subjecct { //观察者的存储集合，不推荐ArrayList，线程不安全， private Vector<Observer> list = new Vector<>(); // 注册观察者方法 public void registerObserver(Observer obs) { list.add(obs); } // 删除观察者方法 public void removeObserver(Observer obs) { list.remove(obs); } // 通知所有的观察者更新 public void notifyAllObserver(int state) { for (Observer observer : list) { observer.update(state); } } }定义具体的，他继承继承Subject类，在这里实现具体业务，在具体项目中，该类会有很多。package com.lijie; //具体主题 public class RealObserver extends Subjecct { //被观察对象的属性 private int state; public int getState(){ return state; } public void setState(int state){ this.state=state; //主题对象(目标对象)值发生改变 this.notifyAllObserver(state); } }运行测试package com.lijie; public class Client { public static void main(String[] args) { // 目标对象 RealObserver subject = new RealObserver(); // 创建多个观察者 ObserverImpl obs1 = new ObserverImpl(); ObserverImpl obs2 = new ObserverImpl(); ObserverImpl obs3 = new ObserverImpl(); // 注册到观察队列中 subject.registerObserver(obs1); subject.registerObserver(obs2); subject.registerObserver(obs3); // 改变State状态 subject.setState(300); System.out.println("obs1观察者的MyState状态值为："+obs1.getMyState()); System.out.println("obs2观察者的MyState状态值为："+obs2.getMyState()); System.out.println("obs3观察者的MyState状态值为："+obs3.getMyState()); // 改变State状态 subject.setState(400); System.out.println("obs1观察者的MyState状态值为："+obs1.getMyState()); System.out.println("obs2观察者的MyState状态值为："+obs2.getMyState()); System.out.println("obs3观察者的MyState状态值为："+obs3.getMyState()); } }
一、Redis是单线程还是多线程Redis6.0之前的单线程指的是其网络IO和键值对读写是由一个线程完成的Redis6.0引入多线程指的是网络请求采用多线程，而键值对读写命令依然是单线程处理的，所以redis依然是并发安全的也就是只有网络请求和模块数据操作模块是单线程的,而其他的持久话、集群数据同步等，其实是由额外的线程执行的二、Redis单线程为什么会这么快命令执行基于内存操作的，一条命令在内存里操作的时间是几时纳秒命令执行是单线程操作，没有线程切换开销基于IO多路复用机制提升Redis的IO效率高效的数据储存结构，全局hash表以及多种高效数据结构，比如：跳表，压缩列表，链表等等三、Redis底层数据是如何跳表来储存的和数据库索引差不多详情可见数据库索引四、Redis Key过期了为什么内存没释放你在使用Redis时，肯定经常使用SET命令SET除了可以设置key- value以外，还可以设置key的过期时间，就像下面这样：127.0.0.1:6379> SET hideyoshi hidesyohi EX 120 OK 127.0.0.1:6379> TTL hideyoshi (integer) 117此时如果想要修改key的值，但只是使用SET命令，而没有加上过期时间的参数，那么这个key的过期时间将会被擦除127.0.0.1:6379> SET hideyoshi hidesyohi OK 127.0.0.1:6379> TTL hideyoshi (integer) -1导致这个问题的原因在于：SET命令如果不设置过期时间，那么Redis会自动擦除这个key的过期时间如果你发现Redis的内存持续增长，而且很多key原来设置了过期时间，后来发现过期时间丢失了，很有可能是因为这个原因导致的。这是你的Redis中就会存在大量不过期的key，消耗过多的内存资源所以，你在使用SET命令是，如果刚开始设置了过期时间，那么修改这个key，也务必要加上过期时间参数，避免过期时间丢失问题Redis对过期key的处理一半有惰性删除和定时删除两种策略惰性删除：当读/写一个已经过期的key时，会触发惰性删除策略，判断key是否过期，如果过期了直接删除这个key定时删除：由于惰性删除无法保证冷数据被及时删除，所以Redis会定期（默认100ms）主动淘汰一批已经过期的key，这里的一批只是部分过期key吗所以可能会出现部分key已经过期但还没有被清理掉的情况，导致内存没有被释放五、Redis Key没有设置过期时间为什么被Redis删除了当redis已用内存超过maxmemory限定时，触发主动清理策略主动清理策略在Redis4.0之前一共实现了6种内存淘汰策略，在4.0后，又增加了两种，总共8种：针对设置了过期时间的key做处理：volatile-ttl：在筛选时，会针对设置了过期时间的键值对，根据过期时间的先后进行删除，越早过期越先被删除。volatile-random：在设置了过期时间的键值对中，进行随机删除。volatile-lru：会使用LRU算法筛选设置了过期时间的键值对删除。（最久未使用）volatile-lfu：会使用LFU算法筛选设置了过期时间的键值对删除。（最不常使用）对所有key做处理：allkeys-random：从所有键值对中随机选择并删除数据allkeys-lru：所有key使用LRU算法删除（最久未使用）allkeys-lfu：所有key使用LFU算法删除（最不常使用）不处理：noeviction：不会剔除任何数据，拒绝所有写入操作并返回客户端信息"（error）OOM command not allowed when used memory"，此时Redis只响应读操作。六、删除Redis Key的命令会阻塞Redis吗删除命令的时间复杂读：O(N), N为被删除的key的数量删除单个字符串的key，时间复杂度为0(1).删除单个列表、集合、有序集合或哈希表类型的的key，时间复杂度为0(M),M为以上数据结构内的元素数量。结论：会阻塞Redis七、Redis主从、哨兵、集群架构优缺点比较哨兵模式在Redis3.0以前的版本要实现集群一般是借助哨兵模式sentinel工具来监控master节点状态，如果master节点异常，则会做主从切换，将一台slave作为master，哨兵的配置略微复杂，并且性能和高可用等各方面表现一般，特别是在主从切换的瞬间存在访问瞬断的情况，而且哨兵模式只有一个主节点对外提供服务，没法支持很高的并发，且单个主节点内存也不宜设置过大，否则会导致持久化文件过大，影响数据恢复或主从同步效率高可用集群模式reids集群是有一个多个主从节点群组成的服务器群，他具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵，也能完成节点移除和故障转移功能。需要将每个节点设置为集群模式，这种集群没有中心节点，可以水平扩展，据官方问当称可以线性扩展到上万节点（官方不推荐超过1000节点）。Redis集群的性能和高可用均优于之前版本的哨兵模式，且集群配置非常简单。八、Redis集群数据hash分片算法是怎么回事Redis Cluster 将所有数据划分为16384个slots（槽位），每个节点负责其中一部分槽位。槽位的信息储存于每个节点中。当Redis Cluster 的客户端来连接集群是，他也会得到一份集群的槽位配置信息并将其缓存存在客户端本地。这样当客户要查询某个key时，可以根据槽位定位算法定位到目标节点。槽位定位算法Cluster默认会对key值使用crc16算法进行hash得到一个整数值，然后用这个数值对16384进行取模来得到槽位。HASH_SLOT = CRC16(key) mod 16384再根据槽位值和Redis节点的对应关系就可以定位到key具体落在哪个Redis节点上。九、Redis执行命令竟然有死循环阻塞bug如果你想随机查看Redis中的一个key，Redis里有一个RANDOMKEY命令可以从Redis中随机取出一个key，这个命令可能导致Redis死循环阻塞。前面的面试题讲过Redis对于过期key的清理策略是定时删除与惰性删除两种方式结合来的，二RANDOMKEY在随机拿出一个key后，首先会检查这个key是否已过期，如果该key已经过期，那么Redis会删除它，这个过程就是惰性删除，但是清理完了还不能结束，Redis还要找出一个没有过期的key，返回给客户端。此时，Redis则会继续随机拿出一个Key，然后哦在判断他是否过期，知道找到一个没有过期的key返回给用户端。这里就有一个问题了，如果此时Redis中，有大量key已经过期，但还未来得及清理，那这个循环就会持续很久才能结束，而且，这个耗时都话费在了清理过期key以及寻找不过期key上，导致的结果就是，RANDOMKEY执行时间变长，影响Redis性能。以上流程，其实是在master上执行的。如果在slave上执行RANDOMKEY，那么问题会更严重。slave自己是不会清理过期key，当一个key要过期时，master会清理删除它，然后master想slave发消息删除key的命令，告知slave也删除这个key，1以达到主从数据一致性。假设Redis中存在大量已过期还未清理的key，哪在slave上执行RANDOMKEY时，就会发生以下问题：slave随机取出一个key，判断是否已过期。key已过期，但slave不会删除它，而是继续随机寻找不过期的key。由于大量key都已经过期，哪slave就会寻找不到符合条件的key，就会陷入死循环。也就是说，在slave上执行RANDOMKEY，有可能会造成整个Redis实例卡死。这其实是Redis的一个BUG，这个BUG一直持续到5.0才被修复，修复方案就是在slave中最多找一定次数，无论是否能找到，都会退出循环。十、一次线上事故，Redis主从切换导致了缓存雪崩假设，slave的机器时间比master走的快很多。此时，Redis master里设置了过期时间key，从slave角度来看，可能很多在master里没过期的数据其实已经过期了如果此时操作主从切换，八slave提升未新的master它成为master，就会开始大量清理过期key，此时就会导致以下结果：mater大量清理过期key，主线进程可能会发生阻塞，无法及时处理客户端请求Redis大量数据过期，导致缓存雪崩。当master与slave机器时钟严重不一致时，对业务影响非仓大。所以，我们一定要保证主从机器时钟的一致性，避免发生这些问题十一、Redis持久话RDB、AOF、混合持久话是怎么回事RDB快照(snapshot)在默认情况下，Redis降内存数据库快照保存在名字为dump.rdb的二进制文件中。你可以对Redis进行设置，让他在"n秒内数据集至少有M个改动"这一条件被满足是，自动保存一次数据集。比如说，以下设置会让Redis在满足"60秒内至少有1000个键被改动"这一条件时，自动保存一次数据集：# save 60 1000 关闭RDB只需要将所有的save保存策略注释掉即可开可以手动执行命令生成RDB快照，进入Redis客户端执行命令save或bgsave可以生成dump.rdb文件，每次命令执行都会将所有redis内存快照到一个新的rdb文件里，并覆盖原有rdb快照文件bgsave的写是复制(COW)机制Redis借助操作系统提供的写时复制技术(Copy-On-Write,COW)，在生成快照的同时，依旧可以正常处理写命令。简单来说，bgsave子进程是由主线程fork生成的，可以共享主线程的所有内存数据。bgsave子进程运行后，开始读取主线程的内存数据，并把他们写入RDB文件。此时，如果主线程对这些数据也都是读操作，那么，主线程和bgsave子进程相互不影响。但是，如果主线程要修改一块数据，那么，这块数据就会被复制一份，生成该数据的副本。然后，bgsave子进程会把这个副本写入RBD文件，而在这个过程中，主线程仍然可以直接修改原来的数据。save与bgsave对比：命令savebgsaveIO类型同步异步是否阻塞Redis其他命令是否（在生成子进程执行调用fork函数式会有短暂阻塞)复杂度O(n)O(n)优点不会消耗额外内存不会阻塞客户端命令缺点阻塞客户端命令需要fork子进程，消耗内存配置自动生成rdb文件后台使用的是bgsave方式。AOF(append-only file)快照功能不是非常耐久：如果Redis因为某些原因而造成故障停机，那么服务器将丢失最近写入、切仍未保存到快照中的那些数据。从1.1版本开始，Redis增加了一种完全耐久的持久化方式：AOF持久化，将修改的每一条命令记录进文件appendonly.aof中（先写入os cache，每隔一段时间fsync到磁盘）你可以通过修改配置文件来打开AOF功能：# appendonly yes从现在开始，每当Reids执行一个改变数据集的命令时（比如SET），这个命令就会被追加到AOF文件的末尾。这样的话，当Redis重启时，程序就可以通过重新执行AOF文件中的命令来达到重建数据集的目的。你可以配置Redis多久才将数据fsync到磁盘一次有三个选项# appendfsync always 每次命令 # appendfsync everysc 每秒一次 # appendfsync no 从不推荐也是默认措施为每秒一次，这种fsync策略可以兼顾速度和安全性AOF重写AOF文件里可能有太多没用指令，所以AOF会定期根据内存的最新数据生成aof文件例如执行如下几条命令incr readcount (integer) 1 incr readcount (integer) 2 incr readcount (integer) 3 incr readcount (integer) 4 incr readcount (integer) 5重写后AOF文件里变成*3 $3 SET $2 readcount $1 5如果两个配置可以控制AOF自动重写频率# auto-aof-rewrite-min-size 64mb //aof文件至少达到64M才会重写，文件太小恢复速度本来就快，重写的意义不大 # auto-aof-rewrite-percentage 100 //aof文件自上次重写后文件大小增长了100%则再次触发重写当然AOF还可以手动重写，进入redis客户端执行bgrewriteaof重写aof注意，AOF重写redis会fork出一个子进程去做（与bgsave命令类似），不会对redis正常命令处理有太多影响RDB 和 AOF，我该用哪一个命令RDBAOF启动优先级低高体积小大恢复速度快慢数据安全性容易丢数据根据策略决定生产环境可以都启用，redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据，因为aof一般来说数据更安全一点。Redis4.0混合持久化重启Redis时，我们很少使用RDB来恢复内存状态，因为会丢失大量数据。我们通常使用AOF日志重放，但是重放AOF日志性能对RDB来说要慢很多，这样在Redis实例很大的情况下，启动需要花费很长时间。Redis4.0为了解决这个问题，带来了一个新的持久化选项--混合持久化。通过如下配置可以开启混合持久化(必须先开启aof):# aof-use-rdb-preamble yes如果开启了混合持久化，AOF重写时，不再是单纯将内存数据转换为RESP命令写入AOF文件，而是将重写这一刻之前的内存叫做RDB快照处理，并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起，都写入新的AOF文件，新的文件一开始不叫appendonly.aof，等到重写完新的AOF文件才会进行改名，覆盖原有的AOF文件，完成新旧两个AOF文件的替换。于是在Redis重启的时候，可以先加载RDB的内容，然后在重放增量AOF日志就可以完全替代之前的AOF全量文件重放，因此重启效率大幅得到提升。混合持久化AOF文件结构如下appendonly.aofRDB格式AOF格式线上持久化策略一般如何设置如果对性能要求比较高，在master最好不要做持久化，可以在某个slave开启AOF备份权限，策略设置为每秒即可。十二、一次线上事故，Redis主节点宕机导致数据全部丢失如果你的Redis采用如下模式部署，就会发生数据丢失的问题：master-slave + 哨兵部署实例。master 没有开启数据持久化功能。Redis进程使用supervisor管理，并配置为进程宕机，自动重启。如果此时master宕机，就会导致下面的问题：master宕机，哨兵还未发起切换，此时master进程立即被supervisor自动拉起。但master没有开启任何数据持久化，启动后是一个空实例。此时slave为了与master保持一致，他会制动清理实例的所有数据，slave也变成了一个空实例。在这个场景下，master / slave 的数据就全部丢失了。此时，业务应用在访问Redis时，发现缓存中没有任何数据，就会把请求全部打到后台数据库上，这还会引发缓存雪崩，对业务影响巨大。这种情况下我们一般不应该给Redis主节点配置进程宕机马上自动重启策略，而应该等哨兵把某个Redis从节点切换为主节点再重启之前宕机的Redis主节点让起变为slave节点。十三、Redis线上数据如何备份写crontab定时调度脚本，每小时都copy一份rdb或aof文件到另外一台机器中去，保留最近48小时的备份每天都保留一份当日的数据备份到一个目录中去，可以保留最近一个月的备份每次copy备份的时候，都把太旧的备份删除十四、Redis主从复制风暴是怎么回事如果Redis主节点有很多从节点，在某一时刻如果所有从节点都同时连接主节点，那么主节点会同时把内存RDB发给多个从节点，这样会导致Redis主节点压力非常大，这就是所谓的Redis主从复制风暴问题。这种问题我们队Redis主从架构做一些优化得以避免，比如可以做下面这种树形复制架构