JVM类加载器

什么是类加载器

虚拟机用来实现让应用程序自己决定如何去获取所需要的类

类和类加载器

  • 对于任何一个类,都需要由加载他的类加载器和这个类本身一起确定其在java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间.也就是比较两个类时候”相等”,只有在这两个类由同一个类加载器加载的前提下,否则两个类来源于同一个class文件,被同一个虚拟机加载,只要这两个类的加载器不同,那这两个类必然不相等
  • 常见的方法有Class对象的equals(),isAssignableFrom(),isInstance()方法,也包括使用instanceof关键字的返回结果

从虚拟机的的角度讲,只存在两种不同的类加载器

  • 一种是启动类加载器(bootstrap classloader),这个类加载器使用C++实现,是虚拟机自身的一部分,无法直接被java程序引用
  • 一种就是所有其他的类加载器,这类加载器都有java实现,独立于虚拟机外部,并且全部集成java.lang.ClassLoader

类加载模型

双亲委派模型

  • 模型层级: 启动类加载器(bootstrap classloader) <- 拓展类加载器(Extension classloader):负责加载<JAVA_HOME>\lib\ext目录中或者被java.ext.dirs系统变量实现的所有类库 <- 应用程序类加载器(Application classloader):开发者可以直接使用的 <- 自定义类加载器
  • 双亲委派模型要求除了顶层的启动类加载器外,其余的类都应当有自己的父类加载器,这里类加载器之间父子关系一般不会用继承的方式,而都是使用组合关系来复用父加载器的代码
    工作过程:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是会把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求的时候(他的搜索范围没有找到所需要的类),子加载器才会尝试自己去加载
  • 参照ClassLoader中的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查请求的类是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException 说明父类无法完成加载请求
}

if (c == null) {
// 如果没有在父类找到该类
// 调用自身的findClass
long t1 = System.nanoTime();
c = findClass(name);

// 这是定义的类加载器;记录统计数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

破坏双亲委派模型

  • 在java中大部分类加载器都遵循双亲委派模型,但是有两种场景会破坏双亲委派:
  • 1.SPI对资源进行集中管理的时候,这时候采用了一种不太优雅的设计线程上线文类加载器(Thread Context ClassLoader),这个类加载器可以用过java.lang.Thread类setContextClassLoader方法进行设置,如果创建的线程还未设置,它将会从父线程集成一个,如果在应用程序全局范围内没有设置过的话,那么这个类加载器默认就是应用程序类加载器,这样就可以实现父类加载器去请求子类加载器,这块在之前dubbo spi中有做说明
    Dubbo SPI

  • 2.在OSGI中实现热部署,模块热部署这些情况下会出现更复杂的网状结构

  • 3.最后要注意的就是这两种类加载模型都不是强一致性的约束

JVM学习笔记(持续更新)

JVM:

  • 程序计数器:线程私有,切换字节码来进行不同命令的执行,每个线程独立执行
  • 虚拟机栈:存放编译期可知的基本数据类型和对象引用(对象指针),线程私有
  • 本地方法栈:虚拟机使用到的native方法的服务,和虚拟机栈类型.由虚拟机厂商自己实现
  • 堆:线程共享,管理对象,GC的场所(新生代,老生代)
  • 方法区:存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码(类方法),线程共享
  • 运行时常量池:方法区的一部分,对于常量池没有具体限制的规范,String则是一个很好的例子
  • 直接内存:不属于JVM,即机器的内存

对象内存分配:

  • 对象头:对象运行时的数据,hashcode,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳,类型指针(指向元数据的指针,并不是所有对象都有类型指针,如果是数组对象,还应该有存储数据长度的一块数据)
  • 实例数据:对象真正存储的有效数据,各种字段内容
  • 对齐填充:不是必然存储,hotspot的自动内存管理要求对象大小必须是8字节的整数倍,而对象头正好是8字节的整数倍,所有实例数据部分如果没有对齐时,就需要通过对齐来自动填充

GC算法:

  • 引用计数算法:给对象中添加一个引用计数器,当一个地方引用它时,计时器就加1,当引用失效时,计数器就减1,任何对象的计数器为0时就是不可能再被使用了,但是如果出现了互相引用,但是该对象已经没有被其他任何对象引用,就会导致计数器永远不为0,导致无法gc
  • 可达性分析算法:通过GC Root作为起点,当一个对象到GC Root没有任何引用链相关时,则证明该对象不可用,将被判断对象不可用

    GCRoot的对象包括:

  • jvm中引用的对象(int,long,boolean,float,double,byte,char,reference的数据)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象(final 的常量值)
  • 本地方法栈JNI的应用对象(即native方法)

    JAVA应用:

  • 强引用:Object obj = new Object()这类引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象
  • 软引用:用来描述一些还有用但并非必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,会将这些对象列入回收范围,并进行二次回收,如果这次回收还是没有足够的内存,才会出现内存溢出的异常,通过SoftReference实现
  • 弱引用:用来描述非必须对象,强度比软引用要更弱,被弱引用关联的对象只能存活到下一次垃圾回收之前,当垃圾回收器开始工作时,无论当前内存是否足够,都会被回收,通过WeakReference实现
  • 虚应用:最弱的一种引用关系,一个对象是否设置虚应用,完全不会影响其生存时间,也无法通过虚引用来取得一个对象的实例,设置虚引用唯一的目的就是能在这个对象被回收的时候拿到一个系统通知,通过PhantomReference实现

类加载时机:

加载->验证->准备->解析->初始化->使用->卸载

有且只有5种情况会必须进行初始化

  • 遇到new,getstatic,putstatic,或者invokestatic这4个字节指令时,如果类没有进行初始化,则需要先触发其初始化,生成这4条指令的最常见的java代码场景就是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰,已经在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
  • 使用反射包中的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化的时候,则需要先触发其父类的初始化(如果是接口,并不要求父接口完全都完成了实例化)
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  • 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个句柄的类没有进行初始化,则需要先进行初始化

mysql redo log

redo log 文件

什么是redo log

  • 记录了对MYSQL数据库innodb存储引擎,记录了innodb存储引擎的事务日志,用于恢复数据。每个innodb存储引擎至少有一个redo log文件组,每个文件组中至少有两个redo log文件,每个文件大小一致,并以循环写入的方式运行.并且redo log不是直接写到磁盘,是先写入redo log缓存,在刷盘到磁盘,当两个文件都写满的时候,会进行checkpoint,将文件1的内容抹除,如图

redo log 和 binlog的区别

这里可能大家会有疑问,binlog可以恢复,那么要redo log有什么意义呢

  • redo log 是innodb引擎特有的,binlog是mysql的server层实现的,所有引擎都可以使用
  • redo log 是物理日志,记录了在某个数据页的修改,binlog是逻辑日志,记录的是这条记录的修改逻辑
  • redo log 是循环写的,空间固定会用完,binlog是可以追加写入的,”追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志
  • redo log 的大小不宜设置的过大或者过小,过小的话会导致频繁的checkpoint导致性能抖动,太大则恢复会需要更长的时间

两阶段提交

  • 为了保证恢复数据时的状态准确性,维护逻辑一致性,采用两阶段提交,例如一次更新数据,如图
  • 根据binlog的特性,我们可以知道binlog是在事务提交前写入,如果事务还未提交的时候,这个时候binlog已经写入了,但是数据库发生了异常宕机,那么根据binlog来恢复,就会把未提交的事务给恢复到库中,造成脏数据,而采用两阶段提交,这个时候我们看到redo log处于prepare阶段,binlog已经写了日志
    则可以对未提交事务的脏数据做处理,防止恢复的时候出现脏数据
  • 如果redo log 里面的事务只有完整的prepare,则判断对应事务binlog是否完整存在,如果是,则提交事务,如果不是回归事务

redo log 的写入

  • 通过设置参数innodb_flush_log_at_trx_commit的值来控制将缓存中的redo log写入磁盘,0代表提交事务时,不将redo log写入磁盘,而是等每次主线程刷新,1和2的区别在于,1表示在commit的时候将redo log从缓存中同步到磁盘,即伴有fsync的调用,2表示将redo log异步写到磁盘,即写到文件系统的缓存中,因此不能完全保证执行commit的时候会一定写入磁盘,为了保证事务的ACID中的持久性,必须将此参数设置为1,也就是每当有事务提交时,必须保证日志写入磁盘
  • 这里要注意,如果有两个事务,如果事务A提交,事务B只写了redo log那么事务B的redo log也会被刷到磁盘上

mysql binlog

binlog 文件

什么是binlog

  • binlog 全称为 (binary log) ,记录了对MYSQL数据库执行的所有操作语句(不包括SELECT,SHOW这类操作,因为这类型的操作不会对数据本身做修改),但是如果操作本身没有对数据库进行修改,那么该操作也会被记录入数据库。

  • 如下表:test

id name
1 test
  • 执行如下sql,是不对test表做数据修改,但是也会记录该操作
1
update test set name = 1 where id = 2

binlog日志的作用

  • 恢复:数据恢复需要binlog
  • 复制:与恢复原理类似,主要用来做主从复制,master->slave
  • 审计:用户可以通过二进制日志中的信息来进行审计,判断是否又对数据库进行注入攻击

binlog日志的格式

从mysql5.1以后,开始引入binlog_format参数用于设置binlog的格式

  • statement:记录逻辑sql语句(如果使用 read commited事务隔离级别,会出现类似丢失更新的现象,导致主从复制出现问题,由于事务未提交且没有gaplock锁 binlog日志在缓冲中未写入binlog文件,写入文件顺序是事务提交的顺序.可能会导致同步是否顺序不一致)
  • row:不在记录简单的sql语句(能够解决statement在read committed事务隔离级别遇到问题,但是日志文件会变得更大)
  • mixed:mysql默认会采用statement格式,但是在一些特定情况下会使用row(相当于两者混合,自适应使用)

binlog日志的写入

  • 在默认情况下,binlog日志并不是每次写的时候就同步到磁盘的(类似于缓冲写,先写缓存,在同步到磁盘),但是如果在此时数据库发生宕机,再会给恢复和复制带来一些问题,有些数据可能没写入binlog,导致最后一部分数据丢失,这个时候sync_binlog参数就可以解决这个问题,sync_binlog这个参数,代表每次缓存写多少次日志同步到磁盘,如果参数设为1,则不写缓存每次操作直接同步到磁盘,但是这样会对系统IO产生一定影响,但是设置为1同时还有一种影响,就是一个事务还未提交前已经将该条日志记录到了磁盘,如果这时候事务还没提交数据库就宕机了,再回复的时候,未提交的事务并没有回滚,就会出现问题,在这种情况下可以通过设置,innodb_support_xa=1来解决
  • 这里可能有人会觉得开启binlog日志会对数据库性能有影响,但是在官方手册中标识,性能损耗只有1%,但考虑到可用性,数据的恢复和复制,这点性能的损耗还是可以接受的

Dubbo SPI 机制

Dubbo SPI 机制

什么是SPI

  • SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。
  • 具体是在JAR包的”src/META-INF/services/“目录下建立一个文件,文件名是接口的全限定名,文件的内容可以有多行,每行都是该接口对应的具体实现类的全限定名.

在哪些地方使用SPI

  • 不同厂商的JDBC驱动
  • Java的日志框架等等

JDK SPI示例

定义一个接口

1
2
3
public interface TestService {
void sayHello();
}

编写该接口的多个实现

  • 实现1
1
2
3
4
5
6
public class PresellService implements TestService {
@Override
public void sayHello() {
System.out.println("test presell");
}
}
  • 实现2
1
2
3
4
5
6
public class HelloService implements TestService {
@Override
public void sayHello() {
System.out.println("test hello");
}
}
  • 在META/services下面新建文件 名为接口完整路径 配置该接口的实现
1
2
com.presell.dubbo.spi.java.spi.impl.HelloService
com.presell.dubbo.spi.java.spi.impl.PresellService
  • 运行
1
2
3
4
5
6
7
8
9
10
public class App {
public static void main(String[] args) {
ServiceLoader<TestService> services = ServiceLoader.load(TestService.class);
//获取迭代器
Iterator<TestService> driversIterator = services.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {
driversIterator.next().sayHello();
}
}
  • 运行结果
1
2
test hello
test presell
  • 可以看到通过JDK内置SPI 可以将配置的两个实现类都执行了 此处可以根据自己的需求来进行配置 对于程序是无入侵的

JDK SPI的实现原理

  • 依赖ServiceLoader
  • 根据ServiceLoader 简单地画了一个执行图

JDK SPI的不足

  • 查找一个具体的实现需要遍历查找,耗时;
  • Collection相较于Map差的地方,map可以直接根据key来获取具体的实现,Dubbo SPI在此基础上做了优化
  • 每个类都要实现资源浪费

Dubbo SPI

  • JDK标准的SPI会一次性实例化所有实现,如果有的拓展不需要既浪费资源,又很耗时
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
  • 增加了对扩展点IoC和AOP的支持
  • 采用ConcurrentMap做缓存,/META-INF/dubbo/中配置文件以键值对形式存储,便于加载
1
2
3
dubbo=com.presell.dubbo.spi.dubbo.spi.TestDubbo
spring=com.presell.dubbo.spi.dubbo.spi.TestSpring
world=com.presell.dubbo.spi.dubbo.spi.TestWorld

Dubbo SPI的核心类

  • 要想对Dubbo SPI机制有个深入的了解 需要仔细阅读源码 这里列举几个核心的类
  • ExtensionLoader SPI核心加载器
  • SPI 用于标记是否是SPI接口
  • Adaptive 用于标记是否是SPI的自适应实现类/方法
  • Activate 用于标记是否是可以激活的实现类/方法 用于对过滤和分组
  • Holder类 对于要加载的类做一层装饰,保证线程安全

Dubbo SPI 执行流程

  • 由于ExtensionLoader中执行比较绕,这里根据ExtensionLoader的执行流程画了一个流程图供大家参考,具体信息还需要大家根据自己跟进源码,进一步深入理解


Mysql Insert Buffer

#Insert Buffer

insert buffer 的架构


什么是insert Buffer

  • 用来提升插入性能(先插入缓存,最后合并,避免非顺序插入造成磁盘离散查询,性能很差)
  • 表的主键是id,id列式自增长的,即当执行插入操作时,id列会自动增长,页中行记录按id顺序存放,不需要随机读取其它页的数据。因此,在这样的情况下(即聚集索引),插入操作效率很高。(不过对于UUID这类型的主键插入也可能是离散的,所以不推荐使用UUID做主键)
  • 除了主键聚合索引外,还产生了一个name列的辅助索引,对于该非聚集索引来说,叶子节点的插入不再有序,这时就需要离散访问非聚集索引页,插入性能变低。

什么情况下使用insert Buffer

  • 索引是辅助索引
  • 索引不是唯一的(如果索引是唯一的,那么插入时需要检验索引的唯一性,还是会直接进行离散IO查询.导致insert Buffer失效)

什么是change Buffer

  • 类似insert buffer,针对于更新和删除操作
  • 1.缓存中标记删除
  • 2.文件中真正删除

insert Buffer内部实现

insert buffer的数据结构是一棵B+树。在MySQL4.1之前的版本中每张表都有一棵insert buffer B+树。而在现在的版本中,全局只有一棵insert buffer B+树,负责对所有的表的辅助索引进行 insert buffer。这棵B+树存放在共享表空间中,默认也就是ibdata1中。因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致check table 失败。这是因为表的辅助索引中的数据可能还在insert buffer中,也就是共享表空间中。所以通过idb文件进行恢复后,还需要进行repair table 操作来重建表上所有的辅助索引。

insert buffer是一棵B+树,因此其也由叶子节点和非叶子节点组成。非叶子节点存放的是查询的search key(键值)。其构造包括三个字段:space | marker | offset|

search key一共占9字节,其中space占4字节,marker占1字节、offset占4字节。space表示待插入记录所在的表空间id,在InnoDB存储引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表。marker是用来兼容老版本的insert buffer。offset表示页所在的偏移量。

当一个辅助索引需要插入到页(space, offset)时,如果这个页不在缓冲池中,那么InnoDB存储引擎首先根据上述规则构造一个search key,接下来查询insert buffer这棵B+树,然后再将这条记录插入到insert buffer B+树的叶子节点中。

对于插入到insert buffer B+树叶子节点的记录,需要根据如下规则进行构造:space | marker | offset | metadata | secondary index record|

启用insert buffer索引后,辅助索引页(space、page_no)中的记录可能被插入到insert buffer B+树中,所以为了保证每次merge insert buffer页必须成功,还需要有一个特殊的页来标记每个辅助索引页(space、page_no)的可用空间。这个页的类型为insert buffer bitmap。

何时将缓存合并到文件中去

  • 辅助索引页被读取到缓冲池时;(索引页被查询的时候,相当于多次插入修改删除的合并)
  • insert buffer bitmap页追踪到该辅助索引页已无可用空间时(缓存池不够)
  • master thread(每10S或者每秒)

insert buffer带来的问题

  • 可能导致数据库宕机后实例恢复时间变长。如果应用程序执行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出现宕机,这时就有大量内存中的插入缓冲区数据没有合并至索引页中,导致实例恢复时间会很长。
  • 在写密集的情况下,插入缓冲会占用过多的缓冲池内存(innodb_buffer_pool),默认情况下最大可以占用1/2,这在实际应用中会带来一定的问题。

程序设计的一些思想

前言:作为一个java程序员,一直都没有写过关于java的分享,今天就在这里跟大家分享一下对于在java学习中的一些思想


面向对象

大家都知道JAVA是一门面向对象的语言(OOP),我们再程序设计中会把现实世界中的种种事物抽象成一个对象到程序中去,从而让我们能够更加方便快捷高效的开发出相应功能


DAO

DAO即数据访问对象(Data Access Object DAO)设计模式,我们最常见的就是JAVA系统操作数据库,因为JAVA系统是无法直接对数据库进行操作,所以就需要引入JDBC作为数据库系统操作的基础,JDBC也是大家最常见的DAO


异构系统的抽象

大家都知道,单纯的JAVA程序是无法对其它系统进行操作的,这里可以把不能直接操作的系统称之为异构系统,我们要想同这些异构系统进行操作,就需要引入相应的jar包,操作数据库就是JDBC,操作消息队列就是JMS,操作Redis就是Jedis,等等….这点大家应该都了解,但是不知道大家是否想过把这类jar做一个归类,其实他们都可以算是DAO,因为都属于同异构系统进行交互,而这些jar就是对相应系统在JAVA程序中的一种抽象,来在JAVA系统当中模拟出对于相应异构系统的一些操作

为什么要这么思考

在程序设计中,系统(接口)的隔离是必要的,以此保证业务(代码)的互相依赖,假如说一个程序中JAVA代码和SQL代码完全混合在一起,那么想必一旦程序出现问题,在进行修改,那真的可能改死人,那么如果说一个系统中有N种系统的集成和N中变成语言的集合,那么这个系统大概写好后就改不动了,所以为了保证系统的可维护性,可拓展性,系统隔离是必要的,其实这也是我们常说的业务的解耦对于系统的一种实现,如何在保证系统隔离的同时,又能更方便快捷的集成其它系统呢,那么就出现了DAO,作为程序员,现在市面上流行的框架,中间件很多很多,我们需要对相应的框架以及中间件做一个很清晰的层级划分,清楚的了解到那一部分框架属于整个架构中的哪个模块,才能让我们更快的更有效的更深入的掌握该技术,JAVA是一门面向对象的语言,如果能够清楚的理清我们要学习的东西属于整个系统的那个部分,我们就可以通过之前学习掌握的类似的部分,进行理解,就好比我之前会用JDBC,那么我了解到Redis是属于DAO层,那么我就可以用理解JDBC的形式来理解Jedis,首先要获取一个连接,然后进行…..操作,最后关闭连接,类似的例子还有很多,需要我们再学习中一点点发现

这些些对于程序设计的想法,其实对于所有面向对象语言都是一样的,希望能够大家带来一些帮助


数据库索引优化

前言:最近由于项目数据量巨增,对于数据库压力增大,需要从多方面对数据库进行调优,这里就从索引来给大家做下简单分享


什么是索引(Index)

这里引用Mysql的官方解释

1
2
>MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。使用索引的目的在于提高查询效率
>

Mysql常用索引

由于Mysql有多种存储引擎(MyISAM、InnoDB、MEMORY),这里只列出各种存储引擎的特点,不做展开

其索引模式为B-Tree和Hash

1
2
3
4
>B-Tree索引的时间复杂度是O(log(n))
>
>Hash索引的时间复杂度是O(1)
>

由此可见如果业务大部分是单条查询,使用Hash索引性能更好


索引创建的几个原则

(1)适合索引的列是出现在WHERE 子句中的列
最适合索引的列是出现在WHERE 子句中的列,或连接子句中指定的列,而不是出现在SELECT 关键字后的选择列表中的列。

(2)使用唯一索引
考虑某列中值的分布。对于惟一值的列,索引的效果最好,而具有多个重复值的列,其索引效果最差。

比如删除标识,各种数据状态等

(3)使用短索引
如果对串列进行索引,应该指定一个前缀长度,只要有可能就应该这样做。例如,如果有一个CHAR(200) 列,如果在前10 个或20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。对前10 个或20 个字符进行索引能够节省大量索引空间,也可能会使查询更快。较小的索引涉及的磁盘I/O 较少,较短的值比较起来更快。更为重要的是,对于较短的键值,索引高速缓存中的块能容纳更多的键值,因此,MySQL也可以在内存中容纳更多的值。这增加 了找到行而不用读取索引中较多块的可能性。

比如,1000-1999,这1000个值,第一位都是1,所以这时候可以建立短索引后三位

(4)利用最左前缀
在创建一个n 列的索引时,实际是创建了MySQL可利用的n 个索引。多列索引可起几个索引的作用,因为可利用索引中最左边的列集来匹配行。这样的列集称为最左前缀。

比如,对用户ID,部门ID做了索引,用户ID的搜索次数大于部门ID,那么用户ID索引应该在部门索引的左边,因为用户ID先被索引命中后,就不会在去部门ID的索引中继续搜索了(可参照B-Tree的数据结构来理解)

(5)不要过度索引
不要以为索引“越多越好”,什么东西都用索引是错的。每个额外的索引都要占用额外的磁盘空间,并降低写操作的性能,这一点我们前面已经介绍 过。在修改表的内容时,索引必须进行更新,有时可能需要重构,因此,索引越多,所花的时间越长。如果有一个索引很少利用或从不使用,那么会不必要地减缓表 的修改速度。此外,MySQL在生成一个执行计划时,要考虑各个索引,这也要费时间。创建多余的索引给查询优化带来了更多的工作。索引太多,也可能会使 MySQL选择不到所要使用的最好索引。只保持所需的索引有利于查询优化。如果想给已索引的表增加索引,应该考虑所要增加的索引是否是现有多列索引的最左 索引。如果是,则就不要费力去增加这个索引了,因为已经有了。

(6)考虑在列上进行的比较类型。
索引可用于“ <”、“ < = ”、“ = ”、“ > =”、“ > ”和BETWEEN 运算。在模式具有一个直接量前缀时,索引也用于LIKE 运算。如果只将某个列用于其他类型的运算时(如STRCMP( )),对其进行索引没有价值。

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


创建索引的时机

(1)什么时候创建索引
较频繁地作为查询条件的字段,也就是说最适合索引的列是出现在WHERE 子句中的列,或连接子句中指定的列,而不是出现在SELECT 关键字后的选择列表中的列。

(2)什么时候不创建索引

表记录太少
如果一个表只有5条记录,采用索引去访问记录的话,那首先需访问索引表,再通过索引表访问数据表,一般索引表与数据表不在同一个数据块,这种情况下ORACLE至少要往返读取数据块两次。而不用索引的情况下ORACLE会将所有的数据一次读出,处理速度显然会比用索引快。

唯一性太差的字段:如状态字段、类型字段。那些只存储固定几个值的字段,例如用户登录状态、消息的status等。

更新太频繁地字段不适合创建索引
当你为这个字段创建索引时候,当你再次更新这个字段数据时,数据库会自动更新他的索引,所以当这个字段更新太频繁地时候那么就是不断的更新索引。
如果一个字段同一个时间段内被更新多次,那么不能为他建立索引。


一些常见的SQL实践

(1)负向条件查询不能使用索引

1
select * from order where status!=0 and stauts!=1

not in/not exists都不是好习惯

可以优化为in查询:

1
select * from order where status in(2,3)

(2)前导模糊查询不能使用索引

1
select * from order where desc like '%XX'

而非前导模糊查询则可以:

1
select * from order where desc like 'XX%'

(3)数据区分度不大的字段不宜使用索引

1
select * from user where sex=1

原因:性别只有男,女,每次过滤掉的数据很少,不宜使用索引。

经验上,能过滤80%数据时就可以使用索引。对于订单状态,如果状态值很少,不宜使用索引,如果状态值很多,能够过滤大量数据,则应该建立索引。

(4)在属性上进行计算不能命中索引

1
select * from order where YEAR(date) < = '2017'

即使date上建立了索引,也会全表扫描,可优化为值计算:

1
select * from order where date < = CURDATE()

或者:

1
select * from order where date < = '2017-01-01'

(5)允许为null的列,查询有潜在大坑

单列索引不存null值,复合索引不存全为null的值,如果列允许为null,可能会得到“不符合预期”的结果集

1
select * from user where name != 'shenjian'

如果name允许为null,索引不存储null值,结果集中不会包含这些记录。所以,请使用not null约束以及默认值。

(6)复合索引最左前缀,并不是值SQL语句的where顺序要和复合索引一致

用户中心建立了(login_name, passwd)的复合索引

1
select * from user where login_name=? and passwd=?
1
select * from user where passwd=? and login_name=?

都能够命中索引

1
select * from user where login_name=?

也能命中索引,满足复合索引最左前缀

1
select * from user where passwd=?

不能命中索引,不满足复合索引最左前缀

(7)使用ENUM而不是字符串

ENUM保存的是TINYINT,别在枚举中搞一些“中国”“北京”“技术部”这样的字符串,字符串空间又大,效率又低。

(8)如果明确知道只有一条结果返回,limit 1能够提高效率

1
select * from user where login_name=?

可以优化为:

1
select * from user where login_name=? limit 1

原因:

你知道只有一条结果,但数据库并不知道,明确告诉它,让它主动停止游标移动

(9)把计算放到业务层而不是数据库层,除了节省数据的CPU,还有意想不到的查询缓存优化效果

1
select * from order where date < = CURDATE()

这不是一个好的SQL实践,应该优化为:

1
2
3
4
$curDate = date('Y-m-d');
$res = mysql_query(
'select * from order where date < = $curDate'
);

原因:

释放了数据库的CPU

多次调用,传入的SQL相同,才可以利用查询缓存


关于http和https的那点事

前言:本篇blog是本人的第一篇blog,主要谈一谈对于http协议和https的见解,以及在实际开发中踩过的一些坑


什么是HTTP协议

这里引用度娘的官方解释

1
2
3
4
5
6
7
8
9
10
11
12
>1.HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide 
> Web )服务器传输超文本到本地浏览器的传送协议。
>
>2.HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。
>
>3.HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,
>经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在
>进行之中,而且HTTP-NG(Next Generation of HTTP)的建议已经提出。
>
>4.HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。
>Web服务器根据接收到的请求后,向客户端发送响应信息
>

简单来说
其实就是客户端和服务端的通信协议
起到连接服务器和客服端的作用


什么是HTTPS协议

1
2
3
4
5
6
7
>1.HTTPS (Secure Hypertext Transfer Protocol)安全超文本传输协议,是一个安全通信通道,它基于HTTP开发用于
>在客户计算机和服务器之间交换信息。
>2.它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版,是使用TLS/SSL加密的HTTP协议。HTTP协议采用
>明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议TLS/SSL具有身份验证、信息加密和完整性校验的功能,可
>以避免此类问题发生。TLS/SSL全称安全传输层协议Transport Layer Security,是介于TCP和HTTP之间的一层安全议,
>不影响原有的TCP协议和HTTP协议,所以使用HTTPS基本上不需要对HTTP页面进行太多的改造
>

简单来说
就是在HTTP协议上加了一道锁,让HTTP变得更加安全


另外HTTP的请求方式以及HTTP请求和相应在这里就不做过多的展开
本篇在这里主要阐述实际应用中,两种请求方式的优缺点

先说一个问题吧,公司平台出现了一笔0.01元购买了1万元产品的订单,后来发现是因为我们的APP被抓包(通过一些第三方软件获取客服端和服务端之间请求内容,并可以对请求进行拦截并修改)了,然后修改订单金额支付金额,从而从中牟利,这个问题首先是服务端对于支付回调的校验没有做好,当然这不是今天讨论的内容,这里被拦截的请求是我们常用的http请求,如果这里我们使用https请求,即使请求被拦截,也是无法修改请求内容的(当然也是特别牛逼的黑客通过破解SSL加密等方式来进行修改,这里不做展开),所以如果通过服务端和通信层的双重校验,就能极大程度上避免请求被篡改带来的风险,目前苹果对于IOS应用已经做了必须全部使用HTTPS协议的限定,之前由于HTTPS证书到期,曾造成IOS应用的彻底瘫痪,所以升级为HTTPS是有必要的

当然,使用HTTPS也是需要付出一些代价的,HTTPS由于对请求的加密和解密性能是不及HTTP的,但是在HTTP/2.0协议中这个问题也得到了一些优化,相信在未来,性能问题也将得到很大改善,不过对于HTTPS协议的使用,可以根据不同应用场景来制定,涉及到支付,用户信息相关的内容,使用HTTPS,那是必要的

另外,对于HTTP安全性方面,还有一个问题就是可以在HTTP请求头中篡改IP信息,这个对于服务器安全来说也是一种挑战,这里一种方案是使用HTTPS,但是仅仅如此是不够的,在服务器中也应设置对虚拟IP的处理,不同服务器配置不一样,这里不做展开,之前博主就通过抓包加模拟IP完成过一些网站的刷票和刷赞等


结语:作为一个经常利用一些网络安全漏洞来做一些实验的博主,对于网络安全这一块也是很有感触的,在这里跟大家简单做下分享,希望能对大家能有帮助,第一篇博客,希望大家多多支持!如有疑问,也欢迎大家与我一起探讨