java基础面试题

索引优化

多线程

mysql优化(库数据过大怎么设计)

联合索引 ABC ac bc是否走索引

mysql执行计划看是否走索引

hash冲突

redis具体使用过期同步啥的

线程池创建参数、

cpu如何调度线程进程

arraylist创建多个对象插入数据会出现null值

spring设计模式

redis数据同步

list怎么排序 例如学生姓名学号实体类按照特定顺序排序插入等

spring启动类注解 resource注解和autowared注解区别

线程的生命周期

包括:新建、就绪、运行、阻塞、销毁。

多线程的创建方式

(1)、继承 Thread 类:但 Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例,并 且,启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线 程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并复写 run()方法,就可以 启动新线程并执行自己定义的 run()方法。例如:继承 Thread 类实现多线程,并在合适的地方启动线程

(2)、实现 Runnable 接口的方式实现多线程,并且实例化 Thread,传入自己的 Thread 实例,调用 run( )方法

(3)、使用 ExecutorService、Callable、Future 实现有返回结果的多线程:ExecutorService、Callable、Future 这 个 对 象 实 际 上 都 是 属 于 Executor 框 架 中 的 功 能 类 。 可返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行 Callable 任务后,可以 获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

在 java 中 wait 和 sleep 方法的不同?

sleep 来自 Thread 类,和 wait 来自 Object 类。

最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。wait 通常被用于线程间交互,sleep 通常被用于暂 停执行。

synchronized 和 volatile 关键字的作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是 立即可见的。 2)禁止进行指令重排序。 volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

1.volatile 仅能使用在变量级别; synchronized 则可以使用在变量、方法、和类级别的

2.volatile 仅能实现变量的修改可见性,并不能保证原子性; synchronized 则可以保证变量的修改可见性和原子性、

3.volatile 不会造成线程的阻塞; synchronized 可能会造成线程的阻塞。

4.volatile 标记的变量不会被编译器优化; synchronized 标记的变量可以被编译器优化

常用的线程池有哪些?

newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大 大小。 newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于 操作系统(或者说 JVM)能够创建的最大线程大小。

newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。 newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

Java线程池七个参数详解

java多线程开发时,常常用到线程池技术,

从源码中可以看出,线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。下面会对这7个参数一一解释。

一、corePoolSize 线程池核心线程大小

线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。

二、maximumPoolSize 线程池最大线程数量

一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

三、keepAliveTime 空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

四、unit 空闲线程存活时间单位

keepAliveTime的计量单位

五、workQueue 工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

六、threadFactory 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等

七、handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

①CallerRunsPolicy

该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

请叙述一下您对线程池的理解?

(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略) 合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

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

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

JAVA中循环删除list中元素的方法总结

印象中循环删除list中的元素使用for循环的方式是有问题的,但是可以使用增强的for循环,然后今天在使用时发现报错了,然后去科普了一下,再然后发现这是一个误区。下面就来讲一讲。。伸手党可直接跳至文末。看总结。。

JAVA中循环遍历list有三种方式for循环、增强for循环(也就是常说的foreach循环)、iterator遍历。

1、for循环遍历list

for(int i=0;i<list.size();i++){
    if(list.get(i).equals("del"))
        list.remove(i);
}

这种方式的问题在于,删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。

2、增强for循环

for(String x:list){
    if(x.equals("del"))
        list.remove(x);
}

这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出。但是删除完毕马上使用break跳出,则不会触发报错。

3、iterator遍历

[复制代码](javascript:void(0)😉

Iterator<String> it = list.iterator();
while(it.hasNext()){
    String x = it.next();
    if(x.equals("del")){
        it.remove();
    }
}

[复制代码](javascript:void(0)😉

这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。

总结:

(1)循环删除list中特定一个元素的,可以使用三种方式中的任意一种,但在使用中要注意上面分析的各个问题。

(2)循环删除list中多个元素的,应该使用迭代器iterator方式。

集合的安全性问题

请问 ArrayList、HashSet、HashMap 是线程安全的吗?如果不是我想要线程安全的集合怎么办?

我们都看过上面那些集合的源码(如果没有那就看看吧),每个方法都没有加锁,显然都是线程不安全的。

在集合中 Vector 和 HashTable 倒是线程安全的。你打开源码会发现其实就是把各自核心方法添加上了 synchronized 关键字。 Collections 工具类提供了相关的 API,可以让上面那 3 个不安全的集合变为安全的。

  1. // Collections.synchronizedCollection© 2. // Collections.synchronizedList(list) 3. // Collections.synchronizedMap(m) 4. // Collections.synchronizedSet(s) 上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实实现原理非常简单,就是将集 合的核心方法添加上了 synchronized 关键字。

HashMap 是线程安全的吗,为什么不是线程安全的?

不是线程安全的;

1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。

2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

1 当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

2 如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位 置还没有其他的数据。假设一种情况,线程A通过if判断,该 位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句 里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执 行,现在线程A直接在该位置插入而不用再判断。这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线 程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能 就直接给覆盖了。

HashMap 的扩容过程

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值(知道这个阈字怎么念吗?不念 fa 值,
念 yu 值四声)—-即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

扩容( resize )就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更
多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法
是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

HashMap hashMap=new HashMap(cap);cap =3, hashMap 的容量为4;cap =4, hashMap 的容量为4;cap =5, hashMap 的容量为8;cap =9, hashMap 的容量为16;

如果 cap 是2的n次方,则容量为 cap ,否则为大于 cap 的第一个2的n次方的数。

HashMap和Hashtable的区别

1.线程安全性:
hashmap:线程不安全;
hashtable:线程安全。

2.同步(synchronization):
hashmap:非同步。要想线程安全同步,
则需要:Map m = Collections.synchronizeMap(hashMap);
hashtable:同步。

3.速度
hashmap:由于是非同步可允许多线程访问的,所以效率高;
hashtable:由于是同步的单线程资源访问的模式,所以效率低。

4.结构:
hashmap:可以存放null的键值对,去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法;
hashtable:不能存入null的任何数据。

List 的三个子类的特点

ArrayList 底层结构是数组,底层查询快,增删慢。

LinkedList 底层结构是链表型的,增删快,查询慢。

voctor 底层结构是数组 线程安全的,增删慢,查询慢。

List 和 Map、Set 的区别

结构特点 List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;

List 中存储的数据是有顺序,并 且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,

Set 中存储的数据是无 序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set 集合根据 hashcode 进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);

List 接口有三个实现类

LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还 存储下一个元素的地址。链表增删快,查找慢;

ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不 便于插入删除;

Vector:基于数组实现,线程安全的,效率低)。

Map 接口有三个实现类

HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;HashTable:线程安全,低效,不支持 null 值和 null 键;

LinkedHashMap:是 HashMap 的一个子类,保存了 记录的插入顺序;

SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。

Set 接口有两个实现类

HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重 写 equals()和 hashCode()方法;LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底 层使用的是 LinkedHashMp。

List,Set,Map区别

List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过 list.get(i)方法来获取集合中的元素;

Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对 象可以重复;

Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定 的方式排序,例如 TreeSet 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator接口来自定义排序 方式。

Java 中 ArrayList 和 Linkedlist 区别?

ArrayList 和 Vector 使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组 中添加,删除,插入新的元素或者数据的扩展和重定向。

LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也决 定了它们将适用于完全不同的工作场景。 LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表。在 JDK 的实现中,无论 LikedList 是否 为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。表项 header 的后驱表项便是链表 中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。

Java中常见集合的默认大小以及扩容机制

list元素时有序的、可重复
Arraylist、vector默认初始化容量为10

vector:线程安全,但速度慢
底层数据结构为数组结构
加载因子为1:即当元素个数超过容量长度时,进行扩容
扩容增量:原容量的1倍
如vector的容量为10,一次扩容后是容量为20

ArrayList:线程不安全,查询速度快
底层数据结构是数组结构
扩容增量:原容量的0.5倍+1
如ArrayList的容量为10,一次扩容后是容量为16

Set元素无序、不可重复
hashset:线程不安全,存取速度快
底层实现是一个HashMap(保存数据),实现Set接口
默认初始化容量为16(为何是16,见下方对HashMap的描述)
加载因子为0.75:即当元素个数超过容量长度的0.75倍时,进行扩容
扩容增量:原容量的1倍
如hashSet的容量为16,一次扩容后是容量为32

Map是一个双列集合
HashMap:默认初始容量为16
(为何是16:16是2^4,可以提高查询效率,另外,32=16<<1)
加载因子为0.75:即当元素个数超过容量长度的0.75配时,进行扩容
扩容增量:原容量的1倍
如hashSet的容量为16,一次扩容后是容量为32

说说你对 Java 中反射的理解

Java 中 的 反 射 首 先 是 能 够 获 取 到 Java 中 要 反 射 类 的 字 节 码 , 获 取 字 节 码 有 三 种 方 法 , 1.Class.forName(className) 2.类名.class 3.this.getClass()。然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类,这些类提供了丰富的方法可以被我们所使用。

你所知道的设计模式有哪些

Java 中一般认为有 23 种设计模式, 总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模 式、状态模式、访问者模式、中介者模式、解释器模式。

懒汉式-双检锁 synchronized + volatile
public class LazySingletonSafe2 {

    //1.构造方法私有化
    private LazySingletonSafe2() {
    }

    //2.私有静态属性
    private static volatile LazySingletonSafe2 hungrySingleton = null;

    //3.对外公开获取方法
    public static LazySingletonSafe2 getInstance() {
        //步骤1
        if (hungrySingleton == null) {
            synchronized (LazySingletonSafe2.class) {
	            //第二次检查,当多线程时,两个线程同时经过了第一次检查,到达synchronized
                //这时将没有限制的创建两次对象,所以在经过第一次判断后,synchronized后
                //仍旧需要一次判断来确认该对象只会被创建一次
                if (hungrySingleton == null) {
                    //步骤2
                    hungrySingleton = new LazySingletonSafe2();
                }
            }
        }
        //步骤3
        return hungrySingleton;
    }
}

Mysql 存储引擎有哪些?

(1)、InnoDB 存储引擎 InnoDB 是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,InnoDB 是默认的 MySQL 引擎。

(2)、MyISAM 存储引擎 MyISAM 基于 ISAM 存储引擎,并对其进行扩展。它是在 Web、数据仓储和其他应用环境下最常使用的存储引擎 之一。MyISAM 拥有较高的插入、查询速度,但不支持事物。

(3)、MEMORY 存储引擎 MEMORY 存储引擎将表中的数据存储到内存中,未查询和引用其他表数据提供快速访问。

(4)、NDB 存储引擎 DB 存储引擎是一个集群存储引擎,类似于 Oracle 的 RAC,但它是 Share Nothing 的架构,因此能提供更高级 别的高可用性和可扩展性。NDB 的特点是数据全部放在内存中,因此通过主键查找非常快。 关于 NDB,有一个问题需要注意,它的连接(join)操作是在 MySQL 数据库层完成,不是在存储引擎层完成,这 意味着,复杂的 join 操作需要巨大的网络开销,查询速度会很慢。

(5)、Memory (Heap) 存储引擎 Memory 存储引擎(之前称为 Heap)将表中数据存放在内存中,如果数据库重启或崩溃,数据丢失,因此它非 常适合存储临时数据。感恩于心,回报于行。 面试宝典系列-Java http://www.itheima.com Copyright© 2018 黑马程序员 369

(6)、Archive 存储引擎 正如其名称所示,Archive 非常适合存储归档数据,如日志信息。它只支持 INSERT 和 SELECT 操作,其设计的主 要目的是提供高速的插入和压缩功能。

(7)、Federated 存储引擎 Federated 存储引擎不存放数据,它至少指向一台远程 MySQL 数据库服务器上的表,非常类似于 Oracle 的透明 网关。

(8)、Maria 存储引擎 Maria 存储引擎是新开发的引擎,其设计目标是用来取代原有的 MyISAM 存储引擎,从而成为 MySQL 默认 的存储引擎。

InnoDB底层原理

InnoDB的内存架构主要分为三大块,缓冲池(Buffer Pool)、重做缓冲池(Redo Log Buffer)和额外内存池

缓冲池

InnoDB为了做数据的持久化,会将数据存储到磁盘上。但是面对大量的请求时,CPU的处理速度和磁盘的IO速度之间差距太大,为了提高整体的效率, InnoDB引入了缓冲池。

当有请求来查询数据时,如果缓存池中没有,就会去磁盘中查找,将匹配到的数据放入缓存池中。同样的,如果有请求来修改数据,MySQL并不会直接去修改磁盘,而是会修改已经在缓冲池的页中的数据,然后再将数据刷回磁盘,这就是缓冲池的作用,加速度,加速写,减少与磁盘的IO交互。

缓冲池说白了就是把磁盘中的数据丢到内存,那既然是内存就会存在没有内存空间可以分配的情况。所以缓冲池采用了LRU算法,在缓冲池中没有空闲的页时,来进行页的淘汰。但是采用这种算法会带来一个问题叫做缓冲池污染。

当你在进行批量扫描甚至全表扫描时,可能会将缓冲池中的热点页全部替换出去。这样一来可能会导致MySQL的性能断崖式下降。所以InnoDB对LRU做了一些优化,规避了这个问题。

MySQL采用日志先行,在真正写数据之前,会首先记录一个日志,叫Redo Log,会定期的使用CheckPoint技术将新的Redo Log刷入磁盘,这个后面会讲。

除了数据之外,里面还存储了索引页、Undo页、插入缓冲、自适应哈希索引、InnoDB锁信息和数据字典。下面选几个比较重要的来简单聊一聊。

插入缓冲

插入缓冲针对的操作是更新或者插入,我们考虑最坏的情况,那就是需要更新的数据都不在缓冲池中。那么此时会有下面两种方案。

来一条数据就直接写入磁盘

等数据达到某个阈值(例如50条)才批量的写入磁盘

MySql 的SQL执行计划查看,判断是否走索引

在select窗口中,执行以下语句:

set profiling =1; – 打开profile分析工具
show variables like ‘%profil%’; – 查看是否生效
show processlist; – 查看进程
use cmc; – 选择数据库
show PROFILE all; – 全部分析的类型
show index from t_log_account; ##查看某个表的索引
show index from t_car_copy; ##查看某个表的索引
– 使用explain命令查看query语句的性能:
EXPLAIN select * from t_car_copy ; ##查看执行计划中的sql性能
EXPLAIN select * from t_car_copy where org_id = ‘3’;
EXPLAIN select * from t_car_copy where 1=1 and org_id = ‘3’;

联合索引,在哪些查询条件下可能会用到

在数据库建了联合索引,abc三字段。
第一:单独使用a,或b,或c查,会不会用到索引
第二:使用ab,bc,ac查,会不会用到索引
使用explain

EXPLAIN select * from mp_dialogue_record p where p.agent_id = 199969;
EXPLAIN select * from mp_dialogue_record p where p.im_type = 2;
EXPLAIN select * from mp_dialogue_record p where p.insert_date = '2018-05-24';
EXPLAIN select * from mp_dialogue_record p where p.agent_id = 199969 and p.im_type = 2;
EXPLAIN select * from mp_dialogue_record p where p.im_type = 2 and p.agent_id = 199969;
EXPLAIN select * from mp_dialogue_record p where p.im_type = 2 and  p.insert_date = '2018-05-24';
EXPLAIN select * from mp_dialogue_record p where p.agent_id = 2 and  p.insert_date = '2018-05-24';
EXPLAIN select * from mp_dialogue_record p where p.agent_id = 199969 and p.im_type = 2 and  p.insert_date = '2018-05-24';

结果就是,abc的联合索引,只要查询条件中,用到a了,就用到索引。
所以创建联合索引,还需要注意顺序。

Sql优化

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0

3.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20

5.in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3

6.下面的查询也将导致全表扫描:
select id from t where name like ‘%abc%’

7.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2

8.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)=‘abc’–name以abc开头的id
应改为:
select id from t where name like ‘abc%’

9.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

10.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。

11.不要写一些没有意义的查询,如需要生成一个空表结构:
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(…)

12.很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)

13.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

14.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,
因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。

15.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

16.尽可能的使用 varchar 代替 char ,因为首先变长字段存储空间小,可以节省存储空间,
其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

17.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

18.避免频繁创建和删除临时表,以减少系统表资源的消耗。

19.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。

20.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,
以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

21.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

22.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

23.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

24.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。
在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

25.尽量避免大事务操作,提高系统并发能力。

26.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

\1. InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID)

2.InnoDB是mySQL默认的存储引擎,默认的隔离级别是RR,并且在RR的隔离级别下更近一步,通过多版本并发控制(MVCC)解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此InnoDB的RR隔离级别其实实现了串行化级别的效果,而保留了比较好的并发性能。

索引

2.索引的优缺点

优点

①提高数据的检索效率,降低数据库的IO成本。

②通过索引列对数据进行排序,降低数据的排序成本,从而降低CPU的消耗。

唯一索引可以确保每一行数据的唯一性

缺点

①索引实际上也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也要占用空间。

②虽然索引大大提高了查询效率,但是降低了更新表的速度,如insert、update和delete操作。因为更新表时,MySQL不仅要保存数据,还要保存索引文件每次更新的索引列字段,并且在更新操作后,会更新相应字段索引的信息。

③索引只是提高查询效率的一个因素,如果你的MySQL有大量的数据表,就需要花时间研究建立最优秀的索引或优化查询语句。

3.索引分类

索引主要分为以下三类:

①单值索引:一个索引只包含单个列,一个表可以有多个单值索引。

②唯一索引:索引列的值必须唯一,但允许有空值,主键就是唯一索引。

③复合索引(联合索引):一个索引包含多个列

索引的结构:

①BTREE索引;②Hash索引;③Full-Text索引;④R-Tree索引。

InnoDB使用的B+ 树的索引模型,那么你知道B+ 树和Hash索引比较起来有什么优缺点吗?

因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。**所以,哈希索引只适用于等值查询的场景。**而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描

B+ Tree索引和Hash索引区别?

哈希索引适合等值查询,但是无法进行范围查询

哈希索引没办法利用索引完成排序

哈希索引不支持多列联合索引的最左匹配规则

如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题

4.使用索引查询一定能提高查询的性能吗?为什么

通常,通过索引查询数据比全表扫描要快.但是我们也必须注意到它的代价.

索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改. 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O. 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢.使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:

基于一个范围的检索,一般查询返回结果集小于表中记录数的30%

基于非唯一性索引的检索

4.建立索引与否的具体情况

①需建立索引的情况

#1.主键自动建立唯一索引。

#2.频繁作为查询条件的字段。

#3.查询中与其他表关联的字段,外键关系建立索引。

#4.高并发下趋向创建组合索引。

#5.查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度。

#6.查询中统计或分组字段。

②不需要创建索引的情况

#1.表记录太少。(数据量太少MySQL自己就可以搞定了)

#2.经常增删改的表。

#3.数据重复且平均分配的字段,如国籍、性别,不适合创建索引。

#4.频繁更新的字段不适合建立索引。

#5.Where条件里用不到的字段不创建索引。

Redis

简单来说 Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。

另外,Redis 除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列。

Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。

redis常用的数据类型

  • String:
    • string 数据结构是简单的 key-value 类型。
    • 虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。
    • 相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。

    使用场景:

    • 常规计数
    • 微博数
    • 粉丝数
    • token令牌
  • List:

    介绍:

    • 单值多Value 有序集合 不唯一
    • 它是一个字符串链表,left,right 都可以插入添加
    • 如果键不存在,创建新的链表,如果值全移除,对应的键也就消失
    • 链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。
    • list就是链表,略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消
      息排行等功能。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务存在List中,然后工
      作线程再用POP操作将任务取出进行执行。Redis还提供了操作List中某一段的api,你可以直接查询,删
      除List中某一段的元素。
    • Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部
      添加或者删除元素,这样List即可以作为栈,也可以作为队列。

    类似与java的双端队列

    使用场景:

    • 用于作为一个聊天系统,还可以作为不同进程间传递消息的队列。关键是,你可以每次都以原先添加的顺序访问数据。
      这不需要任何SQL ORDER BY 操作,将会非常快,也会很容易扩展到百万级别元素的规模。

    • 在博客引擎实现中,你可为每篇日志设置一个list,在该list中推入博客评论
      而且可以截取某一段元素也可以生成分页,比如一些评论加载

    • Set

      介绍:

      • Set 单值多vlaue 无序唯一 在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为
        集合提供了求交集、并集、差集等操作,可以非常方便的实现如
      • set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。
      • 当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。
      • 可以基于 set 轻易实现交集、并集、差集的操作。

      使用场景

      • 比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程
      • 共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
      • (官网举出了一个扑克牌随机发牌的案列,存入52张牌,将此集合复制一份到其他集合中,
        然后依次弹出元素返回到处理程序也就是发出相应的牌,实现随机发牌)
      ZSet

      介绍:

      • 单值多vlaue有序
      • value加了一个score权重系数,可以用作排序 vlaue唯一,score可以不唯一和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列
      • 比如一个存储全班同学成绩的sorted set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。可以用sorted set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

      使用场景

      • 排行榜应用
      • 取TOP N操作 !
      hash

      Redis hash是一个string类型的field和value的映射表

      1. 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
      2. 常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
      3. 应用场景: 系统中对象数据的存储。

      使用场景

      • hash特别适合用于存储对象。存储部分变更的数据,如用户信息等。

      为什么是删除缓存,而不是更新缓存?(针对redis和数据库双写一致的问题)

      原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。

      • 比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
      • 另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。
      • 如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?

      举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

      缓存是为了解决查询操作的效率,为了提高缓存的使用率,删除更好

      怎么保证 redis 和 db 中的数据一致(或双写一致性)?

      1.第一种方案:采用延时双删策略

      在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

      伪代码如下

      public void write( String key, Object data ){    redis.delKey( key );    db.updateData( data );    Thread.sleep( 500 );    redis.delKey( key );}
      

      2.具体的步骤就是:

      1. 先删除缓存
      2. 再写数据库
      3. 休眠500毫秒
      4. 再次删除缓存

      那么,这个500毫秒怎么确定的,具体该休眠多久呢?

      需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

      当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

      3.设置缓存过期时间

      从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

      4.该方案的弊端

      结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

      自己理解 休眠是为了保证,修改操作的完成所耗费的时间!!!因为修改操作不是马上能执行完的!!可能会存在一些服务调用的时长,或者数据匹配的时长,当有其他线程进行读操作的时候,还是会读到以前的数据!!!并缓存到redis中,休眠就是为了保证能够在修改数据库数据成功后再次删掉缓存的旧数据,但是在极端情况下比如网络波动等问题发生,会导致我们的修改操作的调用出现比较长的耗时操作!!!导致我们的休眠时长不够,而提前删除掉了我们的缓存,但数据还没更新完!!!删除缓存之后,又有线程进行访问导致我们的缓存中出现脏数据!!

      2.第二种方案将数据串行话

      更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据+更新缓存”的操作,根据唯一标识路由之后,也发送到同一个 jvm 内部队列中。

      一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。

      这里有一个=优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。

      什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?

      缓存穿透

      一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

      如何避免?

      1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

      2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

      缓存雪崩

      当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

      如何避免?

      1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

      2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

      3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

      存储结构:

      内容是redis通讯协议(RESP )格式的命令文本存储。

      能解释下什么是RESP?有什么特点?(可以看到很多面试其实都是连环炮,面试官其实在等着你回答到这个点,如果你答上了对你的评价就又加了一分)

      RESP 是redis客户端和服务端之前使用的一种通讯协议;

      RESP 的特点:实现简单、快速解析、可读性好

      For Simple Strings the first byte of the reply is “+” 回复

      For Errors the first byte of the reply is “-” 错误

      For Integers the first byte of the reply is “:” 整数

      For Bulk Strings the first byte of the reply is “$” 字符串

      For Arrays the first byte of the reply is “*” 数组

      7.redis关于key过期时间的一些问题?

      参考地址:

      http://www.redis.cn/commands/expire.html

      1.如果key未设置过期时间,那么它是否不过期呢??

      是的

      通常Redis keys创建时没有设置相关过期时间。他们会一直存在,除非使用显示的命令移除,例如,使用DEL命令。

      使用PERSIST命令可以清除超时,使其变成一个永久的key

      2.Redis如何淘汰过期的keys?

      Redis keys过期有两种方式:

      • 被动

        当一些客户端尝试访问它时,key会被发现并主动的过期,比如。对key执行[DEL](http://www.redis.cn/commands/del.html)命令或者[SET](http://www.redis.cn/commands/set.html)命令或者[GETSET](http://www.redis.cn/commands/getset.html)时才会清除。 这意味着,从概念上讲所有改变key的值的操作都会使他清除。 例如,[INCR](http://www.redis.cn/commands/incr.html)递增key的值,执行[LPUSH](http://www.redis.cn/commands/lpush.html)操作,或者用[HSET](http://www.redis.cn/commands/hset.html)改变hash的field所有这些操作都会触发删除动作。

      • 主动方式。

        当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

      具体就是Redis每秒10次做的事情:

      1. 测试随机的20个keys进行相关过期检测。
      2. 删除所有已经过期的keys。
      3. 如果有多于25%的keys过期,重复步奏1.

      这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。

      Redis 提供了不同级别的持久化方式:

      • RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储.
      • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大.
      • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
      • 你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
      • 最重要的事情是了解RDB和AOF持久化方式的不同,让我们以RDB持久化方式开始:

      RDB的优点

      • RDB是一个非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集.
      • RDB是一个紧凑的单一文件,很方便传送到另一个远端数据中心或者亚马逊的S3(可能加密),非常适用于灾难恢复.
      • RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
      • 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些.

      RDB的缺点

      • 如果你希望在redis意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么RDB不适合你.虽然你可以配置不同的save时间点(例如每隔5分钟并且对数据集有100个写的操作),是Redis要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在Redis意外宕机,你可能会丢失几分钟的数据.
      • RDB 需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求.如果数据集巨大并且CPU性能不是很好的情况下,这种情况会持续1秒,AOF也需要fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度.

      AOF 优点

      • 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync.使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据.
      • AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题.
      • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
      • AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

      AOF 缺点

      • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
      • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

      如何选择使用哪种持久化方式?

      一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。

      如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

      有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。

      Note: 因为以上提到的种种原因, 未来我们可能会将 AOF 和 RDB 整合成单个持久化模型。 (这是一个长期计划。) 接下来的几个小节将介绍 RDB 和 AOF 的更多细节。

      快照

      在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用 SAVE或者 BGSAVE , 手动让 Redis 进行数据集保存操作。

      比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:

      save 60 1000
      

      这种持久化方式被称为快照 snapshotting.

      工作方式

      当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

      • Redis 调用forks. 同时拥有父进程和子进程。
      • 子进程将数据集写入到一个临时 RDB 文件中。
      • 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

      这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

      只追加操作的文件(Append-only file,AOF)

      快照功能并不是非常耐久(dura ble): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。

      你可以在配置文件中打开AOF方式:

      appendonly yes
      

      从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

      日志重写

      因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。

      为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。Redis 2.2 需要自己手动执行 BGREWRITEAOF 命令; Redis 2.4 则可以自动触发 AOF 重写, 具体信息请查看 2.4 的示例配置文件。

      AOF有多耐用?

      你可以配置 Redis 多久才将数据 fsync 到磁盘一次。有三种方式:

      • 每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全
      • 每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
      • 从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。
      • 推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

      如果AOF文件损坏了怎么办?

      服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:

      • 为现有的 AOF 文件创建一个备份。

      • 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复:

        $ redis-check-aof –fix

      • (可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。

      • 重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。

      工作原理

      AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制:

      • Redis 执行 fork() ,现在同时拥有父进程和子进程。
      • 子进程开始将新 AOF 文件的内容写入到临时文件。
      • 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
      • 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
      • 搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

      怎样从RDB方式切换为AOF方式

      在 Redis 2.2 或以上版本,可以在不重启的情况下,从 RDB 切换到 AOF :

      • 为最新的 dump.rdb 文件创建一个备份。
      • 将备份放到一个安全的地方。
      • 执行以下两条命令:
      • redis-cli config set appendonly yes
      • redis-cli config set save “”
      • 确保写命令会被正确地追加到 AOF 文件的末尾。
      • 执行的第一条命令开启了 AOF 功能: Redis 会阻塞直到初始 AOF 文件创建完成为止, 之后 Redis 会继续处理命令请求, 并开始将写入命令追加到 AOF 文件末尾。

      执行的第二条命令用于关闭 RDB 功能。 这一步是可选的, 如果你愿意的话, 也可以同时使用 RDB 和 AOF 这两种持久化功能。

      重要:别忘了在 redis.conf 中打开 AOF 功能! 否则的话, 服务器重启之后, 之前通过 CONFIG SET 设置的配置就会被遗忘, 程序会按原来的配置来启动服务器。

      AOF和RDB之间的相互作用

      在版本号大于等于 2.4 的 Redis 中, BGSAVE 执行的过程中, 不可以执行 BGREWRITEAOF 。 反过来说, 在 BGREWRITEAOF 执行的过程中, 也不可以执行 BGSAVE。这可以防止两个 Redis 后台进程同时对磁盘进行大量的 I/O 操作。

      如果 BGSAVE 正在执行, 并且用户显示地调用 BGREWRITEAOF 命令, 那么服务器将向用户回复一个 OK 状态, 并告知用户, BGREWRITEAOF 已经被预定执行: 一旦 BGSAVE 执行完毕, BGREWRITEAOF 就会正式开始。 当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。

      备份redis数据

      在阅读这个小节前, 请牢记下面这句话: 确保你的数据由完整的备份. 磁盘故障, 节点失效, 诸如此类的问题都可能让你的数据消失不见, 不进行备份是非常危险的。

      Redis 对于数据备份是非常友好的, 因为你可以在服务器运行的时候对 RDB 文件进行复制: RDB 文件一旦被创建, 就不会进行任何修改。 当服务器要创建一个新的 RDB 文件时, 它先将文件的内容保存在一个临时文件里面, 当临时文件写入完毕时, 程序才使用 rename(2) 原子地用临时文件替换原来的 RDB 文件。

      这也就是说, 无论何时, 复制 RDB 文件都是绝对安全的。

      • 创建一个定期任务(cron job), 每小时将一个 RDB 文件备份到一个文件夹, 并且每天将一个 RDB 文件备份到另一个文件夹。
      • 确保快照的备份都带有相应的日期和时间信息, 每次执行定期任务脚本时, 使用 find 命令来删除过期的快照: 比如说, 你可以保留最近 48 小时内的每小时快照, 还可以保留最近一两个月的每日快照。
      • 至少每天一次, 将 RDB 备份到你的数据中心之外, 或者至少是备份到你运行 Redis 服务器的物理机器之外。

      容灾备份

      Redis 的容灾备份基本上就是对数据进行备份, 并将这些备份传送到多个不同的外部数据中心。容灾备份可以在 Redis 运行并产生快照的主数据中心发生严重的问题时, 仍然让数据处于安全状态。

      因为很多 Redis 用户都是创业者, 他们没有大把大把的钱可以浪费, 所以下面介绍的都是一些实用又便宜的容灾备份方法:

      • Amazon S3 ,以及其他类似 S3 的服务,是一个构建灾难备份系统的好地方。 最简单的方法就是将你的每小时或者每日 RDB 备份加密并传送到 S3 。 对数据的加密可以通过 gpg -c 命令来完成(对称加密模式)。 记得把你的密码放到几个不同的、安全的地方去(比如你可以把密码复制给你组织里最重要的人物)。 同时使用多个储存服务来保存数据文件,可以提升数据的安全性。
      • 传送快照可以使用 SCP 来完成(SSH 的组件)。 以下是简单并且安全的传送方法: 买一个离你的数据中心非常远的 VPS , 装上 SSH , 创建一个无口令的 SSH 客户端 key , 并将这个 key 添加到 VPS 的 authorized_keys 文件中, 这样就可以向这个 VPS 传送快照备份文件了。 为了达到最好的数据安全性,至少要从两个不同的提供商那里各购买一个 VPS 来进行数据容灾备份。

      另外, 你还需要一个独立的警报系统, 让它在负责传送备份文件的传送器(transfer)失灵时通知你。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值