1、购物车实现流程

1.1、购物车与用户的关系?

  • 一个用户必须对应一个购物车。一个用户不管买多少商品,都会存放在属于自己的购物车中
  • 单点登录一定在开发购物车之前。

1.2、跟购物车有关的操作有哪些?

  • 添加购物车

    • 用户未登录状态的添加
    1. 存放在 Redis 中(京东商城,可以以UUID作为唯一标识)

    2. 存放在 Cookie

    3. 存放在浏览器的 localstorage

    • 用户登录状态的添加(两份)
    1. 存在 Redis 缓存中【读写数据快】
    2. 为了保障数据安全性,我们需要将数据存储在数据库中。
  • 展示购物车

    • 未登录状态的展示

    直接从 CookieRedislocalstorage 中获取数据展示即可。

    • 登录状态的展示

    用户一旦登录,我们必须获取数据库【Redis】与该用户未登录之前添加的全部商品信息。

2、Java类加载器及双亲委派机制

2.1、Java类加载器

JDK自带三个类加载器,从上而下分别是 BootStrapClassLoader(C++实现)、ExtClassLoaderAppClassLoader

BootStrapClassLoaderExtClassLoader 的父类加载器,默认负责加载 %JAVA_HOME%lib包下的jar包和class文件。(rt.jar由此类加载器加载)

ExtClassLoaderAppClassLoader的父类加载器,负责加载 %JAVA_HOME% lib/ext文件夹下的jar包和class类

AppClassLoader是自定义类加载器的父类,负责加载 classpath 下的类文件。(加载程序员写的代码、引入的jar由此类加载器加载),可以由ClassLoader.getSystemClassLoader()获取

继承 ClassLoader 实现自定义类加载器。

2.2、双亲委派机制

如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;

只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

image-20210329201213650

2.3、双亲委派机制工作流程

  • Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
  • Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
  • 如果Bootstrap ClassLoader加载失败(在\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
  • 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
  • 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
  • 如果均加载失败,就会抛出ClassNotFoundException异常。

2.4、双亲委派机制作用

  • 避免类的重复加载。当父类加载器加载了该类时,子类加载器就不会再加载一次。
  • 防止java核心API被随意替换。

我们可以编写一个 java.lang.String 类替换原有的String类吗?

不行,由于有双亲委派机制的存在, Application ClassLoader 不会直接加载我们自定义的类,而是会委托其父类加载器 Extension ClassLoader 加载,而 Extension ClassLoader 又会委托其父类加载器,即 Bootstrap ClassLoader 加载,由于 java.lang.Stringrt.jar 下的类,即系统中的类,所以 Bootstrap ClassLoader 会直接加载系统中的 java.lang.String,而且由于 String 类此时已经被引导加载器记载过,所以不会再加载我们自定义的String类

3、BeanFactoryApplicationContext有什么区别?

  • ApplicationContextBeanFactory 的子接口
  • ApplicationContext提供了更完整的功能
  1. 继承 MessageSource 类,因此支持国际化
  2. 统一的资源文件访问方式。
  3. 提供在监听器中注册Bean的事件
  4. 同时加载多个配置文件
  5. 载入多个上下文

BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用 get bean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的 Spring的配置问题。如果Bean的某一个隅性没有注入, BeanFacotry加载后,直至第一次使用调用 getBean方法才会抛出异常

ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean确保当你需要的时候,你就不用等待,因为它们已经创建好了。

相对于 BeanFactoryApplicationContext唯一的缺点是占用内存空间。当应用程序配置较多 Bean 时,程序启动较慢。

4、简述 Mybatis 的插件运行原理,如何编写一个插件?

4.1、原理

Mybatis 只支持针对 ParameterHandler(设置预编译参数用的) 、ResultSetHandler(处理结果集) 、StatementHandler(处理SQL预编译,设置参数等相关工作)和 Executor(执行增删改查操作) 这四个接口的插件,使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这四个接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandlerinvoke 方法,拦截哪些你指定需要拦截的方法。

4.2、编写插件

实现 MybatisInterceptor 接口,并重写其中的 intercept() 方法,然后再给插件编写注解,指定要拦截哪一种接口的哪些方法即可,需要在配置文件中配置编写的插件。

MyBatis定义插件要实现Interceptor接口,这个接口只声明了三个方法:

  1. intercept:定义拦截的时候要执行的方法
  2. setProperties: 在MyBatis进行配置插件的时候可以配置自定义相关属性
  3. plugin: 插件用于封装目标对象,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理.
  • Mybatis 官方推荐使用 @Intercepts注解来实现拦截器插件的定义,例如
1
2
3
4
5
6
7
@Intercepts(
@Signature(
type=StatementHandler.class,
method="prepare",
args={Connection.class, Integer.class}
)
)
  • 分页插件思路

拦截并获取查询的原始SQL,然后拼装成 countSQLpageSQL,再进行查找取值。而如何获取查询的SQL,就需要使用我们上面分析的拦截器来获取了。

5、缓存雪崩、缓存穿透和缓存击穿

5.1、缓存雪崩

缓存雪崩是指缓存在同一时间内大面积地失效,所以,后面的请求都会落在数据库上,造成数据库在短时间内承受大量请求而崩溃。

除以上原因,缓存重启也可能造成缓存雪崩。

解决方案

  • 缓存数据的过期时间设置为随机,防止同一时间内大量数据过期现象发生。
  • 给每一个缓存数据增加响应的缓存标记,记录缓存是否失效,如果缓存标记失效,那么更新缓存。
  • 缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。

这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

5.2、缓存穿透

缓存穿透是指缓存和数据库中都没有请求需要的数据,导致所有的请求都落在数据库上,造成数据库短时间内承受大量的请求而崩溃

解决方案

  • 在接口层增加校验,如用户鉴权校验或者参数校验,不合法参数直接不予请求。
  • 从缓存中拿不到的数据,在数据库中也没有正常取到,这是可以将 key-value 设置为 key-null ,缓存有效时间可以设置短点,这样可以防止攻击用户反复用一个ID暴力攻击

5.3、缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  • 设置热点数据永不过期
  • 加互斥锁(互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性)

6、深拷贝与浅拷贝

在Java中,当我们需要拷贝一个Java对象时,常见的会有两种方式的拷贝:深拷贝和浅拷贝。

6.1、浅拷贝

浅拷贝只是拷贝了源对象的地址,所以源对象的任何值发生变化时,拷贝对象的值也会随之发生变化。给了你一把原来房子的钥匙,你对这房子做的一切工作都会改变原来的房子。

浅拷贝带走的仅仅是这个对象的地址

image-20210329230548886

下面演示一下浅拷贝

  • 创建一个User类
1
2
3
4
5
@Data
public class User {
private String username;
private String password;
}
  • 进行浅拷贝,可以看到此时源对象和拷贝对象的地址一致
1
2
3
4
5
6
User user = new User();
user.setUsername("芜湖");
user.setPassword("qifei");
//进行浅拷贝
User copyUser = user;
System.out.println(user == copyUser);

查看结果

image-20210329231130175

  • 此时改变 copyUser 的值,然后分别输出以上两个对象的值
1
2
3
4
5
6
7
8
9
10
11
12
User user = new User();
user.setUsername("芜湖");
user.setPassword("qifei");
//进行浅拷贝
User copyUser = user;
System.out.println(user == copyUser);
System.out.println("源对象的值原为:" + user);
System.out.println("---------修改拷贝对象值-----------");
copyUser.setUsername("芜湖起飞");
copyUser.setPassword("起飞!");
System.out.println("源对象的值现在为:" + user);
System.out.println("拷贝对象值现为:" + copyUser);

查看结果,可以看到拷贝对象的值发生变化后,源对象的值也跟着发生变化。

image-20210329231425208

6.2、深拷贝

深拷贝则是拷贝了源对象的所有值而不是地址,所以即使源对象的值发生任何变化时,拷贝对象的值也不会改变。给你建了一座新房子,你对新房子做什么,原来房子里的东西都不会发生改变。

深拷贝中,拷贝对象和源对象不再关联,而是彻底隔离。

image-20210329231619131

  • 常用的几种深拷贝范式
  1. 构造函数方式

image-20210329231905577

  1. 重写clone方法
  2. Apache Commons Lang 序列化
  3. Gson 序列化
  4. Jackson 序列化
  • 使用 Object 类下的 clone 方法深拷贝对象

image-20210330155327714

User 类实现 Cloneable 接口,并重写 Object 类下的 clone 方法,将方法修饰符修改为 public

1
2
3
4
5
6
7
8
9
@Data
public class User implements Cloneable{
private String username;
private String password;
@Override
public User clone() throws CloneNotSupportedException {
return (User)super.clone();
}
}

使用 clone 方法进行深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
User user = new User();
user.setUsername("芜湖");
user.setPassword("qifei");
//使用clone方法进行深拷贝
User copyUser = user.clone();
System.out.println("两对象的地址值是否相等?" + (user == copyUser));
System.out.println("源对象的值原为:" + user);
System.out.println("---------修改拷贝对象值-----------");
copyUser.setUsername("芜湖起飞");
copyUser.setPassword("起飞!");
System.out.println("源对象的值现在为:" + user);
System.out.println("拷贝对象值现为:" + copyUser);

此时输出的结果为:

image-20210330160421800

7、接口和抽象类

1、抽象类

  • 抽象类可以有抽象方法,也可以有普通方法

抽象类中不一定要有抽象方法,但有抽象方法的一定是抽象类。

  • 继承抽象类,那么必须实现抽象父类的所有抽象方法,否则需要将子类也声明为抽象类。

2、接口

  • 接口中定义的方法都是抽象方法,均以 public 修饰,在JDK8后,可以在接口中编写有方法体的方法,但此方法必须以 default 关键字修饰,实现此接口的类会自动实现该 default 方法
1
2
3
4
5
public interface TestInteface {
public default void say() {
System.out.println("我是接口!!!");
}
}
  • 接口中还可以有静态方法,具体使用与类的静态方法一致。
1
2
3
4
5
public interface TestInteface {
public static void wuhu() {
System.out.println("芜湖");
}
}
  • 接口中定义的变量均为 静态常量

3、二者区别

  • 抽象类属于类,是单继承的。
  • 接口可以多继承,且一个类可以实现多个接口。
  • 接口可以看为一个特殊的抽象类

8、Spring 事务的传播行为

传播行为意义
PROPERGATION_MANDATORY(强制的)表示方法必须运行在一个事务中,如果当前事务不存在,就抛出异常
PROPAGATION_NESTED表示如果当前事务存在,则方法应该运行在一个嵌套事务中。否则,它看起来和 PROPAGATION_REQUIRED 看起来没什么俩样
PROPAGATION_NEVER表示方法不能运行在一个事务中,否则抛出异常
PROPAGATION_NOT_SUPPORTED表示方法不能运行在一个事务中,如果当前存在一个事务,则该方法将被挂起
PROPAGATION_SUPPORTS表示当前方法不需要运行在一个是事务中,但如果有一个事务已经存在,该方法也可以运行在这个事务中
PROPAGATION_REQUIRED表示当前方法必须运行在一个事务中,如果当前存在一个事务,那么该方法运行在这个事务中,否则,将创建一个新的事务
PROPAGATION_REQUIRES_NEW表示当前方法必须运行在自己的事务中,如果当前存在一个事务,那么这个事务将在该方法运行期间被挂起

9、排序算法的复杂度一览

image-20210422191044134