Java基础知识
集合
ArrayList保证线程安全的方法
在Java中,ArrayList本身并不是线程安全的,如果多个线程同时修改同一个ArrayList实例,可以会导致数据不一致或者抛出异常,我们可以通过下面的方法来解决这个问题
ArrayList线程不安全产生的原因
1 | |
上面是ArrayList中add方法的源码,下面是不安全的原因
- 非原子操作:
elementData[size++] = e是添加操作,这里会拆分成elementData[size] = e; size++;这两个步骤,这两个步骤是不具备原子性的,所以在高并发场景下会产生线程安全问题 - 数组越界异常:在多线程环境中,如果两个线程同时调用 add() 方法,可能会导致数组越界异常。例如,当两个线程同时尝试在数组的最后一个位置添加元素时,可能会导致一个线程覆盖另一个线程的操作,从而抛出
ArrayIndexOutOfBoundsException - 并发修改异常:在迭代 ArrayList 时,如果另一个线程修改了列表的结构(例如添加或删除元素),会抛出
ConcurrentModificationException。这是因为 ArrayList 的迭代器在检测到列表结构被修改时会抛出此异常
解决方案
使用
Collections.synchronizedList方法: 你可以使用Collections.synchronizedList方法将一个普通的ArrayList包装成一个线程安全的列表1
2List<String> normalList = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(normalList);使用
CopyOnWriteArrayList类:CopyOnWriteArrayList是Java提供的一个线程安全的列表实现。它在每次写操作时都会创建一个新的数组副本,因此读操作不会受到写操作的影响1
List<String> threadSafeList = new CopyOnWriteArrayList<>();- 特点:
- 线程安全:由于每次修改都会复制数组,因此
CopyOnWriteArrayList是线程安全的,不需要额外的同步措施。 - 迭代器不变性:
CopyOnWriteArrayList提供的迭代器不会抛出ConcurrentModificationException。即使在迭代过程中修改了列表,迭代器也会继续迭代修改前的数组 - 写操作成本高:由于写操作需要复制整个数组,所以
CopyOnWriteArrayList在写操作频繁的场景下性能较差 - 内存占用:由于每次写操作都会创建数组的副本,因此内存占用可能会较高
- 读操作高效:对于读多写少的场景,
CopyOnWriteArrayList非常适用,因为它允许多个线程高效地并发读取 - 弱一致性迭代器:
CopyOnWriteArrayList提供的迭代器是弱一致的,这意味着它们提供了对元素的不同步的视图,反映的是迭代器创建时集合的状态 - 使用场景:适用于那些遍历操作远远多于修改操作的场景
- 线程安全:由于每次修改都会复制数组,因此
- 特点:
ArrayList扩容机制
Arraylist订单底层储存结构
ArrayList 的底层存储结构是一个动态数组。具体来说,它是一个 Object 类型的数组,用于存储列表中的元素
- 动态数组:
ArrayList使用一个动态数组来存储元素。当数组容量不足时,会自动扩容,通常扩容为原来容量的 1.5 倍。 - 初始容量:默认情况下,
ArrayList的初始容量为 10。如果在创建时指定了初始容量,则会使用指定的容量。 - 元素存储:元素存储在一个名为
elementData的数组中。这个数组是transient的,这意味着在序列化时不会被序列化。 - 大小管理:
ArrayList通过一个名为size的变量来跟踪当前存储的元素数量
如果是用无参构造方法来初始化ArrayList,则不会立即分配内存,只有在第一次添加元素时才会分配一个默认容量为10的数组
如果是用有参构造函数指定初始容量,那么会立即分配相应大小的内存空间
扩容机制
- 初始容量:当使用无参构造方法创建
ArrayList时,初始容量为 10。如果使用带初始容量的构造方法,则初始容量为指定的大小。 - 添加元素:当向
ArrayList添加元素时,如果当前容量不足以容纳新元素,ArrayList会进行扩容操作。 - 扩容策略:默认情况下,
ArrayList的扩容策略是将当前容量扩大为原来的 1.5 倍。例如,如果当前容量为 10,当需要扩容时,新容量将变为 15。 - 复制元素:扩容时,
ArrayList会创建一个新的数组,并将原数组中的元素复制到新的数组中。然后,新的数组将替换原来的数组。 - 调整容量:扩容完成后,
ArrayList的容量将变为新的容量,可以容纳更多的元素
HashMap、TreeMap、LinkedHashMap的区别
相同点:
- 都是属于Map,主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复
- 都不是线程安全的
不同点:
| HashMap | TreeMap | LinkedHashMap | |
|---|---|---|---|
| 存储顺序 | 不保证顺序 | 按自然顺序或自定义顺序 | 按插入顺序或访问顺序 |
| 数据结构 | 基于哈希表、链表、红黑树(1.8开始) | 基于红黑树 | 基于哈希表和双向链表 |
| 按key排序 | 不支持,按照hashCode进行输出 | 支持,默认按key升序排序。可用Comparator自定义排序,用Iterator 遍历TreeMap时,结果是排过序的 | 不支持 |
| 性能 | 查找和插入操作的时间复杂度为 O(1) | 查找和插入操作的时间复杂度为 O(log n) | 查找和插入操作的时间复杂度为 O(1) |
| null | 允许一个 null 键和多个 null 值 | 不允许 null 键,但允许多个 null 值 | 允许一个 null 键和多个 null 值 |
| 使用场景 | 需要快速查找、插入和删除操作,不关心元素顺序 | 需要按键的自然顺序或自定义顺序遍历键值对 | 需要维护插入顺序或访问顺序 |
JDK 7与JDK 8的HashMap的区别
| JDK 7 | JDK 8 | |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表/红黑树 |
| 插入顺序 | 头插法 | 尾插法 |
| 哈希算法 | 复杂的异或和位运算 | 简化的异或和位运算 |
| 扩容机制 | 元素个数 > 容量 * 加载因子 且插入位置有元素存在 | 元素个数 > 容量 * 加载因子 |
| 扩容倍数 | 2 倍 | 2 倍 |
| 扩容后的容量 | 2 的幂次方 | 2 的幂次方 |
HashMap的容量必须是2的幂次方,这样设计有下面这些优点
- 哈希函数的优化:当容量是 2 的幂次方时,哈希值的低位和高位都能参与到索引计算中。具体来说,
HashMap使用(n - 1) & hash计算索引,其中n是哈希表的容量。这样可以确保哈希值的所有位都能影响最终的索引位置,从而减少哈希冲突 - 减少哈希冲突:由于哈希值的所有位都能参与到索引计算中,键值对在哈希表中的分布会更加均匀,减少了多个键映射到同一个桶(bucket)的概率,从而减少了哈希冲突
- 提高性能:减少哈希冲突意味着链表或红黑树的长度会更短,从而提高查找、插入和删除操作的性能。举个例子,如果容量是 16(2 的 4 次方),哈希值的低 4 位和高 4 位都会参与到索引计算中。如果容量不是 2 的幂次方,例如 15,那么哈希值的某些位可能不会参与到索引计算中,导致哈希冲突增加
如果在创建 HashMap 时指定的初始容量不是 2 的幂次方,HashMap 会自动将其调整为大于或等于指定值的最小的 2 的幂次方
HashMap的底层原理
相关概念
- 数组和链表:HashMap 底层使用一个数组来存储元素,数组中的每个位置称为一个“桶”(bucket)。当插入元素时,HashMap 会根据键(Key)的哈希值来确定元素应该存放在哪个桶中
- 哈希函数:HashMap 使用键对象的
hashCode()方法来计算哈希值,然后通过哈希值确定元素在数组中的位置。如果两个键的哈希值相同,它们将映射到同一个桶中 - 冲突解决:当多个键映射到同一个桶时,会发生冲突。HashMap 通过链表来解决冲突。每个桶内部实际上是一个链表,所有映射到同一个桶的键值对都会以链表的形式存储
- 动态扩容:当 HashMap 中的元素数量达到一定阈值时(通常是数组长度的75%),HashMap 会进行扩容,通常是数组长度的两倍。扩容过程中,所有现有的键值对会被重新分配到新的桶中,这涉及到重新计算哈希值
- 负载因子:负载因子是一个衡量 HashMap 性能的参数,它定义了在进行扩容之前,HashMap 可以存储的元素数量占数组容量的比例。默认的负载因子是 0.75,这意味着当 HashMap 中的元素数量达到数组容量的75%时,就会触发扩容
- 树化:在 JDK 1.8 以后的版本中,如果链表中的元素数量超过了一定阈值(TREEIFY_THRESHOLD,默认为8),链表会转换成红黑树,以提高搜索效率
- null 键和null值:HashMap 允许一个 null 键和多个 null 值
- 线程不安全:HashMap 不是线程安全的。如果多个线程并发访问和修改 HashMap,而没有适当的同步措施,可能会导致数据不一致
HashMap的扩容原理
| JDK 1.7 | JDK 1.8 | |
|---|---|---|
| 扩容条件 | 当前存储的数量大于等于阈值 | 当前存储的数量大于等于阈值 |
| 扩容时机 | 先扩容,再添加(头插法) | 先插入,再判断是否需要扩容(尾插法) |
| 扩容倍数 | 2 倍 | 2 倍 |
| 扩容后的容量 | 2 的幂次方 | 2 的幂次方 |
| 扩容过程 | 重新计算每个元素的哈希值并放入新数组中 | 重新计算每个元素的哈希值并放入新数组中 |
| 链表转红黑树 | 不适用 | 链表长度大于或等于 8 且数组长度大于或等于 64 时,链表转换为红黑树 |
| 多线程问题 | 头插法可能导致环形链表,多线程环境下不安全 | 尾插法避免了环形链表,但多线程环境下仍然不安全,可能会有数据覆盖问题 |
在 JDK 1.8 及以后的版本中,HashMap 的扩容机制是当存储的键值对数量达到或超过阈值(容量 * 加载因子)时触发的。扩容时,HashMap 会创建一个新的数组,其大小是原数组大小的两倍(即 2 的幂次方),并将原数组中的所有元素重新计算哈希值并放入新数组中。此外,当某个桶中的链表长度达到或超过 8 且数组长度大于或等于 64 时,链表会转换为红黑树,以提高查找和插入操作的效率
HashMap保证线程安全的方法
线程不安全的原因
- 数据竞争:在多线程环境下,如果多个线程同时对
HashMap进行读写操作,可能会导致数据竞争。数据竞争会导致数据不一致,甚至可能导致程序崩溃 - 扩容过程中的问题:在
HashMap扩容时,会重新分配所有现有的键值对到新的桶中。如果多个线程同时触发扩容操作,可能会导致数据丢失或覆盖。在 JDK 1.7 中,扩容时使用头插法,可能会导致链表反转,进而形成环形链表,导致死循环 - 哈希冲突处理:当多个线程同时插入键值对并发生哈希冲突时,可能会导致链表或红黑树结构的破坏,进而导致数据不一致
- 非原子操作:HashMap
的许多操作(如put、get、remove` 等)都不是原子操作。在多线程环境下,这些操作可能会被打断,导致数据不一致
| HashTable | Map<String, Object> map = new Hashtable<>(); | synchronized修饰get/put方法。 方法级阻塞,只能同时一个线程操作get或put | 性能很差 |
|---|---|---|---|
| Collections.synchronizedMap | Map<String, Object> map = Collections.synchronizedMap( new HashMap<String, Object>()); | 所有方法都使用synchronized修饰。 | 性能很差, 和HashTable差不多 |
| JUC中的ConcurrentHashMap | Map<String, Object> map = new ConcurrentHashMap<>(); | 每次只给一个桶(数组项)加锁。 | 很好 |
ConcurrentHashMap的原理
Java 1.8及以后的版本中,ConcurrentHashMap 的实现原理发生了显著变化。以下是主要的改进和特点:
数据结构:
- Java 1.8中的
ConcurrentHashMap使用了一个桶(bucket)数组和链表/红黑树来存储数据,去除了段(Segment)的概念。 - 当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高搜索效率。
- Java 1.8中的
并发控制:
CAS操作:
ConcurrentHashMap广泛使用CAS(Compare-and-Swap)操作来确保并发修改的安全性。synchronized同步块:在某些情况下,仍然需要使用synchronized同步块来保护关键代码段,如树化操作和扩容。
哈希计算与定位:
- Java 1.8中的哈希计算过程更加复杂和精细,以减少哈希冲突和提高空间利用率。
- 当发生哈希冲突时,新的键值对会添加到链表或红黑树的末尾。
扩容与重哈希:
- 当
ConcurrentHashMap中的元素数量超过数组的容量阈值时,会触发扩容操作。 - 扩容过程中,创建一个新的数组,并将原有数组中的键值对重新散列到新的数组中。
- Java 1.8中的扩容操作采用了更细粒度的并发控制策略,允许多个线程同时处理不同的小段。
- 当
对象比较为什么重写hashCode和equals?
- 重写equals方法时需要重写hashCode方法,主要是针对Map、Set等集合类型的使用;
- a: Map、Set等集合类型存放的对象必须是唯一的;
- b: 集合类判断两个对象是否相等,是先判断HashCode是否相等,如果HashCode返回TRUE,还要再判断equals返回值是否ture,只有两者都返回ture,才认为该两个对象是相等的。
多线程
什么地方会用到多线程
- 定时任务:定时处理数据进行统计等
- 异步处理:发邮件, 记录日志, 发短信等。比如注册成功后发激活邮件
- 批量处理,缩短响应时间
什么是线程池
线程池是一种管理线程的机制,它创建了一个线程的集合,这些线程可以被用来执行任务。线程池可以减少因频繁创建和销毁线程而产生的开销,提高资源利用率和应用程序的响应速度,线程池有下面的这些特性
- 重用线程:线程池中的线程在执行完一个任务后不会销毁,而是可以被重新用来执行其他任务
- 控制并发:线程池可以限制同时运行的线程数量,防止系统因创建过多线程而耗尽资源。
- 提高响应速度:由于线程池中的线程是预先创建的,因此可以立即执行任务,减少了创建线程所需的时间
- 更好的资源管理:线程池可以对线程的生命周期进行管理,包括线程的创建、销毁和复用
- 任务调度:线程池可以与任务队列配合使用,将任务排队并按顺序或优先级执行
- 灵活性:线程池可以根据需要配置不同的参数,如线程数量、任务队列的大小、线程的存活时间等
1 | |
线程池的优缺点
优点:
- 资源节约:通过重用线程,减少了创建和销毁线程的开销,节省了系统资源
- 提高响应速度:线程池中的线程是预先创建的,可以立即执行任务,减少了线程创建的延迟。
- 控制并发级别:线程池可以限制同时运行的线程数量,避免因线程过多导致系统过载。
- 提高资源利用率:通过合理配置线程池参数,可以更高效地利用系统资源。
- 简化线程管理:线程池提供了统一的线程管理机制,简化了线程的创建和销毁过程。
- 可扩展性:线程池可以根据系统需求进行扩展,适应不同的工作负载。
- 任务调度:线程池可以与任务队列结合使用,实现任务的排队和优先级调度。
缺点:
- 资源分配不均:如果线程池中的线程数量设置不当,可能会导致某些线程过载,而其他线程空闲。
- 死锁风险:线程池中的线程可能会因为等待资源而相互阻塞,导致死锁。
- 任务执行顺序不确定:线程池中的线程可能会以非预期的顺序执行任务,这可能会影响到任务的执行结果。
- 资源限制:线程池的线程数量限制可能会导致某些任务长时间等待执行,影响应用程序的响应性。
- 内存占用:线程池中的线程会占用系统内存,如果线程数量过多,可能会增加内存使用。
- 管理复杂性:虽然线程池简化了线程管理,但在配置和管理线程池时仍然需要考虑许多因素,如线程数量、任务队列大小等。
- 错误传播:线程池中的一个线程发生错误可能会影响其他线程或整个应用程序的稳定性。
线程池的参数
- Core Pool Size 核心线程数:线程池中允许的最小线程数。即使工作队列为空,这些线程也会被保留。
- Maximum Pool Size 最大线程数:线程池中允许的最大线程数。如果队列满了,并且还需要更多的线程来处理任务,线程池会尝试创建新的线程,直到达到这个限制。
- Work Queue 工作队列:一个阻塞队列,用于存储等待执行的任务。如果线程池中的线程都忙,新提交的任务将被放入这个队列中等待。
- Keep-Alive Time 线程存活时间:当线程池中的线程数大于核心线程数时,如果这些多余的线程在一定时间内没有任务执行,它们将被终止。这个参数定义了这个时间间隔。
- Thread Factory 线程工厂:用于创建新线程的工厂。可以自定义线程的名称、优先级等属性。
- Rejected Execution Handler 拒绝策略:当任务太多,工作队列已满,且线程池中的线程数已达到最大值时,新提交的任务将被拒绝。拒绝策略定义了如何处理这些被拒绝的任务。
- Task Executor 任务执行器:负责执行提交给线程池的任务。
- Thread Priority 线程优先级:线程的优先级,影响线程调度的优先顺序。
- Thread Name Prefix 线程名称前缀:线程的名称前缀,有助于在调试时识别线程池中的线程。
- Allow Core Thread Timeout 允许核心线程超时:决定是否允许核心线程超时,如果设置为
true,则核心线程在空闲时也会被终止。
死锁产生的条件
产生原因
死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种僵局。当每个进程都持有一定的资源并等待其他进程释放它们所需的资源时,如果这些资源都被其他进程占有且不释放,那么所有进程都将无法继续执行。死锁产生的条件可以通过以下几个经典的条件来描述,这些条件被称为死锁的必要条件:
- 互斥条件(Mutual Exclusion):系统中的资源分为两类,可重复利用的资源和不可重复利用的资源。死锁中的资源通常是不可重复利用的,即一次只能由一个进程使用。
- 占有和等待条件(Hold and Wait):进程至少持有一个资源,并且正在等待获取其他进程持有的资源。
- 不可剥夺条件(No Preemption):已经分配给一个进程的资源,在未使用完之前,不能被强行夺走,只能由该进程自己释放。
- 循环等待条件(Circular Wait):存在一种进程资源的循环等待关系,即进程间形成了一个头尾相接的循环链,每个进程都在等待下一个进程所占有的资源。
只有当这四个条件同时满足时,死锁才会发生。如果系统中的任何进程不满足这四个条件之一,就可以避免死锁的发生
破坏死锁产生的条件
确保资源请求的一次性和顺序性,允许资源的临时释放或抢占,采用超时机制和锁排序,以及实现死锁检测和恢复机制。通过这些方法,可以破坏死锁产生的四个必要条件之一或多个,从而有效预防和解决多线程或多进程环境中的死锁问题
什么时候触发最大线程条件
在线程池中,触发最大线程条件的情况通常是指当线程池中的线程数量达到了其最大容量,即maximumPoolSize参数所设定的值。以下是一些触发最大线程条件的具体场景:
- 高负载情况:当线程池中的所有核心线程(
corePoolSize)都在忙碌状态,并且工作队列(work queue)已满时,提交新任务会导致线程池尝试创建新的线程,直到达到maximumPoolSize - 任务提交速度超过处理速度:如果任务被提交到线程池的速度超过了线程池处理任务的速度,工作队列会逐渐填满,当填满后,线程池会开始创建新的线程,直到达到最大线程数
- 长时间运行的任务:如果线程池中的线程都卡在长时间运行的任务上,而新的短期任务持续提交,这可能导致需要更多的线程来处理这些新任务,直到达到最大线程数
- 资源限制:在某些情况下,系统资源(如内存或可创建的线程数量)的限制可能会间接导致触发最大线程条件
- 饱和策略:线程池的饱和策略(
RejectedExecutionHandler)定义了当任务太多而不能同时执行时,如何处理这些任务。如果饱和策略允许线程池增长,那么在高负载情况下可能会达到最大线程数
线程池拒绝策略有哪些
在Java的ThreadPoolExecutor中,当线程池达到最大线程数时,如果还有新的任务提交,将根据设置的饱和策略来处理这些任务。饱和策略可以是:
- AbortPolicy:抛出
RejectedExecutionException来拒绝新任务的处理 - CallerRunsPolicy:由调用线程(提交任务的线程)运行该任务
- DiscardPolicy:静默地丢弃无法处理的任务
- DiscardOldestPolicy:丢弃工作队列中等待时间最长的任务,并尝试再次提交新任务
MySQL
MyISAM 和 InnoDB的区别
| 特性/引擎 | MyISAM | InnoDB |
|---|---|---|
| 事务支持 | 不支持 | 支持ACID事务 |
| 锁机制 | 表级锁 | 行级锁和表级锁(默认行级锁) |
| 外键约束 | 不支持 | 支持外键 |
| 崩溃恢复 | 崩溃恢复能力较弱 | 具有崩溃恢复能力 |
| 索引和数据存储 | 索引和数据分离 | 索引包含数据,聚簇索引 |
| 全文索引 | 支持(MySQL 5.6之前是MyISAM的特点) | 支持(从MySQL 5.6开始支持) |
| 存储空间和性能 | 使用较少存储空间,读写速度快 | 可能需要更多存储空间,提供高并发写入和数据完整性 |
| 自动增长列 | 支持 | 支持,实现方式更高效 |
| 读写操作 | 适合读密集型应用 | 适合写密集型应用,提供更好的并发控制 |
| 数据缓存 | 只缓存索引 | 缓存数据和索引 |
| 适用场景 | 读多写少,不需要事务支持的场景 | 需要事务支持、外键约束和高并发写入的场景 |
事务隔离级别
事务并发场景下会产生的问题
脏读(Dirty Read):
- 定义: 当一个事务读取到另一个事务未提交的更改时,称为脏读。这意味着如果这些更改最终没有提交,那么读取的数据将是无效的。
- 例子: 事务A更新了某行数据但尚未提交,事务B读取了这行更新后的数据,然后事务A回滚了更改。事务B读取的数据实际上是从未存在的。
不可重复读(Nonrepeatable Read):
- 定义: 当一个事务重新执行相同的查询,但由于另一个事务的更新导致结果集发生变化时,称为不可重复读。这通常发生在读取某一行数据后,该行数据被另一个事务修改并提交。
- 例子: 事务A读取了一个值,然后事务B更新了这个值并提交了更改。当事务A再次读取相同的数据时,得到的结果与第一次不同。
幻读(Phantom Read):
- 定义: 当一个事务重新执行相同的查询,但由于另一个事务的插入或删除导致结果集的行数发生变化时,称为幻读。幻读主要涉及集合的变更,如新增或删除多行记录。
- 例子: 事务A执行了一个查询,统计了某个条件的记录数,然后事务B插入或删除了一些符合该条件的记录并提交了更改。当事务A再次执行相同的查询时,记录数与之前不同,尽管具体的记录行并没有变化。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | 允许 | 可能发生 | 可能发生 | 事务可以读取到其他未提交事务的更改。 |
| 读已提交(Read Committed) | 不允许 | 可能发生 | 可能发生 | 事务只能读取到其他事务已经提交的更改。 |
| 可重复读(Repeatable Read)(默认管理级别) | 不允许 | 不允许 | 可能发生 | 在同一事务中,多次读取同一数据的结果是一致的,除非数据被自身事务修改。 |
| 串行化(Serializable) | 不允许 | 不允许 | 不允许 | 最高隔离级别,事务依次顺序执行,避免了所有并发问题。 |
ACID
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个点。这意味着事务是数据库状态的不可分割的单元。如果事务中的任何操作失败,整个事务将被回滚,就像它从未被执行过一样
- 一致性(Consistency):事务必须保证数据库从一个一致的状态转移到另一个一致的状态。一致性确保了数据库的完整性约束在事务执行前后保持一致。这意味着所有事务的结果都必须符合预定义的规则和期望
- 隔离性(Isolation):并发执行的事务之间不会互相影响。每个事务都与其他事务隔离开来,具有独立的执行环境。隔离性可以通过不同的事务隔离级别来实现,如读未提交、读已提交、可重复读和串行化
- 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统发生故障也不会丢失。这意味着事务对数据库的改变在提交后会被永久保存,不会因为任何原因(如系统崩溃、电源故障等)而撤销
数据库使用索引的优缺点
优点:
- 索引大大减少了服务器需要扫描的数据量
- 索引可以帮助服务器避免排序和临时表
- 索引可以将随机I/O 变为顺序I/O
缺点:
- 降低了数据写入的效率
- 原因:当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护
- 索引增加了查询优化器的选择时间
- 查询优化器在对一条sql语句进行分析时,会结合一系列的分析计算出一条最优的查询sql
- 添加了索引之后,相当于是在原来的基础上,添加了对索引因素的分析,若在很多字段上创建了索引会增加这个选择的时间
- 索引占物理空间
- 除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大
索引的创建原则
- 查询条件: 为WHERE子句和JOIN条件中常用列创建索引。
- 选择性: 优先索引选择性高的列(不同值占比较高)。
- 最左前缀: 在复合索引中,保持最左列的使用。
- 索引列顺序: 根据查询条件中列的出现频率和顺序安排索引列。
- 避免过度索引: 不要为每个列创建索引,特别是小表或更新频繁的列。
- 更新频率: 考虑列的更新频率,频繁更新的列维护索引成本高。
- 覆盖索引: 创建覆盖索引以包含查询中使用的所有列。
- 避免冗余索引: 避免创建功能重复或部分重叠的索引。
- 数据类型匹配: 确保索引列的数据类型与查询条件一致。
- 监控性能: 定期监控索引使用情况和查询性能。
- 维护成本: 考虑索引在数据变更时的维护成本。
- 唯一索引: 对于保证数据唯一性的列,使用唯一索引
索引失效的场景
- 违反最左前缀法则:如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。匹配最左前缀法则,走索引。如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效
- 范围查询右边的列,不能使用索引
- 不要在索引列上进行运算操作, 索引将失效
- 字符串不加单引号,造成索引失效。由于,在查询时没有对字符串加单引号, MySQL的查询优化器,会自动的进行类型转换,造成索引失效
- 以%开头的Like模糊查询,索引失效。如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效
- OR 语句前后没有同时使用索引。当OR 左右查询字段只有一个是索引,该索引失效,只有当OR 左右查询字段均为索引时,才会生效
联合索引
联合索引(Composite Index)是一种索引类型,它允许数据库查询操作根据多个列的值来快速检索数据。联合索引可以提高查询效率,尤其是当查询条件涉及到多个列时
创建联合索引时,你需要指定多个列,这些列的组合将用于创建索引。索引的列顺序非常重要,因为它决定了索引的效率。通常,最频繁用于查询条件的列应该放在索引的最前面
示例
如果你有一个包含用户信息的表,并且经常需要根据用户的姓(last_name)和名(first_name)来查询用户,你可以创建一个联合索引,将姓和名作为索引的列:
1 | |
在这个例子中,idx_lastname_firstname 是索引的名称,users 是表的名称,而 last_name 和 first_name 是联合索引的列
使用联合索引时,需要注意以下几点:
- 选择性:索引的列应该具有较高的选择性,即列中的不同值应该比较多
- 列顺序:索引的列顺序应该反映查询中列的使用频率和顺序
- 维护成本:索引虽然可以提高查询速度,但会增加插入、删除和更新操作的开销,因为索引需要维护
- 覆盖索引:如果一个查询只需要访问索引列,那么可以使用覆盖索引来提高性能
如何优化SQL效率
- SQL语句优化
- 避免使用
SELECT *,查询时指定需要的列名,避免不必要的数据读取 - 使用合适的
JOIN类型,确保JOIN列上有索引 - 查询大量数据时,使用
LIMIT来限制结果集大小,特别是在分页查询中 - 如果两个查询结果没有交集,使用
UNION ALL代替UNION,因为UNION ALL不需要去重 IN、OR,如果所在的列有索引,则使用IN效率更加高IN、EXISTS、BETWEEN...AND- 查询集合的范围确定且有限的
- 集合内的值连续时,应尽可能使用BETWEEN …AND
- 集合内的值不连续时,用IN,例如IN (1,3,7)
- 查询集合的范围不确定,例如IN(SELECT…),应判断内查询与外查询的关系
- 若内查询的表小于外查询的表,用IN效率高(因为IN先执行内查询,再执行外查询)
- 若外查询的表小于内查询的表,用EXISTS效率高(因为EXISTS先执行外查询,再执行内查询)
- 查询集合的范围确定且有限的
- 避免使用
- 避免SQL语句索引失效
- 表结构优化
- 选择合适的字段类型
- 对于有选项的字段,使用枚举
- 设计索引
- 对于经常用来查询、分组、排序的字段设计索引
- 设计覆盖索引
- 选择区分度高的列作为索引
- 硬件方面
- 增加服务器,数据库主从实现读写分离
- 搭建数据库集群
- 分库分表
Redis
数据类型及其使用场景
String(字符串) - 底层结构:简单动态字符串(SDS)。
- 使用场景:缓存单个值,如用户会话、计数器、实时分析数据等。
- 命令集:
set,get,incr,decr,append,strlen。
List(列表) - 底层结构:双向链表或压缩列表(依据数据量和元素大小)。
- 使用场景:消息队列、文章列表、用户列表等。
- 命令集:
lpush,rpush,lpop,rpop,lrange,llen。
Set(集合) - 底层结构:哈希表或整数集合(依据元素特性)。
- 使用场景:标签系统、好友推荐、共同好友列表等。
- 命令集:
sadd,srem,spop,scard,sunion,sinter。
Sorted Set(有序集合) - 底层结构:哈希表 + 跳跃表(ziplist 编码)或 哈希表 + 树(skiplist 编码)。
- 使用场景:排行榜、带权重的投票系统、时间排序的消息推送等。
- 命令集:
zadd,zrem,zrank,zrevrank,zrange,zrevrange。
Hash(哈希) - 底层结构:哈希表。
- 使用场景:存储对象、缓存用户信息、配置项集合等。
- 命令集:
hset,hget,hmset,hmget,hdel,hgetall。
Bitmaps(位图) - 底层结构:String 类型。
- 使用场景:状态监控、日志记录、用户签到系统等。
- 命令集:
setbit,getbit,bitcount,bitpos。
HyperLogLogs(近似计数) - 底层结构:特殊结构,用于存储基数估计数据。
- 使用场景:独立用户统计、页面访问量统计等。
- 命令集:
pfadd,pfcount,pfmerge。
Geospatial(地理空间) - 底层结构:有序集合(Sorted Set)。
- 使用场景:定位服务、地图服务、附近的人或地点推荐等。
- 命令集:
geoadd,geodist,geohash,geopos,georadius。
Pub/Sub(发布订阅) - 底层结构:发布订阅模式。
- 使用场景:实时消息系统、股票价格更新、实时通知等。
- 命令集:
publish,subscribe,unsubscribe,psubscribe,punsubscribe。
Streams(流) - 底层结构:特殊的数据结构,用于消息队列。
- 使用场景:消息队列、事件源、日志收集等。
- 命令集:
xadd,xread,xrange,xrevrange,xack,xgroup。
为什么性能这么高,速度这么快
- 数据存放在内存中,内存的读写速度是磁盘(数据库)的一百倍左右
- 用C语言实现,C语言更底层, 执行速度相对会更快
- 使用了多路复用,Redis是单线程的,但内部使用了IO多路复用提高性能
Redis单线程的优缺点
单进程单线程优势
- 没有多线程竞争锁的性能消耗。
- 没有多线程导致的切换而消耗CPU。
单进程单线程弊端
无法发挥多核CPU性能。不过可以通过在单机开多个Redis实例来完善
RDB
Redis Database Backup file, Redis数据库备份文件
RDB的工作原理是在Redis需要进行持久化时,它会fork出一个子进程,这个子进程会将当前的数据集写入到一个临时的RDB文件中。当写入操作完成后,临时文件会替换掉旧的RDB文件。这个过程中,主进程不进行任何磁盘IO操作,因此不会影响Redis的性能。这种工作方式也使得Redis可以从写时复制(copy-on-write)机制中获益,即在fork操作时,父子进程共享相同的内存数据,只有当数据需要修改时,才会复制一份数据,从而节省内存资源
RDB的配置主要在Redis的配置文件redis.conf中进行,其中有几个重要的配置项:
save <seconds> <changes>:这个指令定义了自动触发RDB持久化的条件。例如,save 900 1表示如果在900秒内有至少1个key被修改,则触发持久化。dbfilename dump.rdb:定义了RDB文件的名称,默认为dump.rdb。dir ./:定义了RDB文件存放的目录,默认是当前工作目录。rdbcompression yes:表示在创建RDB文件时是否进行数据压缩,默认是开启的。rdbchecksum yes:表示在RDB文件中是否包含校验和,默认也是开启的,这有助于检测文件在写入过程中是否出现错误。
手动触发RDB持久化可以使用SAVE或BGSAVE命令。SAVE命令会阻塞主进程直到持久化完成,而BGSAVE命令则通过创建子进程异步完成持久化,不会阻塞主进程,BGSAVE或fork一个子进程来进行持久化
RDB的优点包括:
- 适合于大规模数据恢复
- 恢复速度快于AOF
- 只包含一个文件,便于备份
RDB的缺点包括:
- 在故障情况下可能会丢失最近的数据
- fork子进程可能会短时间内影响性能
- 备份时会占用额外的内存资源
AOF
Append Only File,追加文件
AOF持久化的工作流程大致如下:
命令追加:每当Redis执行一个写命令,它会被追加到AOF缓冲区。
文件写入:AOF缓冲区中的内容会根据配置的同步策略周期性地写入到AOF文件中
文件同步
:根据配置,AOF文件的数据会被同步到磁盘。Redis提供了三种同步选项:
always:每个写命令执行完立即同步到磁盘,数据安全性最高,但性能影响较大everysec:每秒同步一次,这是默认设置,兼顾数据安全性和性能no:让操作系统决定何时同步,可能会在系统崩溃时丢失数据,但性能最好
文件重写:随着时间的推移,AOF文件可能会变得非常大,因为其中包含了很多不必要的命令。Redis提供了AOF重写功能,它可以创建一个新的AOF文件,其中只包含恢复当前数据集所需的最小命令集
AOF相关配置
appendonly yes:开启AOF持久化appendfilename "appendonly.aof":设置AOF文件的名称,默认为appendonly.aofappendfsync always:每次有数据修改发生时都会写入AOF文件appendfsync everysec:每秒钟同步一次,这是默认策略appendfsync no:从不同步,让操作系统决定何时同步auto-aof-rewrite-percentage 100:当AOF文件大小是上一次重写后大小的一倍时,触发重写auto-aof-rewrite-min-size 64mb:当AOF文件大小达到该值时,才会触发重写aof-load-truncated yes:如果AOF文件损坏,Redis启动时是否加载AOF文件
AOF的优点
- 提供了更高的数据安全性,可以设置为每秒同步一次,最多丢失一秒的数据
- AOF文件易于理解,它是一个只追加数据的日志文件,可以被轻易地分析和阅读
AOF的缺点
- 对于相同的数据集,AOF文件通常比RDB文件大
- 在高负载下,AOF可能会影响Redis的性能,因为它需要不断地写入命令到磁盘
- 恢复数据可能比`RDB慢,因为需要逐条执行命令
缓存穿透
缓存穿透是指查询一个在缓存和数据库中都不存在的数据,由于缓存中找不到,每次请求都要到数据库查询,然后返回空结果。这种情况会导致数据库压力增大,甚至可能因为大量的数据库查询而造成数据库服务的不稳定或崩溃。
解决方案
布隆过滤器:
- 使用布隆过滤器可以判断一个元素是否在一个集合中。
- 在请求到达之前,先经过布隆过滤器,如果布隆过滤器认为该元素不存在,则直接返回,不进行后续的数据库查询。
缓存空对象:
- 当查询数据库发现数据不存在时,仍然将一个空对象或特定的标记存入缓存,这样再次查询时可以直接从缓存中获取结果,避免对数据库的访问。
- 这种方法的缺点是可能会占用缓存空间存储无效数据。
缓存击穿
缓存击穿是指一个热点缓存数据到期或被删除的瞬间,大量请求同时到达,导致这些请求直接穿透缓存,进而对数据库等后端存储系统造成巨大压力的现象。这种情况通常发生在高流量的Web服务中,当某个热点数据失效时,可能会导致数据库短时间内承受大量查询请求,从而影响服务的稳定性
解决方案
互斥锁(Mutex Key):
- 当缓存数据失效时,通过设置一个互斥锁来保证只有一个请求能够访问数据库并加载数据,其他请求等待或重试
- 这种方法可以避免多个请求同时击穿缓存
延迟加载:
- 在缓存数据失效后,不是立即加载新的数据,而是等待一定时间,让其他请求也有机会获取锁或等待数据加载
- 这样可以分散请求,减少数据库的压力
热点数据不过期:
- 对于热点数据,可以设置永不过期,或者使用更长的过期时间,减少击穿发生的概率
- 这种方法适用于那些不经常变化的数据
缓存雪崩
缓存数据大规模同时失效,或者Redis服务器宕机,导致大量请求穿透缓存直接访问数据库等后端存储系统,从而对后端系统造成巨大压力,甚至导致系统崩溃的现象
解决方案
- 数据分级缓存:将数据分级,不同级别的数据设置不同的缓存策略和过期时间,避免同时失效
- 设置随机过期时间:为缓存数据设置随机的过期时间,避免大量数据同时过期
- 读写分离和数据库负载均衡:通过数据库的读写分离,将查询请求分散到多个数据库副本上,减轻单个数据库的压力
- 缓存服务高可用:部署缓存服务的集群,确保缓存服务的高可用性,避免单点故障
内存淘汰策略
Redis、Memcache、MongoDB的区别
缓存一致性
秒杀的设计方案
布隆过滤器
分布式锁
72648885900684613
iot_screen/jianwuBigScreen
?showlogo=false#/
https://bim-jw.xatasoft.com/?showlogo=false
https://api.jizhibao.com.cn/jwuser/lcBmi?orgid=70692244487726981&showlogo=false#/
修改单位