字节-面试题

一面

1.redis的key删除策略

2.redis的渐进式rehash

3.rehash的时候来了删除指令怎么办

4.redis跳表的时间复杂度?为什么用跳表?

5.进程和线程的区别?

二面

1.SCF框架使用的协议?如果你写rpc框架会采用那种协议?

2.客户端没用netty如何进行拆包、粘包的?

3.netty处理拆包,已经读的数据放在哪?

4.MySQL的MVCC?

5.volatile关键字解决了什么?

6.为什么会有可见性问题?volatile怎么解决的?

7.volatile怎么解决有序性问题?

8.ZGC为什么停顿时间短?

9.反射的效率问题?以及如何优化?

10.云原生数据库?

天眼查-面试题

一面

1.项目中的难点

  1. orgagent的线程隔离
  2. 机构的流程配置化

2.MySQL

  1. MySQL的索引结构?

    B+树

  2. B树和B+树的区别

    • B树的每个结点都存储了key和data,B+树的data存储在叶子节点上

      节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少

    • 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录

      由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好

  3. 描述下可重复读和不可能重复读

    同一个事务中发起多次查询,每次读取到的数据相

    同的,无论其他事务有没有对数据进行修改,当不影响当前事务的读取

  4. 描述下MVCC

    MVCC是多版本并发控制,是为了解决InnoDB中的不可重复读现象

    通过把undo log,将所有事务的修改串联成一个版本链,当读取数据时根据read view来判断哪个版本的数据是自己可以读取的

3.GC

  1. 项目中使用的垃圾回收器

    CMS + ParNew

  2. CMS的四个阶段

    • 初始标记(STW)
    • 并发标记
    • 重新标记(STW)
    • 并发清除

4.Redis

  1. redis在项目中的应用
  2. 分布式锁是如何实现的
  3. setnx 为什么要设置超时时间

5.RPC

  1. RPC中都有哪些组件

    基础功能

    通信协议:协议头、协议体、可变协议

    序列化:JDK、JSON、Hession、Protobuf

    网络IO模型:阻塞IO、IO多路复用

    动态代理:JDK动态代理、CGLIB

    服务相关

    服务发现:服务注册、服务发现

    健康检查:检查服务状态、心跳机制、客户端请求成功率

    路由策略:做灰度、全链路灰度等

    服务分组:服务之间进行隔离,彼此不影响

    负载均衡:随机权重、轮询权重

    异常重试:客户端根据指定的异常进行充实

    优雅起停:停止由外到内,启动由内到外

    熔断限流:服务端限流、降级,客户端熔断

    高级优化

    异步RPC:压榨服务的吞吐量

    链路追踪:全局链路跟踪

    动态分组:

    泛化调用:

6.TCP

滑动窗口

7.算法

1
2
3
4
5
6
7
[3,3,4,4,5,5,7,1,1,2,2]

1、数字都是两个两个出现,相等的数一定在相邻的位置
2、有且仅有一个数字落单
3、不会出现三个及三个以上的数相等

写一个算法,定位出落单数的下标

二面

1.API机构线程隔离

  • 机构间Http互相影响

2.API机构差异配置

  • 流程配置
  • 组件配置

3.MySQL

  • redo log
  • bin log

4.Redis

  • 热key的发现与处理
  • 大量key过期导致redis服务卡顿

5.JMM

  • 并发问题的产生原因

6.Java

  • 线程池创建核心线程的流程
  • 项目中线程池的配置,拒绝策略使用什么

7.JVM

  • 用的什么收集器
  • 卡表
  • JVM层级的监控有没有(老年代比例、GC次数等参数监控)

8.限流算法

  • 漏斗
  • 令牌桶(实现简单的令牌桶)

三面

1.实现一个服务治理框架,初期的选型工作都要做哪些?

2.使用的框架出现了难题,找不到好的解决方案怎么办?

3.Kakfa

  • 如何做到高吞吐量
    • 顺序写
      • SSD还有必要顺序写吗
    • 零拷贝
      • mmap和sendfile
    • 批量发送
      • 客户端如何做到批量发送
    • Reactor
  • 如何做到低延迟

4.Redis

  • key过期策略
    • 25%是静态参数,还是动态调整的
  • redis主从切换
    • 主从切换的过程
    • 客观下线和主观下线的区别

5.如何保证服务稳定性,四九9,一个5,SLA

  • 资源监控
  • 服务监控
  • 业务监控

6.十个人十把椅子

有1-10号椅子,1-10号人,第N个人坐第N把椅子的概率是多少

理想汽车-面试题

一面

1.SQL索引优化有哪些?

  • 最左前缀
  • 联合索引
  • 索引下推优化

2.InnoDB数据页大小,一页可以放多少数据?

16k

bigint类型可以放千万级别

3.varchar类型建索引,可以设置的最大长度是多少?

768

4.B-树和B+树的区别?为什么InnoDB索引使用B+树?

是否只在叶子结点存储数据

B+树优化磁盘IO和范围查询

5.项目中流程配置是什么样的?

6.分布式锁使用什么?为什么用lua脚本?为什么setnx不用lua?setnx从什么版本支持原子操作的?

setnx + lua

解锁操作不是一个原子指令,涉及到get、equals、del

setnx在2.6.12版本之后,官方支持原子操作,可以set key value nx ex

7.zookeeper分布式锁是如何实现的?

创建一个临时有序节点,并监听上一个节点的状态

8.线程池中,线程数到达最大线程数怎么办?

配置拒绝策略

9.orgagent线程隔离为什么不用hystrix?

10.MQ中间件了解哪些?

二面

1.RabbitMQ都有哪些组件?

2.公司内部RPC框架有哪些设计的优秀的地方?

3.Netty的线程模型在源码中是如何控制的?

4.介绍下GC的分代回收以及什么情况下对象进入老年代?

5.redis的数据结构及其对应的基本数据结构?

6.事务隔离级别?事务传播机制?

7.聚簇索引是什么?

8.说一些工作中用到的设计模式

9.spring源码看过哪些?

10.CountDownLatch的原理?

三面

1.ElasticSearch的索引的原理?

2.ElsaticSearch的数据写入流程?

3.说下整体的业务流程?

4.流程配置是怎么用的?

5.线程池隔离的具体方案(代码怎么写的)?

6.动态代理都有哪些类型?性能有何区别?

当当-面试题

Java基础

1 CAS原理及实现

2 AQS原理及实现?JUC下的应用?

3 线程池参数?流程描述?

4 JDK7 HashMap成环的原因

5 垃圾回收机制

  • GC Roots 可达性分析
  • 分代算法
  • 标记清除
  • 标记整理
  • CMS
  • G1

6 引用类型

  • 强引用
  • 弱引用
  • 软引用
  • 虚引用

Redis

1 Redis数据类型

1.1 8种数据类型
1.2 string类型 底层实现

2 持久化

  • RDB
  • AOF

3 Redis集群复制原理

4 Redis集群方案对比

  • 主从模式
  • 哨兵模式
  • Redis Cluster

5 缓存雪崩 缓存穿透 原理及解决方法?

5.1 布隆过滤器

6 分布式锁

7 缓存淘汰机制

RocketMQ

1 简单介绍技术架构

2 顺序消费

3 事务消息

4 消息重投

5 消息重试

6 死信队列

MySQL

1 InnoDB和MyISAM的区别

2 ACID

3 三大范式

4 MVCC

5 表锁 行锁 页级锁 间隙琐

6 B+树和B树的区别

7 为什么选择B+树,而不是哈希表,B树?

8 索引失效

9 索引优化

  • 熟悉explain的各字段的含义

10 组合索引的数据结构

  • (a,b) 叶子节点排列顺序应该是先按照a进行排序,排序完成后再按照b进行排序,所以应该是a是全局有序,b是a中有序,所以可满足a,(a,b)为条件的查询

分布式

1 CAP理论

2 dubbo和feign的对比

3 描述一下dubbo的工作中流程

  • 服务注册
  • 服务发现
  • 服务调用
  • 服务降级
  • 负载均衡

4 eureka nacos zookeeper

  • eureka集群 AP
  • zookeeper集群 CP
  • nacos集群 可AP,可CP

5 Hystrix 服务降级

  • 依赖隔离策略
    • 线程池隔离
    • 信号量隔离

6 服务熔断

7 Eureka

  • 服务注册
  • 服务同步
  • 服务续约
  • 服务启动
  • 服务下线
  • 服务发现
  • 服务失效剔除

8 分布式事务

  • 2PC
  • 3PC
  • TCC

美团-面试题

一面

1.线程间传递数据?(线程池中各个线程传递数据,不是共享数据)

2.二叉树Z字形遍历

3.删除链表中倒数第K个节点

二面

1.httpclient参数设置?

2.动态代理两种方式?性能差异?

3.有一个正整数n(n<100),期望找出一个最小的n的倍数m(m<2^64-1)且m只由1、0组成。例如:输入n = 2 输出m = 10,输入n = 3 输出m = 111

三面

1.有n个不重复的数字,随机的选出m个,打印出来。(洗牌问题)

- 任一数字最多被选到1次

- 时间复杂度不高于O(m)

2.MySQL数据表增加字段,如何处理不影响读写请求。

并发编程的基础知识

并发编程的基础知识

Java中的CAS操作

在Java中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度的开销。
Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,不能解决读-改-写等的原子性问题。CAS即Compare And Swap,
是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性

compareAndSwap*

JDK的Unsafe类提供了一系列的compareAndSwap*方法,下面以compareAndSwapLong为例 进行简单介绍。

  • boolean compareAndSwapLong(Object obj, long valueOffset, long except, long update)

    CAS有四个操作数,分别是:对象内存位置,对象中的变量的偏移量,变量的预期值,更新之后的值。其操作含义:如果对象obj中内存偏移量为valueOffset的变量值为except,
    则使用新的值update替换旧的值except。这是处理器提供的一个原子性指令

CAS自旋等待

CAS自旋,一直尝试,直到成功。

1
2
3
4
5
6
7
8
9
public final long getAndAddLong(Object obj, long valueOffset, long add) {
long except;
// 自旋,一直尝试,直到成功
do {
except = this.getLongVolatile(obj, valueOffset);
} while(!this.compareAndSwapLong(obj, valueOffset, except, except + add));
return except;
}

ABA问题

假如线程I使用CAS修改初值值的变量X,那么线程I会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功可,那么此时程序运行不一定正确。
可能在线程I获取变量X的值A后,在执行CAS前,线程II使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。虽然线程I执行CAS时的值是A,但是这个A已经不是线程I获
取时的A了。

ABA问题的产生是因为变量的状态值产生了环形转换,A-B-A。JDK中的AtomicStampedReference类为每个变量的状态值都配备了一个时间戳,从而避免ABA问题的产生。

什么是AQS

AQS(Abstract Queued Synchronizer)是一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int state)和一个先进先出(FIFO)的线程等待队列来实现一个多线程
访问共享资源的同步框架。

AQS原理

AQS为每个共享资源都设置了一个共享资源锁,线程在需要访问共享资源时首先要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程
放入线程等待队列,等待下一次资源调度。许多同步类的实现都依赖于AQS,比如常用的ReentrantLock、countDownLatch。

img.png

state: 状态

Abstract Queued Synchronizer维护了一个volatile int类型的变量,用于表示当前的同步状态。volatile虽然不能保证操作的原子性,但是能保证当前变量state的可见性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 返回共享资源状态,此操作的内存语义为volatile修饰的原子读操作
protected final int getState() {
return state;
}

// 设置共享资源状态,此操作的内存语义为volatile修饰的原子写操作
protected final void setState(int newState) {
state = newState;
}

// 自动将同步状态设置为给定的更新值状态(如果当前状态值等于预期值)
// 此操作的内存语义为volatile修饰的原子读写操作
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS共享资源的方式:共享式和独占式

  • 共享式:多个线程可以同时执行,具体的实现Semaphore和CountDownLatch。
  • 独占式:只有一个线程能执行,具体的实现ReentrantLock。
    AQS只是定义一个框架,只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争用贡献资源的方式也不同,自定义同步器在实现时只需要实现共享
    资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器再做处理。
方法名 资源共享方式 说明
isHeldExclusively() 查询该线程是否正在独占资源,只有用到condition才需要去实现它
tryAcquire(int) 独占方式 尝试获取资源:成功返回true,失败返回false
tryRelease(int) 独占方式 尝试释放资源:成功返回true,失败返回false
tryAcquireShared(int) 共享方式 尝试获取资源:负数表示失败;0表示成功,但没有剩余资源可用;正数表示成功,且有剩余资源
tryReleaseShared(int) 共享方式 尝试释放资源:如果释放资源后允许唤醒后续等待线程,则返回true,否则返回false

一般来说,自定义同步器要么采用独占方式,要么采用共享方式,实现类只需实现tryAcquire、tryRelease或tryAcquireShared、tryReleaseShared中的一组即可。但AQS也支持
自定义同步器同时实现独占和共享两种方式,例如ReentrantReadWriteLock在读取时采用共享方式,在写入时采用独占方式。

ReentrantLock

ReentrantLock中的state初始值为0,表示无锁状态。在线程执行tryAcquire()获取该锁后,ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过
tryAcquire()获取锁时均会失败,直到该线程释放锁后,state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1,因此
ReentrantLock也属于可重入锁。但是获取多少次锁就要释放多少次锁,这样才能保证state最终为0。

CountDownLatch

CountDownLatch将任务分为N个子线程去执行,将state也初始化为N,N与线程的个数一致,N个子线程是并行执行,每个子线程都在执行完后countDown()一次,state会执行CAS操作
并减1。在所有子线程都执行完后(state = 0)时会unpark()主线程,然后主线程会从await()返回,继续执行后续的动作。

秒杀设计

秒杀设计

场景

假设商城策划了一期秒杀活动,活动在每天的00:00开始,仅限前200名,那么秒杀即将开始时,后台会显示用户正在疯狂地刷新APP或者浏览器来保证自己能够尽量早的看到商品。

分析

  1. 用户查询的是少量的商品数据,属于查询的热点数据。可采用缓存策略,将请求尽量挡在上层的缓存。
    80/20原则
  2. 能静态化的数据尽量静态化,提高CDN的命中率。
    减少web服务器的访问压力和带宽负担
  3. Nginx直接访问分布式缓存(redis)。
    减少tomcat的压力

类消息队列在系统设计中的使用

  • 在Java线程池中我们就会使用一个队列来暂时存储提交的任务,等待有空闲的线程处理这些任务
  • 操作系统中,中断的下半部分也会使用工作队列来实现延后执行
  • 实现一个RPC框架时,也会将从网络上接收到的请求写到队列里,再启动若干个工作线程来处理

消息队列在秒杀中的使用

  1. 将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求
  2. 后台启动若干个消费者处理消息队列中的消息,执行库存检验、下单等逻辑
  3. 因为若干个消费者在执行,所以数据库的并发请求也是有限的
  4. 消息队列中的请求是可以被堆积的,当库存被消耗完之后,消息队列中堆积的请求就可以被丢弃了
  5. 通过异步处理,简化秒杀过程中的异步处理

以上作用就是消息队列最主要的作用:流量削峰。可以削平短暂的流量高峰。虽说堆积会造成请求被短暂延迟处理,但是只要我们时刻监控消息队列中的堆积长度,在堆积量超过一定量时,
增加一定数量的消费者即可。

本项目中的SQL语句逻辑整理

需求

记录一下本项目中的SQL语句逻辑。

文章模块

  1. 查看文章的推荐文章
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Repository
    public interface ArticleDao extends BaseMapper<Article> {
    /**
    * 查看文章的推荐文章
    *
    * @param articleId 文章id
    * @return 文章列表
    */
    List<ArticleRecommendDTO> listRecommendArticles(@Param("articleId") Integer articleId);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <select id="listRecommendArticles" resultType="com.minzheng.blog.dto.ArticleRecommendDTO">
    SELECT
    id,
    article_title,
    article_cover,
    create_time
    FROM
    (
    SELECT DISTINCT
    article_id
    FROM
    ( SELECT tag_id FROM tb_article_tag WHERE article_id = #{articleId} ) t
    JOIN tb_article_tag t1 ON t.tag_id = t1.tag_id
    WHERE
    article_id != #{articleId}
    ) t2
    JOIN tb_article a ON t2.article_id = a.id
    WHERE a.is_delete = 0
    ORDER BY
    is_top DESC,id DESC
    LIMIT 6
    </select>

    SQL语句逻辑:

    1. 先查询文章标签表,得到当前文章的所有标签id
    2. 再次连接文章标签表,得到与当前文章同标签的所有文章id(排除掉当前文章)
    3. 通过文章id,内连接文章表 获取文章详细信息(排除已删除的文章)(按是否置顶和id排序,取前6条)

角色模块

  1. 查询路由角色列表(查询出所有页面能被哪些角色访问)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Repository
    public interface RoleDao extends BaseMapper<Role> {

    /**
    * 查询路由角色列表
    *
    * @return 角色标签
    */
    List<ResourceRoleDTO> listResourceRoles();

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <select id="listResourceRoles" resultMap="RolePermissionMap">
    SELECT
    re.id,
    url,
    request_method,
    role_label
    FROM tb_resource re
    LEFT JOIN tb_role_resource rep
    on re.id = rep.resource_id
    LEFT JOIN tb_role r
    on rep.role_id = r.id
    WHERE
    parent_id is NOT NULL
    AND is_anonymous = 0
    </select>

    SQL语句逻辑:

    1. 查询资源表,连接角色资源中间表
    2. 用角色资源中间表,连接角色表,显示出角色信息
    3. 过滤掉非页面的资源(菜单组)(只查询页面),过滤掉不需要登录即可访问的资源(只查询需要登录才可以访问的资源)

策略模式介绍以及简单示例

需求

一个方法,有多种具体的实现方法,而在使用时,可以根据需要,灵活调用对应的实现方法。也可以通过修改配置文件,读取配置,调用配置的实现。

举例:
上传图片:可以用阿里云oss上传,可以直接上传存入到自己服务器。
用户登录:可以qq登录,可以微博登录,可以邮箱登录。

策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

介绍

意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。

主要解决:在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。

何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。

如何解决:将这些算法封装成一个一个的类,任意地替换。

关键代码:实现同一个接口。

应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。

优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

实现

我们将创建一个定义活动的 Strategy 接口和实现了 Strategy 接口的实体策略类。Context 是一个使用了某种策略的类。

StrategyPatternDemo,我们的演示类使用 Context 和策略对象来演示 Context 在它所配置或使用的策略改变时的行为变化。
image.png

步骤 1

创建一个接口。

Strategy.java
1
2
3
public interface Strategy {
public int doOperation(int num1, int num2);
}

步骤 2

创建实现接口的实体类。

OperationAdd.java
1
2
3
4
5
6
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
OperationSubtract.java
1
2
3
4
5
6
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
OperationMultiply.java
1
2
3
4
5
6
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}

步骤 3

创建 Context 类。

Context.java
1
2
3
4
5
6
7
8
9
10
11
public class Context {
private Strategy strategy;

public Context(Strategy strategy){
this.strategy = strategy;
}

public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}

步骤 4

使用 Context 来查看当它改变策略 Strategy 时的行为变化。

StrategyPatternDemo.java
1
2
3
4
5
6
7
8
9
10
11
12
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}

步骤 5
执行程序,输出结果:

1
2
3
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50

自定义注解实现接口限流(接口防刷)

需求

💡 Tips:注解使用起来简单快速,配置传入2个参数即可,每个方法可设置不同值。
● 防止同一个ip地址,短时间内多次请求接口。
● 用到自定义注解、HandlerInterceptorRedisIpUtils,实现接口限流。
创建自定义注解
💡 Tips:全忘完了。
@interface 先声明一个自定义注解AccessLimit。
● 2个属性:senconds 秒、maxCount 最大请求次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimit {

/**
* 单位时间(秒)
*
* @return int
*/
int seconds();

/**
* 单位时间最大请求次数
*
* @return int
*/
int maxCount();
}

实现HandlerInterceptor接口,拦截请求
● 编写WebSecurityHandler方法,实现HandlerInterceptor接口。
● 重写preHandle方法,在方法调用前进行拦截。
● 从方法中获取我们自定义的@AccessLimit注解,如果获取到了,则进行逻辑处理。
● 从注解中获取配置的值:秒数 以及最大次数;从httpServletRequest中获取ip地址和方法名。
● 将ip地址+方法名做为key值,值为1(自增1),过期时间为seconds存入redis中。
● 将自增后的结果返回,与注解中的maxCount比较大小,如果大则拦截,反之则放行。

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
@Log4j2
public class WebSecurityHandler implements HandlerInterceptor {
@Autowired
private RedisService redisService;

@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
// 如果请求输入方法
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
// 获取方法中的注解,看是否有该注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if (accessLimit != null) {
long seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
// 关于key的生成规则可以自己定义 本项目需求是对每个方法都加上限流功能,如果你只是针对ip地址限流,那么key只需要只用ip就好
String key = IpUtils.getIpAddress(httpServletRequest) + hm.getMethod().getName();
// 从redis中获取用户访问的次数
try {
// 此操作代表获取该key对应的值自增1后的结果
long q = redisService.incrExpire(key, seconds);
if (q > maxCount) {
render(httpServletResponse, Result.fail("请求过于频繁,请稍候再试"));
log.warn(key + "请求次数超过每" + seconds + "秒" + maxCount + "次");
return false;
}
return true;
} catch (RedisConnectionFailureException e) {
log.warn("redis错误: " + e.getMessage());
return false;
}
}
}
return true;
}



private void render(HttpServletResponse response, Result<?> result) throws Exception {
response.setContentType(APPLICATION_JSON);
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(result);
out.write(str.getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
}
}

Redis的工具类
每次请求使当前key中的value值自增1,并将value结果返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class RedisServiceImpl implements RedisService {
@Resource
private RedisTemplate<String, Object> redisTemplate;

@Override
public Long incrExpire(String key, long time) {
//使key的value值自增1,并将结果返回
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count != null && count == 1) {
//第一次插入时,设置过期时间
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return count;
}
}