https://javaguide.cn/

https://itwanger.gitee.io/tobebetterjavaer/#/

https://pdai.tech/

一 基础

1 JavaSE

https://javaguide.cn/java/basis/java-basic-questions-01.html

  • StringBuilderStringBuffer:后者是线程安全的,每个公开方法都被synchronized 修饰了

hashMap新增元素

image-20240314172246695


ThreadLocal内存泄漏

image-20240315170346022

  • 一个Thread里面有多个ThreadLocal对象,每个ThreadLocal对象对应一个值,所有的ThreadLocal放在ThreadLocalMap中,里面有一个Entry数组,根据ThreadLocal对象的哈希值来确定在数组中的位置。
  • 内存泄露的根本原因:

image-20240315170633057

  • ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
  • 如果ThreadLocal没有外部强引用,那么在发生垃圾回收的时候,ThreadLocal就必定会被回收,而ThreadLocal又作为Map中的key,ThreadLocal被回收就会导致一个key为null的entry,外部就无法通过key来访问这个entry,垃圾回收也无法回收,这就造成了内存泄漏
  • 如何防止:ThreadLocalremove()方法
    • 在get和set的方法中可能会调用这个remove方法
    • ThreadLocal虽然提供了避免内存泄露的方法,但是ThreadLocal不会主动去执行这些方法,需要我们在使用完ThreadLocal对象中保存的数据后,在finally{}代码块中调用ThreadLocal的remove()方法,加快GC自动垃圾回收,避免内存泄露。

synchronized底层原理

  • Java 对象底层都会关联一个 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。
  • 执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。cc

重写equals方法的时候为什么一定要重写hashcode方法?

  • 通常判断两个对象是否相等会先判断hashcode()返回值,如果不等则一定不等,相等了就继续判断equals()——性能
  • 以HashMap举例,当put元素的时候:
    • 会先计算对象的hashcode(),而Object方法中的hashcode()对比的是不同对象的地址,所以结果一定是false,导致这个元素被认为是新的key被添加,就会出现重复key的情况
  • Set也是这样

获取.Class方式

1
2
3
4
Class alunbarClass = TargetObject.class; // 知道了具体的类
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); // Class.forName传入全路径
Class alunbarClass2 = new TargetObject().getClass(); // 通过对象实例getClass()方法
Class alunbarClass3 = ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject"); // classLoader

ArrayList 扩容?

  • grow()函数

    1
    2
    3
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 每次扩容1.5倍
    // 如果小于最小容量,就设置newCapacity为最小容量
    // 如果大于最大容量,则更新最大容量

RandomAccess 接口

  • 一个标记接口,表明实现该接口的类支持随机访问

hashMap 扩容

  • resize():会伴随一次重新的hash分配,遍历hash表中的所有元素,非常耗时
  • 扩容为原来的两倍

ConcurrentHashMap

  • 1.7之前
    • 由多个segment数组组成,每个segment是一个类似于HashMap的结构,可以扩容,但是segment数组不能扩
    • 默认是16个segment,也就是默认的并发数量16
  • 1.8及之后
    • node数组+链表/红黑树

CopyOnWriteArrayList

  • 写时复制,只有写写操作的时候才会互斥

为什么String不可变

  • 线程安全
  • 缓存Hash值,将String作为Key在hash表中查找时,由于hashCode不变,所以查找速度快
  • 安全性,如果可以变的话会被攻击(密码),因为string不可变,所以对应的hash也不可变
  • 避免重复创建字符串,提高效率

2 Spring

Spring、Spring boot、Spring MVC

  • Spring是一个IOC容器,用来管理Bean,使用依赖注入来实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补面向对象的OOP的代码重复问题,更方便的将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等
  • Spring MVC是spring对web框架的一个解决方案,提供了一个总的前端控制器servlet:DispatcherServlet,用来接收请求,然后定义了一套路由策略(从url到handle 的映射)以及适配执行handle,将handler结果使用视图解析技术生成视图展现给前端
  • springboot是spring提供的一个快速开发工具包,让程序员更方便、更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制)、redis、mongodb、es可以开箱即用

为什么SpringBoot的jar包可以直接运行

  • 引入了spring-boot-maven-plugin
  • springBoot程序打包后生成了一个 Fat jar(jar中包含jar),包含了依赖的jar包和springboot loader相关类
  • java -jar会去找jar中的mainfest文件,在那里找到启动类

SpringBoot如何支持跨域请求

  • CORS:跨源资源共享

    • Controller方法上@CrossOrigin("http://localhost:8081")设置某一个接口允许跨域

    • 实现WebMvcConfigurer,重写其中的addCorsMappings(CorsRegistry registry)方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Configuration
      public class MyWebMvcConfigurer implements WebMvcConfigurer {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
      registry.addMapping("/user/*")
      .allowedOrigins("http://localhost:8080")
      .allowedMethods("GET", "POST", "PUT", "DELETE");
      }
      }
    • 通过配置一个bean控制所有的请求

  • Nginx反向代理

    • 比如前端要发送跨域的请求就统一加上一个 /cors/

    • 在nginx中配置location

      1
      2
      3
      location /cors/ {
      proxy_pass http://localhost:8080/user/;
      }

spring AOP

  • AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
  • 为减少代码冗余,使开发专注于核心业务逻辑,减少代码维护成本而提出的一种思想,是通过动态代理的方式来实现的,将需要注入切面的对象进行代理,在进行调用的时候,将公共的逻辑直接添加进去,而不需要修改原有业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。
    • 切面 ==> 公共代码:通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和在何处完成其功能
    • 切点: 定义了需要执行在哪些连接点上执行通知。
    • 连接点:是在应用执行过程中能够插入切面的一个点。
    • 引入:引入允许我们向现有的类添加新方法或属性
    • 织入:把切面应用到目标对象并创建新的代理对象的过程
    • 通知:定义了何时,做什么。@After @Around @Before @AfterReturning @AfterThrowing

spring IOC

  • 使用的interface ApplicationContextBeanFactory的子类
  • 继承了多个接口扩展功能
  • 有两个具体实现的子类

    • ClassPathXmlApplicationContext:从class path 中加载配置文件,更常用一些;
    • FileSystemXmlApplicationContext :从本地文件中加载配置文件,不是很常用,如果再到 Linux 环境中,还要改路径,不是很方便。
  • 通过对象的set方法注入的

image-20240318205523668

  • 如何实现一个IOC容器

    1、先准备一个基本的容器对象,包含一些map结构的集合,用来方便后续过程中存储具体的对象

    2、进行配置文件的读取工作或者注解的解析工作,将需要创建的bean对象都封装成BeanDefinition对象存储在容器中

    3、容器将封装好的BeanDefinition对象通过反射的方式进行实例化,完成对象的实例化工作

    4、进行对象的初始化操作,也就是给类中的对应属性值就行设置,也就是进行依赖注入,完成整个对象的创建,变成一个完整的bean对象,存储在容器的某个map结构中

    5、通过容器对象来获取对象,进行对象的获取和逻辑处理工作

    6、提供销毁操作,当对象不用或者容器关闭的时候,将无用的对象进行销毁

注意:依赖注入是实现IOC的一个方法


bean生命周期

  • Bean容器找到配置文件中的bean,通过反射创建,用set()方法赋值
  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
  • 如果实现了 *.Aware接口,就调用相应的方法。
    • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
    • 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
  • 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法

  • 使用

  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
  • 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。

image-20240318205941719


spring中的设计模式

  • 工厂模式
    • BeanFactory
    • ApplicationContext
  • 单例模式
    • bean的默认作用域就是单例
  • 代理模式
    • AOP动态代理
  • 模板方法设计模式
    • jdbcTemplate
    • redistemplate
  • 观察者设计模式
  • 适配器设计模式
    • 在Spring MVC中,DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler,解析到对应的Handler(也就是我们常说的Controller控制器)后,开始由HandlerAdapter适配器处理
  • 装饰者设计模式
    • Spring 中配置DataSource的时候,DataSource可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下切换不同的数据源?这个时候据需要用到装饰者模式。

Spring 依赖注入的方式

  • 能注入的数据类型
    • 基本类型+String
    • 其他的bean
    • 复杂类型/集合类型
  • 注入方式
    • 使用构造函数
    • set方法
    • 注解

自动装配 - SpringBoot约定大于配置的理念的产物

  • 依赖注入的plus,简化了依赖注入的配置而生成的

  • 主启动类上的注解@SpringbootApplication是一个复合注解,其中比较重要的

    • @SpringbootConfiguration:springboot的相关配置
    • @EnableAutoConfiguration
    • @ComponentScan:扫描一些包并注入
  • 重点@EnableAutoConfiguration,也是一个复合注解
    • @import(AutoConfigurationImportSelect.class)
  • AutoConfigurationImportSelect implements了DeferredImportSelector

    • 因为是Deferred,所以自动配置类会放在最后加载,方便扩展和覆盖

    • 其中重写了selectImports()方法:获取所有符合条件的类的全限定类名,加载到IOC容器中

      • 判断是否开启了自动装配:spring.boot.enableautoconfiguration=true

      • 获取@EnableAutoConfiguration中的excludeexcludeName

      • 读取所有spring boot start下面的classpath:/META-INF/spring.factories文件(key-value形式)

      • 通过@ConditionalOn排除无效的自动配置类

  • 配置文件的信息如何加载到bean中:classpath:/META-INF/spring.factories文件存储了一些键信息(SpringBoot约定大于配置的理念),然后加载的时候配置文件的值就被加载到这个文件中了
  • 两种
    • @Autowired:根据类型自动注入
      • @Qualifier:格外指定bean的id(当IOC根据属性类型去容器中找找到多个相同类型的组件的时候需要使用)
    • @Resource:根据bean的名称自动注入
      • 这个是Java规范

代理

  • 不直接访问目标类,而是通过访问代理类来实现目标类方法,形成方法增强

  • 静态代理

    • 预先编写,执行前有了编译好的字节码文件
  • 动态代理
    • JDK动态代理:
      • Proxy 类的newProxyInstance()来生成代理对象
      • 实现invocationHandler,重写invoke()方法实现代理类方法的增强
      • 通过反射执行
      • getInstance()传入代理对象 生成代理类
    • CGLIB(Spring AOP默认)
      • Enhancer类的create()方法创建代理类
      • 实现MethodInterceptor,重写intercept()方法实现增强
      • why Spring AOP默认:非接口代理
  • 对比jdk和cglib
    • 都是运行期间生成字节码,jdk直接生成class字节码,cglib使用ASM框架写的class字节码。后者更加复杂,代理类生成效率更低
    • jdk动态代理是通过反射机制来执行方法,cglib是通过FastClass机制(索引分配直接调用)直接调用方法,后者动态代理类执行效率更高

spring boot 常用注解

  • @RestController <==> @Controller + @ResponseBody :返回的是json,不能返回html页面
  • @ResquestMapping@PutMapping@PGetMapping@PostMapping@DeleteMapping
  • @PathVarible
  • @Value@Autowired@Resource

  • @ControllerAdvice@ExceptionHandler

  • @Configuration@Component@Service@Mapper

  • @Transactional
  • @FeignClient

Spring MVC 过程

image-20240308102514363

  • filter:在1和11过程中发挥作用
  • interceptor
    • preHandle()方法在4之前的时候执行
    • postHandle()在9之前完成
    • afterCompletion()在10渲染完成后执行
  • 过滤器和拦截器一般使用
    • 前者一般用于过滤敏感词汇(sql注入)、设置字符编码、URL权限访问控制、压缩响应信息
    • 后者一般用于登录验证(JWT)、访问资源权限验证、日志记录、处理cookie、本地化/国际化/主题、性能监控:请求处理时长

spring 三级缓存

  • 解决循环依赖

    • 前提:单例bean;依赖注入的方式不能全是构造器注入的方式
  • 一级缓存singletonObjects:已经初始化好的bean,即已经完成初始化好的注入对象的代理

  • 二级缓存earlySingletonObjects:还没有完全被初始化好的中间对象代理

  • 三级缓存singletonFactory:存放的是还未初始化完的bean,不是代理对象

  • 过程:https://www.cnblogs.com/daimzh/p/13256413.html

    image-20240321165705162

spring的事务

  • 编程式事务:通过 TransactionTemplate或者TransactionManager手动管理事务,少用
  • 声明式事务:基于AOP实现,一般使用@Transactional 的注解

spring 事务失效

  • bean对象没有被容器管理
  • 方法修饰符不是public
  • 自身调用自身方法
  • 数据源没有配置事务管理器
  • 数据库不支持事务
  • 异常被捕获
  • 异常类型错误或配置错误

3 SpringCloud

  • 配置中心与注册中心:Nacos
    • eureka注册中心
  • 统一网关:Gateway
  • Feign的远程调用
    • 熔断处理
  • CAP:

    • Consistency(一致性):任意时刻都一致
    • Availability(可用性):任意时刻都可用
    • Partition tolerance(分区容忍性):由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉
    • CP:秒杀
    • CA:银行
  • BASE理论

    • Basically Available:基本可用
    • Soft-state:软状态
    • Eventually Consistent:最终一致

事务一致性

  • 事务四大特:原子性、一致性、隔离性、持久性
  • 事务一致性是指数据库处理前后结果应与其所抽象的客观世界中真实状况保持一致。
    • 银行AB转账,总和为100(A50 B50),转账后不可能为A-50 B150

数据一致性

  • 根本原因在于数据的复制
  • 强一致性:CP
    • 主库更新后会一直等待所有从库更新再返回更新成功。保证了主从一直一致,保证了数据的完整性
    • 银行
  • 弱一致性:AP: 最终一致性、读写一致性、单调读、因果一致性
    • 主库更新后就直接返回更新成功
    • 互联网场景一般使用这个来保证高可用

4 Git

image-20240307145205270

  • rebase和merge

    • merge就是直接合

    • rebase会分块

      image-20240307145801991

5 Linux

  • 切换用户

    1
    2
    su yao               #切换为用户"yao",输入后回车需要输入该用户的密码
    exit #退出当前用户
  • 目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    cd # 切换
    ls # 查看
    ll # 查看详细信息
    mkdir # 创建
    rm # 删除remove
    mv # 移动move
    cp # 拷贝 copy
    find # 搜索
    pwd # 显示当前路径
  • 文件

    1
    2
    3
    4
    5
    rm
    mv
    vi # 编辑
    vim # 编辑 更高级
    chmod # 授权
  • 权限说明

    1
    # 'r' 代表可读(4),'w' 代表可写(2),'x' 代表执行权限
  • 压缩与解压

    1
    2
    3
    4
    5
    6
    #  .zip、.rar        //windows系统中压缩文件的扩展名
    # .tar //Linux中打包文件的扩展名
    # .gz //Linux中压缩文件的扩展名
    # .tar.gz //Linux中打包并压缩文件的扩展名
    tar -zxvf a.tar #解包至当前目录
    tar -zcvf a.tar file1 file2,... #多个文件压缩打包
  • 安装

    1
    2
    3
    4
    5
    6
    rpm -ivh httpd-2.2.3-22.0.1.el5.i386.rpm    # 使用rpm文件安装apache 
    rpm -uvh httpd-2.2.3-22.0.1.el5.i386.rpm # 使用rpm更新apache
    rpm -ev httpd # 卸载/删除apache
    yum install httpd # 使用yum安装apache
    yum update httpd # 更新apache
    yum remove httpd # 卸载/删除apache
  • 运行docker

    1
    systemctl start docker

文件描述符File descriptor(fd)

  • 内核为了高效管理这些已经被打开的文件所创建的索引
  • 所有执行IO操作的系统调用都通过文件描述符来实现
  • 0是标准输入,1是标准输出,2是标准错误
  • 打开一个文件,它的文件描述符会是3,再打开一个文件文件描述符就是4…

6 Docker

  • 镜像(image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。

  • 容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。

  • 通过命令启动docker:

    1
    2
    3
    4
    5
    systemctl start docker  # 启动docker服务

    systemctl stop docker # 停止docker服务

    systemctl restart docker # 重启docker服务
  • 镜像操作

    1
    2
    3
    4
    5
    docker push # 推送镜像
    docker pull # 从服务器拉取镜像
    docker build # 构建镜像
    docker save # 保存镜像为压缩包
    docker load # 加载压缩的镜像
  • 容器操作

    1
    2
    3
    4
    5
    6
    7
    8
    docker run # 镜像加载为容器
    docker exec # 进入容器执行命令
    docker logs # 查看日志
    docker ps # 查看所有运行的容器
    docker pause # 暂停
    docker stop # 容器停止
    docker start # 容器启动
    docker rm # 删除

7 Redis:NoSQL 数据库

  • 见苍穹和学成
  • 主从和哨兵可以解决高可用、高并发读的问题

redis数据类型

  • 5种基本数据类型:

    • String

    • List:双向链表数据结构

    • Hash:类似于HashMap(数组+链表)还做了些优化,一个key下面有多个键值对

      image-20240321105748037

    • Set:类似于HashSet

    • Zset:有序的set

    1
    2
    3
    4
    5
    SET、 MSET(批量) 、GET 、SETNX 、SETEX 、EXISTS 、DEL 、EXPIRE key seconds 								-- String
    RPUSH(尾部/右边添加元素)、LPUSH 、LSET key index value(设置值,指定了索引)、LPOP、RPOP(移除并获取)、LLEN(长度) -- List
    HSET key field value、HMSET key field1 value1 field2 value2...、HGET key field、HEXISTS key field、 -- Hash
    SADD key member1 member2 ...、 -- Set
    SETBIT key offset value、GETBIT key offset、BITOP operation destkey key1 key2...(位运算,and or nor..) -- Bitmap
  • 3种特殊数据类型:

    • Bitmap(位图) SETBIT GETBIT:就是一个01的数组

      image-20240321113223104

    • HyperLogLog(基数统计)

    • Geospatial (地理位置)

为什么快?

  • 基于内存存储
  • 单线程事件循环和 IO 多路复用
  • 内置了多种优化过后的数据类型/结构实现,性能非常高

redis的IO多路复用

  • 多路指多个网络连接客户端,复用指同一个进程/线程,redis的I/O 多路复用其实是使用一个线程来检查多个Socket的就绪状态,在单个线程中通过记录跟踪每一个socket(I/O流)的状态来管理处理多个I/O流
  • Reactor设计模式
  • 当一个客户端建立连接的时候,会生成一个套接字描述符(是文件描述符fd的一种)
  • redis将这个fd注册到监听表中,监听多个fd的读写状态
  • 当有事件产生的时候I/O 多路复用模块就会将那些产生了事件的套接字fd传送给文件事件分派器。
  • 文件事件分派器接收到I/O多路复用程序传来的套接字fd后,并根据套接字产生的事件类型,将套接字派发给相应的事件处理器来进行处理相关命令操作

缓存读写策略

  • Cache Aside Pattern 旁路缓存模式:适合读比较多的情况
    • 写:先更新db后删除cache
    • 读:先从cache读,没有数据再从db读,再写入数据到cache
  • Read/Write Through Pattern 读写穿透 (减小db压力)
    • 写:先查cache,不存在则更新db;存在则更新cache,然后cache服务自己更新db(同步)
    • 读:从cache读,读到则返回;没读到则从db加载到cache再返回
  • Write Behind Pattern 异步缓存协议
    • 写:与上不同之处在于cache服务自己更新db是批量更新(异步)
    • 读:同上

redis的应用场景

  • 缓存
  • 分布式锁:互斥、高可用、可重入、高性能、非阻塞
    • SETNX lockKey uniqueValue 来获取锁; DEL lockKey 来释放锁
    • 为了防止误删到其他锁,可以使用lua脚本通过key value来判断一下是不是这个锁
    • 为了防止程序突然挂掉,需要给锁设置一个失效时间:SET lockKey uniqueValue EX 3 NX——3s过期,也可以用PX(单位是ms)
  • 限流
    • redis+lua(为了保证操作的原子性)+AOP
  • 消息队列(没人用)
    • List 数据类型
  • 延时队列
    • Redisson 内置了延时队列,基于Sorted Set
  • 分布式session

常见实现

  • 购物车用Hash:用户id位key,商品id为field,商品数量为value

  • 排行榜用ZSet

  • 抽奖用Set:

    1
    2
    3
    SADD key member1 member2 ...:向指定集合添加一个或多个元素。
    SPOP key count:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
    SRANDMEMBER key count : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
  • 统计活跃用户:bitmap,日期作为key,用户id为offset(类似于数组下标),活跃就设置为1,没活跃就设置为0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    SETBIT 20210308 1 1
    SETBIT 20210308 2 1
    SETBIT 20210309 1 1
    // 用户1 两天都登录了, 用户2只在3.8登录

    BITOP and desk1 20210308 20210309
    BITCOUNT desk1 //总活跃人数:1(两天都登录)

    BITOP or desk2 20210308 20210309
    BITCOUNT desk2 // 在线活跃人数:2(只要登录就行)

redis有序集合为什么用跳表不用平衡树、红黑树、B+树

  • 平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n),但是插入和删除都要额外的维护平衡而旋转(耗时)
  • 红黑树需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
  • B+树更适合作为数据库和文件系统中常用的索引结构之一,它核心是为了减少IO来快速定位到索引,但是redis并不需要这个

image-20240321111144680

redis持久化

  • AOF(append only file) :appendonly yes开启 先写到AOF缓冲区 再写到系统内核缓冲区 最后写到AOF文件
    • 命令追加(append):所有的写命令会追加到 AOF 缓冲区。
      • 先执行命令后记录日志(mysql相反)
        • 避免额外的检查开销;不会阻塞当前的命令执行。
        • 风险是可能会发送数据丢失
    • 文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。这一步需要调用write函数(系统调用),write将数据写入到了系统内核缓冲区之后直接返回了(延迟写)。注意!!!此时并没有同步到磁盘。
    • 文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。这一步需要调用 fsync 函数(系统调用), fsync 针对单个文件操作,对其进行强制硬盘同步,fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。
      • fsync:file synchronization
      • 根据刷盘时机可以区分几种AOF方式:write后马上fsync,write后由后台线程fsync,write后由操作系统fsync(linux为30秒)
    • 文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
    • 重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
  • RDB(redis database):快照(快速恢复)
    • save: 同步保存操作,会阻塞 Redis 主线程;
    • bgsave:fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。

redis线程问题:

  • 单线程:文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗
  • 4.0之后加入了多线程的支持,但是针对大键值对的删除操作
  • 6.0之后就基本多线程了
  • 为什么不使用多线程
    • 单线程编程容易并且更容易维护;
    • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
    • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
  • redis后台线程
    • bio_close_file:释放 AOF / RDB 等过程中产生的临时文件资源。
    • bio_aof_fsync:调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
    • bio_lazy_free:后台线程释放大对象(已删除)占用的内存空间

redis的过期删除策略

  • 惰性删除:取出key的时候删
  • 定期删除:定期删

redis内存淘汰策略

  • LRU(最近最少使用)
  • LFU(最不经常使用)
  • Random
  • TTL(生存时间)
  • Maxmemory Policy(最大内存策略)

redis性能优化

  • 使用批量操作减少网络传输 MSET MGET HMGET HMSET SADD

  • 大量key的集中过期问题

    • key随机过期时间
    • 惰性删除
  • 避免使用bigkey(大key:value内存过大的key) --bigkeys查找

  • 对于hotkey(热点key) --hotkeys查找

    • 读写分离:主节点写,从节点读
    • 使用redis集群,将热点数据分散
    • 采用二级缓存,将 hotkey 存放一份到 JVM 本地内存中
  • 内存碎片

    • 原因

      • Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间
      • 频繁的修改reids的数据
    • 怎么清理

      1
      2
      3
      4
      5
      config set activedefrag yes
      # 内存碎片占用空间达到 500mb 的时候开始清理
      config set active-defrag-ignore-bytes 500mb
      # 内存碎片率大于 1.5 的时候开始清理
      config set active-defrag-threshold-lower 50

redis 生产问题

  • 缓存穿透:大量请求打在不存在的key
    • 缓存无效的key,设置过期时间
    • 布隆过滤器
    • 接口限流
  • 缓存击穿:热点key失效导致大量请求打在db
    • 热点数据不过期或者过期时间长
    • 针对热点数据提前预热,提前放到缓存
    • 锁:请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求落到数据库上,减少数据库压力
  • 缓存雪崩:大量key同时失效导致大量请求打在db
    • 针对redis服务器:集群;限流;多级缓存(本地+redis)
    • 针对缓存失效:随机失效时间;缓存预热;

缓存预热的方式

  • 分布式任务调度系统:xxl-job
  • 消息队列

如何保证redis和数据库的同步


8 ES

  • Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例

  • 倒排索引过程

    image-20240304153213090

  • 概念

    • 文档:一条完整的数据,包括索引、名称等,以json方式存储
    • 字段:文档中的具体字段,类似于表格的列
    • 索引:就是相同类型的文档的集合,类似于表。eg:用户的索引、商品的索引、订单的索引…
    • 映射(mapping):索引中字段的约束信息,类似于表的结构约束。
  • 安装ik分词器

  • 索引库的操作

    image-20240304154138185

  • 文档的操作

    image-20240304154206085

  • 搜索结果处理

    • 排序
    • 分页
    • 高亮
      • 给文档中的所有关键字都添加一个标签,例如<em>标签
      • 页面给<em>标签编写CSS样式
  • demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Test
    void testMatchAll() throws IOException {
    // 1. 准备Request对象,对应 GET /hotel/_search
    SearchRequest request = new SearchRequest("hotel");
    // 2. 组织DSL参数 对应 "query": {"match_all": {}}
    request.source().query(QueryBuilders.matchAllQuery());
    // 3. 发送请求,得到相应结果
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    System.out.println(response);
    }
    /*
    创建SearchRequest对象,指定索引库名
    利用request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等
    利用client.search()发送请求,得到响应
    关键API:
    一个是request.source(),其中包含了query、order、from、size、highlight等所有功能
    另一个是QueryBuilders,其中包含了match、term、function_score、bool等各种查询
    */
  • 数据聚合:用来实现对数据的统计分析计算等

9 RabbitMQ

  • 概念
    • channel:操作MQ的工具
    • exchange:路由消息到队列中
    • queue:缓存消息
    • virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
  • 常见模型
    • 基本消息队列
    • 工作消息队列
    • 发布订阅:广播/路由/主题
  • 异步通讯

image-20240306162652779

死信队列?如何导致?

  • DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
  • 死信的原因
    • 消息被拒
    • 消息过期
    • 队列满了无法添加

什么是延时队列

  • 延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
  • 实现
    • 使用死信交换机Exchange和设置消息的存活时间TTL
    • 插件
  • 场景
    • 订单到期未支付

如何确保消息100%不丢失?

  • 消息丢失可能发生于:消息生产阶段(生产者 -> MQ Broker)、消息存储阶段(MQ内部完成)、消息消费阶段(MQ Broker -> 消费者)
  • 解决:版本递增号校验(多生产端多消费端的时候失效)、全局唯一 ID
  • 具体实现:
    • 生产者 -> Broker:开启RabbitMQ事务,但是缺点是性能消耗大;使用confirm机制,这个是异步的
      • 代价比较大,需要在生产端额外写相关的确认代码,相对于消息丢失的概率来说代价昂贵
      • 消息入库:发送成功后生产端修改消息表的state为0,broker接收到后修改为1,生产端需要开一个定时器来检索消息表,确保发送成功
    • Broker内部:开启rabbitMQ的持久化机制,将消息持久化到磁盘上;设置MQ集群的镜像模式
    • Broker -> 消费者端:ACK确认机制

如何保证消息的顺序性

  • 拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;
  • 或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理
  • 用缓存来记录消息的消费情况

如何确保消息只会被消费一次(任务幂等性的问题)?

  • 建消息表(消息id,是否被消费)
  • 或者利用redis存储
  • 上述两个问题的关键在于消息全局id的唯一性:数据库自增主键、UUID、Redis,Twitter-Snowflake 算法等

  • 如何处理消息积压问题?

    • 与生产端没关系,一般中间件的性能也很高,所以一般问题出现在消费端

    • 扩容、降级一些非核心的业务

消息队列的作用

  • 通过异步处理提高系统性能(减少响应所需时间)
  • 削峰/限流:先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。
  • 降低系统耦合性。

消息队列带来的问题

  • 系统可用性降低
    • 需要考虑消息丢失或者说 MQ 挂掉等等的情况
  • 系统复杂性提高
    • 需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题
  • 一致性问题

10 Mybaits

Mybatis设计模式

  • 缓存模块:装饰器模式
  • 日志模块:适配器模式
  • SqlSessionFactory:工厂模式
  • Mapper接口:代理模式
  • SqlssionFactoryBuiler:建造者模式

xml中的常见标签

  • select insert update delete
  • <resultMap>、 <parameterMap>、 <sql>、 <include>、 <selectKey>
  • 动态sql trim|where|set|foreach|if|choose|when|otherwise|bind

Executor执行器类别

  • SimpleExecutor:执行一次销毁
  • ReuseExecutor:重复执行
  • BatchExecutor:批处理

工作原理/过程

  • 启动加载:SqlSessionFactory完成解析,保存相关配置
  • 通过SqlSession对象处理请求,先走二级缓存,再一级缓存最后走数据库
  • 交给StatementHandler来处理,通过ParameterHandler处理SQL中的占位符,通过ResultSetHandler处理结果集的映射

自定义sql的过程

  • 首先在mapper中自定义一个方法以及参数,可以用@Param标注
  • 在相应的xml中实现sql
    • if标签,when标签等
    • #{} 取参数
  • 四个一致:
    • Mapper接口名和xml文件名一致
    • Mapper中的方法名和xml中对应的id一致
    • Mapper中的方法返回值类型和xml中的resultType一致
    • Mapper全路径名和xml中的namespace一致

分页查询

  • PageHelper:原理是存储分页信息在ThreadLocal中,在执行sql前拦截器会读取到分页信息动态插入到sql中再执行

    image-20240224180047211


批量插入

  • 法一:循环插入:频繁建立和关闭连接,资源消耗大
  • 法二:forEach标签,通过拼接SQL语句的方式完成批量操作的。但是当拼接的SQL过多,导致SQL大小超过了MySQL服务器中max_allowed_packet变量的值时,会导致操作失败
  • 法三:批处理,效率最高,但是比较麻烦

image-20240224182755036

mapper:

image-20240224182807077


参数传递

  • xxxMapper.java中方法的形参名可以不与.xml一致,MyBatis 会按照参数的位置进行匹配而不是参数的名字,若有多个参数,则应该按顺序使用

  • 也可以使用@ParamList<TeachplanDto> selectTreeNodes(@Param("courseId") Long courseId, @Param("name") String name);指定mapper中的名字


多表联查

  • 定义好实体类,接口等

  • 在xml中定义resultMap的映射

    • property表示属性对应的是实体类的字段名称
    • column表示的是自己定义的属性值,与sql语句定义的字段名称相同
    • associationjavaType在一对一关联查询的时候使用
    • association里映射的是被关联查询的表和属性值
  • 编写主要的联查语句,resultMap中指定刚刚定义的id

    • 如果是一对多的话就用外连接 left join
    • 如果是多对多,需要一个中间表然后两个 left join
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <resultMap id="husbandAndWife" type="husband">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="wife" javaType="wife">
    <id property="id" column="wid"/>
    <result property="name" column="wname"/>
    </association>
    </resultMap>

    <select id="findByName" parameterType="string" resultMap="husbandAndWife">
    select h.id,h.name,w.id wid, w.name wname
    from t_husband h,t_wife w
    where h.id = w.id and h.name = #{name}
    </select>

11 MySQL

SQL: 是一种结构化查询语言(Structured Query Language),专门用来与数据库打交道,目的是提供一种从数据库中读写数据的简单有效的方法。

sql常用关键字:SELECT, INSERT,UPDATE,DELETE,BETWEEN,EXISTS,WHERE,FROM,AND,NOT,OR,LIKE,JOIN,ALTER

函数处理:AVG,COUNT,MAX,MIN,SUM

三种删除:Drop,Delete,Truncate(清空数据)

group by:分组

1
2
3
SELECT customer_id, SUM(order_amount) AS total_amount
FROM orders
GROUP BY customer_id;

子查询:用于嵌套查询,放在()里面

mysql索引:索引是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。

  • 优点:可以快速检索,唯一性保证(唯一索引)缺点:创建和维护耗时,存储需要消耗物理空间

    • 为什么可以加快?
      • 索引会按照列的值建立一个有序的数据结构(B/B+树),这样查找的时候就不用线性扫描表了
      • 由于只读取索引所在的数据页,大大减少了磁盘IO
  • 分类

    • 按照数据结构划分:BTree索引、hash索引、RTree 索引、全文索引

    • 按照底层存储方式划分:

      • 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB的主键索引就是

        • 默认是主键
        • 如果表没有定义主键,但有唯一非空索引,那么第一个唯一非空索引会被用作聚集索引。
        • 如果表既没有主键也没有唯一非空索引,InnoDB存储引擎会自动生成一个隐藏的聚集索引
      • 非聚簇索引(非聚集索引):不存在一起的索引

      image-20240320172122032

      • 区别
        • 聚簇索引只能有一个,后者可以有多个
        • 聚集索引存储记录是物理上连续存在
        • 聚集索引在叶子节点上存储的是数据
    • 按照引用维度划分

      • 主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个
        • 除了这个其他的索引都是二级索引,二级索引数据位置存储的是主键
      • 普通索引:加速查询
      • 唯一索引:加速查询 + 列值唯一(可以有 NULL)
      • 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值
      • 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
      • 全文索引:对文本的内容进行分词,一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替
1
2
3
4
CREATE INDEX idx_name(索引名) ON 表名 (字段名); # B-Tree 索引,默认索引
CREATE UNIQUE INDEX idx_name ON 表名 (字段名); # 唯一索引
ALTER TABLE 表名 ADD PRIMARY KEY (字段名); # 主键索引
ALTER TABLE 表名 ADD FULLTEXT INDEX idx_name (字段名);ALTER TABLE 表名 ADD FULLTEXT INDEX idx_name (字段名); # 全文索引
  • 索引的底层数据结构选型

    • hash表:快,但是不支持范围查询
    • 二叉查找树:性能很依赖其平衡性
    • AVL树(高度差不超过1):需要频繁地进行旋转操作来保持平衡,且一个节点只存一个数据,磁盘IO性能开销大
    • 红黑树:自平衡二叉查找树,平衡性稍弱(不追求完全的平衡)所以有些查询效率较低(多次IO),但是增删效率高
    • B树:所有节点既存放key又存放date
    • B+树:多路平衡查找树,更稳定快速。原因:数据存放在叶子节点,保证了其他节点能够存放更多的索引,大大压缩了树的高度,减少磁盘IO次数;且叶子节点之间用双向链表连接

索引的设计原则

  • 适合索引的列是出现在where字句中的列,或者连接子句中指定的列
  • 定义有外键的数据列一定要创建索引
  • 更新频繁的字段不要有索引
  • 大文本、大对象不要创建索引

MYSQL字段类型:数值类型、字符串类型(char/varchar)、日期时间类型

mysql架构

image-20240320164909208

  • 连接器: 身份认证和权限相关(登录 MySQL 的时候)。
  • 查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
  • 分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
  • 优化器: 按照 MySQL 认为最优的方案去执行。
  • 执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
  • 插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。

InnoDB和MyISAM

  • MyISAM是表级锁、不支持MVCC、不支持事务、不支持外键且数据库崩溃后数据不可恢复
  • MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。

数据库事务

  • 数据库事务是一个或一系列操作的最小逻辑单元
  • 特性:原子性、一致性、隔离性、持久性

并发事务的带来的问题

  • 读读:没有问题,不需要并发控制
  • 读写:脏读、不可重复读、幻读
  • 写写:丢失修改

如何控制?

  • 锁:
    • 共享锁(S锁)/读锁:读读并行
    • 排他锁(X锁)/写锁:如果有写就禁止其他线程读写了
  • MVCC:多版本并发控制方法,主要依赖的手段:三个隐藏字段、read view、undolog。

    • 三个隐藏字段:

      • DB_TRX_ID:表示最后一次插入或更新该行的事务 id
      • DB_ROLL_PTR:回滚指针,指向该行的 undolog
      • DB_ROW_ID:隐藏主键
    • undolog 用于记录某行数据的多个版本的数据。

    • read view 和 三个隐藏字段 : 用来判断当前版本数据的可见性。

InnoDB存储引擎实现MVCC

  • 仅针对写操作
  • 为要修改的数据行创建版本,将修改后的数据写入新版本,旧版本的数据仍然存在
  • 事务的提交后所作的修改对所有事务可见;回滚后修改被撤销,其他事务不可见
  • 版本的回收:MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。

SQL定义的事务隔离级别:基于锁和MVCC实现

https://blog.csdn.net/baidu_40120883/article/details/118755990?utm_source=miniapp_weixin

  • 读取未提交、读取已提交、可重复读(InnoDB默认)、可串行化
    • 读取未提交:不设任何限制 ==> 问题脏读:直接读取到错误的数据
    • 读取已提交:只能读取到别人提交/回滚的事务 ==> 问题不可重复读:两次读取之间有别的事务提交导致读取到了不一样的数据
    • 可重复读:在事务写数据的时候将数据加锁,其他事务无法读写该数据 ==> 问题幻读:两次查询之间有别的事务提交导致读取到了行数不同的数据
    • 可串行化:直接禁止事务的并发

image-20240320173313410

InnoDB如何解决幻读

  • 间隙锁

mysql日志

  • 二进制日志(归档日志)binlog:数据库的备份,同步

    • 写入时机:事务提交时从binlog cache中写入到binlog
  • 事务日志(重做日志)redolog:InnoDB独有,mysql崩溃恢复的能力

    • 记录的是物理级别的操作,比如页号xxx、偏移量yyy写入了’zzz’数据

    • 刷盘时机(将用户在缓冲区的修改刷到磁盘上持久化):事务提交、log buffer 空间不足、事务日志缓冲区满、定期检查刷盘、正常关闭服务器

    • 两阶段提交:保证两个log的一致性,但是增加了磁盘IO和锁竞争

      • 若发生异常:判断 redolog 是否完整,如果判断是完整的,就立即提交。如果 redolog 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redolog, 不完整就回滚事务。

      image-20240321095820154

  • 回滚日志(回滚日志)undolog:保证事务

    • 记录的是逻辑操作:比如INSERT操作后,undolog就记录了DELETE
    • 事务中会写到undolog buffer中,事务提交后写到undolog

一个完整的mysql语句执行(写)

  • 连接器:权限校验
  • 查询缓存:命中则返回
  • 分析器:sql语句的词法语法分析
  • 优化器:优化执行顺序
  • 执行器:调用存储引擎(InnoDB)执行sql
    • 开启事务,修改数据
    • 写入redolog,状态为prepare
    • 写入binlog
    • 事务提交,redolog状态修改为commit

mysql的锁有哪些

  • 基于属性分:共享锁/S锁,排他锁/X锁

  • 基于粒度:行级锁(innodb ) 、表级锁( innodb、myisam)、页级锁( innodb引擎)、记录锁、间隙锁、临键锁。

  • 基于状态:意向共享锁、意向排它锁。

如何给字符串建立一个索引

  • 使用前缀索引
  • 需要定义好前缀的长度
  • 必要的时候可以选择倒排存储,比如 422100200003018877 存储为 778810300002001224 这样前六位就很有辨识度了

分库分表

  • 分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成 ,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
  • 垂直分表:将一个表按照字段分成多个表,每个表存储一部分字段
    • 不常用的字段放一张表
    • text,blob等大字段拆分出来放在附表
    • 经常组合查询的列放在一张表
  • 垂直分库:按照业务分类,分布到不同数据库上,每个库可以放在不同的服务器
    • 解耦
    • 高并发的场景可以提升IO
  • 水平分库:按照一定规则把同一个表中的数据拆到不同的数据库中
    • 比如单数在一个表,双数在另一个表
    • 解决了单库大数据问题
  • 水平分表:是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中

慢查询如何排查优化

  • sql语句:是否加载了不必要的字段
  • 分析sql执行,是否命中的索引
  • 看sql涉及到的表结构
  • 可以考虑分表分库
  • 利用缓存,减少查询

mysql索引失效

  • 不合适的索引类型
  • 索引列上使用了函数
  • 使用了or操作符:当子条件中的列没有索引的时候,整个条件都无法使用索引
  • 索引列存在NULL
  • 数据量小的时候索引查询可能比全表查询还慢

12 并发

线程和进程 并发和并行

开启线程

  • 继承Thread类
  • 实现Runnable接口(还是要通过Thread调用)
  • 实现Callable接口(同上)
  • 使用线程池

线程生命周期和状态

  • New
  • Runnable
  • Blocked
  • Waiting
  • TIME_WAITING:超时等待
  • TERMINATED

sleep和wait

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁
  • 后者常用于线程间的通信,需要别的线程在同一个对象上使用notify()或者notifyAll()唤醒,也可以用wait(long timeout)超时唤醒

volatile 关键字

  • 指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
  • 在编译的时候禁止指令重排

乐观锁和悲观锁

  • 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁

    • synchronizedReentrantLock
  • 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了

    • 版本号或者CAS算法
    • CAS算法三个参数:当仅当V==E的时候才会修改N

      • V:要更新的变量值(Var)
      • E:预期值(Expected)
      • N:拟写入的新值(New)
    • 存在的问题:

      • ABA问题:在变量前面加版本号
      • 循环时间长开销大:
      • 只能保证一个共享变量的原子操作

死锁的形成条件与解决

  • 条件

    • 互斥条件:一个资源每次只能被一个进程使用。

    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

    • 不剥夺/非抢占条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

  • 解决:破坏四个条件之一

    • 破坏互斥:让资源可以同时访问(往往行不通)
    • 破坏非抢占:剥夺式调度算法,但是会导致资源利用率低(不怎么用)
    • 破坏请求与保持:静态分配策略:在进程执行之前先获取到他需要的所有资源,如果不满足就不开启(会导致资源利用率低,因为部分资源可能在进程后期才会用)
    • 破坏循环等待:层次分配策略:将资源分层,先释放低层次的资源再获取高层次的资源

synchronized

  • 修饰实例方法:给对象加锁
  • 修饰静态方法:给类加锁
  • 修饰静态代码块:对括号里面的类/对象加锁
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能
  • 底层原理:使用的monitorenter锁住,使用monitorexit退出

线程池

  • 创建

    • ThreadPoolExecutor创建(推荐)
    • 通过Executor框架的工具类 Executors 来创建。
  • 线程池参数:

    • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
    • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
    • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
    • keepAliveTime:当线程数大于corePoolSize 且没有新的任务提交,多余的空闲线程的等待时间
    • unit : keepAliveTime 参数的时间单位。
    • threadFactory :executor 创建新线程的时候会用到。
    • handler :饱和策略
      • ThreadPoolExecutor.AbortPolicy: 抛出 RejectedExecutionException来拒绝新任务的处理。
      • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
      • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
      • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。如果执行程序已关闭,则会丢弃该任务
  • 参数设置原则

    • 最佳corePoolSize ,N为CPU的核数

      • 如果是IO密集型(CPU计算时间短,而等待IO操作(如读写文件、网络通信等)的时间长)的任务就设置为2N

        • 文件处理,数据库读写,网络操作等
      • 如果是CPU密集型(几乎没有IO等待)就设置为N+1

        • 数值计算、图像视频处理、加密解密、模型训练等
      • 还可以这样计算:((线程等待时间+线程CPU时间) / 线程CPU时间)* CPU数目

  • 执行过程

    image-20240321213330649

Java中的锁机制

  • 互斥锁synchronized关键字:JVM底层实现,最基本的线程同步机制
  • 可重入锁:实现类ReentrantLock,允许更细粒度的锁控制
  • 读写锁ReadWriteLock
  • 乐观锁悲观锁
  • synchronized性能的优化
    • java对象头mark word存储了一些信息
    • 偏向锁:把线程ID放入对象的Mark Word字段中
    • 轻量级锁:采用CAS自旋锁的方式来完成加锁
    • 重量级锁:让抢占锁的线程从用户态转变为内核态,开销很大
  • 自旋锁:让线程获取锁的时候不断的重试获取锁的操作,而不是立即进入阻塞状态
  • 分段锁:如ConcurrentHashMap

synchronizedReentrantLock

  • 前者是JVM实现,后者是JDK实现
  • 两者性能大致相同(因为新版本JVM对synchronized进行了优化,也支持了自旋锁等
  • 前者不可中断,后者可以
  • 优先使用前者,因为是JVM原生支持,且不用担心死锁问题,因为JVM会保证锁的释放

Java主流锁

https://pdai.tech/md/java/thread/java-thread-x-lock-all.html

  • 线程是否锁同步资源
    • 乐观锁:采用无锁编程实现,CAS算法常用
    • 悲观锁:synchronized关键字和Lock的实现类实现
      • ReentrantLockReadLockWriteLock(后两者是ReentrantReadWriteLock内部类)
  • 锁住同步资源失败时,线程要不要阻塞
    • 为什么要非阻塞:阻塞线程要切换CPU的状态,耗时,自旋就是不让线程阻塞(及不放弃CPU的时间片)
    • 阻塞
    • 非阻塞
      • 自旋锁
      • 适应性自旋锁
  • 多个线程竞争同步资源的流程细节
    • 这是针对synchronized的优化,表示锁的四个状态
    • 无锁
    • 偏向锁:同一个线程执行同步资源时自动获取资源
    • 轻量级锁:多个线程竞争时,没获取资源的线程自旋等待所释放
    • 重量级锁:多个线程竞争时,没获取资源的线程阻塞等待被唤醒
  • 多个线程竞争锁时要不要排队
    • 公平锁
    • 非公平锁:先尝试插队,失败了再排队
  • 一个线程的多个流程能不能获取同一把锁(前提是锁的是同一个对象或者class)
    • 可重入锁:ReentrantLocksynchronized
    • 不可重入锁
  • 多个线程能否共享锁
    • 共享锁:ReentrantReadWriteLock,本质是里面的两把锁,读锁和写锁
    • 排他锁:synchronized和JUC中Lock的实现类就是互斥锁。

13 计网

七层模型

  • 应用层:FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS
    • 为计算机用户提供服务
  • 表示层:JPEG、MPEG、ASII
    • 数据处理(编解码、加密解密、压缩解压缩)
  • 会话层:NFS、SQL、NETBIOS、RPC
    • 管理(建立、维护、重连)应用程序之间的会话
  • 传输层:TCPUDP、SPX
    • 为两台主机进程之间的通信提供通用的数据传输服务
  • 网络层:路由和寻址(决定数据在网络的游走路径)
    • IP:分为ipv4 和 ipv6,定义数据包格式,对数据包进行路由和寻址
    • ARP:地址解析协议,用于解决网络层和数据链路层的地址的转换问题(IP地址转MAC地址)
      • 维护一个表:<IP, MAC, TTL>TTL为生存时间
    • ICMP:互联网控制报文协议,ping命令
    • NAT:网络地址转换协议,内部网到外部网的转换
    • OSPF(Open Shortest Path First):内部网关协议
  • 数据链路层:PPP、FR、HDLC、VLAN、MAC(网桥、交换机)
    • 帧编码和误差纠正控制
  • 物理层:RJ45、CLOCK、IEEE802.3(中继器、集线器)
    • 透明地传送比特流传输

四层模型

  • 应用层 1-3
  • 传输层 4
  • 网络层 5
  • 网络接口层 6-7

三握四挥

  • 名词解释

    • seq:sequence ,序号,用于确认数据是否准确
    • ack:acknowledgement ,确认号,用于确认数据是否准确
    • flags:标志位,一共有6个,用于确认/更改连接状态
      • URG(ent):紧急指针(urgent pointer)有效。
      • ACK(nowledgment):确认序号有效。(为了与确认号ack区分开,我们用大写表示)
      • P(u)SH:接收方应该尽快将这个报文交给应用层。
      • R(e)S(e)T:重置连接。
      • SYN(chronization):同步,发起一个新连接。
      • FIN(ish):释放一个连接
  • 握手

    image-20240322223936856

    1
    2
    3
    4
    sequenceDiagram
    客户端 ->> + 服务器 : SYN(seq=x)
    服务器 ->> 客户端 : ACK(ack=1),SYN(seq=y)
    客户端 ->> + 服务器 : ACK(ack=y+1)
  • 挥手

    image-20240322223944455

1
2
3
4
5
sequenceDiagram
客户端 ->> + 服务器 : FIN(seq=m)
服务器 ->> 客户端 : ACK(ack=m+1)
服务器 ->> 客户端 : FIN(seq=y)
客户端 ->> + 服务器 : ACK(ack=y+1)

TCP传输稳定性

  • 数据传输之前会有三次握手来进行连接
  • 在数据传输时:
    • 基于数据块(报文)
    • 对数据包排序防丢失和重复
    • 校验和:将保持它首部和数据的检验和
    • 重传机制(直到收到ACK应答)
    • 流量控制:控制发送的速率,保证能及时接收,利用滑动窗口
      • 发送窗口:四个部分 ==> 已发送确认 + 已发送未确认 + 可以发送 + 不可发送
      • 接收窗口:三个部分 ==> 已发送确认 + 已接收未确认 + 不可接收
    • 拥塞控制:发送方维持一个拥塞窗口 ,四个算法 ==> 慢开始,拥塞避免,快重传,快恢复
    • ARQ协议:自动重传请求(Automatic Repeat-reQuest),在数据链路层和传输层都有
      • 停止等待ARQ协议:每发送一个请求就等待对方确认ACK
      • 连续ARQ协议:连续发送n个消息,接收方返回连续成功接收的第n个消息的序号,表示前n个消息都收到了
        • 优点:信道利用率高,容易实现,即使确认丢失,也不必重传。
        • 缺点:如果发送了5条消息,第3条消息丢失了,那么接受方返回的是前两条的确认信息,发送方会重新发送345条消息
  • 数据传输之后会进行四次挥手断开连接来节约系统资源。

TCP和UDP

  • Transmission Control Protocol传输控制协议
  • User Dategram Protocol用户数据协议
  • TCP面向连接(传输之前需要建立连接)
  • TCP传输可靠
  • TCP传输有状态
  • TCP是面向字节传输,UDP面向报文
    • 面向字节出现粘包
  • TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。
  • TCP点对点,UDP都支持

Http和Https

  • https协议要申请证书到ca,需要一定经济成本;

  • http是明文传输,https是加密的安全传输;

  • 连接的端口不一样,http是80,https是443;

  • http3.0之前是基于tcp连接的,https在此基础上还增加了SSL/TLS协议用作加密和安全认证

    • SSL/TLS:TLS是SSL发展来的,一般就混在一起了,核心是用非对称加密传递对称加密的密钥,之后用对称加密来传输数据

    • https带证书的公钥传输机制:https://www.bilibili.com/video/BV1mj421d7VE/?spm_id_from=333.337.search-card.all.click&vd_source=1a39594354c31d775ddc587407a55282

      • 设有服务器 Bob,客户端 Alice,和第三方信赖机构 CA。目的:让Alice知道Bob的公钥,且这个过程不会被别人篡改

      • Bob 信任 CA,CA 是知道 Bob 公钥的,CA 向 Bob 颁发证书。

        • 数字证书内容:Bob的公钥+Bob的身份+CA私钥对前两者hash运算结果的签名
        • 签名是为了保证Bob公钥和身份没有被篡改,如果第三方修改了,那么两个部分就对不上了
      • Bob 获得 CA 颁发的证书,将该证书传递给 Alice。

      • Alice 获得 Bob 的证书,并且还有CA的公钥(浏览器安装的时候会附带安装CA相关信息),

        • 使用 CA 公钥对证书上的签名解密,得到Bob的身份和Bob的公钥hash运算后的结果,也就是摘要
        • 将证书的另一部分(Bob的公钥和身份)进行hash运算,对比两个摘要相同的话就说明证书是真实的
      • 如果 Alice 验证 Bob 证书是真实的,则信任 Bob 的公钥(在 S 证书中)。

      • 注意:中间对文件hash运算一次的原因是为了压缩传输信息,加快传输速度(不可逆)

        image-20240322205844944

  • http连接很简单,没有状态;

  • https是ssl加密的传输,身份认证的网络协议,相对http传输比较安全。

http常见的header

1
2
3
4
5
6
7
8
9
Accept   			能够接受的回应内容类型
Accept-Charset 能够接受的字符集
Accept-Encoding 能够接受的编码方式列表
Accept-Language 能够接受的回应内容的自然语言列表。
Authorization 用于超文本传输协议的认证的认证信息
Content-Type 请求体的 多媒体类型 (用于 POST 和 PUT 请求中)
Host 服务器的域名以及服务器所监听的传输控制协议端口号
Origin 发起一个针对跨来源资源共享的请求。
User-Agent 浏览器的浏览器身份标识字符串

http1.0和http1.1

  • 优化了缓存

  • tcp连接:1.0是短连接:每进行一次http操作就建立一次连接,1.1是长连接

  • host头处理:1.1在请求头中加入了Host字段,也就是主机名

    1
    2
    3
    4
    5
    //请求http://example1.org/home.html
    GET /home.html HTTP/1.0

    GET /home.html HTTP/1.1
    Host: example1.org
  • 带宽优化及网络连接的使用:1.1请求头引入了 range 头域,它允许只请求资源的某个部分

  • 状态响应码新增

http2.0和http1.1

  • 多路复用:
  • 2.0用二进制帧,1.1用文本传输
  • 1.1支持body压缩,2.0额外支持了header压缩
  • 服务器推送

浏览器输入url发生了什么

  • 从事件来看

    • DNS解析查询到对应的ip地址
  • 根据id地址和端口号,浏览器向服务器发送一个TCP连接请求(发送数据前客户端和服务队建立通道)
  • 在TCP连接上,发送HTTP请求获取网页内容
  • 服务器处理请求并返回HTTP报文
  • 浏览器解析响应中的HTML,渲染结构和样式,同时根据HTML中的其他URL发送请求,获取资源直到网页完全加载
  • 从4层来看

    • 应用层:
      • 浏览器先访问缓存的host,如果没有则使用DNS解析URL,最后得到IP地址
      • 发送http报文,使用http或者https协议
    • 传输层:
      • http基于tc协议
    • 网络层:核心:路由与转发
      • 转发:将分组从路由器的输入端口转移到合适的输出端口。
      • 路由:确定分组从源到目的经过的路径。

一个URL的组成

  • 协议:Http或者Https,也可能是ftp
  • 域名
  • 端口
  • 资源路径
  • 参数 :?开始,&隔开 ,采用k-v的形式
  • 锚点:#开始,相当于一个小书签。并且不会作为请求的一部分发送给服务端。

DNS:Domain Name System 域名管理系统

  • 基于UDP协议,端口53
  • 跟DNS服务器
  • 顶级域DNS服务器(TLD 服务器):指域名的后缀,com org net edu
  • 权威DNS服务器
  • 本地DNS服务器
  • 查询过程:cis.poly.edu 查询 gaia.cs.umass.edu

image-20240322204013957

状态码

  • 2xx:成功
  • 3xx:重定向
  • 4xx:客户端出错
  • 5xx:服务器出错

websocket和http

  • WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。
  • 区别
    • WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。并且,HTTP 协议下的通信只能由客户端发起,服务器无法主动通知客户端。
    • WebSocket 使用 ws:// 或 wss://(使用 SSL/TLS 加密后的协议,类似于 HTTP 和 HTTPS 的关系) 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。
    • WebSocket 通信数据格式比较轻量,用于协议控制的数据包头部相对较小,网络开销小,而 HTTP 通信每次都要携带完整的头部,网络开销较大(HTTP/2.0 使用二进制帧进行数据传输,还支持头部压缩,减少了网络开销)

ping

  • PING 基于网络层的 ICMP(Internet Control Message Protocol,互联网控制报文协议),其主要原理就是通过在网络上发送和接收 ICMP 报文实现的。

14 操作系统

操作系统的功能

  • 进程和线程的管理:进程的创建、撤销、阻塞、唤醒,进程间的通信等。
  • 存储管理:内存的分配和管理、外存(磁盘等)的分配和管理等。
  • 文件管理:文件的读、写、创建及删除等。
  • 设备管理:完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。
  • 网络管理
  • 安全管理:用户的身份认证、访问控制、文件加密等,以防止非法用户对系统资源的访问和操作。

内存管理

  • 内存的分配和回收
  • 地址转换:程序中的地址转为内存中的物理地址
  • 内存扩充:虚拟内存技术
  • 内存映射:将文件映射到内存,加快文件读取速度
  • 内存优化
  • 内存安全

内存碎片

  • 内部内存碎片/内碎片:分配了但没使用的空间

  • 外部内存碎片/外碎片:由于段式分配导致一些没有用的小间隙

    image-20240323143949565

内存管理方法

  • 连续内存管理:容易造成较多内存碎片,内存利用率不高
  • 非连续内存管理
    • 段式管理:以段的形式存储,程序的内存被分为大小不等的段,每段有实际的逻辑含义:主程序段、子程序段、数据段、栈段
      • 优点:无内存碎片
      • 缺点:段换入换出的时候会产生外碎片
    • 页式管理:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。
      • 优点:无外碎片
      • 缺点:可能有内碎片,因为页框可能填不满
    • 段页式管理:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。

用户态和内核态

  • 两种形态是进程的运行级别(一个用了用户栈一个用了内核栈)

  • 用户态: 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。当需要进行磁盘读写的时候就会申请进入内核态

  • 内核态:几乎可以访问系统的任何资源(不止一个)

  • 用户态到内核态的三种方式

    • 系统调用

      • 用户态的进程发起系统调用,由于权限不足,因此会中断执行(trap)

      • 发生中断后,当前CPU执行的程序中断,内核程序开始运行处理系统调用

      • 内核处理完成后,主动触发trap,再次中断切换回用户态

        image-20240323134044281

    • 中断:外围设备完成请求后向CPU发送中断信号,这个时候如果进程是用户态就会被切换到运行态。比如比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等

    • 异常

线程和进程

  • 进程:计算机中运行的实例
  • 线程:多个线程在一个进程中运行,共享进程中的资源(内存空间、文件句柄、网络连接等)
  • 进程切换开销大,线程开销小
  • 同一进程下的线程共享文件,因此他们之间的通信无需调用内核
  • 多个线程可以并发处理不同任务,而进程只能在一个时间段干一件事,遇到阻塞如IO阻塞就会挂起

进程状态

  • 与线程类似:创建、就绪、运行、阻塞、结束

进程间通信

  • 管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
  • 有名管道(Named Pipes)
  • 信号
  • 消息队列
  • 信号量
  • 共享内存
  • 套接字

进程调度算法

  • 先到先服务
  • 短作业优先
  • 时间片轮转调度
  • 多级反馈队列调度:既能使高优先级的作业得到响应又能使短作业(进程)迅速完成best
  • 优先级调度

僵尸进程和孤儿进程

  • 僵尸进程:子进程已经终止,但是其父进程仍在运行
  • 孤儿进程:一个进程的父进程已经终止或者不存在,但是该进程仍在运行

线程间的同步方式

  • 互斥锁:比如java中的synchronized关键字和各种lock
  • 读写锁:只有一个线程可以写,可以多个线程同时读
  • 信号量:允许同一时刻多个线程访问资源,但是需要控制线程数量
  • 屏障:java中的CyclicBarrier:多个线程到了同一状态再一起行动
  • 事件:Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

线程的上下文切换

  • CPU通过分配时间片来执行任务,一个任务从保存到加载的过程就是一次上下文切换
  • 自发性的切换
    • Thread.sleep()
    • Object.wait()
    • Thread.yeild():运行 ==> 就绪
    • Thread.join()
  • 非自发性的切换
    • 时间片用完
    • 更高优先级的线程需要运行
    • 虚拟机的垃圾回收动作

15 IO及IO模型

  • 应用程序发起IO调用,由内核来执行具体的IO操作:内核等待IO设备准备好数据 ==> 内核将数据从内核空间拷贝到用户空间

Java常见IO类

  • 磁盘操作:File
  • 字节操作:InputStreamOutputStream
  • 字符操作:ReaderWriter
  • 对象操作:Serializabletransient
    • 只是一个标准,实现前者接口表示需要序列化,后者关键字标识表示不会被序列化
  • 网络操作:Socket

UNIX系统下的IO模型

  • 同步阻塞IO

    • 应用程序发起read调用,然后阻塞

    • 内核空间准备数据,拷贝数据

    • 直到拷贝数据结束的这段实际,线程处于阻塞

    • 缺点:客户端连接数量多无法处理

      image-20240323150836151

  • 同步非阻塞IO

    • 应用程序发起read调用

    • 内核空间准备数据,拷贝数据,在此期间应用程序不断的read轮询(polling),如果每准备好就会马上返回错误(所以这段时间可以视为没阻塞)

    • 准备完毕,read调用然后内核拷贝数据到用户空间,此间线程阻塞,直到拷贝完毕

    • 缺点:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

      image-20240323150859310

  • IO多路复用:

    • 多路:多个请求/网络连接

    • 复用:同一个进程/线程

    • 应用程序发起select/poll/epoll调用,询问内核数据是否准备就绪(读写就绪),然后线程处于阻塞

    • 内核空间准备数据,查询相关文件描述符的状态,准备完毕发送ready(可能是部分的文件描述符)

    • 用户线程再发起 read 调用

    • read 调用的过程(数据从内核空间 -> 用户空间),所有数据获取完毕则解除阻塞

    • 优点:

      • 通过减少无效的系统调用,减少了对CPU的消耗
      • 一个线程可以检测多个文件描述符(linux中的文件索引)
    • select几乎所用操作系统都支持,epoll是linux对select的升级,优化了IO执行效率

    • 举例

      • 应用程序A使用select/poll/epoll等系统调用监控多个文件描述符(例如是ab两个文件),随后处于阻塞
      • 内核去查询文件描述符的状态,如果不可操作就进行数据准备
      • 当文件描述符就绪(如b可读取),内核就会通知应用程序A
      • A拿到通知后,会检查有哪些文件描述符就绪,并对就绪的文件描述符进行IO操作
      • 如果有多个应用程序都在等b,内核就会通知所有相关的程序,这也是IO多路复用的精髓

      image-20240323150252477

    • select/poll/epoll区别

      • select:程序指定一组文件描述符,用bitmap存储,然后阻塞等待其中任何一个文件描述符就绪。
        • 不足:文件描述符数量有限(0-1023),每次调用都需要将全部文件描述符集合从用户态拷贝到内核态,效率低
      • poll:使用了更灵活的链表来管理文件描述符集合,所以文件描述符数量没有限制,但本质上仍然是一种轮询机制。
        • 不足:在大量文件描述符的情况下需要线性循环扫描,效率依然不高。
      • epoll:linux内核提供,更高效,红黑树+链表
    • select/poll过程

      • 用户空间拷贝fd集合到内核空间(这部分的开销很大,因为内核空间要修改里面的值,所以要拷贝)
      • 注册回调函数_pollwait
      • 遍历所有的fd,调用其poll方法
      • poll方法返回时会返回一个描述读写操作是否就绪的mask掩码
      • 遍历完所有的fd,还没有返回mask掩码,则使当前的进程进入睡眠状态,当资源可读写后再唤醒,如果长时间没有唤醒则调用select/poll的进程会重新唤醒获取CPU,进而重新遍历fd,判断有没有就绪
      • 如果文件就绪,则将fd集合的引用返回给用户空间
      • 用户再遍历fd集合,判断相应的文件描述符状态
    • epoll过程:event poll,复杂度降低到了O(1)

      • 内核空间创建epoll结构体,即一个红黑树和一个就绪链表
      • 用户线程调用epoll_ctl向内核空间注册文件描述符,内核空间会将文件描述符加入到epoll的红黑树中,并建立回调关系
      • 当文件描述符状态发生变化的时候(可读、可写、异常),内核空间会将就绪事件加入到epoll链表中
      • 用户空间调用epoll_wait等待就绪的事件,内核空间将就绪事件从就绪链表中返回给用户空间
      • 用户空间线性扫描就绪链表,获取就绪文件描述符号,调用read()等方法进行IO操作
  • 信号驱动IO

  • 异步IO:基于事件和回调机制实现,应用系统不需要自己检查IO操作

    image-20240323153018146

同步非阻塞和IO多路复用?

  • 前者发送read后收到错误就解除阻塞了,如果是数据准备完成就直到获取完数据就解除阻塞,后者在发送系统调用后就一直阻塞,直到内核返回文件描述符就绪信息
  • 并发请求比较少的情况下前者的效率更高,后者的优势在于一个线程监测多个文件描述符,可以实现文件描述符的复用,减少不必要的资源消耗

java中的三种IO模型

  • 不同的是内核空间变成了服务端,用户空间变成了客户端

  • BIO(Blocking I/O):同步阻塞IO

  • NIO(Non-blocking/New I/O),可以看作是IO多路复用模型
    • 三个核心组件:
      • Channel:通道,用于传递信息
      • Buffer:缓冲区,连接Channel和客户端/服务端的缓冲区
      • Selector:选择器,用于监控Channel中的IO事件的组件
  • AIO(Asynchronous I/O):异步 IO 模型

16 JVM

JVM 内存模型

image-20240315171612257

image-20240324104828031

  • 运行时内存
    • 线程私有:
      • 程序计数器:不会产生StackOverflowErrorOutOfMemoryError
      • Java虚拟机栈:可能出现StackOverflowErrorOutOfMemoryError
        • 栈帧:栈的小单元
          • 局部变量表
          • 操作数栈
          • 动态链接:一个方法调用另一个方法的情景
          • 方法返回地址
      • 本地方法栈
    • 线程共享:
      • 方法区:存储编译器编译后的代码缓存数据:类信息、字段信息、方法信息、常量、静态变量
        • JDK8之前用的永久代(JAVA堆的内存),JDK8之后用的元空间(操作系统的内存)
        • 包含的的是整个程序唯一的元素:static,class变量
        • 运行时常量池
      • java堆:内存最大的一块,存放实例对象
        • 结构:可以分为新生代和老年代,JDK8之前还有一个永久代区域用于方法区
          • 新生代:由Eden、S0(From Sruvivor)、S1(To Sruvivor)组成:8:1:1
          • 老年代:
        • 字符串常量池
  • 本地内存
    • 元空间:也就是JDK8之后的方法区
    • 直接内存

为什么要分两个S区?

  • 为了保证任何时候总有一个survivor是空的(幸存区),主要为了解决内存的碎片化问题,防止频繁触发GC,影响程序的性能和响应速度。
  • 从Eden进入survivor时,如果S区不是空的,就会产生内存碎片,本来S区的内存就不大

垃圾回收

  • 主要是对堆内存的垃圾回收

  • 分类

    • Partial GC

      • young GC:只收集新生代

      • Old GC:只收集老年代

      • Mixed GC :新生代+部分老年代

    • Full GC:整个Java堆和方法区(不会影响元空间)

  • 内存分配

    • 优先在新生代的Eden区分配,空间不足就发起一次GC
    • 大对象(需要大量连续内存空间的对象:字符串、数组等)直接进老年代
    • 长期存活对象进老年代
      • 在Eden区第一次出生,经过一次 Minor GC 后存活则进入Survivor区(S0或者S1)
      • 每熬过一次GC就加一岁,默认到15岁进入老年区
  • 死亡对象判断(并不会马上宣判死亡)

    • 引用计数法:当被引用就+1,计数器为0就判断死亡

    • 可达性分析算法:用一系列成为”GC Root“的对象作为起点,如果对象不在以这个Root为起点的链上,就死亡

      • 常见GC Root:

        • 虚拟机栈(栈帧中的局部变量表)中引用的对象

        • 本地方法栈(Native 方法)中引用的对象

        • 方法区中类静态属性引用的对象

        • 方法区中常量引用的对象

        • 所有被同步锁持有的对象

        • JNI(Java Native Interface)引用的对象

四大引用

  • 强引用:用的多,不会被回收
  • 软引用:内存空间够就不会回收
  • 弱引用:发现了就会被回收
  • 虚引用:

如何判断废弃常量和无用的类

  • 常量:没用引用
  • 类:满足条件就“可以”被回收
    • 堆中没有实例
    • 加载该类的ClassLoader被回收
    • 该类对应的java.lang.Class没有被引用,也没有反射调用

垃圾清除算法

  • 标记-清除算法

    • 内存碎片问题;效率问题

    image-20240324113013046

  • 标记-复制算法:内存切半,一次用一半

    • 可用内存变小;不适合老年代(对象数量大,复制性能差)

    image-20240324113118170

  • 标记-整理算法:

    • 多了整理这一步,效率不高。
    • 适合老年代这种垃圾回收频率不是很高的场景。

    image-20240324113228667

  • 分代收集算法

    • 分为新生代和老年代
    • 新生代中每次收集都有很多对象死去,可用标记-复制算法
    • 老年代中对象存活几率是比较高,必须选择“标记-清除”或“标记-整理”算法
    • 这也是堆分代的原因

垃圾收集器

  • Serial(串行)收集器:单线程

image-20240324114051981

  • ParNew 收集器:多线程版本

image-20240324114117775

  • Parallel Scavenge 收集器

image-20240324114202214

  • Serial Old 收集器

image-20240324114310340

  • Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本

image-20240324114336382

  • CMS收集器:注重用户的体验的应用上使用,实现用户线程和GC线程并发
    • 过程
      • 初始标记:暂停用户线程,标记与Root直连的对象
      • 并发标记:开启用户线程,用一个闭包记录可达对象(是针对于上一个时刻的状态)
      • 重新标记:暂停用户线程,再次标记并发标记过程中用户线程变动的标记情况(相当于一个同步)
      • 并发清除:开启用户线程,GC线程清除未标记区域
    • 缺点:从 JDK9 开始,CMS 收集器已被弃用
      • 对 CPU 资源敏感;
      • 无法处理浮动垃圾;
      • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

image-20240324114611636

  • G1收集器:JDK就之后的默认收集器

    • 低延迟,高吞吐量
    • 大内存,多核场景
    • 标记整理

    image-20240324115056539

类的生命周期

  • 加载(Loading)
    • 类的加载是通过类加载器完成ClassLoader
    • 用哪个加载器是通过 双亲委派模型 决定
  • 验证(Verification)
    • 确保 Class 文件的字节流中包含的信息符合安全
  • 准备(Preparation)
    • 为类在方法区分配空间
  • 解析(Resolution)
    • 虚拟机将常量池内的符号引用替换为直接引用的过程
  • 初始化(Initialization)
    • 执行初始化方法 <clinit> ()方法
  • 使用(Using)
  • 卸载(Unloading)
    • 三个条件
      • 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
      • 该类没有在其他任何地方被引用
      • 该类的类加载器的实例已被 GC
    • JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。
    • 自定义的类加载器的实例是可以被回收的

类加载器

  • 负责加载类的对象,ClassLoader 是一个抽象类

  • .class文件加载到JVM中

  • 每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

  • 动态加载:要用的时候再加载

  • JVM内置类加载器
    • BootstrapClassLoader(启动类加载器)
      • 最顶层,C++实现,通常表示为 null
        • getClassLoader()得到的是null,因为C++实现,没有对应的Java类
      • 加载核心类库:%JAVA_HOME%/lib下的rt.jarresources.jarcharsets.jar等 jar 包和类
        • rt.jar就是基础类库
      • 还有被-Xbootclasspath参数指定的路径下的所有类
    • ExtensionClassLoader(扩展类加载器)
      • %JRE_HOME%/lib/ext下的jar
      • java.ext.dirs 系统变量所指定的路径下的所有类。
    • AppClassLoader(应用程序类加载器)
      • 加载当前classpath 下的类和jar包
  • 自定义类加载器

    • protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。
    • protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
    • 区别:
      • 不想打破双亲委派,就重写findClass方法,无法被父类加载器加载的都过这个加载器加载
      • 想打破双亲委派,就重写loadClass
    • Tomcat:自定义了WebAppClassLoader加载相关目录下的类
    • Spring:自定义了ThreadContextClassLoader线程上下文类加载器,
  • 双亲委派模型(就一个父类)

    • ClassLoader 类使用委托模型来搜索类和资源。

    • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

    • ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。

      image-20240324134630162

    • 注意:

      • 只是JDK的一种推荐,并不是强制的约束
      • 类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
    • 执行流程

      • 判断类有没有被加载过
      • 没有被加载则先委托给父类加载(父类的loadClass)(也就是所有的请求都会从BootstrapClassLoader开始)
      • 父类无法加载则子加载器再加载(findClass)
      • 若还是无法加载,则抛出ClassNotFoundException异常

JVM 参数

  • 堆内存相关

    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
    # 堆内存
    -Xms<heap size>[unit] # 最小 unit:g,m,k
    -Xmx<heap size>[unit] # 最大
    -----------------------------------
    -Xms2G -Xmx5G
    ====================================
    # 新生代
    -XX:NewSize=<young size>[unit]
    -XX:MaxNewSize=<young size>[unit]
    -----------------------------------
    -XX:NewSize=256m
    -XX:MaxNewSize=1024m
    ====================================
    # 新生代2
    -Xmn<young size>[unit]
    -----------------------------------
    -Xmn256m
    ====================================
    # 永久代
    -XX:PermSize=N # 方法区 (永久代) 初始大小
    -XX:MaxPermSize=N # 方法区 (永久代) 最大大小
    ====================================
    #元空间
    -XX:MetaspaceSize=N # 设置 Metaspace 的初始大小
    -XX:MaxMetaspaceSize=N # 设置 Metaspace 的最大大小
    ====================================
  • 垃圾收集相关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 选择垃圾回收器
    -XX:+UseSerialGC # 串行垃圾收集器
    -XX:+UseParallelGC # 并行垃圾收集器
    -XX:+UseParNewGC # CMS 垃圾收集器
    -XX:+UseG1GC # G1 垃圾收集器
    =====================================
    # 日志
    # 打印基本 GC 信息
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    # GC日志输出的文件路径
    -Xloggc:/path/to/gc-%t.log
    # 每个文件上限大小,超过就触发分割
    -XX:GCLogFileSize=50M

17 SQL

https://sqlmother.solargod.cn/#/learn

CRUD

  • 增:INSERT

  • 删:DELETE(删除记录),TRUNCATE(清空表),DROP(删除表)

  • 改:UPDATE

  • 查:

    • SELECTLIMITORDER BYASCDESC

    • IS NULLIS NOT NULL

    • GROUP BY

      • 分组之前过滤用WHERE
      • 分组之后用HAVING
    • 子查询常用在 WHERE 子句和 FROM 子句后边,放在()里面,EXISTSNOT EXISTS

    • IN(XX,XX,XX...)

    • BETWEEN x AND y

    • AND OR NOT

    • DISTINCT:去重,如果有多个字段就是组合去重

      1
      select distinct class_id, exam_num from student;
    • 列操作

      1
      select name, score, score * 2 as double_score from student;
    • LIKE

      • %:任意字符出现任意次数
      • _:任意字符出现一次
      • eg:WHERE prod_name LIKE '%bean bag%';
    • JOINON表示连接条件

      1
      2
      3
      4
      5
      select c.cust_name, o.order_num
      from Customers c
      inner join Orders o
      on c.cust_id = o.cust_id
      order by c.cust_name;
    • UNION UNION ALL连接两个表,后者不去重

    • 条件分支

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
       -- 一个表有name和age列,返回name,age_level(age > 60为老同学,>=20为年轻,为空或者<20为小同学),最后结果用name升序排
      select
      name,
      case
      when (age > 60) then "老同学"
      when (age >= 20) then "年轻"
      else "小同学"
      end as age_level
      from
      student
      order by
      name ASC;

函数

  • 开窗函数 PARTITION BY xx分组依据 ORDER BY xx 排序依据

    • SUM(xx) OVER:计算累计值,比如某个用户的累计消费

      1
      2
      3
      4
      5
      6
      SUM(计算字段名) OVER (PARTITION BY 分组字段名)
      -- 每个学生的详细信息,并且按照分数升序的方式累加计算每个班级的学生总分
      select
      id, name, age, score, class_id,
      SUM(score) OVER (PARTITION BY class_id order by score asc) as class_sum_score
      from student
    • AVG(xx) OVER:计算平均值,比如某个班的平均分,加在学生信息后面

      1
      2
      3
      4
      select
      id, name, age, score, class_id,
      AVG(score) OVER (PARTITION BY class_id) as class_avg_score
      from student
    • RANK() OVER

      1
      2
      3
      4
      5
      SELECT
      id,name,age,score,class_id,
      RANK() OVER (PARTITION BY class_id ORDER BY score DESC) as ranking
      FROM
      student;
    • ROW_NUMBER() OVER:和RANK() OVER不一样的地方在于这个得到的排序是唯一的

    • Lag() OVER Lead() OVER

      1
      2
      3
      4
      -- Lag 函数用于获取 当前行之前 的某一列的值 
      LAG(column_name, offset, default_value) OVER (PARTITION BY partition_column ORDER BY sort_column);
      -- Lead 函数用于获取 当前行之后 的某一列的值
      LEAD(column_name, offset, default_value) OVER (PARTITION BY partition_column ORDER BY sort_column)
      1
      2
      3
      4
      5
      6
      -- 以班级为单位按照分数降序的方式获取每个班级内的学生的前一名学生姓名(prev_name)、后一名学生姓名(next_name)。
      select
      id, name, age, score, class_id,
      LAG(name, 1, NULL) OVER (PARTITION BY class_id ORDER BY score desc) as prev_name,
      LEAD(name, 1, NULL) OVER (PARTITION BY class_id ORDER BY score desc) as next_name
      from student

18 设计模式

装饰者模式

image-20240508114653108

  • 抽象组件:Component

  • 被装饰类:ConcreteComponent

  • 装饰者组件:Decorator

  • 具体装饰:ConcreteDecoratorAConcreteDecoratorB

  • 核心是Decorator内维护一个Component类型的变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Decorator implements Component {

    protected Component component;

    public BitmapDecorator(Component component) {
    component = component;
    }

    @Override
    public void doOperation() {
    component.component();//可以做基础操作
    // 子类ConcreteDecoratorA、ConcreteDecoratorB再对这个方法进行装饰增强
    }
    }
  • 使用

    1
    2
    3
    4
    ConcreteComponent concreteComponent = new ConcreteComponent();//创建被装饰者

    concreteComponent.doOperation();//无任何装饰
    new ConcreteDecoratorA(concreteComponent).doOperation();//装饰器A增强
  • 具体使用,IO类

    image-20240508115513966

    1
    2
    3
    4
    5
    FileInputStream fileInputStream = new FileInputStream(filePath);

    // 两种不同的装饰
    BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
    DataInputStream dataInputStream = new DataInputStream(fileInputStream);

二 项目

1 FS开发

  • 难点:高频次的呼叫+自定义的移库策略导致任务呼叫时与任务实际执行时库位的状态不一致,且无法通过设置库位预定占用量来确定,无法获取准确的库位信息进而无法确定空闲库位;
  • 解决:自定义呼叫池,延后实际的呼叫事件触发时间,在池中维护一个队列,每次呼叫成功后再从池中获取任务,再根据当前的状态进行任务的调度。

涉及到的设计模式

  • 2 苍穹

2.0 亮点

使用AOP+自定义注解的方式实现业务的公共字段的自动插入/更新

  • 定义@AutoFill注解,并定义OperationType value();属性是一个枚举类:更新和插入

    image-20240319103405484

  • 标识切面类AutoFillAspect@Aspect修饰)

    image-20240319103353768

  • 定义切入点@PointCut:限制扫描的包以及需要被@AutoFill修饰

    image-20240319103343189

  • 定义前置通知@Before("autoFillPointCut()")

    • 通过JoinPoint对象获取实体对象
    • 通过反射来设置对象的值

ThreadLocal对象

  • 实现了线程之间的数据隔离
  • 一个线程里面有一个ThreadLocalMap对象,里面维护了一个Entry数据,key就是Thread对象,value是保存的值
  • 在本项目中实现:
    • 在拦截器中对JWT解析,解析通过后保存当前用户id
    • MyBatis中的分页插件Pagehelper底层使用的也是ThreadLocal来保存
  • ThreadLocal内存泄漏:见java基础

2.1介绍

  • 本项⽬包括管理端和小程序端两部分。其中管理端主要给内部员⼯使⽤,可以对菜品的分类、套餐、订单、员⼯信息进⾏管理维 护,对订单进⾏统计分析;小程序端主要给⽤⼾使⽤,可以在线浏览菜品、添加购物⻋、下单、⽀付等
  • SpringBoot+Mybatis+Redis+MySQL

2.2 数据表

  • 地址表 address_book

    image-20240311162824258

  • 套餐分类表 category

    image-20240311162842385

  • 菜品表 dish

    image-20240311162908810

  • 菜品口味表 dish_flavor

    image-20240311162923552

  • 员工表 employee

  • 用户表 user

  • 订单明细表 order_detail

    image-20240311163113434

  • 订单表 orders

    image-20240311163219518

  • 套餐表 setmeal

    image-20240311163331594

  • 套餐明细表 setmeal_dish

    image-20240311163313463

  • 购物车表 shopping_cart

    image-20240311163341344

2.3 技术点

JWT

  • 三个部分,头部声明了加密算法HS256合加密类型JWT,载荷声明用户信息,尾部是前两部分的签名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //头部
    {
    "alg": "HS256", //第三段字符串的不可逆加密类型HS256,为什么不可逆?
    "typ": "JWT" //token类型JWT
    }
    //载荷
    {
    "sub": "1234567890", //用户id
    "name": "John Doe", //用户名
    "exp": 1516239022 //token过期时间
    }
  • 生成过程

    • 首先将1、2部分进行base64加密(这个过程是可逆的,只要别人知道了使用base64加密就可以直接解析出来)
      • base64: ‘a’到 ‘z’ ‘A’ 到’Z’ ‘0’ 到 ‘9’ 再加上 ‘+’ ‘/‘一共64个字符
      • 任何符号都可以转换成这个字符集中的字符,这个转换过程就叫做base64编码
    • 然后使用第一部分声明的HS256和保存在服务器的自定义盐来进行加密得到第三部分
  • 解析过程

    • 服务端解析1、2部分得到加密类型和信息
    • 根据签名及加密类型解密判断第三部分得到的信息是否与1、2部分一致来判断是否被篡改
    • 进一步判断第2部分进行业务操作

HandlerInterceptor拦截器实现JWT校验

  • 实现这个接口,里面有三个方法:
    • preHandle:Controller方法处理之前
      • 本项目中用于JWT令牌的校验
    • postHandle:Controller方法处理完之后,DispatcherServlet进行视图的渲染之前,也就是说在这个方法中你可以对ModelAndView进行操作
    • afterCompletion:DispatcherServlet进行视图的渲染之后

微信登录

  • 前端传入授权码发送至服务端
  • 服务端携带授权码code + appid + secret 采用HttpClient发送请求到https://api.weixin.qq.com/sns/jscode2session

  • 得到微信服务端返回的openid + session_key,其中openid也就是微信用户的唯一标识

  • 根据openid查询数据库中是否存在此用户(openid不是主键),若不存在则自动创建,存在则返回该用户信息

  • 为该用户生成jwt令牌(密钥+过期时间+信息)

    • 密钥用于加密解密jwt信息
    • 信息claims是一个map,在本项目中只存储了userId这一个信息
  • 返回VO对象(token +用户id+微信openid)


redis序列化的方式

  • JdkSerializationRedisSerializer:默认,将Java对象序列化为字节数组
  • StringRedisSerializer:默认,使用String类型作为Redis的key和value的序列化方式
  • GenericToStringSerializer:可以将任何对象泛化为字符串并序列化
  • Jackson2JsonRedisSerializer :一种更加轻量级的序列化方式,它仅仅支持JSON格式的序列化和反序列化
  • GenericJackson2JsonRedisSerializer:使用Jackson库将Java对象序列化为JSON格式的字符串,以便在Redis中存储和检索 — 常用

Reids与SpringCache

  • key的命名:用户Id+业务名使用心得

    • 取key的时候通过定义工具类来取(输入用户id然后输出key)
  • redis缓存菜品分类

    • 根据分类id查询菜品存储在List中,随后redis的key为dish_id号,value是list的内容

    • 当新增、修改、删除菜品的时候删除所有的dish_*的缓存(这里可以做的更细一点)

      image-20240224195415885

  • SpringCache缓存套餐

    • 本质:SpringCache在JVM中,集群中不可用,当然也可以实现其整合redis等缓存中间件

    • application类上添加@EnableCaching

    • 在根据分类id查询套餐前判断setmealCache这个缓存管理器中有没有指定的分类id的value,有则直接返回,没有则再执行方法,再将查询结果存储在缓存中

      image-20240224201425719

    • 在批量删除套餐的时候清理所有缓存

      image-20240224201343039


定时任务

  • Spring Task和xxl-job区别:后者是分布式的,更稳定

微信支付

  • 用户点击支付,向接口发送请求
  • 服务端生成订单:获取到当前用户的id,采用JSAPI下单,传入appid、mchid(商户号)、description、out_trade_no、notify_url(回调地址)等信息,发送post请求给微信端(httpclient)https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
  • 微信端返回预支付订单信息返回给服务端
  • 解析返回结果:prepay_id(关键,这个要给前端): 预支付交易会话标识,根据prepay_id及appid、时间等信息生成二次签名(预支付交易单),生成vo对象给前端,前端生成支付二维码
  • 支付成功请求回调地址:完成读取数据、解密数据、业务处理:修改订单状态、给微信响应

百度地图和阿里云OSS


来单提醒/用户催单

  • 客户端通过/ws/{sid}与服务端建立websocket长连接,会话对象Session存储在map中,当需要发送消息的时候调用session.getBasicRemote().sendText(message);方法即可

  • 在客户支付成功之后调用

  • websocket基于TCP连接

    • TCP三次握手
    • 四次挥手

多用户抢单

  • 乐观锁

订单超时未支付怎么处理

  • 扫表轮询
    • 用一个定时任务获取数据库中的待支付状态数据,批量关闭超时订单
    • 缺点:表的数据多的时候就会耗时
  • 懒删除
    • 用户查询订单的时候在业务逻辑中额外做一个判断
    • 缺点:如果没有查询的话就会一直挂起,库存数据就一直被占用
  • 消息队列实现(多)
    • 下单的时候发送消息到延时队列,到期的时候检测支付状态,未支付就关闭
  • redis实现
    • 发送一个redis消息,设置过期时间,然后设置过期回调,处理检测支付状态
    • 缺点:redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性检查是否过期并删除过期键。不能保证一到过期时间就删除

3 学成

3.0 项目亮点

  • Spring Security整合了OAuth2.0,JWT完成分布式的认证

    • 自定义继承DaoAuthenticationProvider,取消里面的密码校验,checks()方法直接返回null
    • WebSecurityConfig配置类中注入刚刚的对象
    • 自定义类继承UserDetailService接口,重写loadUserByUsername() 方法,根据登录方式在方法里面通过ApplicationContext容器找到对应的service bean,调用对应的方法(策略模式),最终返回UserDetails对象(username修改为用户信息,password随便写,但不能为空,authorities表示权限的数组)
  • 分布式事务控制的幂等性

    • 使用一个任务表记录任务的执行情况,保证了任务只会被执行一次
  • 使用redis对缓存进行优化,将课程的发布信息存入到redis中,减少对数据库的访问压力

3.1介绍

  • 本项目是一个在线学习网站项目,采用SpringBoot、SpringCloud技术栈开发,划分的微服务包括网关服务、认证授权服务、注册中心服务、配置中心服务、内容管理服务、媒资管理服务、搜索服务、订单支付服务、学习中心服务、系统管理服务等,可以实现用户的网课在线学习等功能.

  • SpringBoot+SprintCloud Alibaba+Spring Security+ Mybatis-Plus+Redis+RabbitMQ+Elasticsearch+MinIO

    • 本项目包括两个端:用户端(学生端)、机构端

      核心模块包括:内容管理、媒资管理、课程搜索、订单支付、选课管理、认证授权等,每个模块对应了一个微服务,

      采用前后端分离架构,后端采用SpringBoot、SpringCloud技术栈开发,数据库使用了MySQL,还使用的Redis、消息队列、分布式文件系统、Elasticsearch等中间件系统。

      我在这个项目中负责了课程内容管理、媒资管理、订单支付管理、搜索管理、认证授权管理模块的开发。

    • 内容管理模块.是对平台上的课程进行管理,课程的相关信息比较多这里在数据库设计了课程基本信息表、课程营销表、课程计划、课程师资表进行存储 ,培训机构要发布一门课程需要填写课程基本信息、课程营销信息、课程计划信息、课程师资信息,填写完毕后需要提交审核,由运营人员进行课程信息的审核。课程审核通过即可选择发布课程,课程的相关信息会聚合到课程发布表中,这里不仅要将课程信息写到课程发布表还要将课程信息写到redis、课程索引写进ES索引库、课程内容静态页面上传分布式文件系统,所以这里存在分布式事务的问题,项目使用本地消息表加任务调度的方式去解决这里的分布式事务,保存数据的最终一致性。

    • 媒资管理模块。对平台上的媒资信息进行管理,主要包括相关图片视频的分布式存储,采用的MINIO分布式文件系统,特别的,实现了对大文件的分块上传及校验;以及使用分布式任务调度平台xxl-job来实现视频转码的功能。

    • 搜索模块。目前是完成了对课程的搜索,通过定义索引的操作接口(增删改查)及搜索接口,实现用户快速进行关键字索引课程的需求

      • RestHighLevelClient对象的indexupdatedelete方法进行索引的管理

      • RestHighLevelClient对象的search进行搜索

    • 认证授权管理模块。在这个模块中主要是通过SpringSecurity实现了统一登录接口,具体的支持账号密码及微信扫码登录两种方式;此外也实现了基于角色的访问控制(RBAC),通过定义不同类型的用户来实现角色的权限控制。

    • 支付模块。目前是使用一个支付宝的沙箱环境完成了一个模拟在线支付的行为,了解到了支付宝的支付流程,在选课支付成功后通过向MQ中发送消息更新相关的用户课程信息来实现业务的解耦。

3.2 数据表

  • 数据表

3.3 模块点

内容管理服务

  • 表:课程基本信息表、课程营销表、课程计划表、课程师资表、课程媒资表
  • 为什么用多张表?方便不同的服务使用,比如课程媒资表

  • 课程提交审核、课程发布、课程查看(Freemarker)

    • 为什么使用Freemarker?(FreeMarker、Velocity、Thymeleaf)
      • 页面静态化是指使用模板引擎技术将一个动态网页生成html静态页面,满足以下条件可以考虑使用静态化
        1. 该页面被访问频率高,例如:商品信息展示、讲师介绍页面
        2. 页面上数据变化频率低,例如:商品发布后对商品信息的修改频率低、讲师介绍信息修改频率低
      • 静态化的技术很多,Freemarker是一个成熟的开源的模板引擎工具,简单易用,功能强大,本项目使用Freemarker将课程信息静态化
        1. 使用Freemarker的标签编写课程信息的模板
        2. 调用接口获取模板上需要的模型数据
        3. 调用Freemarker的API生成静态页面
        4. 生成的静态页面最终会上传到文件系统方便访问
  • 师资的增删改查

媒资管理服务

  • 表:媒资信息表、视频任务处理表

  • 云计算厂家:阿里云OSS、百度对象存储BOS,这里用的MINIO

  • 上传图片/视频
  • 断点续传(视频分块)
  • 视频处理(分布式任务调度xxl-job,FFmpeg工具)
  • 视频下载:FileOutputStream,InputStream
  • 分块合并:
    • MinioClientcomposeObject()方法
    • 合并完成后下载,校验md5
    • 信息入库并删除分块

认证授权服务

image-20240318161649210


订单服务

  • 支付宝支付(订单表+消息通知表)
    • 请求学习中心服务创建选课记录
    • 请求订单服务创建商品订单、生成支付二维码(存储到订单表数据库,payNo主键
    • 用户扫码请求订单支付服务,订单支付服务请求第三方支付平台生成支付订单(根据payNo,支付金额,订单名称,回调地址等信息,使用AlipayClient调用sdk请求下单
    • 前端唤起支付客户端(支付宝会返回一个表单数据,用户前端会生成支付宝支付的界面),用户输入密码完成支付
    • 第三方支付平台支付完成后,发起支付通知
    • 订单支付服务接收支付通知结果(支付宝在支付完成后会自动的请求这个回调通知接口
      • 获取支付宝传来的DTO,然后根据payNo在数据库中找到订单,然后更新订单数据
      • 向MQ中添加消息(解耦
    • 用户在前端查询支付结果,请求订单支付服务查询支付结果,如果订单服务还没有收到支付结果,则请求学习中心查询支付结果
    • 订单支付服务向学习中心通知支付结果
    • 学习中心服务收到支付结果,如果支付成功则更新选课记录,并添加到我的课程表

搜索服务

  • 本项目使用ElasticSearch开发搜索服务,步骤如下
    1. 创建索引(相当于数据库表),将课程信息添加到索引库,对课程信息进行分词,存储到索引库
    2. 开发一个搜索服务,编写课程搜索接口,调用ElasticSearch的rest接口根据关键字、课程分类等信息进行搜索
  • 如何保证索引同步?
    • 本项目时使用本地任务表 + xxl-job任务调度进行索引同步,具体流程如下
      1. 添加或修改或删除课程的同时,向任务表插入一条记录,这条记录就记录了是添加还是修改还是删除了哪个课程
      2. 任务调度定时扫描任务表,根据任务表的内容对课程信息进行同步
        • 如果添加了课程,将课程添加到索引库
        • 如果修改了课程,就修改索引库的课程
        • 如果是删除了课程,将课程信息从索引库删除

3.4 技术点

异常处理

  • @ControllerAdvice@ExceptionHandler用来捕获异常

    • @ControllerAdvice 是一个特殊的 @Component(可以通过源码看得到),用于标识一个类,这个类中被以下三种注解标识的方法:@ExceptionHandler@InitBinder@ModelAttribute,将作用于所有@Controller 类的接口上。
  • @ResponseStatus用来确定响应数据

    image-20240303152434784


传入数据是否为空的方法

  • 在controller方法内部手动判断:StringUtils.isNotEmpty

  • 实体类的数据上标注@NotNull,然后在controller中的形参前面加上@Valid注解,再在全局异常中添加BindException异常类的捕获

    image-20240303153429684


controller与前端的数据传递

  • 形参不带注解:此时参数名称一定要和请求参数的名称一致
    • get方式提交,直接写参数
    • 通过HttpServletRequest接收:request.getParameter("username")
    • 通过java类对象接收
  • 形参带注解
    • @RequestParam:有value、require、defaultValue字段可填充
    • @PathVariable:支持类似于:user/get/mac/{macAddress}的请求
    • @ModelAttribute("user"):会将客户端传递过来的参数按名称注入到指定对象中,并且会将这个对象自动加入ModelMap中,便于View层使用

树形查询

  • left join连接表(可以是相同的表)

  • 指定resultMap为一个值,然后再在指定的resultMap中设置映射关系并将结果返回。

    image-20240303160733160

    image-20240303160740937


视频分块上传

  • RandomAccessFile实现文件的分块和合并
  • 判断是否一致 ==> DigestUtils.md5Hex()

@Transactional

  • 自调用的时候需要声明一个代理对象来方便进行事务的管理

    image-20240304110055984

  • @Transactional即声明式事务管理, 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。


freemarker

  • controller中返回ModelAndView对象,在setViewName中设置重定向的页面文件,在addObject中传值

image-20240304142757986


xxj-job

  • 工作原理:xxl-job分布式任务调度服务由调度中心和执行器组成,调度中心负责按任务调度策略向执行器下发任务,执行器负责接收任务,执行任务
  • 获取执行器序号、 执行器总数,然后用表的id % 总数 == 序号来保证一个任务只会被一个执行器执行,用字段标识来保证一个任务只会被执行一次(乐观锁)
  • 用于视频转码作业
  • 课程发布操作后,先更新数据库中的课程发布状态,更新后向Redis、ElasticSearch、MinIO写课程信息,只要在一定时间内最终成功写入数据即可
    • 这里使用了feign远程调用,在向MinIO写信息的时候调用了Media服务的相关接口
      • 熔断降级:返回null对象,然后再去判断是否为null来判断是否发生了故障
      • 注意启动类开启注解@EnableFeignClients
  • 如何保证任务幂等性?
    • 数据库约束,例如:唯一索引、主键
    • 乐观锁,长用户数据库,更新数据时根据乐观锁状态去更新
    • 唯一序列号,操作传递一个唯一序列号,操作时判断与该序列号相等,则执行
    • 在这里我们在数据库视频处理表中添加状态处理字段,视频处理完成更新状态为完成,执行视频前判断状态是否完成,如果完成则不再处理

ES

  • 如何解决索引同步的问题?
    • xxl-job 详见模块点中搜索模块

Spring Security

  • 继承UserDetailsService接口,重写loadUserByUsername()方法

    • 返回UserDetails对象

      image-20240305102255518

  • DaoAuthenticationProviderCustom extends DaoAuthenticationProvider

    • 刚刚我们重写的loadUserByUsername()方法是由DaoAuthenticationProvider调用的
    • 重写additionalAuthenticationChecks()里面会比对密码,但不是所有的登录方式都有密码
    • 然后在WebSecurityConfig extends WebSecurityConfigurerAdapter中配置刚刚定义的 DaoAuthenticationProvider

  • OAuth2.0

    • 客户端请求资源拥有者授权
    • 资源拥有者授权客户端,即用户授权目标网站访问自己的用户信息
    • 目标网站携带授权码请求认证
    • 认证通过,颁发令牌
    • 目标网站携带令牌请求资源服务器,获取资源
    • 资源服务器校验令牌通过后,提供受保护的资源
  • JWT:

    • 好处:在认证服务颁发令牌给客户端后,客户端携带令牌请求其他服务的资源时,其他服务可以直接校验令牌合法性,无需再经过认证服务。
    • 缺点:JWT令牌占用空间较大
    • 无状态认证:用户身份信息存储在令牌中,服务端从JWT解析出用户信息
    • 有状态认证:用户信息通过session存储在服务端
    • 组成
      • 头部Header:令牌类型及使用的哈希算法
      • 负载Payload:用户信息
      • 签名Sugbature:根据密钥进行加密前两部分,防止篡改
  • SecurityContextHolder获取当前访问的用户信息:Authentication对象

    • 原理:ThreadLocal
  • 用户授权

    • 授权认证服务器@EnableAuthorizationServer ==> 配置资源列表:xuecheng-plus
    • 资源服务器@EnableResourceServer ==> 定义资源id:xuecheng-plus
    • 接口添加@PreAuthorize("hasAuthority('权限标识符')")即可
    • 授权定义在loadUserByUsername方法中返回的UserDetails中,是一个String类型的数组
    • 没有权限抛出AccessDeniedException在全局异常处理器中捕获即可

验证码

  • Kaptcha

RabbitMQ

  • 4 仿真项目

三 笔试

3.13 携程

  • long

3.16 蚂蚁

数组每次可以变化ij下标的值(+2 和 -2),如果可以在有限次数让所有值的最大公因数为素数,则输出所有的可能

  • sum不变,分奇偶
  • 讨论 [3, sum/n]区间内的所有素数
  • 如果全是偶数(even个),则只能是2
  • 如果有奇数(odd个)
    • 记最大素数为i
    • 先分配偶数,则一共分配 2 * i * even
    • 判断剩下 sum - 2 * i * even 能否恰好分配给odd个奇数 ==> (sum-2*i*even) % i == 0 && (sum-2*i*even) / i >= odd

3.16 美团

小美拿到了一个数组,她每次操作会将除了第x个元素的其余元素翻倍,一共操作了q次。请你帮小美计算操作结束后所有元素之和。 由于答案过大,请对10^9+7取模。16%

  • 快速幂法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    static long qpow(int a, int b, int mod) {
    // 求a翻倍b次,结果取模mod
    long res = 1, t = a;
    while (b > 0) {
    if ((b & 1) == 1) { // 只计算末尾是1的指数,因为任何数的0次方=1,就没必要乘了
    res = (res * t) % mod;
    }
    b >>= 1; // 指数位右移动:1011(2) -> 101(2)
    t = (t * t) % mod;
    }
    return res;
    }
  • 调用

    1
    2
    3
    for (int i = 0; i < n; i++) {
    res = (res + qpow(2, s[i + 1], mod) * a[i] % mod) % mod;
    }
  • eg:比如求3^45

    • 指数位置 45(10) = 101101(2)
    • 也就只要求 2^5 2^3 2^2 2^0这几个幂指数

小美拿到了一个数组只包含1和2,她希里你求出所有区间众数之和 70%

  • 树状数组/二叉索引树(Binary Indexed Tree)解决的问题:对于每个index,左边有多少数字小于等于它,有多少数字大于等于它

    • O(logn)得到任意前缀和

    • 由数组arr构建二叉索引数bit规则:

      • bit[x] = arr[x],当x为奇数

      • bit[x] = a[0] + a[1] + … + a[x],当x为偶数且x为2的幂

      • bit[x] = a[x-2^k+1]+ … + a[x],当x为偶数且不为2的幂,其中k = lowbit(x)

        image-20240318135832990

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
from https://codefun2000.com/p/P1704 实战最重要 加了点注释
* */
public class Main {
static int n;
// 有效部分[n+1, :] tr[i]表示前缀和为i的出现次数
static int[] tr;

// 返回一个整数在二进制表示中最低位的1及其后面的0构成的数 10(10) <==> 1010(2) -> 10(2) <==> 2
// 可以用这个方法获取一个数对应的所有二进制位为1对应的十进制数(递归)
// 比如:lowbit(11)=1 然后 11-1=10;再lowbit(10)=2,然后10-2 = 8; 再lowbit(8)=8, 8-8 -= 0;结束
static int lowbit(int x){
return x & -x;
}

// 在树状数组bit中的指定索引idx处增加1。这个操作相当于更新前缀和。
// 比如:add(12)
static void add(int idx){
idx += n + 1; // 这里的索引idx可能为负数(如果是2的情况是--1的),然后是从bit[1]开始算的 所以 += n+1

for(int i =idx;i<tr.length;i += lowbit(i)){
tr[i] += 1;
}
}

// 计算前缀和
// 比如:pre(12) = Bit(12) + Bit(8) = a[12]+..+a[9] + a[8]+...a[1]
// pre(13) = Bit[13] + Bit[12] + Bit[8]
static int pre(int idx){
idx += n + 1;
int sum = 0;
for(int i = idx;i > 0;i-=lowbit(i)){
sum += tr[i];
}
return sum;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
int[] A = new int[2 * n + 100];
tr = new int[2 * n + 10];
for(int i = 1; i <= n;i++) {
A[i] = sc.nextInt();
}
int presum = 0; // A[0, index] 当前位置之前(包括当前位置)的前缀和。其中1对应-1 2对应1
add(presum);
long ans = 0;
for(int i = 1; i <= n;i++){
if(A[i] == 1) {
presum += 1;
} else {
presum -= 1;
}

// pre(presum):在当前位置之前,有多少个区间的众数是1
// i - pre(presum):在当前位置之前,有多少个区间的众数是2
ans += (long) pre(presum) + 2L *(i - pre(presum));
add(presum);
}
System.out.println(ans);
}
}

3.29 小红书

  • 一个数组,一个目标值,每个元素至多用一次,并且可以变成自己的一半(向下取整),当取出的元素之和为目标值时,计算最小的元素个数

    1
    2
    3
    输入: arr:1 2 3 4 11 target:8
    输出: 2
    解释:取3和11,其中11向下除以2取整变成5 则3+5=8
    • 写了一个动态规划复杂度过高

4.3 淘天

  • 给定一个长度为n的数组arr, 定义一个函数f(l,r,x)表示区间[l,r]x出现的次数,现在给出数组arr,求所有的数对(i,j)满足f(0,i,arr[i]) > f(j, n-1, arr[j])

    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public void findPairs(int[] arr) {
    int n = arr.length;

    // 前缀map数组和后缀map数组
    // prefixCounts[i]存储了[0, i]所有元素出现的次数
    // suffixCounts[j]存储了[j, n]所有元素出现的次数
    Map<Integer, Integer>[] prefixCounts = new HashMap[n];
    Map<Integer, Integer>[] suffixCounts = new HashMap[n];

    // 初始化map数组
    for (int i = 0; i < n; i++) {
    prefixCounts[i] = new HashMap<>();
    suffixCounts[i] = new HashMap<>();
    }

    // 填充前缀map数组
    for (int i = 0; i < n; i++) {
    if (i != 0) {
    // 复制前一个map到当前map
    prefixCounts[i].putAll(prefixCounts[i - 1]);
    }
    prefixCounts[i].put(arr[i], prefixCounts[i].getOrDefault(arr[i], 0) + 1);
    }

    // 填充后缀map数组
    for (int i = n - 1; i >= 0; i--) {
    if (i != n - 1) {
    // 复制后一个map到当前map
    suffixCounts[i].putAll(suffixCounts[i + 1]);
    }
    suffixCounts[i].put(arr[i], suffixCounts[i].getOrDefault(arr[i], 0) + 1);
    }

    int count = 0;
    // 遍历数组,寻找符合条件的数对(i, j)
    for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
    int prefixCount = prefixCounts[i].getOrDefault(arr[i], 0);
    int suffixCount = suffixCounts[j].getOrDefault(arr[j], 0);
    if (prefixCount > suffixCount) {
    // 输出符合条件的数对(i, j)
    System.out.println("(" + i + ", " + j + ")");
    count++;
    }
    }
    }

    System.out.println("Total pairs: " + count);
    }

4.16 钉钉

  • 输入一个十进制数x,数字n以及进制k,求反转后的数。反转的定义:x转为k进制后变为x’,将x’上的所有位上的数p变为k-p

    • 例:n=4,x=6,k=2 ,则k’=0110(需要保留高位的0) ,反转后k’’ = 2002 ,对应十进制为:18
    • 思路:将所有位变为k比如n=4,k=2,就变为2222,对应的就是一个等比数列2 4 8 16的和,求这个和记为a,则结果就是a-x(过程中注意取模)
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class CorrectKBaseToDecimal {

    private static final long MOD = 1_000_000_007L;

    // 快速幂计算 (a^b) % mod,计算 a 的 b 次方对 mod 取模的结果。
    private static long powMod(long a, long b) {
    long result = 1;
    a %= MOD; // 确保 a 在 MOD 下
    while (b > 0) {
    if ((b & 1) == 1) {
    result = (result * a) % MOD;
    }
    a = (a * a) % MOD;
    b >>= 1;
    }
    return result;
    }

    // 计算所有位都是k的k进制数并减去x,然后取MOD
    public static long compute(long n, long k, long x) {
    // 使用等比数列求和公式计算所有位都是k的k进制数的十进制表示
    long sum = k * ((powMod(k, n) - 1 + MOD) % MOD) % MOD;
    // 计算 (k-1) 的逆元
    long inverse = powMod(k - 1, MOD - 2);
    // 计算公式结果
    long result = (sum * inverse % MOD - x + MOD) % MOD;
    return result;
    }

    public static void main(String[] args) {
    // 示例输入
    int n = 3, k = 2;
    long x = 14;
    // 计算并输出结果
    System.out.println(compute(n, k, x));
    }
    }

4.18 阿里云

  • 给定一个字符串str,求他所有连续子串的权值之和,其中一个字符串权值的定义为:该字符串包含“a”, “li”,“yun”的数量,比如”ali”权值为2,”syuns”权值为1

    • “aliyun”权值为18
    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
    public static int getTotalWeight(String str) {
    int re = 0;
    int n = str.length();

    char[] chs = str.toCharArray();

    for (int i = 0; i < n; i++) {
    if(chs[i] == 'a') {

    // 计算包含当前字符的所有子串数量
    re += n;

    }else if(i >= 1 && chs[i-1] == 'l' && chs[i] == 'i') {

    //这个 li 字符串前面有 i-1 个字符 后面有 n-i-1 个字符
    // xxxlixxxx 那么含有li的子串数量为 3*4 + 3 + 4 + 1(这个1的意思是只包含li)
    re += (i-1) * (n-i-1) + i-1 + n-i-1 + 1;

    }else if(i >= 2 && chs[i-2] == 'y' && chs[i-1] == 'u' && chs[i] == 'n') {

    // yun 前面有 i-2 后面有 n-i-1
    re += (i-2) * (n-i-1) + i-2 + n-i-1 + 1;
    }

    }

    return re;
    }

4.26 小红书2

  • n个粉丝中选k个最狂热的粉丝,给n行输入,每行有2列,第一列是粉丝i点赞数,第二列是粉丝i的收藏数。定义狂热度:点赞数是1分,收藏数是2分。如果两个粉丝狂热度一样就比较收藏数,如果收藏数也一样序号小的狂热度高

    • 优先级队列:

      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
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      static void solve() throws IOException {
      int n = in.nextInt();
      int k = in.nextInt();
      int[][] a = new int[n + 1][3];

      PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
      @Override
      public int compare(int[] o1, int[] o2) {
      int score1 = o1[1] + o1[2] * 2;
      int score2 = o2[1] + o2[2] * 2;
      if (score1 != score2) {
      return score2 - score1;

      } else if (o1[2] != o2[2]) {
      return o2[2] - o1[2];

      } else {
      return o1[0] - o2[0];
      }
      }
      });

      for (int i = 1; i <= n; i++) {
      a[i][0] = i;
      a[i][1] = in.nextInt();
      a[i][2] = in.nextInt();
      pq.offer(a[i]);
      }

      int[] ans = new int[k];
      while (k-- > 0) {
      int[] p = pq.poll();
      ans[k] = p[0];
      }
      Arrays.sort(ans);
      for (int id : ans) {
      out.print(Integer.toString(id) + ' ');
      }
      }
  • 给一个数组arr以及一个目标数x,每个粉丝群可转移 a[i]/2 个粉丝,也可以选一个粉丝群转移 a[i] 个粉丝,求获得目标粉丝数所需的最少粉丝群数量。

    • 背包问题:

      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
      29
      30
      31
      static void solve() throws IOException {
      int n = in.nextInt();
      int x = in.nextInt();
      int[] a = new int[n];

      for (int i = 0; i < n; i++) {
      a[i] = in.nextInt();
      }
      int ans = func(a, -1, x);
      for (int i = 0; i < n; i++) {
      ans = Math.min(ans, func(a, i, x) + 1);
      }
      out.println(ans == 0x3f3f3f3f ? -1 : ans);
      }

      static int func(int[] a, int idx, int target) {
      if (idx != -1) target -= a[idx];
      if (target < 0) return 0x3f3f3f3f;
      int n = a.length;
      int[] dp = new int[target + 1];
      Arrays.fill(dp, 0x3f3f3f3f);
      dp[0] = 0;
      for (int i = 1; i <= n; i++) {
      if (i == idx + 1) continue;
      int val = a[i - 1] / 2;
      for (int j = target; j >= val; j--) {
      dp[j] = Math.min(dp[j], dp[j - val] + 1);
      }
      }
      return dp[target];
      }

四 面试

4.1 携程一面 3.20

  • 自我介绍10min 有点多嗯

  • 苍穹中担任什么职务,比较棘手的点?做学习用的,设计数据表,对相关业务的开发;用户高并发的情况

  • 那多用户抢单的高并发过程怎么考虑?时间戳记录版本信息

    • 高并发:同一时刻有大量请求访问系统;大量请求并行访问系统。
    • 提升机器的性能。
    • 使用redis缓存数据,记录binlog日志信息,存入消息队列中,实现缓存的最终一致性。
  • 怎么理解任务幂等性?怎么实现? 一个任务只会被执行一次;任务消息表

    • 一次和多次请求某一个资源对于资源本身应该具有同样的结果,任意多次执行对资源本身所产生的影响均与一次执行的影响相同
    • 使用唯一索引防止新增脏数据
    • 乐观锁:通过版本号来记录
    • 分布式锁:redis,先获取锁再操作表再释放锁
  • 怎么理解AOP?定义+jdk/cglib 原理及区别

  • AOP的几个概念?切面+连接点+切入点+通知

  • AOP用什么捕获异常?@AfterThrowing

  • redis线程?之前是单线程,之后是多线程。提到了线程不是瓶颈及IO多路复用

  • redis跳表?g

  • setEx setNx,如何设置key过期时间?g

    • set key value :将字符串值 value 关联到 key
    • setNX key value: 将 key 的值设为 value ,当且仅当 key 不存在。

      • setnx = SET if Not eXists
    • setEX key seconds value:将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。

      • setex = set expire
  • 基本数据类型和包装类型区别?在JVM存储区域

  • 为什么静态方法不能调用非静态变量?

    • 在一个类的静态成员中去访问非静态成员之所以会出错是因为在类的非静态成员不存在的时候静态成员就已经存在了,访问一个内存中不存在的东西当然会出错。
  • java三大特性?继承封装多态

  • HashMap底层原理?数组+链表/红黑树,然后讲了元素的插入过程,数组长度64 链表长度8

  • 乐观锁和悲观锁?g

    • 悲观锁假设冲突会发生,在访问共享资源之前先锁定资源,确保数据的一致性。悲观锁的特点是在访问共享资源之前会先锁定资源,确保其他线程无法修改该资源,直到当前线程完成操作。常见互斥锁和读写锁
    • 乐观锁假设冲突较少,允许多个线程同时读取和操作共享资源,只在更新时进行冲突检查和处理。当要更新共享资源时,乐观锁会检查在操作期间是否有其他线程修改了该资源。如果没有冲突发生,操作继续进行;如果发现冲突,乐观锁会回滚操作并重新尝试。用版本号和时间戳来记录。
  • 线程池常用参数?提高了最大连接数和线程数量

    • 核心线程数:corePoolSize
    • 最大连接数:maximumPoolSize
    • 空闲线程存货时间:keepAliveTime
    • 时间单位:unit
    • 工作队列:workQueue:存放待执行任务的队列
    • 线程工厂:threadFactory
    • 拒绝策略:handler
  • 线程池策略?g

    • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    • DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
    • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
    • CallerRunsPolicy:由调用线程处理该任务。
  • JVM内存区域?java堆,方法区+java栈,本地方法栈,线程计数器

  • mysql索引类型?g

    • 主键索引
    • 普通索引
    • 唯一索引
    • 全文索引
    • 前缀索引
    • 组合索引
    • 空间索引
  • 序数索引和非序数索引?g

    • 聚簇索引和非聚簇索引?

      • 聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB的主键索引就是
      • 非聚簇索引(非聚集索引):不存在一起的索引

      image-20240320172122032

4.2 美团一面 3.22 外卖平台

  • 自我介绍3min 然后
  • 问了项目什么时候做的,有无团队一起做
  • 问了一下FS立库的算法还有引擎的优化 15min 讲的不太清除
  • 苍穹的表结构 菜品表和套餐表
  • 两个表的关系 多对多
  • 怎么管理套餐的 增删改查
  • 怎么做套餐的修改的,提供了哪些修改
  • 库存怎么存储的 redis+mysql
  • redis还存了什么 购物车和菜品
  • redis和mysql一致性? 先修改mysql再删除redis
  • 为什么这样做?高并发带来的数据不一致
  • 为什么不先更新redis后更新mysql?举了一个例子
  • redis是什么时候更新?查询的时候更新
  • 数据修改频繁呢?异步缓存,以redis缓存为主,先修改redis,由后台修改mysql
  • 为什么用redis?内存 + IO多路复用

  • IO多路复用? 单线程处理多个并发,提到了同步IO 异步IO(应该是阻塞IO和非阻塞IO)

  • mysql B+树的优点?对比了hash表、二叉搜索树、平衡树、红黑表、B树
  • AOP底层原理?JDK cglib,原理,生成字节码,执行效率的对比
  • java多态解决了什么问题?

    • 简化对应用软件的代码编写和修改过程
  • 进程和线程的区别?进程是操作系统的最小单位由多个线程组成,线程可以上下文切换

  • 线程调度过程,上下文切换

    • 线程上下文切换是指操作系统为了能够让多个线程并发执行,在运行一个线程前,需要保存当前线程的 CPU 寄存器、程序计数器、栈指针和其他硬件上下文信息,以便于在恢复该线程时还原到之前的状态。而将这些信息保存起来、加载其他线程运行所需的上下文信息,然后再切换到该线程继续执行的过程就被称为线程上下文切换
  • MQ解决了什么问题?业务解耦,服务的通讯

    • 业务解耦
    • 流量削峰
  • TCP为什么是可靠的?三挥四握,传输过程中,全双工
    • 数据传输之前会有三次握手来进行连接
    • 在数据传输时候,有确认、滑动窗口、超时重传、拥塞控制之类机制
    • 数据传输之后会进行四次挥手断开连接来节约系统资源。
  • 算法:二叉树最长路径和 20min

4.3 饿了么一面 3.25

  • 一直问fs,然后问了毕业情况,能不能出来

  • fs中的设计模式?

    • 单例设计模式。图标
    • 代理模式。堆垛机

4.4 美团二面 3.26

  • 介绍自己 5min
  • 主要在第一个项目
    • 是怎么做优化的,有什么对比,用什么方式评估达到了要求
    • 开发团队是怎么配合的
    • 你们的软件的主要用途
    • 这个过程中挑战比较大的地方 代码上手+立库
    • 这个过程中收获了什么 了解了java,开发的规范性
    • 是怎么沟通的
  • 你是怎么看java后端开发的
  • 为什么选择做外卖项目 学习,和生活贴近
  • 后端都是自己搭的吗?中间有什么问题? 版本的改动,自己的解决办法
  • redis怎么用的?购物车,菜品数据
  • 并发量不这么大为什么要用redis?学习
  • redis为什么这么快?内存IO+IO多路复用
  • 讲一下IO多路复用。 讲了同步阻塞模型、同步非阻塞模型、IO多路复用(文件描述符)
  • 有什么想问的
    • 做的商家事业部:商家线上注册、外卖、帮助线上商家能卖更多的东西
    • 关于结果:一周内
  • 闲聊时间 10min
    • 对面试的过程有什么感想
    • 有什么职业规划
    • 怎么看java后端的
    • 什么时候可以实习,实习时间段,准备在北京发展吗
    • 怎么看互联网这个行业
    • 怎么看很多人去选择进入体制内
    • 为什么还在机械学院读研不去计算机学院

4.5 腾讯一面 3.27

  • 如何解决多个恶意请求(黑名单多到缓存放不下)

    • 用布隆过滤器,缺点是有可能会造成白名单误被认为是黑名单的情况

    • 如何优化:可以加大哈希函数的数量

  • 输入一个url到完成请求的过程

  • 在使用DNS解析的过程,使用了什么连接

    • 本地查询、递归查询、最后向下返回ip地址
    • UDP(一开始说的TCP,但是又说TCP比较慢,应该用的UDP)
  • TCP和UDP区别

    • 面向连接
    • 可靠
    • 数据量
    • 头部开销
  • TCP如何确保传输的稳定的
    • 对数据包有一个排序校验
    • 重传机制
    • 滑动窗口的流量控制:保证发送方不会发过多数据导致接收方处理不过来或者缓冲区溢出
    • 拥塞控制:保证网络中的传输速率不超过网络容量
  • TCP三次握手时,在收到客户端的SYN包后一定会返回ACK吗?如果服务端建立了过多的连接可能不返回(不确定)

    • 防火墙导致入站连接被过滤了
    • 网络出现拥塞或者故障
    • 为了防止TCP SYN 攻击,某些服务器可能会配置特殊的防护机制,例如 SYN 队列限制或 SYN cookie,这可能会导致服务端不返回 SYN+ACK 包。
    • 如果服务端的资源(如内存、CPU)不足,可能会导致服务端无法处理新的连接请求,从而不返回 SYN+ACK 包。
  • mysql慢查询是怎么发现的,用什么命令

    • 启用慢查询日志,在配置文件中

      1
      2
      3
      slow_query_log = 1 -- 启动
      slow_query_log_file = /path/to/slow-query.log -- 指定慢查询日志文件的路径和名称。
      long_query_time = 1 -- 定义执行时间超过多少秒的查询被认为是慢查询
    • 重启

    • 查看日志

      1
      cat /path/to/slow-query.log
  • 那是如何优化一个sql的

    • 优化sql语句
      • 尽量避免SELECT *
      • 尽量减少JOIN,考虑使用子查询或者临时表
      • 避免在WHERE子句中使用函数,因为这可能会导致索引失效。
        • where中使用了函数
        • where中发生了类型的转换
        • where中使用了模糊查询
        • where中使用了非索引列
    • 索引
    • 优化表的设计:分表分库
  • 在采用水平分表的时候,如果是用数据id取模确定数据库的情况下,假如出现了某个数据库的突然宕机该怎么办

    • 方案一:修改id取模值,但是这种方式代价相对大,因为他需要将所有的数据进行重新的备份
    • 方案二:对于新插入的数据,当其取模后得到的值对应的是宕机的数据库后,将其id增加一位,保证数据插入到正常数据库中。优点是对于原有的数据可以不用修改
  • linux中如何查看某个端口的使用情况

    1
    2
    3
    netstat -tuln | grep <端口号>  # t表示tcp u表示udp l表示仅显示监听状态 n表示直接显示IP地址和端口号而不进行反向解析(根据IP查域名)
    ss -tuln | grep <端口号>
    lsof -i :<端口号>
  • 了解kafka吗 没用过

  • 手撕:一个字符串数组,然后一个字符串,问匹配的最长字符串是什么

    1
    2
    3
    4
    输入:
    数组:ABC abs ASs ABCD
    字符串:ABCssadoasosahfeiowhoawjsdohABCDasfdhw
    输出:ABCD
    • 想了一个暴力,面试官说优化一下,没想出来,然后提示说字典树会不会,不会

    字典树,本质就是维护一个路径

    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    class TrieNode {
    TrieNode[] children;
    boolean isEnd;

    public TrieNode() {
    children = new TrieNode[52]; // 考虑大小写字母,总共52个字符
    isEnd = false;
    }
    }

    public class LongestSubstringMatching {
    public static void main(String[] args) {
    String[] arr = {"AB", "ABC", "ABCD", "asbasd", "asfa"};
    String str = "asfhsadfhiABasdoiahABCDasdhooABC";

    String longestMatchingSubstring = findLongestMatchingSubstring(arr, str);
    System.out.println("Longest matching substring: " + longestMatchingSubstring);
    }

    public static String findLongestMatchingSubstring(String[] arr, String str) {
    TrieNode root = new TrieNode();
    String longestMatchingSubstring = "";

    // 构建字典树
    for (String s : arr) {
    insert(root, s);
    }

    int n = str.length();
    for (int i = 0; i < n; i++) {
    TrieNode node = root;
    for (int j = i; j < n; j++) {
    char c = str.charAt(j);
    int index = getIndex(c);
    if (node.children[index] == null) {
    break;
    }
    node = node.children[index];
    if (node.isEnd && j - i + 1 > longestMatchingSubstring.length()) {
    longestMatchingSubstring = str.substring(i, j + 1);
    }
    }
    }

    return longestMatchingSubstring;
    }

    private static void insert(TrieNode root, String word) {
    TrieNode node = root;
    for (char c : word.toCharArray()) {
    int index = getIndex(c);
    if (node.children[index] == null) {
    node.children[index] = new TrieNode();
    }
    node = node.children[index];
    }
    node.isEnd = true;
    }

    private static int getIndex(char c) {
    if (c >= 'a' && c <= 'z') {
    return c - 'a';
    } else {
    return c - 'A' + 26;
    }
    }
    }
  • 又给了一次机会,手写一个LRU数据结构,轻松拿下

4.6 腾讯二面 3.29

  • 自我介绍3min
  • 你不是计算机专业的
  • 网络五层结构
  • 网络如何定位一个进程:端口+IP
  • TCP为什么要三次握手?而不是两次
  • linux五种IO?BIO NIO IO多路复用 AIO
    • 同步阻塞IO
    • 同步非阻塞IO
    • IO多路复用
    • 信号驱动IO
    • 异步IO
  • 说一下IO多路复用
  • 双向链表中放的是什么?文件描述符及状态
  • 数组和链表查删综合复杂度
  • 手撕算法:一个乱序数组,输出超过1/2的众数(一定存在),5min
    • 用的map,时间复杂度O(n)
    • 面试官问快排然后返回中间的那个 O(logn)
  • 手撕算法:LRU结构

  • 说一说mysql为什么用B+树

  • 联合索引
  • 问项目,亮点是什么 redis
  • 缓存穿透?怎么解决
    • 布隆过滤器
    • 缓存那个不存在的key
    • 接口限流
  • 怎么学习的
  • 看过什么java的书 — java核心技术卷
    • 还有阿里巴巴开发手册,面试官说那个没用
  • 组织活动的时候怎么解决人与人之间的冲突
  • 怎么解决别人对你的冲突
  • 你是不是一个有责任心的人 举例
  • 反问
    • 是不是用java
    • 对我的面试评价(不能说)
    • 多久出结果(1周内)
  • 总结:感觉十分看重是不是科班

4.7 蚂蚁一面 4.2 做中间件架构

  • 自我简单介绍,没说项目

  • 手撕算法:给一个图,从源节点广播,多久能够广播所有的节点,如果不能则返回-1

    • 用的dfs,结构出来了,貌似没通过
  • 在校成绩、在校比赛、论文情况

  • 在校的课程 c++ java

  • fs团队分工

  • TCP四次挥手过程

  • 如何在linux上查询TCP的过程 netstat -nat

  • 如果是time wait状态是哪个?

    • 主动断开方(蒙对了)
  • 乐观锁和悲观锁

  • mysql怎么用行级锁

    • 共享锁

      1
      SELECT * FROM table_name WHERE condition LOCK IN SHARE MODE
    • 排他锁

      1
      SELECT * FROM table_name WHERE condition FOR UPDATE;
  • java进程内存占用高 jvm堆空间

    1
    2
    3
    pgrep -lf java   # 查看当前java进程的pid
    jmap -heap PID # 查看java堆的详细信息
    jmap -histo PID # 查看java堆中对象的相关信息,包含数量以及占用的空间大小
  • 线程池核心参数,corePoolSize 和 maximumPoolSize 区别

  • 主要做的是中间件架构的设计

4.8 蚂蚁二面 4.9

  • 问了好多微服务相关
  • 自我介绍 4min
  • 实习的软件和专业的关系
  • hashcode和equals区别
  • 三次握手四次挥手
  • 怎么理解注册中心

    • 注册中心是服务实例信息的存储仓库,也是服务提供者和服务消费者进行交互的桥梁。它主要提供了服务注册和服务发现这两大核心功能。
  • nacos上注册中心的信息,存储的信息

    • 服务名
    • 分组名
    • 集群数
    • 实例数
    • 健康实例数
    • 触发保护阈值
  • 怎么理解CAP,怎么实现

    • CA
    • CP
  • Seata 有没有了解 无

  • 分布式事务
  • 有什么技术难点?学成在线的同意登录接口
  • 微服务性能瓶颈怎么处理

    • 请求:恶意请求过滤
    • 硬件:实例扩容
    • 业务:服务熔断降级
  • 怎么理解AOP

  • redis怎么用的

    • 购物车
    • 课程/菜品信息
  • redis和数据库的同步

    • 数据更新不频繁:先更新数据库后删除redis
    • 数据更新频繁:先更新redis,后台线程同步到数据库
  • 怎么理解微服务? 网关、注册中心、配置中心、feign远程调用

  • 地点:北京、上海、杭州、成都
  • 语言转换?java 会 golang最好