Java
⽐亚迪 Java ⾯试
最近,看到⽐亚迪软 件开发岗位校招薪资开奖了,我从⽹上爆料薪资的数据来看,薪资范围⼤概
有这⼏个层级:
9k 〜15k × 1.36 × 12 =14.6w 〜24.4w (本科 211 、985 居多,也有硕⼠ 211 、985 在这个范围)
18k × 1.36 × 12 = 29.3w (硕⼠ 985 居多)
23k × 1.36 × 12 = 37.5w (⽬前发现开的最⾼薪资档位,同学 bg 是海归硕⼠)
其中,1.36 是绩效奖励,如果⼯作绩效正常,就会多拿这部分绩效薪资。
听⼀些学历⽐较好的同学反馈,说⽐亚迪点击就送?
那肯定不是说学历到位,投了⼀定拿 offer ,还是会有技术⾯+hr ⾯的,技术⾯不过关⼀样会挂
的,可能就是⾯试难度相⽐于互 联⽹中⼤⼚来说,会⼩很多。
那具体⽐亚迪⾯试难度在哪⾥?
我找 了⼀位同学⽐亚迪Java 软件开发岗位的⾯经,技术⾯主要问了 30 分钟,主要是 10 个⼋股问
题,主要考察的知识点是 Java 基础、集合、Spirng 、MySQL 这些,没有算法。
相信如果⾯多了互 联⽹中⼤⼚的同学,看这个⾯经就会觉得很 简单了,不过对于没怎么⾯过试的
同学,难度就刚刚 好,不是特别难,但是都是⽐较⾼频⾯试知识点。

Java

Java 的集合介绍⼀下
List 是有序的Collection ,使⽤此接⼝能够精确的控制每个元素的插⼊位置,⽤⼾能根据索引访问
List 中元素。常⽤的实现List 的类有LinkedList ,ArrayList ,Vector ,Stack 。
ArrayList 是容量可变的⾮线程安全列表,其底层使⽤数组实现。当⼏何扩容时,会创建更⼤的
数组,并把原数组复制到 新数 组。ArrayList ⽀持对元素的快速随机访问,但插⼊与删除速度很
慢。
LinkedList 本质是⼀个双向链表,与ArrayList 相⽐,,其插⼊和删除速度更快,但随机访问速度更
慢。
Set 不允许存在重复的元素,与List 不同,set 中的元素是⽆序的。常⽤的实现有HashSet ,
LinkedHashSet 和TreeSet 。
HashSet 通过HashMap 实现,HashMap 的Key 即HashSet 存储的元素,所有Key 都是⽤相同的
Value ,⼀个名为PRESENT 的Object 类型常量。使⽤Key 保证元素唯⼀性,但不保证有序性。由
于HashSet 是HashMap 实现的,因此线程不安全。
LinkedHashSet 继承⾃HashSet ,通过LinkedHashMap 实现,使⽤双向链表维护元素插⼊顺序。
TreeSet 通过TreeMap 实现的,添加元素到集合时按照⽐较规则将其插⼊合适的位置,保证插⼊
后的集合仍然有序。
Map 是⼀个键值对集合,存储键、值和之间的映射。Key ⽆序,唯⼀;value 不要求有序,允许重
复。Map 没有继承于 Collection 接⼝,从 Map 集合中检索元素时,只要给出键对象,就会返回对
应的值对象。主要实现有TreeMap 、HashMap 、HashTable 、LinkedHashMap 、
ConcurrentHashMap
HashMap :JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主
要为了 解决哈希冲突⽽存在的(“拉链法”解决冲 突),JDK1.8 以后在解决哈希冲突时有了较⼤的
变化 ,当链表⻓度⼤于阈值(默认为 8)时,将链表转化为红⿊树,以减少搜索时间
LinkedHashMap :LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结
构即由数组和链表或红⿊树组成。另外,LinkedHashMap 在上⾯结构的基础上,增加了⼀条双
向链表,使得上⾯的结构可以保 持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现
了访问顺序相关逻辑。
HashTable :数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了 解决哈希冲突⽽
存在的
TreeMap :红⿊树(⾃平衡的排序⼆叉树)
ConcurrentHashMap :Node 数组+链表+红⿊树实现,线程安全的(jdk1.8 以前Segment 锁,
1.8 以后volatile + CAS 或者 synchronized )
ArrayList 和LinkedList 的区别是什么 ?
ArrayList 和LinkedList 都是Java 中常⻅的集合类,它们都实现了List 接⼝。
底层数据结构不同:ArrayList 使⽤数组实现,通过索引进⾏快速访问元素。LinkedList 使⽤链表
实现,通过节点之间的指针进⾏元素的访问和操作。
插⼊和删除操作的效率不同:ArrayList 在尾部的插⼊和删除操作效率较⾼,但在中间或开头的
插⼊和删除操作效率较低,需要移动元素。LinkedList 在任意位置的插⼊和删除操作效率都⽐较
⾼,因为只需要调整节点之间的指针。
随机访问的效率不同:ArrayList ⽀持通过索引进⾏快速随机访问,时间复杂度为O(1) 。
LinkedList 需要从头或尾开始遍历链表,时间复杂度为O(n) 。空间占⽤:ArrayList 在创建时需要分配⼀段连续的内存空间,因此会占⽤较⼤的空间。
LinkedList 每个节点只需要存储元素和指针,因此相对较⼩。
使⽤场景:ArrayList 适⽤于频繁随机访问和尾部的插⼊删除操作,⽽LinkedList 适⽤于频繁的中
间插⼊删除操作和不需要随机访问的场景。
线程安全:这两个 集合都不是线程安全的,Vector 是线程安全的
HashMap 、Hashtable 、ConcurrentMap 这三个 有什么 区别?
HashMap 线程不安全,效率⾼⼀点,可以存储的key 和value ,的key 只能有⼀个,的value 可以
有多个。默认初始容量为16 ,每次 扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2
的幂次⽅⼤⼩。底层数据结构为数组+链表,插⼊元素后如果链表⻓度⼤于阈值(默认为8),先
判断数 组⻓度是否⼩于64 ,如果⼩于,则扩充数组,反之将链表转化为红⿊树,以减少搜索时
间。
HashTable 线程安全,效率低⼀点,其内 部⽅法基本都经过synchronized 修饰,不可以有的key
和value 。默认初始容量为11 ,每次 扩容变为原来的2n+1 。创建时给定了初始容量,会直接⽤给
定的⼤⼩。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以⽤
ConcurrentHashMap 。
ConcurrentHashMap 是Java 中的⼀个线程安全的哈希表实现,它可以在多线程环境下并发地进
⾏读写操作,⽽不需要像传统的HashTable 那样在读写时加锁。ConcurrentHashMap 的实现原理
主要基于分段锁和CAS 操作。它将整个哈希表分成了多Segment (段),每个Segment 都类似于
⼀个⼩的HashMap ,它拥有⾃⼰的数组和⼀个独⽴的锁。在ConcurrentHashMap 中,读操作不
需要锁,可以直接对Segment 进⾏读取,⽽写操作则只需要锁定对应的Segment ,⽽不是整个
哈希表,这样可以⼤⼤提⾼并发性能。
Java 的反射是什么 ?
Java 反射机制是在运⾏状态中,对于任意⼀个类,都能够知道这个类中的所有属性和⽅法,对于
任意⼀个对象,都能够调⽤它的任意⼀个⽅法和属性;这种动态获取的信息以及动态调⽤对象的
⽅法的功能称为 Java 语⾔的反射机制。
反射具有以下特性:
- 运⾏时类信息访问:反射机制允许程序在运⾏时获取类的完整结构信息,包括类名、包名、⽗
类、实现的接⼝、构造函数、⽅法和字段等。
- 动态对象创建:可以使 ⽤反射API 动态地创建对象实例,即使在编译时不知道具体的类名。这是
通过Class 类的newInstance() ⽅法或Constructor 对象的newInstance() ⽅法实现的。- 动态⽅法调⽤:可以在运⾏时动态地调⽤对象的⽅法,包括私有⽅法。这通过Method 类的
invoke() ⽅法实现,允许你传 ⼊对象实例和参数值来执⾏⽅法。- 访问和修改字段值:反射还允许程序在运⾏时访问和修改对象的字段值,即使是私有的。这是

通过Field 类的get() 和set() ⽅法完成的。代理介绍⼀下,jdk 和cglib 的区别?
Java 的动态代理是⼀种在运⾏时动态创建代理对象的机制,主要⽤于在不修改原始类的情况下对⽅
法调⽤进⾏拦截 和增强。
Java 动态代理主要分为两 种类型:
基于接⼝的代理(JDK 动态代理): 这种类型的代理要求⽬标对象必须实现⾄少⼀个接⼝。Java
动态代理会创建⼀个实现了相同接⼝的代理类,然后在运⾏时动态⽣成该类的实例。这种代理
的实现核⼼是 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接⼝。每
⼀个动态代理类都必须实现 InvocationHandler 接⼝,并且每个代理类的实例都关联到⼀个
handler 。当通过代理对象调 ⽤⼀个⽅法时,这个⽅法的调⽤会被转发为由
InvocationHandler 接⼝的 invoke() ⽅法来进⾏调⽤。基于类的代理(CGLIB 动态代理): CGLIB (Code Generation Library )是⼀个强⼤的⾼性能的代
码⽣成库,它可以在运⾏时动态⽣成⼀个⽬标类的⼦类。CGLIB 代理不需要⽬标类实现接⼝,⽽
是通过继承的⽅式创建代理类。因此,如果⽬标对象没有实现任何 接⼝,可以使 ⽤CGLIB 来创建
动态代理。
Spring 的事务⽤过吗?
⽤过。
Spring 事务是指在Spring 框架中对于数据库操作的⼀种⽀持,它通过对⼀组数据库操作进⾏整体控
制来保证数据的⼀致性和完整性。Spring 事务可以保 证在⼀组数据库操作执⾏时,要么所有操作都
执⾏成功,要么所有操作都回滚到之前的状态,从⽽避免了数据不⼀致的情况。
Spring 事务可以通过编程式事务和声明式事务两种⽅式来实现。
编程式事务需要在代码中⼿动控制事务的开始、提交和回滚等操作
⽽声明式事务则 是通过在配置⽂件中声明事务的切⼊点和通知等信息来⾃动控制事务的⾏为。
Spring 的事务隔离级别有哪些?
Spring 事务的隔离级别是指多个事 务之间的隔离程度,它可以通过设置 isolation 属性来指定。
Spring 事务的隔离级别包括以下五 种:
DEFAULT :默认的隔离级别,由底层数据库引擎决定。
READ_UNCOMMITTED :最低的隔离级别,允许读取未提交的数据。该级别会导致“脏读”、“不可重
复读”和“幻读”等问题。
READ_COMMITTED :只允许读取已经提交的数据。该级别可以避免“脏读”,但可能会导致“不可重
复读”和“幻读”等问题。
REPEATABLE_READ :保证在同⼀个事 务中多次读取同⼀数据时,该数据的值不会发⽣变化 。该
级别可以避免“脏读”和“不可重复读”,但可能会导致“幻读”等问题。
SERIALIZABLE :最⾼的隔离级别,强制事务串⾏执⾏,避免了“脏读”、“不可重复读”和“幻
读”等问题。但是该级别会对性能产⽣较⼤的影响,因此⼀般不建议使⽤。
在选择隔离级别时,需要根据具体的业务需求来选择合适的级别。⼀般来说,如果不需要在事务
中读取未提交的数据,那么可以选择 READ_COMMITTED 级别;如果需要避免“不可重复读”问题,可以选择 REPEATABLE_READ 级别;如果需要避免“幻读”问题,可以选择 SERIALIZABLE 级别。但是需要注意的是,隔离级别越⾼,事务的并发性越差,因此需要根据具体业务场景来权 衡隔离级别和性能。
MySQL
mysql 的隔离级别有哪些
读未提交(read uncommitted ),指⼀个事 务还没提交时,它做的变更就能被其他事 务看到;
读提交(read committed ),指⼀个事 务提交之 后,它做的变更才能被其他事 务看到;
可重复读(repeatable read ),指⼀个事 务执⾏过程中看到的数据,⼀直跟这个事 务启动时看到
的数据是⼀致的,MySQL InnoDB 引擎的默认隔离级别;
串⾏化(serializable );会对记录加上读写锁,在多个事 务对这条记录进⾏读写操作时,如果发
⽣了读写冲 突的时候,后访问的事务必须等前⼀个事 务执⾏完成,才能继续执⾏;
按隔离⽔平⾼低排序如下:
针对不同的隔离级别,并发事务时可能发⽣的现象也会不同。
也就是说:
在「读未提交」隔离级别下,可能发⽣脏读、不可重复读和幻读现象;
在「读提交」隔离级别下,可能发⽣不可重复读和幻读现象,但是不可能发⽣脏读现象;
在「可重复读」隔离级别下,可能发⽣幻读现象,但是不可能脏 读和不可重复读现象;
在「串⾏化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发⽣。
接下来,举个 具体的例⼦来说明这四种隔离级别,有⼀张账⼾余额表,⾥⾯有⼀条账⼾余额为 100
万的记录。然后有两个 并发的事务,事务 A 只负责 查询余额,事务 B 则会将我的余额改成 200
万,下⾯是按照时间顺序执⾏两个事 务的⾏为:

在不同隔离级别下,事务 A 执⾏过程中查询到的余额可能会不同:
在「读未提交」隔离级别下,事务 B 修改余额后,虽然没有提交事 务,但是此时的余额已经可
以被事务 A 看⻅了,于是事务 A 中余额 V1 查询的值是 200 万,余额 V2 、V3 ⾃然也是 200 万了;
在「读提交」隔离级别下,事务 B 修改余额后,因为没有提交事 务,所以事务 A 中余额 V1 的
值还是 100 万,等事务 B 提交完后,最新的余额数据才能被事务 A 看⻅,因此额 V2 、V3 都是
200 万;
在「可重复读」隔离级别下,事务 A 只能看⻅启动事务时的数据,所以余 额 V1 、余额 V2 的值
都是 100 万,当事务 A 提交事 务后,就能看⻅最新的余额数据了,所以余 额 V3 的值是 200
万;
在「串⾏化」隔离级别下,事务 B 在执⾏将余额 100 万修改为 200 万时,由于此前事务 A 执⾏
了读操作,这样就发⽣了读写冲 突,于是就会被锁住,直到事务 A 提交后,事务 B 才可以继续
执⾏,所以从 A 的⻆度看,余额 V1 、V2 的值是 100 万,余额 V3 的值是 200 万。
这四种隔离级别具体是如何实现的呢?
对于「读未提交」隔离级别的事务来说,因为可以读到未提交事 务修改的数据,所以直接读取
最新的数据就好了;
对于「串⾏化」隔离级别的事务来说,通过加读写锁的⽅式来避免并⾏访问;
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们
的区别在于创建 Read View 的时机不同,「读提交」隔离级别是在「每个语句执⾏前」都会重新
⽣成⼀个 Read View ,⽽「可重复读」隔离级别是「启动事务时」⽣成⼀个 Read View ,然后
整个事 务期间都在⽤这个 Read View 。
索引有哪些,区别是什么
MySQL 可以按照四个⻆度来分类索引。
按「数据结构」分类:B+tree 索引、Hash 索引、Full-text 索引。
按「物理存储」分类:聚簇索引(主键索引)、⼆级索引(辅助索引)。
按「字段特性」分类:主键索引、唯⼀索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
接下来,按照这些⻆度来说说 各类索引的特点。
按数据结构分类
从数据结构的⻆度来看,MySQL 常⻅索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
每⼀种存储引擎⽀持的索引类型不⼀定相同,我在表中总结了 MySQL 常⻅的存储引擎 InnoDB 、
MyISAM 和 Memory 分别 ⽀持的索引类型。

InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引
擎采⽤最多的索引类型。
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
如果有 主键,默认会使 ⽤主键作为聚簇索引的索引键(key );
如果没有主键,就选择第⼀个不 包含 值的唯⼀列作为聚簇索引的索引键(key );
在上⾯两个 都没有的情况下,InnoDB 将⾃动⽣成⼀个隐式⾃增 id 列作为聚簇索引的索引键
(key );其它索引都属于辅助索引(Secondary Index ),也被称为⼆级索引或⾮聚簇索引。创建的主键索引
和⼆级索引默认使⽤的是 B+Tree 索引。
按物理存储分类
从物理存储的⻆度来看,索引分为聚簇索引(主键索引)、⼆级索引(辅助索引)。
这两个 区别在前⾯也提到了:
主键索引的 B+Tree 的叶⼦节点存放的是实际数据,所有完整的⽤⼾记录都存放在主键索引的
B+Tree 的叶⼦节点⾥;
⼆级索引的 B+Tree 的叶⼦节点存放的是主键值,⽽不是实际数据。
所以,在查询时使⽤了⼆级索引,如果查 询的数据能在⼆级索引⾥查询的到,那么就不需要回
表,这个过程就是覆盖索引。如果查 询的数据不在⼆级索引⾥,就会先检索⼆级索引,找到对应
的叶⼦节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。
按字段特性分类
从字段特性的⻆度来看,索引分为主 键索引、唯⼀索引、普通索引、前缀索引。
主键索引
主键索引就是建⽴在主键字段上的索引,通常在创建表的时候⼀起创建,⼀张表最多只有⼀个主
键索引,索引列的值不允许有空值。
在创建表时,创建主键索引的⽅式如下:
CREATE TABLE table_name (
....
PRIMARY KEY (index_column_1) USING BTREE
);唯⼀索引
唯⼀索引建 ⽴在 UNIQUE 字段上的索引,⼀张表可以有多个唯⼀索引,索引列的值必须唯⼀,但
是允许有空值。
在创建表时,创建唯⼀索引的⽅式如下:
CREATE TABLE table_name (
....
UNIQUE KEY(index_column_1,index_column_2,...)
);建表后,如果要创建唯⼀索引,可以使 ⽤这⾯这条命令:
CREATE UNIQUE INDEX index_name
ON table_name(index_column_1,index_column_2,...);普通索引
普通索引就是建⽴在普通字段上的索引,既不要求字段为主 键,也不 要求字段为 UNIQUE 。
在创建表时,创建普通索引的⽅式如下:
INDEX(index_column_1,index_column_2,...)
);建表后,如果要创建普通索引,可以使 ⽤这⾯这条命令:
CREATE INDEX index_name
ON table_name(index_column_1,index_column_2,...);前缀索引
前缀索引是指对字符类型字段的前⼏个字符建⽴的索引,⽽不是在整个字段上建⽴的索引,前缀
索引可以建⽴在字段类型为 char 、 varchar 、binary 、varbinary 的列上。
使⽤前缀索引的⽬的是为了 减少索引占⽤的存储空间,提升查询效率。
在创建表时,创建前缀索引的⽅式如下:
CREATE TABLE table_name(
column_list,
INDEX(column_name(length))
);建表后,如果要创建前缀索引,可以使 ⽤这⾯这条命令:
CREATE INDEX index_name
ON table_name(column_name(length));按字段个数分类
从字段个数的⻆度来看,索引分为单列索引、联合索引(复合索引)。
建⽴在单列上的索引称为单列索引,⽐如主键索引;
建⽴在多列上的索引称为联合索引;
通过将多个字段组合成⼀个索引,该索引就被称为联合索引。
⽐如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name) ,创建联合索引的⽅式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引(product_no, name) 的 B+Tree ⽰意图如下(图中叶⼦节点之间我画了单向链表,但是实际上是双向链表,原图我找 不到了,修改不了 ,偷个懒我不重画了,⼤家脑补成双向链表就⾏)。

可以看到,联合索引的⾮叶⼦节点⽤两个 字段的值作为 B+Tree 的 key 值。当在联合索引查询数据
时,先按 product_no 字段⽐较,在 product_no 相同的情况下再按 name 字段⽐较。
也就是说,联合索引查询的 B+Tree 是先按 product_no 进⾏排序,然后再 product_no 相同的情况再按 name 字段排序。
因此,使⽤联合索引时,存在最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配。在使
⽤联合索引进⾏查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就⽆法利⽤到
索引快 速查询的特性了。
⽐如,如果创建了⼀个 (a, b, c) 联合索引,如果查 询条件是以下这⼏种,就可以匹配上联合索引:
where a=1 ;
where a=1 and b=2 and c=3 ;
where a=1 and b=2 ;
需要注意的是,因为有查 询优化器, 所以 a 字段在 where ⼦句的顺序并 不重要。但是,如果查 询条件是以下这⼏种,因为不 符合最左匹配原则,所以就⽆法匹配上联合索引,联
合索引就会失效:
where b=2 ;
where c=3 ;
where b=2 and c=3 ;
上⾯这些查询条件之所以会 失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再 按b 排序,在 b 相同的情况再 按 c 排序。所以,b 和 c 是全局⽆序,局部相对有序的,这样在没有遵
循最左匹配原则的情况下,是⽆法利⽤到索引的。
联合索引有⼀些特殊情况,并不是查询过程使⽤了联合索引查询,就代表联合索引中的所有字段
都⽤到了联合索引进⾏索引查询,也就是可能存在部分字段⽤到联合索引的 B+Tree ,部分字段没
有⽤到联合索引的 B+Tree 的情况。
这种特殊情况就发⽣在范围查询。联合索引的最左匹配原则会⼀直向右 匹配直到遇到「范围查询」
就会停⽌匹配。也就是范围查询的字段可以⽤到联合索引,但是在范围查询字段的后⾯的字段⽆
法⽤到联合索引。
索引失效场景有哪些?
6 种会发⽣索引失效的情况:
当我们使 ⽤左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种⽅式都会造成索
引失效;
当我们在查询条件中对索引列使⽤函数,就会导致索引失效。
当我们在查询条件中对索引列进⾏表达式计算,也是⽆法⾛索引的。
MySQL 在遇到字符串和数字⽐较的时候,会⾃动把字符串转为数字,然后再进⾏⽐较。如果字
符串是索引列,⽽条件语句中的输⼊参数是数字的话,那么索引列会发⽣隐式类型转换,由于
隐式类型转换是通过 CAST 函数实现的,等同于对索引列使⽤了函数,所以就会导致索引失
效。
联合索引要能正确使⽤需要遵循最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配,
否则就会导致索引失效。
在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那么索
引会失效。
索引优化的思路是什么 ?
常⻅优化索引的⽅法:
前缀索引优化:使⽤前缀索引是为了 减⼩索引字段⼤⼩,可以增加⼀个索引⻚中存储的索引
值,有效提⾼索引的查询速度。在⼀些⼤字符串的字段作为索引时,使⽤前缀索引可以帮助我
们减⼩索引项的⼤⼩。
覆盖索引优化:覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶⼦节点上都能找
得到的那些索引,从⼆级索引中查询得到记录,⽽不需要通过聚簇索引查询获得,可以避免回
表的操作。
主键索引最好是⾃增的:
如果我们使 ⽤⾃增主键,那么每次 插⼊的新数 据就会按顺序添加到 当前索引节点的位置,不
需要移动已有的数据,当⻚⾯写满,就会⾃动开辟⼀个新⻚⾯。因为每次 插⼊⼀条新记录,
都是追加操作,不需要重新移动数据,因此这种插⼊数据的⽅法效率⾮常⾼。
如果我们使 ⽤⾮⾃增主键,由于每次 插⼊主键的索引值都是随机的,因此每次 插⼊新的数据
时,就可能会插⼊到现有数据⻚中间的某个位置,这将不得不移动其它数据来满⾜新数 据的
插⼊,甚⾄需要从⼀个⻚⾯复制数据到另外⼀个⻚⾯,我们通常将这种情况称为⻚分裂。⻚
分裂还有可能会造成⼤量的内存碎⽚,导致索引结构不紧凑,从⽽影响查询效率。
防⽌索引失效:
当我们使 ⽤左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种⽅式都
会造成索引失效;
当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失
效;
联合索引要能正确使⽤需要遵循最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹
配,否则就会导致索引失效。
在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那
么索引会失效。
怎么解决redis 和mysql 的缓存⼀致性问题
对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache 。对于写数
据,我会选择更新 db 后,再删除缓存。

缓存是通过牺牲 强⼀致性来提⾼性能的。这是由CAP 理论决定的。缓存系统适⽤的场景就是⾮强⼀
致性的场景,它属于CAP 中的AP 。所以,如果需要数据库和缓存数据保持强⼀致,就不适合使⽤
缓存。
所以使 ⽤缓存提升性能,就是会有数据更新的延迟。这需要我们在设计 时结合业务仔细思考是否
适合⽤缓存。然后缓存⼀定要设置过期时间,这个时间太短、或者太⻓都不好:
太短的话请求可能会⽐较多的落到数据库上,这也意味着失去了缓存的优势。
太⻓的话缓存中的脏数据会使 系统⻓时间处于⼀个延迟的状态,⽽且系统中⻓时间没有⼈访问
的数据⼀直存在内存中不 过期,浪费内存。
但是,通过⼀些⽅案优化处理,是可以最终⼀致性的。
针对删除缓存异常的情况,可以使 ⽤ 2 个⽅案避免:
删除缓存重试策略(消息队列)
订阅 binlog ,再删除缓存(Canal+ 消息队列)
消息队列⽅案
我们可以引⼊消息队列,将第⼆个操作(删除缓存)要操作的数据加⼊到消息队列,由消费者来
操作数据。
如果应⽤删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试
机制。当然,如果重试超过的⼀定次数,还是没有成功,我们就需要向业务层发送报错信息
了。
如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个 例⼦,来说明重试机制的过程。

重试删除缓存机制还可以,就是会造成好多 业务代码⼊侵。
订阅 MySQL binlog ,再操作缓存
「先更新数 据库,再删缓存」的策略的第⼀步是更新数 据库,那么更新数 据库成功,就会产⽣⼀
条变更⽇志,记录在 binlog ⾥。
于是我们就可以通过订阅 binlog ⽇志,拿到具体要操作的数据,然后再执⾏缓存删除,阿⾥巴巴
开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从 复制的交互 协议,把⾃⼰伪装成⼀个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal ,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使⽤。
下图是 Canal 的⼯作原理:

将binlog ⽇志采集发送到MQ 队列⾥⾯,然后编写⼀个简单的缓存删除消息者订阅binlog ⽇志,根
据更新log 删除缓存,并且通过ACK 机制确认处理这条更 新log ,保证数据缓存⼀致性
