HMaster 启动的时候会把 Meta 的信息表记录在 Zookeeper 中。这个元数据信息表存储了 HBase 中所有的表,以及 Region 的详细信息。如 Region 开始和结束的 Key,所在的 RegionServer 的地址。HBASE 的 Meta 表相当于一个目录。通过访问 Meta 表可以快速的定位到数据的实际位置,所以读写操作只需要与 Zookeeper 和对应的 RegionServer 进行交互,而 HMaster 只需要负责维护 table 和 Region 的元数据信息,协调各个 RegionServer,也因此减少了 HMaster 的负载。
简写 | 全拼 | 中文翻译 |
---|---|---|
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
修改一个类的原因应该只有一个。
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
类应该对扩展开放,对修改关闭。
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。
子类对象必须能够替换掉所有父类对象。
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
不应该强迫客户依赖于它们不用的方法。
因此使用多个专门的接口比使用单一的总接口要好。
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
依赖于抽象意味着:
IOC 是 Inversion of Control 的缩写,多数书籍翻译成“控制反转”。
IOC 理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。
软件系统在没有引入 IOC 容器之前,如图 1 所示,对象 A 依赖于对象 B,那么对象 A 在初始化或者运行到某一点的时候,自己必须主动去创建对象 B 或者使用已经创建的对象 B。无论是创建还是使用对象 B,控制权都在自己手上。
软件系统在引入 IOC 容器之后,这种情形就完全改变了,如图所示,由于 IOC 容器的加入,对象 A 与对象 B 之间失去了直接联系,所以,当对象 A 运行到需要对象 B 的时候,IOC 容器会主动创建一个对象 B 注入到对象 A 需要的地方。
通过前后的对比,我们不难看出来:对象 A 获得依赖对象 B 的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
2004 年,Martin Fowler 探讨了同一个问题,既然 IOC 是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由 IOC 容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现 IOC 的方法:注入。所谓依赖注入,就是由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
理解以上概念需要搞清以下问题:
AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充。它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。
所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
AOP 常见的使用场景
Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 中的动态代理方法主要有两种:
如果目标对象实现了接口,则默认采用 JDK 动态代理。
如果目标对象没有实现接口,则默认采用 Cglib 进行动态代理。
如果目标对象实现了接口,则强制 Cglib 代理,则采用 Cglib 进行代理。
// 采用注解开启强制代理
@EnableAspectJAutoProxy(proxyTragetClass = true)
/**
* bean的生命周期:
* bean创建---初始化----销毁的过程
* 容器管理bean的生命周期;
* 我们可以自定义初始化和销毁方法;容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁方法
*
* 构造(对象创建)
* 单实例:在容器启动的时候创建对象
* 多实例:在每次获取的时候创建对象
*
* BeanPostProcessor.postProcessBeforeInitialization
* 初始化:
* 对象创建完成,并赋值好,调用初始化方法。。。
* BeanPostProcessor.postProcessAfterInitialization
* 销毁:
* 单实例:容器关闭的时候
* 多实例:容器不会管理这个bean;容器不会调用销毁方法;
*
*
* 遍历得到容器中所有的BeanPostProcessor;挨个执行beforeInitialization,
* 一但返回null,跳出for循环,不会执行后面的BeanPostProcessor.postProcessorsBeforeInitialization
*
* BeanPostProcessor原理
* populateBean(beanName, mbd, instanceWrapper);给bean进行属性赋值
* initializeBean
* {
* applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
* invokeInitMethods(beanName, wrappedBean, mbd);执行自定义初始化
* applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
*}
*
*
*
* 1)、指定初始化和销毁方法;
* 通过@Bean指定init-method和destroy-method;
* 2)、通过让Bean实现InitializingBean(定义初始化逻辑),
* DisposableBean(定义销毁逻辑);
* 3)、可以使用JSR250;
* @PostConstruct:在bean创建完成并且属性赋值完成;来执行初始化方法
* @PreDestroy:在容器销毁bean之前通知我们进行清理工作
* 4)、BeanPostProcessor【interface】:bean的后置处理器;
* 在bean初始化前后进行一些处理工作;
* postProcessBeforeInitialization:在初始化之前工作
* postProcessAfterInitialization:在初始化之后工作
*
* Spring底层对 BeanPostProcessor 的使用;
* bean赋值,注入其他组件,@Autowired,生命周期注解功能,@Async,xxx BeanPostProcessor;
*
* @author lfy
*
*/
Spring 组件注册主要有以下三种方式:
applicationContext.getBean("bookDao")
@Autowired(required=false);
BookService{
@Autowired
BookDao bookDao;
}
@Autowired 构造器,参数,方法,属性;都是从容器中获取参数组件的值。
可以和@Autowired 一样实现自动装配功能;默认是按照组件名称进行装配的;
没有支持@Primary 功能,没有支持 @Autowired(reqiured=false)
需要导入 javax.inject 的依赖,和 @Autowired 的功能一样。
javax.inject
javax.inject
1
没有支持 @Autowired(reqiured=false) 的功能;
AutowiredAnnotationBeanPostProcessor:解析完成自动装配功能;
自定义组件想要使用Spring容器底层的一些组件(ApplicationContext,BeanFactory,xxx);
自定义组件实现xxxAware;在创建对象的时候,会调用接口规定的方法注入相关组件;均实现 `` 接口;
把Spring底层一些组件注入到自定义的Bean中;
xxAware 均有对应的 xxxProcessor 进行值的注入;
ApplicationContextAware==》ApplicationContextAwareProcessor;
文件读写主要有以下集中常用的方法:
通过测试 ,可以发现,就写数据而言,BufferedOutputStream
耗时是最短的,而性能FileWriter
最差;读数据方面,BufferedInputStream
性能最好,而FileReader
性能最差劲。
原因一般出现为以下两点:
其对应的两个术语为:
HashMap 中,length 为 2 的幂次方,h &(length-1)等同于求模运算 h%length
HashMap 采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap 定位哈希桶索引位置时,也加入了高位参与运算的过程。
垃圾收集主要是针对堆和方法区进行。
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
具备如下特点:
在 Java 1.7 以前,编译生成的字节码中并不会包含方法的参数信息。因此无法获取到方法的参数名称信息。
但是在 Java 1.8 以后,开始在 class 中保存参数名,并且增加了对应的类Parameter
。使用的示例代码如下:
public static List getParameterNameJava8(Class clazz, String methodName){
List paramterList = new ArrayList<>();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (methodName.equals(method.getName())) {
Parameter[] params = method.getParameters();
for(Parameter parameter : params){
paramterList.add(parameter.getName());
}
}
}
return paramterList;
}
如果编译等级低于 1.8,则得到的参数名依旧为无效的参数名,例如 arg0、arg1……
同时,想要保留参数名也需要通过修改编译选项 javac -parameters
进行打开,默认是关闭的。
idea 设置保留参数名:
在 preferences-》Java Compiler-> 设置模块字节码版本 1.8,Javac Options 中的 Additional command line parameters: -parameters
详细:在使用这些工具前,先用 JPS 命令获取当前的每个 JVM 进程号,然后选择要查看的 JVM。
Java 运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找。
负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是 Class 对象。类加载器不是只有一个,一般程序运行时,都会有三个(适用于 Java 9 之前)类加载器。
这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader 的父亲是 Extension ClassLoader,Extension 的父亲是 Bootstrap ClassLoader。注意不是父子继承关系,而是父子委派关系,即子 ClassLoader 有一个变量 parent 指向父 ClassLoader,在子 ClassLoader 加载类时,一般会首先通过父 ClassLoader 加载,具体来说,在加载一个类时,基本过程是:
这个过程一般被称为“双亲委派”模型,即优先让父 ClassLoader 去加载。
继承 ClassLoader 重写 loadClass 方法使得自己先加载之后尝试父类进行数据的加载。
线程池的构造函数如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler)
其中对于核心线程数,还有以下几点需要注意:
step 1:
Future> submit(Runnable task);
step 2:
public Future> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
step 3:
void execute(Runnable command);
step 4:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { //提交我们的额任务到workQueue
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) //使用maximumPoolSize作为边界
reject(command); //还不行?拒绝提交的任务
}
step 5:
private boolean addWorker(Runnable firstTask, boolean core)
step 6:
w = new Worker(firstTask); //包装任务
final Thread t = w.thread; //获取线程(包含任务)
workers.add(w); // 任务被放到works中
t.start(); //执行任务
ThreadPoolExecutor 要求的队列类型是阻塞队列 BlockingQueue。
如果队列有界,且 maximumPoolSize 有限,则当队列排满,线程个数也达到了 maximumPoolSize。此时,新任务会触发线程池的任务拒绝策略。
默认情况下,提交任务的方法(如 execute/submit/invokeAll 等)会抛出异常,类型为 RejectedExecutionException
。其中拒绝策略是可以自定义的,需要实现 RejectedExecutionHandler
接口。ThreadPoolExecutor 实现了四种方式。
拒绝策略只有在队列有界,且 maximumPoolSize 有限的情况下才会触发。
所以,在任务量非常大的场景中,让拒绝策略有机会执行是保证系统稳定运行很重要的方面。
线 程 个 数 = ( 运 行 时 间 + 等 待 时 间 ) 运 行 时 间 × C P U 核 数 线程个数 = \frac{(运行时间 + 等待时间)}{运行时间} \times {CPU核数} 线程个数=运行时间(运行时间+等待时间)×CPU核数
例如运行时间如果为 1s,等待时间为 5s,cpu 核数为 4,那么应该设置的线程池大小为 24
关于公式的理解,因为就是一个将等待时间用来运行线程的思想,比如刚才那个例子:运行时间 1s,等待时间 5s,那么等待的这段时间就可以多运行 5 / 1 个线程
线 程 个 数 = C P U 核 数 + 1 线程个数 = CPU 核数 + 1 线程个数=CPU核数+1
类 Executors 提供了一些静态工厂方法,可以方便地创建一些预配置的线程池,主要方法有:
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newCachedThreadPool()
public static ExecutorService newSingleThreadExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
只使用一个线程,使用无界队列 LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。该线程池适用于需要确保所有任务被顺序执行的场合。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
使用固定数目的 n 个线程,使用无界队列 LinkedBlockingQueue,线程创建后不会超时终止。和 newSingleThreadExecutor 一样,由于是无界队列,如果排队任务过多,可能会消耗过多的内存。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L,
TimeUnit.SECONDS, new SynchronousQueue());
}
它的 corePoolSize 为 0,maximumPoolSize 为 Integer.MAX_VALUE,keepAliveTime 是 60 秒,队列为 SynchronousQueue。它的含义是:当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果 60 秒内没有新任务,就终止。
实际中,应该使用 newFixedThreadPool 还是 newCachedThreadPool 呢?
在系统负载很高的情况下,newFixedThreadPool 可以通过队列对新任务排队,保证有足够的资源处理实际的任务,而 newCachedThreadPool 会为每个任务创建一个线程,导致创建过多的线程竞争 CPU 和内存资源,使得任何实际任务都难以完成,这时,newFixedThreadPool 更为适用。
不过,如果系统负载不太高,单个任务的执行时间也比较短,newCachedThreadPool 的效率可能更高,因为任务可以不经排队,直接交给某一个空闲线程。
在系统负载可能极高的情况下,两者都不是好的选择,newFixedThreadPool 的问题是队列过长,而 newCachedThreadPool 的问题是线程过多,这时,应根据具体情况自定义 ThreadPoolExecutor,传递合适的参数。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
破除互斥等待条件 -> 一般无法破除
破除 hold and wait -> 一次性获取所有资源
编程语言方面可能不支持,但是我们可以在尝试获取第一个锁之后再去尝试获取第二个锁,增加超时等待。如果超时则把两个锁全部释放。再去重新获取。
**缺点:**因为两个锁不能同时获取到就要舍弃,进行再次尝试。其中尝试的次数,每次尝试之间的等待时间。都会给用户带来不好的体验
破除循环等待 -> 按顺序获取资源
比如业务对象具有本身的 ID,根据一定的规则,比如按照 ID 的大小进行锁的获取。这样大家都是先获取 ID 值较小的对象的锁,这样就不会产生循环等待
破除无法剥夺的等待 -> 加入超时
LRU 是 Least Recently Used 的缩写,翻译过来就是“最近最少使用”,LRU 缓存就是使用这种原理实现,简单的说就是缓存一定量的数据,当超过设定的阈值时就把一些过期的数据删除掉
比如我们缓存 10000 条数据,当数据小于 10000 时可以随意添加,当超过 10000 时就需要把新的数据添加进来,同时要把过期数据删除,以确保我们最大缓存 10000 条,那怎么确定删除哪条过期数据呢,采用 LRU 算法实现的话就是将最老的数据删掉。
Java 里面实现 LRU 缓存通常有两种选择,一种是使用 LinkedHashMap,一种是自己设计数据结构,使用链表+HashMap。
LinkedHashMap 自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回 false,即不删除数据,我们使用 LinkedHashMap 实现 LRU 缓存的方法就是对 LinkedHashMap 实现简单的扩展,扩展方式有两种,一种是 inheritance(继承),一种是 delegation(委派)。
//LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//LinkedHashMap自带的判断是否删除最老的元素方法,默认返回false,即不删除老数据
//我们要做的就是重写这个方法,当满足一定条件时删除老数据
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
采用 inheritance 方式实现比较简单,而且实现了 Map 接口,在多线程环境使用时可以使用 Collections.synchronizedMap() 方法实现线程安全操作
package lru;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Created by Joe on 2018/8/21.
*/
public class LRUCacheInheritance extends LinkedHashMap {
private final int MAX_CACHE_SIZE;
public LRUCacheInheritance(int cacheSize) {
// 根据cacheSize和加载因子计算hashmap的capactiy,+1确保当达到cacheSize上限时不会触发hashmap的扩容
super((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true);
MAX_CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Set> set = entrySet();
for (Map.Entry entry : set) {
sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
}
return sb.toString();
}
}
在实际使用中还可以有如下的实现:
final int cacheSize = 100;
Map map = new LinkedHashMap((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > cacheSize;
}
};
delegation 方式实现更加优雅一些,但是由于没有实现 Map 接口,所以线程同步就需要自己搞定了
package lru;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Created by Joe on 2018/8/21.
*/
public class LRUCacheDelegation {
private final int MAX_CACHE_SIZE;
private final float DEFAULT_LOAD_FACTOR = 0.75f;
private LinkedHashMap map;
public LRUCacheDelegation(int cacheSize) {
MAX_CACHE_SIZE = cacheSize;
//根据cacheSize和加载因子计算hashmap的capactiy,+1确保当达到cacheSize上限时不会触发hashmap的扩容
int capacity = (int) Math.ceil(MAX_CACHE_SIZE / DEFAULT_LOAD_FACTOR) + 1;
map = new LinkedHashMap(capacity, DEFAULT_LOAD_FACTOR, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
};
}
public synchronized void put(K key, V value) {
map.put(key, value);
}
public synchronized V get(K key) {
return map.get(key);
}
public synchronized void remove(K key) {
map.remove(key);
}
public synchronized Set> entrySet() {
return map.entrySet();
}
public synchronized int size() {
return map.size();
}
public synchronized void clear() {
map.clear();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : map.entrySet()) {
sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
}
return sb.toString();
}
}
package lru;
import java.util.HashMap;
/**
* Created by Joe on 2018/8/21.
*/
public class LRUCache {
private final int MAX_CACHE_SIZE;
private Entry first;
private Entry last;
private HashMap> hashMap;
public LRUCache(int cacheSize) {
MAX_CACHE_SIZE = cacheSize;
hashMap = new HashMap<>();
}
public void put(K key, V value) {
Entry entry = getEntry(key);
if (entry == null) {
if (hashMap.size() > MAX_CACHE_SIZE) {
hashMap.remove(last);
removeLast();
}
entry = new Entry<>();
entry.key = key;
}
entry.value = value;
moveToFirst(entry);
hashMap.put(key, entry);
}
public V get(K key) {
Entry entry = getEntry(key);
if (entry == null) return null;
moveToFirst(entry);
return entry.value;
}
public void remove(K key) {
Entry entry = getEntry(key);
if (entry != null) {
if (entry.pre != null) entry.pre.next = entry.next;
if (entry.next != null) entry.next.pre = entry.pre;
if (entry == first) first = entry.next;
if (entry == last) last = entry.pre;
}
hashMap.remove(key);
}
private Entry getEntry(K key) {
return hashMap.get(key);
}
private void removeLast() {
if (last != null) {
last = last.pre;
if (last == null) {
first = null;
} else {
last.next = null;
}
}
}
private void moveToFirst(Entry entry) {
// 如果本就是第一个,直接返回
if (entry == first) {
return;
}
// 修改当前节点的前驱和后继节点,把当前节点独立出来
if (entry.pre != null) {
entry.pre.next = entry.next;
}
if (entry.next != null) {
entry.next.pre = entry.pre;
}
// 如果entry为最后一个节点,则把last置前
if (entry == last) {
last = last.pre;
}
// 如果插入的为第一个节点
if (first == null && last == null) {
first = last = entry;
return;
}
// 移动到first
entry.next = first;
first.pre = entry;
first = entry;
entry.pre = null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Entry entry = first;
while (entry != null) {
sb.append(String.format("%s:%s ", entry.key, entry.value));
entry = entry.next;
}
return sb.toString();
}
class Entry {
public Entry pre;
public Entry next;
public K key;
public V value;
}
}
FIFO 是 First Input First Output 的缩写,也就是常说的先入先出,默认情况下 LinkedHashMap 就是按照添加顺序保存,我们只需重写下 removeEldestEntry 方法即可轻松实现一个 FIFO 缓存,简化版的实现代码如下
final int cacheSize = 5;
LinkedHashMap lru = new LinkedHashMap() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > cacheSize;
}
};
二叉树 -> 二叉排序树,二叉排序树是一种比较有用的折衷方案。
数据的搜索比较方便,可以直接使用下标,但是删除或者插入元素比较麻烦。
而链表插入和删除元素很快,但是查找很慢。
而二叉排序树会兼有上面两个的好处,有序二叉树天然具有对数查找效率;二叉树天然具有链表特征。在处理大批量的动态数据比较有用。但是删除节点方法相对复杂。
public void quickSort(int[] array, int _left, int _right) {
int left = _left;
int right = _right;
int pivot;
if (left < right) {
// 获取基准线
pivot = array[left];
while (left != right) {
//从右往左找到比基准线小的数
while (left < right && pivot < array[right]) {
right--;
}
//将右边比基准线小的数换到左边
array[right] = array[left];
//从左往右找到比基准线大的数
while (left < right && pivot > array[left]) {
left++;
}
//将左边比基准线大的数换到右边
array[left] = array[right];
}
//此时left和right指向同一位置
array[left] = pivot;
quickSort(array, _left, left - 1);
quickSort(array, left + 1, _right);
}
}
package other;
import java.util.Arrays;
/**
* Created by Joe on 2018/8/21.
*/
public class HeapSort {
public static void main(String args[]) {
int []a = new int[] {16,25,34,27,30,5,7,4,41,55};
heapSort(a);
System.out.println(Arrays.toString(a));
}
static int parent(int i) {
return (i - 1) / 2;
}
static int left(int i) {
return 2 * i + 1;
}
static int right(int i) {
return 2 * i + 2;
}
/**
* 最大堆调整
* @param a 待排序数据
* @param i 第 i 个节点
* @param heapSize 数组中实际排序元素的长度
*/
static void maxHeapfy(int[] a, int i, int heapSize) {
int left = left(i);
int right = right(i);
int largest = i;
if (left < heapSize && a[left] > a[largest]) {
largest = left;
}
if (right < heapSize && a[right] > a[largest]) {
largest = right;
}
if (largest != i) {
a[largest] ^= a[i];
a[i] ^= a[largest];
a[largest] ^= a[i];
// 交换之后依旧需要保证最大根堆的性质
maxHeapfy(a, largest, heapSize);
}
}
static void buildMaxHeap(int[] a, int heapSize) {
// heapSize是长度,而heapSize-1是最后一个节点的索引,或者叫下标
// 再得到他的父节点应该是(heapSize-1 -1)/2
for (int i = parent(heapSize - 1); i >= 0; i--) {
maxHeapfy(a, i, heapSize);
}
}
static void heapSort(int[] a) {
int len = a.length;
// 建立初始堆
buildMaxHeap(a, len);
// 之后进行交换
a[len - 1] ^= a[0];
a[0] ^= a[len - 1];
a[len - 1] ^= a[0];
// 初始建堆之后还要排a.length-2次
for (int i = 0; i < len - 2; i++) {
maxHeapfy(a, 0, len - 1 - i);
a[len - 2 - i] ^= a[0];
a[0] ^= a[len - 2 - i];
a[len - 2 - i] ^= a[0];
}
}
}
package other;
import java.util.Arrays;
/**
* Created by Joe on 2018/8/21.
*/
public class MergeSort {
public static void merge(int[] a, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (a[i] < a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = a[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = a[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
a[k2 + low] = temp[k2];
}
}
public static void mergeSort(int[] a, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
mergeSort(a, low, mid);
// 右边
mergeSort(a, mid + 1, high);
// 左右归并
merge(a, low, mid, high);
System.out.println(Arrays.toString(a));
}
}
public static void main(String[] args) {
int a[] = { 51, 46, 20, 18, 65, 97, 82, 30, 77, 50 };
mergeSort(a, 0, a.length - 1);
System.out.println("排序结果:" + Arrays.toString(a));
}
}
OSI七层模型 | 对应网络协议 |
---|---|
应用层 | HTTP、TFTP、FTP、NFS、WAIS、SMTP |
表示层 | Telnet、Rlogin、SNMP、Gopher |
会话层 | SMTP、DNS |
传输层 | TCP、UDP |
网络层 | IP、ICMP、ARP、RARP、AKP、UUCP |
数据链路层 | FDDI、Ethernet、Arpanet、PDN、SLIP、PPP |
物理层 | IEEE 802.1A、IEEE 802.2到IEEE 802.11 |
应用程序 | FTP | TFTP | TELNET | SMTP | DNS | HTTP | SSH | MYSQL |
---|---|---|---|---|---|---|---|---|
熟知端口 | 21,20 | 69 | 23 | 25 | 53 | 80 | 22 | 3306 |
传输层协议 | TCP | UDP | TCP | TCP | UDP | TCP |
HTTP/0.9 只支持客户端发送 Get 请求,且不支持请求头。HTTP 具有典型的无状态性。
无状态是指协议对于事务处理没有记忆功能。缺少状态意味着,假如后面的处理需要前面的信息,则前面的信息必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要前面信息时,应答就较快。直观地说,就是每个请求都是独立的,与前面的请求和后面的请求都是没有直接联系的。
HTTP/1.0 在 HTTP/0.9 的基础上支持客户端发送 POST、HEAD。HTTP 1.0 需要使用 keep-alive 参数来告知服务器端要建立一个长连接,但默认是短连接。
HTTP/2 相比 HTTP/1.0 更简单,高效,强大。HTTP/2 的首要目标是通过完全的请求,响应多路复用,头部的压缩头部域来减小头部的体积,添加了请求优先级,服务端推送等。
有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
各种首部字段及其含义如下(不需要全记,仅供查阅):
首部字段名 | 说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Connection | 控制不再转发给代理的首部字段、管理持久连接 |
Date | 创建报文的日期时间 |
Pragma | 报文指令 |
Trailer | 报文末端的首部一览 |
Transfer-Encoding | 指定报文主体的传输编码方式 |
Upgrade | 升级为其他协议 |
Via | 代理服务器的相关信息 |
Warning | 错误通知 |
首部字段名 | 说明 |
---|---|
Accept | 用户代理可处理的媒体类型 |
Accept-Charset | 优先的字符集 |
Accept-Encoding | 优先的内容编码 |
Accept-Language | 优先的语言(自然语言) |
Authorization | Web 认证信息 |
Expect | 期待服务器的特定行为 |
From | 用户的电子邮箱地址 |
Host | 请求资源所在服务器 |
If-Match | 比较实体标记(ETag) |
If-Modified-Since | 比较资源的更新时间 |
If-None-Match | 比较实体标记(与 If-Match 相反) |
If-Range | 资源未更新时发送实体 Byte 的范围请求 |
If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
Max-Forwards | 最大传输逐跳数 |
Proxy-Authorization | 代理服务器要求客户端的认证信息 |
Range | 实体的字节范围请求 |
Referer | 对请求中 URI 的原始获取方 |
TE | 传输编码的优先级 |
User-Agent | HTTP 客户端程序的信息 |
首部字段名 | 说明 |
---|---|
Accept-Ranges | 是否接受字节范围请求 |
Age | 推算资源创建经过时间 |
ETag | 资源的匹配信息 |
Location | 令客户端重定向至指定 URI |
Proxy-Authenticate | 代理服务器对客户端的认证信息 |
Retry-After | 对再次发起请求的时机要求 |
Server | HTTP 服务器的安装信息 |
Vary | 代理服务器缓存的管理信息 |
WWW-Authenticate | 服务器对客户端的认证信息 |
首部字段名 | 说明 |
---|---|
Allow | 资源可支持的 HTTP 方法 |
Content-Encoding | 实体主体适用的编码方式 |
Content-Language | 实体主体的自然语言 |
Content-Length | 实体主体的大小 |
Content-Location | 替代对应资源的 URI |
Content-MD5 | 实体主体的报文摘要 |
Content-Range | 实体主体的位置范围 |
Content-Type | 实体主体的媒体类型 |
Expires | 实体主体过期的日期时间 |
Last-Modified | 资源的最后修改日期时间 |
服务器返回的 响应报文 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
状态码 | 类别 | 原因短语 |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
200 OK
204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。
301 Moved Permanently :永久性重定向
302 Found :临时性重定向
303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
304 Not Modified :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
307 Temporary Redirect :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
400 Bad Request :请求报文中存在语法错误。
401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
403 Forbidden :请求被拒绝。
404 Not Found
500 Internal Server Error :服务器正在执行请求时发生错误。
503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
HTTP 协议是基于TCP/IP
的,从协议角度来说是和服务器端具有双向的通信能力。
但是使用 HTTP 协议的 WEB 服务器不能主动推送信息给客户浏览器。现在可以使用WebSocket
协议进行替代。或者使用SSE
协议让服务器端给客户端进行信息的推送。
原因是:HTTP
主要是给web 服务
使用的协议,对于 web 服务来说,并发的数量最为重要。如果采用 TCP 的有状态协议,那么用户只要打开浏览器,则与服务器的HTTP
连接就不会切断。但是服务器所能同时开启的连接数是有限制的。因此为了服务器的性能着想,基于HTTP
的服务器端并不能和客户端进行双向通信。
主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
其对应的 Client 端的 FIN_WAIT_2
状态。实际上 FIN_WAIT_2 状态下的 SOCKET,表示半连接,也即有一方要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传送给你 (ACK 信息),稍后再关闭连接。(主动方)。
MSL 即 Maximum Segment Lifetime,就是最大报文生存时间,是任何报文在网络上的存在的最长时间,超过这个时间报文将被丢弃。TCP 允许不同的实现可以设置不同的 MSL 值。(《TCP/IP 详解》中是这样描述的:MSL 是任何报文段被丢弃前在网络内的最长时间。RFC 793 中规定 MSL 为 2 分钟,实际应用中常用的是 30 秒、1 分钟、2 分钟等。)
GET | POST |
---|
与 POST 相比,GET 的安全性较差,因为所发送的数据是 URL 的一部分。
在发送密码或其他敏感信息时绝不要使用 GET !
POST 比 GET 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中。 可见性 数据在 URL 中对所有人都是可见的。 数据不会显示在 URL 中。因此,可以考虑将登陆等重要的信息保存在 session 中,其他相对不中要的数据可以存储在 cookie 中。
MySQL 主要有以下几种索引类型: 普通索引,唯一索引,主键索引,组合索引,全文索引。
创建索引的语句如下:
CREATE TABLE table_name[col_name data type]
[unique|fulltext][index|key][index_name](col_name[length])[asc|desc]
unique|fulltext
为可选参数,分别表示唯一索引、全文索引index
和 key
为同义词,两者作用相同,用来指定创建索引col_name
为需要创建索引的字段列,该列必须从数据表中该定义的多个列中选择index_name
指定索引的名称,为可选参数,如果不指定,默认 col_name
为索引值length
为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度asc
或 desc
指定升序或降序的索引值存储是最基本的索引,它没有任何限制。
CREATE INDEX index_name ON table(column(length))
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
CREATE UNIQUE INDEX indexName ON table(column(length))
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` char(255) NOT NULL ,
PRIMARY KEY (`id`)
);
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合:
ALTER TABLE `table` ADD INDEX name_city_age (name,city,age);
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext 索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 where 语句的参数匹配。fulltext 索引配合 match against 操作使用,而不是一般的 where 语句加 like。它可以在 create table,alter table ,create index 使用,不过目前只有 char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用 CREATE index 创建 fulltext 索引,要比先为一张表建立 fulltext 然后再将数据写入的速度快很多。
CREATE FULLTEXT INDEX index_content ON article(content)
a 1 and b=2 and c>3 and d=4
如果建立 (a,b,c,d) 顺序的索引,d 是用不到索引的,如果建立 (a,b,d,c) 的索引则都可以用到,a,b,d 的顺序可以任意调整。a = 1 and b = 2 and c = 3
建立 (a,b,c) 索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式count(distinct col)/count(*)
,表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就 是 0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条 记录from_unixtime(create_time) = ’2014-05-29’
就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本 太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);
注意:选择索引的最终目的是为了使查询的速度变快。上面给出的原则是最基本的准则,但不能拘泥于上面的准则。读者要在以后的学习和工作中进行不断的实践。根据应用的实际情况进行分析和判断,选择最合适的索引方式。
// 列 key_part1 均匀的分布在 1-100 之间。下面的 sql 则不会使用索引
// 或者数据库表数量极少
select * from table_name where key_part1 > 1 and key_part1 <90
如果查询条件中有 or ,则不会使用索引查询。如果使用 or,且想让索引生效。则每一列都需要加上索引。
// 如果在 key1 上有索引而在 key2 上没有索引,则该查询也不会走索引
select * from table_name where key1='a' or key2='b';
复合索引,如果索引列不是复合索引的第一部分,则不使用索引(即不符合最左前缀)
// 复合索引为 (key1,key2) ,以下 sql 将不会使用索引
select * from table_name where key2='b';
如果 like 是以‘%’开始的,则该列上的索引不会被使用。
select * from table_name where key1 like '%a';
存在索引列的数据类型隐形转换,则用不上索引,比如列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。
// 如果 key1 列保存的是字符串,即使 key1 上有索引,也不会被使用。
select * from table_name where key1=1;
数据唯一性差的时候不推荐使用索引(比如一个字段取值只有几种)。
比如性别,只有两种可能数据。意味着索引的二叉树级别少,多是平级。这样的二叉树查找无异于全表扫描。
频繁更新的字段不推荐使用索引。
比如 login_count 登录次数,频繁变化导致索引也频繁变化,增大数据库工作量,降低效率。
字段不在 where 语句出现的时候不推荐使用索引;如果 where 包含:IS NULL
, IS NOT NULL
, LIKE '%判断字符%'
等条件也不推荐使用索引。
只有在 where 语句出现,mysql 才会去使用索引
WHERE 子句中对索引列使用不等于(<>)
,使用索引效果也一般。
show_query_log=ON
来启动记录慢查日志;long_query_time
阈值,也会被记录下来,可以帮助提前发现由于数据库条数比较小,但是未使用索引的 SQL,可以提前对 SQL 进行优化,避免数据量增长之后出现 SQL 运行速度过缓的问题。为MySQL官方所带的慢查询日志分析工具,汇总除查询条件外,完全相同的SQL,并将分析结果按照参数中所指定的顺序进行输出。
使用示例如下:
mysqldumpslow -s r -t 10 slow-mysql.log
其中 -s order(c,t,l,r,at,al,ar)
参数表示慢查询日志的排序方式。
其中-t top
,表示指定取前 top 条作为结束输出。