Java学习笔记

Java中实现登录校验拦截器的流程

为什么需要登录校验拦截器?
不同请求可能请求到不同的Controller,每一次都要先校验当前用户的登录状态,这样导致相同的代码写了很多遍。使用拦截器在请求Controller层时先将请求拦截下来,在拦截器中统一校验登录状态,决定是否放行。

实现登录校验拦截器的流程?
1.自定义一个拦截器类实现HandlerInterceptor接口
2.实现接口中的方法,一般实现两个方法:preHandle()和afterCompletion(),前者是前置拦截,在Controller执行之前进行登录校验,后者在视图渲染完成后执行,用于销毁对应的用户信息,避免内存泄漏。
3.配置拦截器。编写一个mvcConfig配置类实现webmvcConfig接口,并加上@Config注解。
4.在配置类中实现方法addInterceptors(),使用拦截器的注册器添加拦截器及其要拦截的路径

使用JWT令牌的流程

  1. 引入jwt令牌操作的工具类
  2. 登录成功之后生成jwt令牌返回给前端
  3. 之后前端的每次请求都要携带jwt令牌
  4. 后续的请求在登录校验拦截器中进行jwt令牌的解析.

JWT令牌的组成
三部分:1. 令牌头,用于存储签名算法以及令牌类型。 2.有效载荷,存储我们自定义的信息。 3. 签名,根据签名算法及前面两部分的内容生成的,防止Token被篡改,保证安全性。
前两部分原本都是Json格式的字符串,后来经过Base64编码。最后一部分不是经过Base64编码生成而是经过签名算法生成。

使用Spring Task的流程

  1. 引入Spring Task 的依赖
  2. 在启动类上加上@EnableScheduling注解,开启任务调度
  3. 自定义一个定时任务类,定时任务类上需要加上@Component注解,交给Spring容器管理
  4. 在自定义类中定义方法,编写要定时执行的代码。在该方法上添加@Scheduled注解,注解中编写cron表达式定义定时执行的时间
  5. 标准的 Cron 表达式由 6 或 7 个字段组成,字段之间用空格分隔:秒 分 时 日 月 周 [年]其中 [年] 是可选的,通常可以省略。

在我的项目中的应用:
1.买书者挑选了想要买的书并点击下单按钮后订单进入待支付状态,用户有十分钟时间完成付款操作,如果10分钟内没有付款视为超时,取消订单。
而使用Spring Task 在每分钟检查是否有超时的订单,如果存在超时的订单,将该订单清除(设置订单状态为已取消)

  1. 用户收到货后一直不点击已收货(则当前订单状态一直为派送中),每天凌晨1点处理定时执行处理所有的未收货订单,将其订单状态改为已完成。

使用WebSocket的流程

通过Websocket实现管理端页面和服务端保持长连接状态。

  1. 导入webSocket的maven坐标

  2. 导入webSocket服务端组件webSocketServer,用于和客户端通信

  3. 导入配置类webSocketConfiguration,注册webSocket的服务端组件

  4. 导入定时任务类,在定时任务类中编写向客户端发送的数据

    • 定时任务类上面首先有@Component注解,表明该类也是交给Spring容器管理。
    • 还有个@ServerEndPoint注解里面填写了访问路径以及路径参数
    • 定时类中方法有@OnOpen(连接建立时调用),@OnMessage(收到客户端信息时调用)@OnClose(关闭连接方法),上面的方法都是加了对应的注解,自动执行。还有一个自定义的方法sendToAllClient,这个方法向所有会话发送信息,需要手动调用。

乐观锁是什么?怎么实现乐观锁?

乐观锁是锁的一种设计理念,在访问共享资源时乐观的认为此次访问不会出现线程安全问题,等到要修改时判断该数据有没有被修改过,如果被修改过则认为出现了安全问题,需要再次尝试修改。
乐观锁的实现一般有以下两种

  1. 直接使用要访问的共享资源的数量为依据判断是否出现了并发安全问题,但是这种情况下会出现ABA问题,即修改时发现数据跟第一次访问时的数据一致,但是并不能说明没有没有别的线程对该资源进行修改,可能是多个资源来回修改最后又修改回了原来的值。
  2. 新增一个版本号字段,每次有线程修改共享数据后都让版本号字段+1,线程修改数据时先判断当前版本号与第一次读取出来的版本号是否一致,如果不一致则发生了线程安全问题,需要自旋重试。

怎么实现一人一单?

在单机模式下,通过直接加悲观锁synchronized可以解决,但是在分布式的情况下不行。

在你的项目中使用了Redisson,为什么要使用?

为了实现一人一单功能,我最开始使用了synchronized锁来实现,但是在集群模式下这样做就会出现多线程并发安全问题,因为不同的tomcat服务器对应不同的JVM,那么synchronized锁住的对象也就成了不同的对象,因此需要添加分布式锁来实现。
最开始使用redis实现分布式锁(使用setnx,ex命令),redis中存储key为userId(用用户id作为锁实现对每个用户的请求加锁而不是对所有请求加锁),value为线程标识,线程标识用于实现在释放锁时比对标识,如果不是自己的锁则不能释放。
但是这样又会出现一个新的问题,对比锁与释放锁的操作不是原子性的,如果在对比锁完成后线程发生了阻塞则又会出现线程安全问题。
因此需要保证这两次操作的原子性,使用Lua脚本可以方便的实现redis多条命令的原子性执行。
以上都是基于redis的分布式锁,但是还有点小问题:1.不可重入
Redisson是在redis基础上实现的分布式工具的集合,我们可以直接使用Redisson提供的可重入的分布式锁。以下是在java中使用Redisson的基本流程:

  1. 引入Redisson的依赖
  2. 编写配置类配置Redisson
  3. 在业务中,先创建锁对象,再尝试获取锁,最后释放锁
    Redisson的底层就是通过Lua脚本实现的。

mysql的事务隔离级别有哪几个,都能解决什么问题?

  • READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
    image-20250414142535344

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)

synchronized 加在不同地方分别是给谁加的锁?

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
  • synchronized 关键字加到实例方法上是给对象实例上锁;

Spring事务底层原理,事务失效的场景有哪些?

在你的项目中使用了微服务拆分思想,从单体架构划分为5个微服务,说一说微服务拆分的原则,并说说你的项目都分成了哪几个微服务?

我们在做服务拆分时一般有两种方式:1. 纵向拆分 2.横向拆分
所谓纵向拆分,就是按照项目的功能模块来拆分。例如优乐购商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。
而横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。
我的项目中采用的就是纵向拆分的原则。分为五个大模块:用户模块,商品模块,购物车模块,订单模块,支付模块
确定好了拆分原则,就要确定项目结构,对于微服务拆分一般有两种拆分的项目结构:

  1. 把每一个微服务模块作为一个独立Project,创建一个空的文件夹,把每一个微服务模块的project都放到这个文件夹中管理,耦合性最低
  2. 每一个微服务模块作为一个Project下的module,建立一个父模块,把其余各个微服务模块作为子模块进行管理,采用maven聚合的方式进行管理。

在你的项目中使用ThreadLocal配合网关过滤器、feign拦截器,封装用户上下文全局工具,实现校验、保存和传递用户信息的功能,详细讲讲。

  1. 网关到微服务之间的上下文传递。
    单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
    既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了。
    如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了。
    我的项目采用自定义GlobalFilter,简单很多,直接实现GlobalFilter接口即可,而且也无法设置动态参数。
    现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
    由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。

  2. 微服务之间的上下文传递。
    前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。
    但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。比如下单业务,流程如下:
    用户下单流程
    下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!
    由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。
    微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?
    这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
    我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

在你的项目中使用用Sentinel服务保护框架实现微服务保护,包括请求限流、线程隔离、服务熔断,解释一下。

  1. 服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。
  2. 当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。

为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。
3. 线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。
所以,我们要做两件事情:

  • 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
  • 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑

Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加SpringCloudAlibaba中,我们只需要引入依赖,并在配置文件中进行相关配置,就可以访问Sentinel提供的控制台对我们的接口进行监控

SeataAT模式跟XA模式的区别

你是怎么使用使用RabbitMQ,完成30分钟未支付订单的取消和商品库存的回滚功能的?

在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常都会在用户下单时立刻扣减商品库存。例如电影院购票、高铁购票,下单后就会锁定座位资源,其他人无法重复购买。
但是这样就存在一个问题,假如用户下单后一直不付款,就会一直占有库存资源,导致其他客户无法正常交易,最终导致商户利益受损!
因此,电商中通常的做法就是:对于超过一定时间未支付的订单,应该立刻取消订单并释放占用的库存。
像这种在一段时间以后才执行的任务,我们称之为延迟任务,而要实现延迟任务,最简单的方案就是利用MQ的延迟消息了
在RabbitMQ中实现延迟消息也有两种方案:

  • 死信交换机+TTL
  • 延迟消息插件

详细做法见:https://b11et3un53m.feishu.cn/wiki/A9SawKUxsikJ6dk3icacVWb4n3g

怎么使用布隆过滤器解决缓存穿透

怎么实现redis跟mysql的双写一致性?

问题:我们现在有个数据要更新,是先删除缓存还是先修改数据库?
仔细想想,先删除缓存再更新数据库发生问题的概率更大,而先更新数据库再删除缓存,只有当缓存正好过期时才会出现问题。
(1.线程1先查询缓存,此时换缓存正好过期,则线程1去查询数据库。
2.在查询到数据库还没将查到的新数据写入缓存时,线程2来更新数据
3.线程2更新了数据库并删除了缓存
4.线程1将他查询到的数据写入缓存
5.此时数据库中是线程2修改后的数据,而缓存中是线程1读到的数据,即修改前的数据)

想要保证双写一致性,有两类不同的方法

  1. 可以保证强一致性的:加读写锁,读写锁中又有两种锁,共享锁与排他锁,其中共享锁可以实现读读不互斥,读写互斥,而排他锁读读与读写都互斥。
  2. 不保证强一致性:使用异步通知的方式(借助MQ或者canal中间件异步通知)
    ps:延迟双删也是一个常见的解决方法,但是其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。

讲讲nginx的反向代理

反向代理的主要作用就是接收客户端发送过来的请求并转发给服务端,获得服务端响应数据后返回给客户端。从用户的角度看,公司的核心服务器仿佛就是反向代理服务器,但事实上它只是一个中转站而已。
nginx配置反向代理的流程如下:
反向代理是在 Nginx 配置文件中的 location 配置项中配置的,在 location 配置项中使用 proxy_pass 关键字来指定被代理服务器的地址,proxy_pass 关键字后面的被代理服务器(后端服务器)的地址可以用域名、域名+端口、IP、IP+端口这几种方式表示。

如何分析sql的性能?

我们可以使用 EXPLAIN 命令来分析 SQL 的 执行计划 。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。EXPLAIN 并不会真的去执行相关的语句,而是通过 查询优化器 对语句进行分析,找出最优的查询方案,并显示对应的信息。
可以查看网络链接获取更详细的:https://javaguide.cn/database/mysql/mysql-query-execution-plan.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E6%89%A7%E8%A1%8C%E8%AE%A1%E5%88%92

ArrayList的扩容原理

ArrayList 底层使用的是 Object 数组,不保证线程安全。
初始化的集中不同情况:

  1. 调用空参

    1
    2
    3
    public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  2. 调用空参时比较特殊,虽然DEFAULTCAPACITY_EMPTY_ELEMENTDATA为空,但是默认大小却是10,在第一次添加数据时会调用扩容方法将数组扩容到10。

  3. 在grow方法中也会判断当前是不是针对DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组的第一次扩容,如果是则将要扩容的大小赋值为10,如果不是则正常的扩容到原数组大小的1.5倍。

  4. 数据扩容时调用了Arrays.copyof方法将老数组整个复制到新数组。

hashmap的底层数据结构

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。

AOP的使用

评论