ElasticSearch

ES介绍 ES是一个建立在Apache Lucene之上的分布式、Restful风格的搜索和数据分析引擎。 基本概念 集群(Cluster):由多台服务器组成,共同存储全部数据,对外提供统一入口。 节点(Node):集群中的一台服务器就是一个Node,一个Node对应一个ES实例。 主节点(Master):管理集群范围的操作,如创建、删除索引、分配分片。只做轻量级元数据管理。 数据节点(Data):存储数据,执行数据相关的CRUD、搜索、聚合。 协调节点(Coordinating):转发请求,合并结果。每个节点默认都是协调节点,但可专门分离出来。 预处理节点(Ingest):在写入前对文档做简单处理,如重命名字段、删除字段等。 索引(Index):一类文档的集合,类似关系型数据库里的数据库。 文档(Document):一条记录,用JSON表示,是最基本的信息单元。 字段(Field):记录里的key,比如:name、title。 分片(Shard):一个索引可以水平分割为多个分片,分布在不同的节点上。分片又分为主分片和从分片。 副本(Replica):每个分片可以有一个或多个副本,用于容灾和提高查询性能。 倒排索引 基本思想:从文档找词 转变为 从词找文档。倒排索引建立的是 词项 到 文档ID 的映射。 逻辑层面:ES发起搜索请求时,面对的是一个倒排索引。 物理层面:每个倒排索引实际存储在多个Segment中,每个Segment都是一个独立的、自包含的小型倒排索引,拥有自己的词典、文档列表和评分数据。 示例: 正排索引: Doc1 → [ "elasticserch", "is", "fast" ] Doc2 → [ "fast", "search", "with", "elasticserch" ] 倒排索引: "elasticserch" → { Doc1, Doc2 } "fast" → { Doc1, Doc2 } "search" → { Doc2 } "is" → { Doc1 } "with" → { Doc2 } 假设要搜索"fast"这个词项,倒排索引必须回答两个问题: ...

Java并发

原子性:一个或多个操作,要么全部执行完且中间不被任何干扰,要么一个都不执行。 可见性:一个线程对共享变量的修改,其他线程能立刻看到。 有序性:程序执行的顺序,按照代码的书写顺序来。 Java内存模型 JMM 主要解决可见性、有序性,基本保证原子性操作。 JMM是什么 Java内存模型(Java Memory Model),简称JMM,是一套抽象的规范。定义了多线程环境中共享变量的访问规则。 JMM将内存划分为主内存和工作内存。 主内存:所有线程共享,存放变量的正式值。 工作内存:每个线程私有,存放该线程用到的主内存变量的副本。 JMM解决什么问题 JMM解决可见性、有序性两个问题: 可见性问题:CPU多级缓存与缓冲区。 有序性问题:编译器和处理器为了性能会进行指令重排序。 解决可见性 建立"强制刷新/失效"协议。JMM规定,线程对变量的操作不能一直停留在工作内存里,必须在特定时刻同步到主内存。 volatile: 写操作:新值必须立即刷新到主内存。 读操作:每次读取前强制从主内存重新加载,并让其他线程的副本实效。 synchronized: 加锁后:必须清空工作内存中变量副本,强制从主内存重新加载。 解锁前:工作内存的修改必须全部刷新到主内存。 final: 只要构造期间没有让this引用逸出,构造完成后final字段的值对其他线程立刻可见,无需额外同步。 解决有序性 定义Happens-Before原则。规定哪些操作不可重排序。 保证基本的原子性 JMM只保证基本读写操作的原子性,除了long、double外,其他变量的单次读写操作都是原子的。复合操作必须通过锁或其他方式保证原子性。 JMM怎么实现 JMM的规范是抽象的, 需要靠JIT编译器插入内存屏障、处理器提供的硬件指令来落地。 内存屏障 JIT在编译字节码时,会在关键位置插入四种内存屏障指令。 屏障类型 作用 LoadLoad 禁止屏障前后的读操作重排 StoreStore 禁止屏障前后的写操作重排 LoadStore 禁止屏障前的读与屏障后的写重排 StoreLoad 禁止屏障前的写与屏障后的读重排(最重,同时具备其他三者效果) volatile: 写操作:写前插入 StoreStore,写后插入 StoreLoad。 读操作:读后插入 LoadLoad 和 LoadStore。 synchronized:使用字节码指令monitorenter 和 monitorexit触发内存屏障,保证临界区内的读写在锁释放后可见。 final:在构造方法末尾插入 StoreStore屏障,保证final字段赋值不会与对象引用赋值被重排序。 硬件指令 内存屏障主要通过CPU指令lock xxx实现。 强制将当前CPU缓存刷新到主内存。 通过MESI缓存一致性协议,将其他CPU缓存中对应数据失效。 阻止处理器对lock前后的指令进行重排序。 volatile volatile 最轻量的同步机制,通过写前 StoreStore + 写后 StoreLoad、读后 LoadLoad + LoadStore 的内存屏障策略,保证多线程环境下的可见性和有序性,但不保证原子性。 ...

Java集合

Java集合总览 Collection 单列集合 Collection 是单值存储的根接口,继承自Iterable接口,表示一组元素的集合。 List 特点:有序,可重复 ArrayList 数据结构:动态数组。 特点: 查询快:数组在内存中是连续的,可以通过索引下标计算出内存地址,所以查询快,时间复杂度为:O(1)。 写入慢: 在数组首部添加元素:需要移动其他元素,时间复杂度为:O(n)。 在数组尾部添加元素:直接加入到数组末尾,可能伴随扩容,时间复杂度均摊下来为:O(1)。 扩容机制:初始化时仅构造空数组,在第一次执行add操作时,将数组大小扩容为默认大小:10。当插入元素等于当前数组容量时,扩容为原有数组的1.5倍。 是否线程安全:否。 适用场景:适合查询多、写入少的场景。 LinkedList 数据结构:双向链表。 特点: 写入快:通过节点的前驱和后继指针关联节点,无需移动其他元素。时间复杂度:O(1)。 查询慢:需遍历链表逐个查询。 是否线程安全:否。 适用场景:适合操作元素多、查询少的场景。 Vector 数据结构:动态数组。 是否线程安全:是。对所有方法增加synchronized关键字,实现线程安全。 Set 特点:无序,不可重复。 HashSet 数据结构:基于HashMap实现,使用HashMap的key作为数据存储,value为不可变的Object对象。 是否线程安全:否。 LinkedHashSet 数据结构:继承于HashSet,使用HashSet中的特殊构造方法,基于LinkedHashMap实现。 是否线程安全:否。 TreeSet 数据结构:基于TreeMap实现,底层实现为红黑树。 特点:有序,唯一。 是否线程安全:否。 Queue 数据结构:队列。 特点:通常遵循FIFO(先进先出)原则,但也有支持按优先级排序或双端操作的变体。 是否线程安全:大部分非线程安全,但也有线程安全的队列,如:BlockingQueue。 Map 双列集合 双列集合的根接口,用于存储键值对。每个键最多映射到一个值,键不允许重复。(通过 equals 和 hashcode 判断)。,用于存储键值对。每个键最多映射到一个值,键不允许重复。(通过equals和hashcode判断)。 HashMap 数据结构: JDK1.7:数组+链表,时间复杂度:O(n)。 JDK1.8:数据+链表+红黑树,时间复杂度:O(logn)。 特点:存储键值对,允许一个null键,允许多个null值。 写入流程: 扩容判断:判断数组是否为空,为空则进行扩容。 哈希计算:通过hashcode+扰动函数: (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),计算插入元素key的哈希值,在通过hash&(n-1)确定桶位置。 写入数据: 桶内没有元素:创建新节点,插入键值对。 桶内有元素(哈希冲突):通过equals和hashcode方法判断桶内的首个节点是否与key相同。 相同:直接更新对应值。 不相同: 节点是树节点:在红黑树中插入键值对。 节点不是树节点:在链表插入键值对。 扩容判断:判断实际存储的键值对是否超过了当前容量,超过则扩容。 扩容机制:初始化时默认负载因子为:0.75,第一次执行put操作时,将容量扩充为默认大小:16。当插入元素超过 当前容量x负载因子 时,扩容为原有容量的2倍。 树化时机:参考泊松分布,当单个桶中链表的长度大于8,且数组容量大小大于64时,链表会转换为红黑树,查询时间复杂度由O(n)降至O(logn)。当红黑树节点数少于6时,退化为链表。 重新计算索引:扩容时会触发哈希重算,元素的位置为 原桶位置 或 原桶位置+原桶容量。因为HashMap容量值为2次幂,计算桶位置是通过 hash & oldCapacity。如果元素的hash值高位为1,则位置变化为原桶位置+原桶容量。如果元素的hash值高位为0,则位置不变化。 是否线程安全:否。 LinkedHashMap 数据结构:数组+双向链表+红黑树。基于HashMap实现,增加了双向链表。 是否线程安全:否。 TreeMap 数据结构:红黑树。 特点:有序。 是否线程安全:否。 ConcurrentHashMap 数据结构: JDK1.7:Segment数组 + HashEntry链表。 JDK1.8:Node数组+链表+红黑树。 特点:线程安全,高性能。 并发控制: JDK1.7:分段锁。将桶分为多个段(Segment),每个段是一个独立的可重入锁(ReentrantLock)。每个段包含一个HashEntry数组,HashEntry为链表结构。 JDK1.8:CAS+synchronized。 计算哈希值:对 key 进行哈希运算,以定位到数组中的相应桶(位置)。 空桶处理:若桶为空,通过CAS插入新Node节点。 非空桶处理:通过synchronized锁住第一个Node节点,表示有线程在操作这个桶。 插入数据: 存在相同key:直接更新对应值。 不存在相同key:在链表或红黑树中新建Node节点插入键值对。 释放第一个Node节点。 扩容机制: 多线程并发扩容:每个线程负责一段桶区间,迁移桶时锁住头节点,迁移过的桶标记为ForwardingNode节点。其他线程遇到该节点会协助或跳过。 无锁读取:遇到ForwardingNode节点则路由到新桶中。 ConcurrentHashMap数据结构 - JDK1.7: ...

JVM

JVM是什么 JVM是Java虚拟机,解决的核心问题是一次编写,到处运行。JVM通过在操作系统之上建立一个抽象的计算机,让字节码文件可以跨平台执行。 JVM组成结构 类加载器:加载字节码文件到内存,进行加载、链接、初始化操作,生成对应的Class对象。 运行时数据区:JVM的内存管理区域,包含共享区域(堆、方法区)、线程私有区域(虚拟机栈、本地方法栈、程序计数器)。 执行引擎:将字节码翻译为操作系统可以执行的机器指令,包含解释器、JIT编译器两种方式。 本地方法接口(JNI):一套标准接口,允许Java代码调用c/c++等语言实现的本地方法。 本地方法库:用c/c++等编写并编译好的动态链接库,提供Java无法直接完成的系统级操作,通过JNI被JVM加载和调用。 类加载机制 一个类的生命周期:加载 -》 验证 -》准备 -》解析》初始化 -》使用 -》卸载 双亲委派模型是类加载机制的核心,当一个类加载器收到加载请求时,自己不先尝试加载,而是逐级向上委派给父加载器,直到最顶层的Bootstrap ClassLoader。只有父加载器无法加载该类时,子加载器才尝试加载。 启动类加载器:Bootstrap ClassLoader。 扩展类加载器:Extension ClassLoader。 应用类加载器:Application ClassLoader。 为什么需要双亲委派模型? 安全:保证Java核心类库由启动类加载器加载,避免被篡改。 唯一:优先交给父加载器加载,避免重复加载。 如何打破双亲委派模型? 通过自定义类加载器,打破"父加载器无法直接加载子加载器可见类"。 实现方式:每个线程都有一个contextClassLoader,默认是应用类加载器。核心库的代码通过Thread.currentThread().getContextClassLoader()拿到线程上下文加载器,然后用它来加载子类,从而打破"父加载器无法直接加载子加载器可见类"。 类的生命周期: 加载:根据全限定名找到字节码,在堆中生成Class对象。 验证:检查字节码是否安全合规,防止恶意代码。 准备:为静态变量分配内存并赋予默认值。普通静态变量设置为0、null或false,编译器静态变量(static final修饰的基本类型或String类型)设置为代码中指定值。 解析:把符号引用转成能直接定位的内存引用,如类引用、方法引用等。 初始化:执行类的构造器方法,为静态变量赋值、执行静态代码块。 使用:程序通过Class对象创建实例或调用方法。 卸载:该类的Class对象被回收,方法区数据清除。 运行时数据区 运行时数据区是JVM的内存管理区域,规定了程序在运行时的数据该放在哪里、由谁共享、何时创建销毁等。 堆 线程安全:线程共享 JVM中最大的一块内存,几乎所有对象实例都在此分配。从GC视角,堆采用分代设计,将内存分为: 新生代:包含Eden区和两个Survivor区,比例为:8:1:1。绝大多数对象诞生在Eden区,熬过垃圾回收的对象会晋升到Survivor区,再从Survivor区晋升到老年代(每熬过一次Minor GC,则对象的GC年龄+1,达到默认值15时,晋升到老年代)。 TLAB:线程本地分配缓冲区,全称:Thread Local Allocation Buffer。TLAB是JVM为加速多线程下对象分配而设计的核心优化,在Eden区为每个线程划分一块独享空间,让线程在自己的独享空间内分配内存,与其他线程互不干扰。 老年代:存放长期存活对象,如缓存、数据库连接池等。 因为大部分对象都"朝生夕死",所以在不同生命周期的对象采用不同的垃圾回收算法。新生代大部分对象生命周期较短,适合标记复制算法。老年代对象生命周期相对较长,适合标记整理或标记清除算法。 对象分配流程: 线程创建对象,先检查TLAB剩余空间是否足够,足够则直接在TLAB内指针碰撞,完成分配。 若TLAB剩余空间不够: 申请新TLAB分配:申请一块更大的TLAB,在新TLAB分配。 在Eden区分配:对象大小适中,但新申请TLAB不划算,则在Eden区的公共区域通过同步操作分配。 在老年区分配:对象较大,则跳过Eden区,直接在老年代分配,避免在新生代频繁复制。 方法区 线程安全:线程共享 存储类的元数据、运行时常量池、静态变量、JIT编译后的代码缓存等。 类的元数据:存放类的全限定名、字段描述、方法描述等。 运行时常量池:存放字面量和符号引用。(动态链接就靠它) 静态变量:static修饰的变量。从JDK1.7起,字符串常量池和静态变量从原来的方法区移动到了堆中,但逻辑上还是方法区。 JIT编译后的代码缓存:热点方法被JIT编译称本地机器码后,缓存在方法区。 演变历史: JDK1.7:在堆内实现为永久代,容易OOM。 JDK1.8:改为使用本地内存的元空间,可以直接使用本地系统内存,仅受物理内存限制。同时,原来在永久代的字符串常量池和静态变量转移到了堆中。 虚拟机栈 线程安全:线程私有 ...

MySQL

MySQL是开源关系型数据库管理系统,使用客户端-服务端模型,支持可插拔存储引擎。 整体架构 整体分为两层:Server层 和 存储引擎层。 Server层: 连接器:负责与客户端建立TCP连接,验证身份、权限,维持会话状态等。 分析器:词法分析、语法分析,验证SQL合法性。 优化器:优化SQL、索引选择等,选择最优执行计划。 执行器:调用执行引擎读写接口进行数据交互。 存储引擎层: InnoDB:默认引擎,支持ACID、行锁、MVCC、外键等。 MyISAM:早期默认引擎,不支持事务、表级锁等。 InnoDB引擎 Buffer Pool:数据与索引的缓存池。 Log Buffer:redo log的缓存。 Buffer Pool Buffer Pool 是 InnoDB 最大的内存区域,以 页(Page,默认 16KB) 为单位缓存数据。所有对数据的读写操作,都会优先经过它。 它缓存什么? 数据页与索引页:无论是聚簇索引还是二级索引,它们的 B+Tree 节点都会被加载到这里。 Change Buffer:当修改非唯一二级索引,而目标页不在 Buffer Pool 时,更改会先写到这里,等页被读入时再合并,减少随机 I/O。它物理上就占用 Buffer Pool 空间。 自适应哈希索引 (AHI):InnoDB 自动对高频访问的 B+Tree 页构建哈希索引,加速等值查询。同样存在 Buffer Pool 中。 锁信息:行锁、表锁等内存数据结构也在此维护。 数据字典:表的元数据信息。 三大链表管理页的生命周期 Buffer Pool 内部通过三张链表,精密管理所有缓存页的状态: Free 链表:存储"空闲页",需要加载新页时,从这里取。 LRU 链表:存储"已被使用的页",并按最近最少使用排序。InnoDB 将 LRU 链表分为 Young 区(热数据) 和 Old 区(冷数据)。新读入的页不会直接插入 Young 区头部,而是插入 Old 区头部。只有在 Old 区存活足够时间并被再次访问时,才会晋升到 Young 区。这有效防止了全表扫描把真正的热数据冲走。 Flush 链表:存储"脏页"(内存中被修改过,但还没刷入磁盘的页),按第一次变脏的时间排序。后台线程会按此链表顺序将脏页写入磁盘,并更新 LSN(日志序列号)。 Log Buffer Log Buffer 是一块独立于 Buffer Pool 的内存区域,专门缓存redo log 条目。 ...