面试总结(某电商)

面试总结(某电商)

Mybatis 懒加载

什么是懒加载

延迟加载又叫懒加载,也叫按需加载,也就是说先加载主信息,需要的时候,再去加载从信息。代码中有查询语句,当执行到查询语句时,并不是马上去DB中查询,而是根据设置的延迟策略将查询向后推迟。

作用

减轻DB服务器的压力,因为我们延迟加载只有在用到需要的数据才会执行查询操作。

如何配置

  • Mybatis-config.xml里配置:
1
2
3
4
5
<settings>
<setting name ="aggressiveLazyLoading" value="false"/>
<!--开启延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
  • Mapper里配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mapper.AccountMapper">
<!-- 定义封装 Account和User 的resultMap -->
<resultMap id="userAccountMap" type="Account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 配置封装 User 的内容
select:查询用户的唯一标识
column:用户根据id查询的时候,需要的参数值
-->
<association property="user" column="uid" javaType="User" select="com.xxx.mapper.UserMapper.findById"></association>
</resultMap>

<!-- 根据查询所有账户 -->
<select id="findAll" resultMap="userAccountMap">
SELECT * FROM account
</select>
</mapper>
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mapper.UserMapper">
<!-- 定义User的resultMap-->
<resultMap id="userAccountMap" type="User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="telephone" column="telephone"></result>
<result property="birthday" column="birthday"></result>
<result property="gender" column="gender"></result>
<result property="address" column="address"></result>
<collection property="accounts" ofType="account">
<id property="id" column="aid"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
</collection>
</resultMap>

<!-- 根据id查询用户 -->
<select id="findById" parameterType="INT" resultType="User">
select * from user where id = #{uid}
</select>
</mapper>

就可以实现按需加载,达到我们的目的。

分布式锁如何实现

基于数据库

在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

1
2
3
4
5
6
7
8
9
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`desc` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

执行某个方法后,插入一条记录

1
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');

因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

成功插入则获取锁,执行完成后删除对应的行数据释放锁:

1
delete from method_lock where method_name ='methodName';

优点:易于理解实现

缺点:

(1)因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换。

(2)不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程信息相同,若相同则直接获取锁;

(3)没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在锁中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

(4)不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

基于Redis

选择Redis分布式锁的原因:

  • redis有很高的性能;

  • redis对此支持的命令较好,实现起来比较方便

使用的命令:

  • SETNX:SETNX key val 当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0
  • expire:expire key timeout 设置超时时间,单位为s,超过这个时间就会自动释放锁,避免死锁
  • delete:delete key 释放锁

实现思想:

  1. 获取锁的时候,使用setnx加锁,并使用expire命令给锁加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行进行锁释放。

优点:

  1. 吞吐量高
  2. 有锁失效自动删除机制,保证不会阻塞所有流程

缺点:

  1. 单点故障问题
  2. 锁超时问题:如果A拿到锁之后设置了超时时长,但是业务还未执行完成且锁已经被释放,此时其他进程就会拿到锁从而执行相同的业务。如何解决?Redission定时延长超时时长避免过期。为什么不直接设置为永不超时?为了防范业务方没写解锁方法或者发生异常之后无法进行解锁的问题
  3. 轮询获取锁状态方式太过低效

基于Zookeeper

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

基于ZooKeeper实现分布式锁的步骤如下:

  1. 创建一个目录mylock;
  2. 线程A想获取锁就在mylock目录下创建临时顺序节点;
  3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
  4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己小的节点;
  5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如redis方式,强依赖zk

DynamoDB 优势

  • 规模性能

    DynamoDB 通过在任意规模环境中提供一致的个位数毫秒响应时间,支持世界上一些最大规模的应用程序。您可以构建吞吐量和存储空间几乎无限的应用程序。DynamoDB 全局表可跨多个 AWS 区域复制您的数据,使您能够快速在本地访问全局分布的应用程序的数据。对于需要以微秒级延迟执行更快访问的使用案例,DynamoDB Accelerator (DAX) 提供了完全托管的内存缓存

  • 无须管理服务器

    DynamoDB 是无服务器服务,无需预配置、修补或管理服务器,也不需要安装、维护或操作软件。DynamoDB 可自动纵向扩展和缩减表,以针对容量做出调整并保持性能。由于内置了可用性和容错能力,您无需为这些功能构建应用程序。DynamoDB 提供预配置和按需容量模式,使您能够通过指定每个工作负载的容量或只为您使用的资源付费,从而优化成本。

  • 企业级

    DynamoDB 支持 ACID 事务,使您能够大规模构建业务关键型应用程序。DynamoDB 默认加密所有数据,并为您的所有表提供细粒度的身份和访问控制。您可以立即创建数百 TB 数据的完整备份,而不会对您的表性能产生影响,并且可以恢复到先前的 35 天内的任何时间点,而无需停机。DynamoDB 还提供有服务级别协议,从而确保可用性。

Spring 解决循环依赖

循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错。下面说一下Spring是如果解决的。

Spring对循环依赖的处理有三种情况:

  1. 构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。
  2. 单例模式下的setter循环依赖,通过“三级缓存”来处理循环依赖。
  3. 非单例Bean的循环依赖,无法处理。

DefaultSingletonBeanRegistry的实现方式:

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
/** Cache of singleton objects: bean name –> bean instance */
private final Map singletonObjects = new ConcurrentHashMap(256);
/** Cache of singleton factories: bean name –> ObjectFactory */
private final Map> singletonFactories = new HashMap>(16);
/** Cache of early singleton objects: bean name –> bean instance */
private final Map earlySingletonObjects = new HashMap(16);

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
//isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
//allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
//从singletonFactories中移除,并放入earlySingletonObjects中。
//其实也就是从三级缓存移动到了二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

这个接口在AbstractBeanFactory里实现,并在核心方法doCreateBean()引用下面的方法:

1
2
3
4
5
6
7
8
9
10
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

Mysql 索引的数据结构

索引是什么

索引是帮助数据库高效获取数据的数据结构

索引的分类

从存储结构上来划分
  • Btree 索引(B+tree,B-tree)
  • 哈希索引
  • full-index 全文索引
  • RTree
从应用层次上来划分
  • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引。
  • 唯一索引:索引列的值必须唯一,但允许有空值。
  • 复合索引:一个索引包含多个列。
从表记录的排列顺序和索引的排列顺序是否一致来划分
  • 聚集索引:表记录的排列顺序和索引的排列顺序一致。
  • 非聚集索引:表记录的排列顺序和索引的排列顺序不一致。
聚集索引和非聚集索引
  • 聚集索引:就是以主键创建的索引。
  • 非聚集索引:就是以非主键创建的索引(也叫做二级索引)

Mysql 索引的数据结构

哈希索引

可能直接想到的就是用哈希表来实现快速查找,就像我们平时用的 hashmap 一样,value = get(key), O(1) 时间复杂度一步到位,确实,哈希索引 是一种方式。

定义

哈希索引就是采用一定的哈希算法,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。本质上就是把键值换算成新的哈希值,根据这个哈希值来定位

局限性

  • 哈希索引没办法利用索引完成排序。
  • 不能进行多字段查询。
  • 在有大量重复键值的情况下,哈希索引的效率也是极低的(出现哈希碰撞问题)。
  • 不支持范围查询。

在 MySQL 常用的 InnoDB 引擎中,还是使用 B+ 树索引比较多。InnoDB 是自适应哈希索引的(hash 索引的创建由 InnoDB 存储引擎自动优化创建)。

B 树

目前索引常用的数据结构是 B+ 树,先介绍一下什么是 B 树(也就是 B- 树)。

B 树的特点:

  • 关键字分布在整棵树的所有节点。
  • 任何一个关键字 出现且只出现在一个节点中
  • 搜索有可能在 非叶子节点 结束。
  • 其搜索性能等价于在关键字全集内做一次二分查找
B+ 树

了解了 B 树,再来看一下 B+ 树,也是 MySQL 索引大部分情况所使用的数据结构

B+ 树基本特点

  • 非叶子节点的子树指针与关键字个数相同。
  • 非叶子节点的子树指针 P[i],指向关键字属于 [k[i],K[i+1]) 的子树(注意:区间是前闭后开)。
  • 为所有叶子节点增加一个链指针
  • 所有关键字都在叶子节点出现

B+ 树的特性

  • 所有的关键字 都出现在叶子节点的链表中,且链表中的关键字是有序的。
  • 搜索只在叶子节点命中
  • 非叶子节点相当于是 叶子节点的索引层,叶子节点是 存储关键字数据的数据层

相对 B 树,B+ 树做索引的优势

  • B+ 树的磁盘读写代价更低。B+ 树的内部没有指向关键字具体信息的指针,所以其内部节点相对 B 树更小,如果把所有关键字存放在同一块盘中,那么盘中所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相应的,IO 读写次数就降低了
  • 树的查询效率更加稳定。B+ 树所有数据都存在于叶子节点,所有关键字查询的路径长度相同,每次数据的查询效率相当。而 B 树可能在非叶子节点就停止查找了,所以查询效率不够稳定。
  • B+ 树只需要去遍历叶子节点就可以实现整棵树的遍历

索引失效

  1. 违反最左匹配原则

    最左匹配原则:最左优先,以最左边的为起点任何连续的索引都能匹配上,如不连续,则匹配不上。

    如:建立索引为 (a,b) 的联合索引,那么只查 where b = 2 则不生效。换句话说:如果建立的索引是 (a,b,c),也只有 (a),(a,b),(a,b,c) 三种查询可以生效

  2. 在索引列上做任何操作

    如计算、函数、(手动或自动)类型转换等操作,会导致索引失效而进行全表扫描。

  3. 使用不等于(!= 、<>)

  4. like 中以通配符开头 (’%abc’)

    索引失效

    1
    explain select * from user where name like '%zhangsan';

    索引生效

    1
    explain select * from user where name like 'zhangsan%;
  5. 字符串不加单引号索引失效

    1
    explain select * from user where name = 2000;
  6. or 连接索引失效

    1
    explain select * from user where name = '2000' or age = 20 or pos ='cxy';
  7. order by

    正常(索引参与了排序),没有违反最左匹配原则

    1
    explain select * from user where name = 'zhangsan' and age = 20 order by age,pos;
  8. group by

    正常(索引参与了排序)

    1
    explain select name,age from user where name = 'zhangsan' group by age;

    违反最左前缀法则,导致产生临时表(会降低性能)。

    1
    explain select name,age from user where name = 'zhangsan' group by pos,age;

SpringBoot 运行原理

Spring 启动流程

  1. 监听开始启动
  2. 创建上下文, createApplicationContext默认创建Servlet上下文,可以指定webApplicationType类型
  3. 准备上下文,准备上下文的外部化配置等
  4. 启动上下文,其实就是运行Spring的Application的refresh启动上下文,并扫描和注册Spring组件
  5. 监听已启动
  6. 监听开始运行

Spring 自动装配

spring.factories里配置了默认的自动装配类,会根据条件进行选择装配

Dubbo 服务挂了,如何实现重试?

如果注册中心挂了,消费者会从本地缓存里读取服务提供者的地址,进行通讯,直到注册中心恢复,然后重新去注册中心获取服务列表,

保证服务的高可用。

K8s的作用,如何编排

容器编排平台, 我们将单体式的架构拆分成越来越多细小的服务,运行在各自的容器中,那么该如何解决它们之间的依赖管理,服务发现,资源管理,高可用等问题

在容器环境中,编排通常涉及到三个方面:

  • 资源编排 - 负责资源的分配,如限制 namespace 的可用资源,scheduler 针对资源的不同调度策略;
  • 工作负载编排 - 负责在资源之间共享工作负载,如 Kubernetes 通过不同的 controller 将 Pod 调度到合适的 node 上,并且负责管理它们的生命周期;
  • 服务编排 - 负责服务发现和高可用等,如 Kubernetes 中可用通过 Service 来对内暴露服务,通过 Ingress 来对外暴露服务。

G1和CMS的区别

CMS是并发清除,清除死亡对象,所以不需要暂停用户线程,但是会产生空间碎片;

按分代收集来说,CMS是老年代收集器,G1则是混合收集,它开创了混合收集的模式,衡量标准不在是属于哪个分代,而是哪块内存值得收集,哪块内存中存放的垃圾数量最多,回收收益最大来进行收集。

G1将堆内存分为多个大小相同的独立区域Region,并将其作为单次回收的最小单元;

G1解决跨区域引用,每个Region都需要一个卡表,需要使用写后屏障来维护卡表;

按收集算法来说,CMS收集器是基于标记-清除的垃圾收集器,由于CMS是一款基于“标记-清除”算法实现的收集器,就会造成大量空间碎片产生,如果空间碎片过多时,当需要足够大大连续空间来分配大对象大时候,会不得不提前触发Full GC的情况;而G1从整体来看是基于“标记-整理”算法实现的收集器,从局部上看(两个Region之间)又是基于“标记-复制”算法实现,无论如何,这两种算法在运行期间都不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存

Mysql数据库的默认隔离级别

默认隔离级别是:REPEATABLE_READ

gRPC框架的原理,序列化在哪一层

gRPC 默认使用 Protocol Buffers 作为 RPC 序列化框架,通过 Protocol Buffers 对消息进行序列化和反序列化,然后通过 Netty 的 HTTP/2,以 Stream 的方式进行数据传输。

由于存在一些特殊的处理,gRPC 并没有直接使用 Netty 提供的 Protocol Buffers Handler, 而是自己集成 Protocol Buffers 工具类进行序列化和反序列化,下面一起分析它的设计和实现原理。

gRPC 如何实现服务发现以及使用的什么负载均衡策略

服务发现与负载均衡
当server端是集群部署时,client调用server就需要用到服务发现与负载均衡。通常有两总方式:

一种方式是在client与server之间加代理,由代理来做负载均衡
一种方式是将服务注册到一个数据中心,client通过数据中心查询到所有服务的节点信息,然后自己选择负载均衡的策略