
理解原理,建立链接
关于optional类
optional提供了一些方法去处理空值,来帮助我们简化业务代码中空值的判断
java自动内存管理机制
由于jvm的存在,使得java的内存分配,相比c++要容易得多,
java的自动内存管理机制,内存管理,个人理解即对象的创建与回收,
在程序运行过程中,我们调用方法区的方法,将对象创建在java堆中,
对于线程,jvm中有专门的区域(java虚拟机栈,本地方法栈,程序计数器),这个区域随线程创建而生,
随线程结束而亡。
对于对象的创建,jvm有新生代和老年代之分,新生代的存储设有Eden和survial区,通过多次的GC(垃圾回收线程),新生代的年龄会逐渐升高,然后被转移到老年代区,
对于对象的回收,jvm中有垃圾回收器负责,这会涉及到一些垃圾回收算法,例如:标记-清除法,标记-整理法,复制法。
涉及到垃圾回收算法的实现,会有一些垃圾回收器,例如Serial,ParNew(Serial的多线程版本),Parallel Scavenge/Parallel Old收集器,还有CMS收集器等等。
Java IO流
关于输入和输出流,inputstream和outputstream,调用这两个类的时候,实际上是建立了一个链接,
然后再实现输入和输出
B树和B+树
B树和B+树非常适合做数据库的索引
B树的特征:
根节点至少有两个子节点。
每个中间节点都包含k-1个元素(也被称为关键字)和k个孩子,其中m/2 <= k <= m。
每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m。
所有的叶子节点都位于同一层。
每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
B+树是B树的升级,解决了B树进行范围查询效率低的问题
B+树特征:
有k个子树的中间节点包含k个元素(B树中是k-1个元素),每个元素不保存数据,所有数据都保存在叶子节点。
所有的叶子节点包含了全部元素,依照元素的大小升序排列,叶子节点之间用双向指针相连接。
所有中间节点的元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
红黑树
红黑树也是自平衡的二叉查找树,他的搜索插入删除的时间复杂度都为o(logn)
红黑树的出现是为了解决非平衡二搜索树的效率低的问题。
红黑树的5个性质:
1.节点是红色或黑色
2.根是黑色
3.叶子节点(外部节点,空节点)都是黑色,这里的叶子节点指的是最底层的空节点(外部节点),下图中的那些null节点才是叶子节点,null节点的父节点在红黑树里不将其看作叶子节点
4.红色节点的子节点都是黑色
4.1红色节点的父节点都是黑色
4.2从根节点到叶子节点的所有路径上不能有 2 个连续的红色节点
5.从任一节点到叶子节点的所有路径都包含相同数目的黑色节点
红黑树的问题可以等价为四阶B树。
红黑树和AVL树的比较:
1.在平衡性方面
AVL对平衡性的要求很严格,要求高度差不能超过1,是一种强平衡
红黑树通过自身的5条性质,与四阶B树等价,因为B树是矮树,这样红黑树就保证了这个树不会太高,
在平衡标准上,红黑树要求没有一条路径会大于其他路径的两倍,这个标准较为宽松,是弱平衡,
也是黑高度(每条路径黑节点数量相同)平衡
2.在性能方面
红黑树性能平均来看要优于AVL树,
红黑树的插入删除所要进行的旋转操作都是O(1)次,
AVL树会有极端情况,删除会最多需要O(logn)次调整。
关于如何选择:
搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树
相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树
红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树
关于分库分表
- 什么是分库分表
随着应用不断发展,数据量不断增大,单表单库查询效率持续低下而衍生出的技术方案 - 为什么分库
当数据量过大时,我们首先想到的解决方案是加容量,但是当同一时间的事务过多,就会造成单库的连接过多,导致后续事务无法访问数据库 - 为什么分表
数据量过大导致查询性能过低,无法通过优化mysql来解决,具体问题在innoDB的存储引擎的底层原理,通过分表来提高查询效率 - 什么时候分库分表
当sql调优等传统优化策略收效甚微 - 如何分库分表
垂直分库:根据业务类型对数据库进行分类
垂直分表:针对业务操作比较频繁的字段,将使用频率高的字段抽出单独建表
水平分库:把同一个表按一定规则拆分到不同数据库
水平分表:在同一个数据库内,将一个表按一定的规则分成多个表存储 - 确定数据存在哪个数据库的表
取模算法:类似hash,id字段%库数量 这样数据分布均匀,但对集群伸缩支持不友好,集群中有一台数据库宕机就会影响数据的存储与查询范围限定:按时间或ID区分,比如按月建表存储,或者按1~2000w条存一个,2000~4000w存一个
存在数据热点问题,例如双十一会造成十一月表数据倍增且访问频繁,数据库分担压力不均范围限定+取模:范围限定分库,取模分表,解决数据不均匀问题
地理位置分片:按地理位置建表
预定义算法:事先已经明确知道分库和分表的数量,可以直接将某类数据路由到指定库或表中,查询的时候亦是如此。
- 分库分表带来的问题
分页,排序,跨节点联合查询:就是分库分表之后,相比单表,我们将会多出一步合并操作,这个合并比较麻烦事务一致性:出现了跨库事务问题
主键唯一性缺失:有些表分布在不同数据库,这个时候就需要分布式id来作为主键,实现分布式管理
多数据库高效治理
历史数据迁移
- 分库分表架构模式
client模式:分库分表直接嵌在应用中,分库分表操作在本地执行,与数据库直连
proxy模式:通过代理服务器,分库分表和映射操作在代理服务器中执行 - 如何选择架构模式
如何选择 client 模式和 proxy 模式,我们可以从以下几个方面来简单做下比较。
1、性能
性能方面 client 模式表现的稍好一些,它是直接连接 MySQL 执行命令; proxy 代理服务则将整个执行链路延长了,应用 -> 代理服务 ->MySQL,可能导致性能有一些损耗,但两者差距并不是非常大。
2、复杂度
client 模式在开发使用通常引入一个 jar 可以; proxy 代理模式则需要搭建单独的服务,有一定的维护成本,既然是服务那么就要考虑高可用,毕竟应用的所有 SQL 都要通过它转发至 MySQL。
3、升级
client 模式分库分表一般是依赖基础架构团队的 Jar 包,一旦有版本升级或者 Bug 修改,所有应用到的项目都要跟着升级。小规模的团队服务少升级问题不大,如果是大公司服务规模大,且涉及到跨多部门,那么升级一次成本就比较高;
proxy 模式在升级方面优势很明显,发布新功能或者修复 Bug,只要重新部署代理服务集群即可,业务方是无感知的,但要保证发布过程中服务的可用性。
4、治理、监控
client 模式由于是内嵌在应用内,应用集群部署不太方便统一处理;proxy 模式在对 SQL 限流、读写权限控制、监控、告警等服务治理方面更优雅一些。
MVCC和三锁
- 邻键锁:行级锁(即锁定表的行而不是整个表)的一种,锁定的行数左开右闭
- 间隙锁:也是行级锁,锁定行数左开右开
- 记录锁:锁定一条记录,即一行
唯一索引等值查询:
1.当查询的记录是存在的,next-key lock 会退化成「记录锁」。
2.当查询的记录是不存在的,next-key lock 会退化成「间隙锁」。
非唯一索引等值查询:
1.当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
2.当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。
非唯一索引和主键索引的范围查询的加锁规则不同之处在于:
唯一索引在满足一些条件的时候,next-key lock 退化为间隙锁和记录锁。
非唯一索引范围查询,next-key lock 不会退化为间隙锁和记录锁。
mvcc:是一种在数据库系统中实现并发控制的技术,允许多个事务并发地读取,修改数据,而不发生数据冲突或不一致的问题,mvcc的核心思想是版本控制,这样不同的事务可以读取或修改不同的数据版本,从而避免了传统的锁机制所带来的性能瓶颈。
mvcc只在REPEATABLE READ(可重复读) 和 READ COMMITTED(已读提交)这俩种隔离级别下适用。
mvcc实现原理是通过 隐藏字段(创建时版本号、回滚指针、删除版本号)、undo log 、Read view来实现的。
undolog记录历史版本,Read view 记录一系列未提交的事务的id号,最小id和最大id,通过和这两个id的比较,来确认此版本是否可见
Mysql四种隔离级别和三种常见并发问题
READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
脏读:读到了还未提交的数据,这个未提交的数据后续可能会被修改导致之前读取数据错误
不可重复读:对于原来的一个数据行,一个事务在读取数据时另一个事务将这个数据行修改,导致后续读取像同行与之前读取的数据不一致
幻读:原来读到了一些数据行,一个事务在读取的同时其他事务对一些数据行进行了修改,导致后续再次读取看到了一些之前未见的数据。
Java缺点的思考
1.性能问题,相比C++,C这些在本地编译的,java性能的表现要较差
2.内存问题,由于Java的堆内存和垃圾回收机制,导致Java的内存占用会比较大
3.长时间运行会存在内存泄漏和垃圾回收效率低下的问题
关于java内存泄漏的一些了解
- 内存泄漏:一些对象不会被用到了,但是没有被及时回收,导致一些内存始终不能被使用
- 内存溢出(Out of memory):申请内存时,可用内存不够
- 二者的关系:内存泄漏——>内存溢出
- 导致内存泄漏的一些常见情况:
在长周期生命对象(静态集合类等)中引用了短周期生命对象,导致短周期的这个对象无法被释放单例对象持有对外部对象的引用,单例对象的生命周期和jvm一致
内部类持有外部类
各种连接,数据库连接
hash值改变
缓存泄漏
监控器和回调
关于java单例模式的一些了解
定义:保证一个类只有一个实例对象,并提供一个访问他的全局节点
双重检测锁实现单例模式
单例模式一般有饿汉模式(程序开始运行时就创建表)和懒汉模式(需要这个实例了再创建)
双重检查锁就是对懒汉模式的优化
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
这里有一个volatile关键字,这个关键字的作用是为了防止编译器的代码优化(通俗说法)
一般volatile用来声明变量的线程可见性,即带有volatile关键字的变量对所有线程可见
还有可以用来防止指令重排序,即上面提到的防止编译器的代码优化
JAVA序列化的理解
基础
Java序列化是一种机制,它可以将一个Java对象的状态转换为可存储或可传输的形式,这样,以后就可以在需要时重新创建这个对象。以下是我对Java序列化的理解:
1. **定义**:序列化是指将对象的状态信息转换为字节流的过程,而反序列化则是将字节流恢复为对象的过程。
2. **目的**:
– **持久化**:将对象的状态保存到文件、数据库或远程服务器上,以供后续使用。
– **网络传输**:在网络中传输对象时,需要将对象序列化成字节流,然后在接收端重新构造对象。
3. **实现方式**:
– 要使一个类可序列化,它必须实现`java.io.Serializable`接口。这个接口是一个标记接口,本身没有定义任何方法,但它指示Java虚拟机(JVM)这个类的对象可以被序列化。
– 如果一个对象的成员变量是基本数据类型或实现了`Serializable`接口,那么这个对象就可以被序列化。
4. **注意事项**:
– **静态变量**:序列化不会包含静态变量,因为静态变量属于类,而不是对象。
– **transient关键字**:如果一个对象的成员变量被声明为`transient`,那么在序列化时,这个变量会被忽略,不会序列化到字节流中。
– **版本号**:通过在类中定义`serialVersionUID`,可以控制序列化版本,确保不同版本的类在反序列化时能够正确处理。
5. **安全性**:序列化可能会引入安全问题,因为对象的状态包括敏感信息,如果序列化后的字节流被截获,可能会造成信息泄露。
6. **性能考虑**:序列化是一个耗时操作,对于频繁序列化的对象,需要考虑性能的影响。
7. **替代方案**:除了Java原生的序列化机制,还有其他如JSON、XML等更加灵活、可读性更好的序列化方式,它们通常用于Web服务和应用程序间的数据交换。
在面试中,还可能会被问到如何自定义序列化行为,或者序列化与反序列化过程中可能遇到的问题及解决方案。理解这些概念对于Java后端开发者来说是非常重要的。
拓展
1.如何自定义序列化行为
在Java中,可以通过以下几种方式实现自定义序列化行为:
1. **重写`writeObject`和`readObject`方法**:
在实现`Serializable`接口的类中,可以添加`private void writeObject(ObjectOutputStream out) throws IOException`和`private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException`方法。这两个方法分别会在序列化和反序列化时被调用,从而允许开发者自定义对象的序列化过程。
示例代码:
“`java
import java.io.*;
public class CustomSerializableObject implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private transient String password; // transient关键字表示不序列化该字段
// 省略构造函数、getter和setter方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认序列化非transient字段
// 自定义序列化行为,例如加密密码
out.writeObject(encryptPassword(password));
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认反序列化非transient字段
// 自定义反序列化行为,例如解密密码
password = decryptPassword((String) in.readObject());
}
// 加密和解密方法(示例)
private String encryptPassword(String password) {
// 实现加密逻辑
return password; // 返回加密后的密码
}
private String decryptPassword(String encryptedPassword) {
// 实现解密逻辑
return encryptedPassword; // 返回解密后的密码
}
}
“`
2. **使用`Externalizable`接口**:
`Externalizable`是`Serializable`接口的一个子接口,它提供了完全自定义序列化行为的能力。实现`Externalizable`接口的类必须实现`writeExternal`和`readExternal`方法。
示例代码:
“`java
import java.io.*;
public class ExternalizableObject implements Externalizable {
private int id;
private String name;
// 省略构造函数、getter和setter方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 自定义序列化行为
out.writeInt(id);
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 自定义反序列化行为
id = in.readInt();
name = (String) in.readObject();
}
}
“`
使用自定义序列化行为时,需要注意以下几点:
– `writeObject`和`readObject`方法必须是私有的,否则序列化机制不会调用它们。
– 如果类实现了`Externalizable`接口,那么在反序列化时,即使有默认构造函数,也必须有一个可访问的空构造函数,因为反序列化时会通过反射调用空构造函数来创建对象实例。
– 自定义序列化行为可以提高安全性,比如对敏感数据进行加密处理,但在进行自定义序列化时,也要确保维护序列化版本的一致性,以避免在不同版本间的兼容性问题。
2.java序列化的安全风险
序列化的安全风险详解
敏感数据泄露:当对象被序列化时,其所有字段(包括私有字段)都会被转换为字节流。如果这些字段包含敏感信息,如密码、个人身份信息等,这些信息可能会在序列化过程中被无意中暴露。一旦序列化数据被未授权访问,敏感信息就可能被泄露。
远程代码执行:序列化数据可以被恶意篡改,当这些被篡改的数据被反序列化时,可能会执行非预期的代码。这种攻击通常利用了Java的反射机制,攻击者可以通过构造特定的序列化数据来触发任意代码的执行。
拒绝服务攻击:恶意构造的序列化数据可能导致应用程序在反序列化时消耗大量资源,如CPU、内存等,从而导致服务不可用。这种攻击不需要执行代码,仅通过消耗资源就能达到攻击目的。
解决方案与最佳实践详解
避免序列化敏感数据:对于包含敏感信息的类,应避免实现Serializable接口。如果必须序列化,确保敏感数据字段被标记为transient,这样它们就不会被包含在序列化数据中。此外,可以考虑使用加密技术来保护序列化数据。
使用安全的序列化库:除了Java原生的序列化机制,还有其他更安全的序列化库,如Google的Protocol Buffers和Apache的Avro。这些库通常提供更严格的类型检查和更小的攻击面,从而减少安全风险。
输入验证:在接受外部输入进行反序列化之前,进行严格的输入验证。这包括检查数据格式、类型和长度等。确保只接受预期的数据格式和类型,避免接受未知或不受信任的数据源。
白名单反序列化:实现一个白名单机制,只允许反序列化特定的类。这可以通过自定义ObjectInputStream的子类并重写resolveClass方法来实现。在resolveClass方法中,可以检查反序列化的类是否在白名单中,如果不是,则抛出异常。
使用序列化过滤器:Java 9引入了序列化过滤器,允许在序列化和反序列化之前对数据进行检查和修改。这可以用来阻止序列化或反序列化不安全的类。例如,可以设置过滤器来阻止序列化包含敏感信息的类。
定期更新和打补丁:确保使用的Java版本是最新的,并定期检查和应用安全补丁。Java社区经常发布安全更新,以修复已知的安全漏洞。
示例:使用序列化过滤器
Java 9及更高版本提供了序列化过滤器功能,可以在序列化过程中对数据进行过滤。以下是如何使用序列化过滤器来增强安全性的示例:
// 设置序列化过滤器
SerializationFilter filter = new SerializationFilter() {
@Override
public boolean filter(Object object) {
if (object instanceof SensitiveClass) {
return false; // 阻止序列化SensitiveClass
}
return true; // 允许其他对象
}
};
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“data.ser”))) {
oos.setObjectInputFilter(filter);
// 序列化对象
} catch (IOException e) {
e.printStackTrace();
}
结论
Java序列化是一个强大的工具,但如果不加以适当的安全措施,可能会导致严重的安全问题。通过避免序列化敏感数据、使用安全的序列化库、实施输入验证和使用序列化过滤器等措施,可以显著降低安全风险。开发者应始终保持警惕,确保序列化过程的安全性,以保护应用程序免受潜在的安全威胁。
JAVA反射
反射在java中的作用就是让java可以动态的创建对象,调用对象的方法
反射是计算机编程中的一个重要概念,它指的是在运行时动态地获取对象的状态信息并对其进行操作的能力。在面向对象编程中,反射通常用于在程序运行时动态地创建对象、调用方法和修改属性等操作。
在Java中的定义
- 给定一个类对象(Class对象),通过反射获取这个对象(Class对象)的所有成员结构;
- 给定一个具体的对象,能够动态的调用它的方法及对任意属性值进行获取和赋值
- 这种动态获取类的内容、创建对象、以及动态调用对象方法及操作属性的机制,就叫做Java的反射机制。
反射优点
反射的优点主要包括灵活性、动态性和可扩展性。通过反射,程序可以在运行时动态地加载和使用不同的类和方法,而不需要在编译期进行硬编码。这使得程序更加灵活,可以根据不同的需求和场景动态地调整行为。此外,反射还可以用于实现插件架构和模块化设计,使得程序更加易于扩展和维护。
对于java而言
1.增加程序灵活性,逻辑可以不用写死
2.代码简洁,可读性强,提高代码复用率
反射缺点
首先,反射操作可能会影响程序的性能,因为它们需要在运行时动态地解析类和方法等信息。其次,反射操作可能会破坏封装性,使得程序在运行时可以访问和修改对象的私有属性和方法。此外,反射操作可能会引入安全风险,例如代码注入攻击等。
对于java而言
1.相较直接调用在创建对象比较多的情 景下反射性能下降
2.内部暴露和安全隐患(破坏单例)
拓展(反射破坏单例模式和预防)
1.如何破坏
反射是 Java 提供的一种功能强大的机制,允许在运行时动态访问类的私有成员、构造函数和方法。尽管单例模式通过将构造函数私有化来防止外部创建对象(这是单例模式的核心),反射仍能绕过这一限制。通过调用私有构造函数,反射可以实例化新的对象,破坏单例模式的唯一性。
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
try {
// 获取单例实例
Singleton instance1 = Singleton.getInstance();
// 通过反射获取私有构造方法
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 设置可访问性
// 通过反射创建新的实例
Singleton instance2 = constructor.newInstance();
// 比较两个实例是否相同
System.out.println("instance1 == instance2: " + (instance1 == instance2));
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.如何预防
为了防止反射破坏单例模式,我们可以在构造方法中添加逻辑判断,确保即使通过反射调用,实例也不能被多次创建。具体做法是在构造方法中检查实例是否已经存在,如果存在则抛出异常。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 防止反射创建新实例
if (INSTANCE != null) {
throw new RuntimeException("单例模式禁止反射创建实例!");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
}
反射性能慢的原因
1.寻找类Class字节码的过程,比如通过ClassName找到对应的字节码Class,然后加载、解析,会比较慢,而new的方式则无需寻找,因为在Linking的解析阶段已经将符号引用转为了直接引用
2.安全管理机制的权限验证等
3.若需要调用native方法调用时JNI接口的使用
4.入参校验
反射获取class对象的组成

反射获取class对象的方法
- 通过ClassLoader对象的loadClass()方法
- 类名.class
- Class.forName()
- object.getClass()
/**
* 获取Class文件的方式
* 1、类名.class
* 2、Class.forName //反射
* 3、对象.getClass()
* 4、通过类加载器.loadClass()
*/
public class CoreMain {
public static void main(String[] args) throws Exception{
//1、 不会堆类初始化
Class clazz01 = Person.class;
System.out.println(clazz01);
//2、MySQL
Class<?> clazz02 = Class.forName("com.javacore.reflection.pojo.Person");
System.out.println(clazz02);
//3、已知实例对象
Class<? extends Person> clazz03 = new Person().getClass();
System.out.println(clazz03);
//4、通过类加载器
Class<?> clazz04 = CoreMain.class.getClassLoader().loadClass("com.javacore.reflection.pojo.Person");
System.out.println(clazz04);
//通过Clazz获取基本信息;属性信息;方法信息;注解信息
/**
* 获取的是类的修饰符
*/
System.out.println("--------------");
// 类中存在的修饰符,返回的是一个整数,可以通过toString方法展示出来是哪个修饰符
int modifiers = clazz01.getModifiers();
System.out.println(Modifier.toString(modifiers);
System.out.println(clazz01.getPackage());
System.out.println(clazz01.getName());
System.out.println(clazz01.getSimpleName());
System.out.println(clazz01.getClassLoader());
System.out.println(clazz01.getInterfaces());
System.out.println(clazz01.getSuperclass());
System.out.println(clazz01.getAnnotations());
/**
* 属性基本操作
*/
System.out.println("--------------");
Class clazz = User.class;
User user = clazz.newInstance(); // 反射调用的无参的构造方法
Field[] fields = clazz.getFields(); //所有public字段、包括继承来的
for(Field field:fields){
System.out.println(field.getName());
}
System.out.println("--------------");
Field[] declaredFields = clazz.getDeclaredFields();//获取当前类中定义的
for(Field field:declaredFields){
System.out.println(field.getName());
}
System.out.println("--------------");
Field addField = clazz.getDeclaredField("address");
addField.setAccessible(true); //设置字段的强制访问,代表反射的不安全,private的都能访问
addField.set(user,"北京西三旗");
System.out.println(user.getAddress());
System.out.println("--------------");
Field nationalty = clazz.getDeclaredField("nationalty");
nationalty.set(null,"中国");
System.out.println(user.nationalty);
System.out.println("--------------");
// 构造函数
clazz.getDeclaredConstructors();
Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class, String.class);
declaredConstructor.setAccessible(true);
User user1 = declaredConstructor.newInstance("idCard", "address");
User user2 = clazz.newInstance(); //通过无参的构造函数反射出来的,无参的构造函数需要是public
System.out.println(user1);
System.out.println("--------------");
SingleDemo instance1 = SingleDemo.getInstance();
SingleDemo instance2 = SingleDemo.getInstance();
System.out.println(instance1==instance2);
Constructor<? extends SingleDemo> constructor = instance1.getClass().getDeclaredConstructor();
constructor.setAccessible(true);
SingleDemo instance3 = constructor.newInstance();
System.out.println(instance1==instance3);//false
// System.out.println("--------------");
// ClassPathXmlApplicationContext con = new ClassPathXmlApplicationContext("spring-ioc.xml");
// Object user2 = con.getBean("user");
// System.out.println(user2);
/**
* class.newInstance();底层使用的是反射出的无参的构造器,需要可见
* Constructor.newInstance()’;任何的构造器都能够构造出一个实例,private也可以
*/
}
}
代理问题
代理
什么是代理
代理也被叫做网络代理,是一种比较特殊的网络服务,允许一个终端(通常指客户端)通过这个服务与另一个终端(通常指服务器端)进行非直接的连接。例如:一些网关、路由器等网络设备都具备网络代理的功能。代理服务有利于保障网络终端的隐私或者安全,可以在一定程度上阻止网络攻击(因为通过代理,可以隐藏真正的服务器端/客户端)
代理服务器

代理过程

代理协议
主要有socks和http
socks和http代理的区别
socks代理:
- 工作原理: SOCKS 代理是一种更底层的代理,它只处理网络连接,并不解析 HTTP 请求。它就像一个“隧道”,将客户端的网络请求转发到目标服务器。
- 支持协议: SOCKS 代理主要支持 TCP 和 UDP 协议,可以用于各种网络应用,例如 HTTP、HTTPS、FTP、SSH 等。
- 透明性: SOCKS 代理对客户端和目标服务器是透明的,这意味着目标服务器不知道客户端的真实 IP 地址。
- 安全性: SOCKS 代理本身不提供任何加密或身份验证,因此安全性依赖于底层网络连接的安全性。
http代理:
- 工作原理: HTTP 代理专门处理 HTTP 请求,它会解析 HTTP 请求并根据需要修改请求头和请求内容,然后将请求转发到目标服务器。
- 支持协议: HTTP 代理只支持 HTTP 协议,只能用于处理 HTTP 请求。
- 非透明性: HTTP 代理对目标服务器是不透明的,目标服务器可以获取客户端的真实 IP 地址。
- 安全性: HTTP 代理通常提供一些安全功能,例如身份验证、加密等,以提高安全性。
区别总结

二者代理过程
1.HTTP代理:
解析出的地址和端口: 通常是代理服务器的 IP 地址和端口号。
连接方式: OkHttp 使用 HTTP 协议向代理服务器发送请求,请求中包含目标服务器的地址和端口号。代理服务器收到请求后,会将请求转发给目标服务器,并接收目标服务器的响应,再将响应转发给客户端。
举例:
假设您要访问网站 www.example.com,使用 http://proxy.example.com:8080 作为 HTTP 代理服务器。
Proxy.Type.HTTP: OkHttp 会解析出 proxy.example.com 和 8080。
连接流程:
OkHttp 向 proxy.example.com:8080 发送一个 HTTP 请求,请求中包含目标服务器 www.example.com 的地址和端口号。
代理服务器收到请求后,会将请求转发给 www.example.com。
www.example.com 返回响应给代理服务器。
代理服务器将响应转发给 OkHttp。
OkHttp 收到响应并处理。
2. SOCKS 代理
协议: SOCKS 协议 (通常是 SOCKS4 或 SOCKS5)
解析出的地址和端口: 通常是代理服务器的 IP 地址和端口号。
连接方式: OkHttp 使用 SOCKS 协议与代理服务器建立连接,并将目标服务器的地址和端口号传递给代理服务器。代理服务器负责将连接转发到目标服务器,并进行数据转发。
举例:
假设您要访问网站 www.example.com,使用 socks://proxy.example.com:1080 作为 SOCKS5 代理服务器。
Proxy.Type.SOCKS: OkHttp 会解析出 proxy.example.com 和 1080。
连接流程:
OkHttp 使用 SOCKS5 协议与 proxy.example.com:1080 建立连接。
OkHttp 将 www.example.com 的地址和端口号传递给代理服务器。
代理服务器负责将连接转发到 www.example.com。
OkHttp 与 www.example.com 进行数据通信,所有数据都会通过代理服务器转发。
选择哪种代理:
如果需要代理各种网络应用,例如 HTTP、HTTPS、FTP、SSH 等,可以选择 SOCKS 代理。
如果只处理 HTTP 请求,并且需要一些安全功能,可以选择 HTTP 代理。
正向代理
正向代理,代理服务器在客户端

正向代理过程

正向代理适用场景
- 访问被禁止的资源(让客户端访问原本不能访问的服务器。可能是由于路由的原因,或者策略配置的原因,客户端不能直接访问某些服务器。为了访问这些服务器,可通过代理服务器来访问)
突破网络审查 - 再比如客户端IP被服务器封禁,可以绕过IP封禁
也可以突破网站的区域限制
隐藏客户端的地址(对于被请求的服务器而言,代理服务器代表了客户端,所以在服务器或者网络拓扑上,看不到原始客户端) - 进行客户访问控制
可以集中部署策略,控制客户端的访问行为(访问认证等)
记录用户访问记录(上网行为管理)
内部资源的控制(公司、教育网等) - 加速访问资源
使用缓冲特性减少网络使用率(代理服务器设置一个较大的缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度。) - 过滤内容(可以通过代理服务器统一过滤一些危险的指令/统一加密一些内容、防御代理服务器两端的一些攻击性行为)
反向代理

反向代理适用场景
- 负载均衡
如果服务器集群中有负荷较高者,反向代理通过URL重写,根据连线请求从负荷较低者获取与所需相同的资源或备援。可以有效降低服务器压力,增加服务器稳定性 - 提升服务器安全性
可以对客户端隐藏服务器的IP地址
也可以作为应用层防火墙,为网站提供对基于Web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等 - 加密/SSL加速:将SSL加密工作交由配备了SSL硬件加速器的反向代理来完成提供
- 缓存服务,加速客户端访问
- 对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务
- 数据统一压缩
- 节约带宽
- 为网络带宽不好的网络提供服务
- 统一的访问权限控制
- 统一的访问控制
- 突破互联网的封锁
- 为在私有网络下(如局域网)的服务器集群提供NAT穿透及外网发布服务
- 上传下载减速控制
正向代理与反向代理的异同
- 正向代理在客户端,反向代理在服务端
- 代理服务器均承担收发请求和响应的功能
关于java的面向对象
面向对象三大特征:封装,继承,多态
封装
封装是套壳屏蔽细节,对外只提供接口和对象进行交互
java的封装靠一些访问限制符号,如:public,private,protected,default
还有java的导包过程,这也是封装思想的体现
继承
基于一个类构造新类,继承已存在的类就是复用这些类的方法和属性。在此基础上还可以添加一些新的方法和属性以满足新的需求。通过继承,可以达到对重复代码复用,减少代码量。
继承主要解决了共性抽取,实现代码复用
多态
多态通俗来讲,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
在Java中要实现多态,就必须在继承体系下,且子类必须对父类中的方法进行重写,并且可以通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
重写和重载的区别
1、重写的参数列表一定不能修改,而重载的参数列表必须修改
2、重写的返回类型不能修改,除非可以构成父子类关系,重载的返回类型可以修改
3、重写的访问权限不能低于父类的访问权限,而重载可以修改访问限定符
4、重载是一个类的多态性表现,而重写是子类与父类的一种多态性表现
静态绑定(Static Binding):也称为前期绑定(早绑定),在编译时就确定要调用的方法。当我们使用对象调用一个方法时,编译器会根据引用变量的类型来确定要调用的方法,而不是根据实际运行时对象的类型。这种绑定发生在编译阶段。静态绑定适用于静态方法和私有方法等无法被继承和覆盖的方法。典型代表方法重载。
动态绑定(Dynamic Binding):也称为后期绑定(晚绑定),在运行时根据实际对象的类型来确定要调用的方法。当我们使用父类引用指向子类对象,并通过该引用调用一个被子类重写的方法时,会根据实际运行时对象的类型来决定要调用的方法,而不是根据引用变量的类型。这种绑定发生在运行阶段,因此也被称为晚期绑定。动态绑定实现了多态的特性,允许在运行时根据对象的实际类型选择具体的方法实现。
向上转型和向下转型
向上转型,JAVA中的一种调用方式。向上转型是对父类的对象的方法的扩充,即父类的对象可访问子类从父类中继承来的和子类重写父类的方法。向上转型后,子类不能使用原来子类中特有的字段,和方法
语法格式:父类类型 对象名 = new 子类对象
public class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "正在吃饭");
}
}
public class Dog extends Animal{
public Dog(String name,int age){
super(name,age);
}
@Override
//重写父类的eat()方法
public void eat() {
System.out.println(name + "正在吃狗粮");
}
public void lookDoor(){
System.out.println(name + "在看门");
}
}
public class Test {
public static void main(String[] args) {
Animal dog = new Dog("旺财",2);//直接传参,使用子类对象赋值给父类对象
dog.eat();
dog.lookDoor()//编译报错。不能调用子类特有的方法。
}
}
向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("旺财",2);
//向上转型
Animal animal1 = dog;
if(animal1 instanceof Dog){//判断是否可以安全转换
dog = (Dog)animal1;
dog.lookDoor();
}
}
}
多态的优缺点
- 优点
- 可以降低代码的圈复杂度避免使用大量的if-else.提高代码的可读性。
- 增强了代码的可扩展性,降低代码的修改成本
- 缺点
降低代码运行效率
,
Redis Lua脚本理解
Lua 本身是一种轻量小巧的脚本语言,在Redis2.6版本开始引入了对Lua脚本的支持。通过在服务器中嵌入Lua环境,Redis客户端就可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。在Redis中Lua有两种执行方式:Eval和EvalSHA。
Eval
通过Redis内置的 Lua 解释器,可以使用 EVAL 命令(也可以使用redis-cli 的–eval 参数)对 Lua 脚本进行解析。需要注意的点是执行Lua也会使Redis阻塞。

eval执行步骤
- 根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数,Lua函数的名称实际上是f_为前缀加上脚本本身计算出来的SHA1值组成,如f_ddfsdfjgjbg33rndgj00,SHA1的长度是40字符,而函数体则是脚本本身。
- 将客户端给定的脚本保存到lua_scripts字典,简单来说就是添加一个key-value,key就是Lua脚本的SHA1校验和,而值是Lua脚本本身,这主要是用于以后使用。
- 执行第一步在Lua环境中定义的函数,以此来执行客户端中给定的Lua脚本。
样例
eval 脚本内容 key个数 key列表 参数列表 eval "return 'hello lua'" 0 127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world 执行返回:"hello world"
Evalsha
个人理解,初次执行用Eval,之后可以使用sha1码用Evalsha执行
EvalSHA 中方式的是拆分成两个步骤,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验码。然后使用EvalSHA命令使用SHA1作为参数可以直接执行对应Lua脚本。这样做的好处是可以避免每次发送Lua脚本的开销,而脚本也会常驻在服务端,脚本功能得到了复用。缺点是要怎么管理这些脚本和命令过多的话会占用Redis的内存。

样例
evalsha sha1值 key个数 key列表 参数列表 eval "return 'hello lua'" 0 // 执行完eval后,Lua环境就定义了一个函数名为:f_dfaujjgdgu388vdf83803(),那么就可以根据对应的sha1值来调用函数了。 evlsha "dfaujjgdgu388vdf83803" 0
Redis对于lua脚本的管理
1.script flush
script flush命令用于清除服务器中的所有和Lua脚本相关的信息,这个命令会释放并重建lua_scripts字典,关闭现有的Lua环境并重建一个新的Lua环境。
2.script exists
script exists命令会根据输入的SHA1校验和,检查校验和对应的脚本是否存在于服务器中。该命令主要是通过检查给定的SHA1是否存在于lua_scripts字典来实现的。
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa 1) (integer) 1
3.script load
127.0.0.1:6379> script load "return 'hello lua'" "5ea77eda7a16440abe244e6a88fd9df204ecd5aa"
4.script kill
如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或 者外部进行干预将其结束,就可以使用script kill来杀掉正在执行的 Lua 脚本。
## 写一个死循环的lua脚本并在redis客户端执行 127.0.0.1:6379> eval 'while 1==1 do end' 0 ## 重新起一个客户端去执行命令,返回了报错信息,此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待。 ## 但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选择script kill,当script kill执行之后,客户端调用会恢复 127.0.0.1:6379> get test (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. ## 另起一个客户端,停止脚本执行 127.0.0.1:6379> script kill OK 127.0.0.1:6379> get k1 "11"
此外,Redis提供了一个lua-time-limit参数,用于配置Lua脚本执行的超时时间,当 Lua 脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,并不会停止掉服务端和客户端的脚本执行,所以当某个Lua脚本执行达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个 busy 的脚本。
实际上,如果Redis服务端设置了lua-time-limit参数,那么在每次执行Lua脚本之前,服务器都会在Lua环境里面设置一个超时的处理钩子(hook)。超时处理钩子再脚本运行期间,会定期检查脚本已经运行了多长时间,一旦钩子发现脚本的运行时间已经超过了lua-time-limit设定的时长,那么钩子将定期在脚本运行的间隙中,查看是否有script kill或者shutdown nosave到达服务器。如果超时运行的脚本未执行过任何的写入操作,那么客户端可以通过script kill命令停止脚本的运行,并向执行该脚本的客户端发送一个错误回复。处理完script kill命令后服务器可以继续运行。
此外,如果超时脚本已经执行过写入操作,那么客户端只能用shutdown nosave命令来停止服务器,从而防止不合法的数据被写入。
Lua在Redis中原子执行性的原理
在Redis中,Lua脚本能够保证原子性的主要原因还是Redis采用了单线程执行模型。也就是说,当Redis执行Lua脚本时,Redis会把Lua脚本作为一个整体并把它当作一个任务加入到一个队列中,然后单线程按照队列的顺序依次执行这些任务,在执行过程中Lua脚本是不会被其他命令或请求打断,因此可以保证每个任务的执行都是原子性的。
个人理解,将lua脚本当作一个线程来处理
Redis单线程的理解
redis的线程模型
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所以每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
Redis 内部使用文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。也就是说Redis 的单线程指的是执行 Redis 命令的核心模块是单线程的,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令等。Redis 采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分: 多个 socket、IO 多路复用程序、文件事件分派器、事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

客户端 socket01 向 Redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
假设此时客户端发送了一个 set key value 请求,此时 Redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
为什么redis是单线程,效率还能这么高
- 纯内存访问:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础。
- 非阻塞I/O:Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了事件,不在I/O上浪费过多的时间。
- 单线程避免了线程切换和竞态产生的消耗。
redis单线程的好处
并发和并行通常被认为是不同的概念,有什么区别?
1、并发性I/O流,意味着能够让一个计算单元来处理来自多个客户端的流请求。
2、并行性,意味着服务器能够同时执行几个事情(具有多个计算单元),这是不同的。
例如,酒吧能够服务几个顾客,同时他只能一次准备一个饮料。所以他可以提供没有并行性的并发服务。
Redis虽然是单线程程序,但可以通过使用I / O多路复用同一个线程和事件循环在I / O级别上提供并发性。像Redis这样的高效存储引擎的瓶颈通常是网络,在CPU之上。因此,Redis原子性(隔离事件循环)在没有额外成本的情况下提供并发性(不需要进程、线程同步),无需支付同步开销。并行性有一个代价:现在硬件设备都是多核,当然多核速度肯定比单核效率高,但进程线程之间的同步是非常昂贵的。好比,人家redis只要一台服务器可以搞定的事情,你干嘛一定要让我使用多台服务器。它轻巧,可作为构建高效可扩展的服务器,何必纠结一定要装波音747的发动机(对应的设计比较复杂),redis设计有它自己架构的好处!官网也说了,要发挥多核CPU性能,可以通过在单机开多个Redis core实例来完善,一样实现分布式
Redis6之后引入多线程
传统阻塞I/O模型

伪异步I/O模型

Reactor设计模式

单线程时代
Redis 是基于 Reactor 单线程模式来实现的。
IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器。对于后续的操作,和在 reactor 单线程实现方案里看到的一样,整个过程都在一个线程里完成,因此 Redis 被称为是单线程的操作。

多线程模型
单线程在网络IO操作上有一定的瓶颈
在 Redis 中,单线程的性能瓶颈主要在网络IO操作上。也就是在读写网络 read/write 系统调用执行期间会占用大部分 CPU 时间。如果要对一些大的键值对进行删除操作的话,在短时间内是删不完的,那么对于单线程来说就会阻塞后边的操作。
回想下上边讲得 Reactor 模式中单线程的处理方式。针对非连接事件,Reactor 会调用对应的 handler 完成 read->业务处理->write处理流程,也就是说这一步会造成性能上的瓶颈。
Redis 在设计上采用将网络数据读写和协议解析通过多线程的方式来处理,对于命令执行来说,仍然使用单线程操作。
文件事件处理器(file event handler)
Redis 基于 Reactor 模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
总结
Reactor模式
传统阻塞IO模型客户端与服务端线程1:1分配,不利于进行扩展。
伪异步IO模型采用线程池方式,但是底层仍然使用同步阻塞方式,限制了最大连接数。
Reactor 通过 I/O复用程序监控客户端请求事件,通过任务分派器进行分发。
单线程时代
基于 Reactor 单线程模式实现,通过IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器进行处理。
多线程时代
单线程性能瓶颈主要在网络IO上。
Redis 的多线程部分只是用来处理网络数据的读写和协议解析,对于命令执行来说,仍然使用单线程操作。之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。
Mysql的索引失效
原因
因为sql优化器的存在,优化器是基于开销进行优化,不考虑语义,不考虑规则,按开销最小的来,所以当使用索引的开销大于全表查询时,数据库就不会使用索引查询了
索引失效情况
- 没有满足最佳左前缀法则,例如索引(a,b,c),只有查询(a),(a,b),(a,b,c)会走索引,而(b),(b,c),(c)都不会走索引。
- 没有建立全值匹配,导致索引效率低
- 你的主键插入顺序不是自增的
- 在判断语句中使用了计算或者函数
- 在判断语句中使用了类型转换
- 范围条件右边的列索引失效,所以范围匹配要尽量放到最后(模糊查询like不是范围索引)
- 没有覆盖索引,“!=”导致索引失效
- 没覆盖索引,is not null,not like导致索引失效
- 没覆盖索引,左模糊索引导致索引失效
- OR前后存在非索引列,导致索引失效
- 不同字符集导致索引失效,建议utf8mb4
isnull可以使用索引,is not null不能使用索引
在进行索引扫描时,MySQL 会优先利用索引中已经存在的值进行查询,在查询时直接跳过为 NULL 的那些行。但是,如果使用了 IS NOT NULL 条件,那么 MySQL 无法在索引中找到 NULL 值,也就是说,MySQL 无法像前面那样跳过那些为 NULL 的行,只能扫描整张表来找到符合条件的行,因此无法使用索引。
Mysql隐式转换
MySQL中的隐式转换(Implicit Conversion)指的是在SQL语句的执行过程中,数据库管理系统(DBMS)自动进行的数据类型转换。这种转换通常发生在数据类型不匹配但需要进行比较、计算或赋值等操作时。
常见的场景
- 字符串与数字
- 日期类型和其他类型
- NULL值处理
- 整数和小数
- 二进制和字符类型
查看隐式转换
使用explain语句
EXPLAIN SELECT * FROM your_table WHERE some_column = some_value;
隐式转换是有可能导致索引失效的
MySql的索引
聚簇和非聚簇索引
聚簇索引,他的叶子节点存储了每一列的信息,也就是说包含了一个完整的记录行

非聚簇索引
它的叶子节点只包含一个主键值,通过非聚簇索引查找记录要先找到主键,然后通过主键再到聚簇索引中找到对应的记录行,这个过程被称为回表。

覆盖索引
非聚簇索引中因为不含有完整的数据信息,查找完整的数据记录需要回表,所以一次查询操作实际上要做两次索引查询。而如果所有的索引查询都要经过两次才能查到,那么肯定会引起效率下降,毕竟能少查一次就少查一次。
不用回表,需要查询的列都涉及并存储在索引里了
联合索引

联合索引会涉及到最左前缀匹配原则,还有一个索引下推,这里解释一下索引下推
新版本的 MySql(5.6以上)中引入了索引下推的机制:可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
例如针对上面表中的(name、age)做联合索引,正常情况下的查询逻辑:
通过 name 找到对应的主键 ID;
根据 id 记录的列匹配 age 条件。
这种做法会导致很多不必要的回表,例如表中存在(张三、10)和(张三、15)两条记录,此刻要查询(张三、20)的记录。查询时先通过张三定位到所有符合条件的主键 ID,然后在聚簇索引中遍历满足条件的行,看是否有符合 age = 20 的记录。实际情况是没有满足条件的记录的,这个回表过程也相当于是在做无用之功。
索引下推的主要功能就是改善这一点,在联合索引中,先通过姓名和年龄过滤掉不用回表的记录,然后再回表查询索引,减少回表次数。
唯一索引
唯一索引是一种不允许具有相同索引值的索引,系统在创建该索引时检查是否有重复的键值,每次对更新或增加记录时都会检查这一点。主键索引就是唯一索引。
事实上,MySql 的唯一限制和主键限制都是通过索引实现的。
聚簇索引和唯一索引的区别
-
存储方式: 聚簇索引决定了数据的物理存储顺序,而唯一索引不影响数据的物理存储顺序。
-
唯一性: 唯一索引强制列值唯一,而聚簇索引不强制唯一性(虽然通常主键是唯一的,并且主键通常用作聚簇索引)。
-
数量: 一个表只能有一个聚簇索引,但可以有多个唯一索引。
-
性能影响: 聚簇索引通常对范围查询和排序操作有利,而唯一索引对所有查询类型都有性能提升,特别是那些需要查找唯一值的查询。
-
用途: 聚簇索引用于优化数据的物理存储,而唯一索引用于数据完整性和查询优化。
什么情况下唯一索引是聚簇索引
聚簇索引是什么,就是你这个索引的数据的存储顺序和实际的数据在数据库中的存储顺序一致时,你的这个索引就可以叫做聚簇索引,所以你可以创建一个唯一索引,然后以这个唯一索引的数据存储顺序存储数据,你就可以说这个唯一索引是聚簇索引
