Java服务_使用Guava缓存实现服务限流

Java服务_使用Guava缓存实现服务限流

1.Guava Cache原理

guava cache就是单个应用运行时保存在本地内存中的缓存,是单机版的。本质与concurrentmap相似,但是concurrentmap只能显式地remove数据,guava cache提供了很多自动回收机制。guava cache常用于服务限流。

guava cache适用于小量被读取频繁的数据,本质是以空间换时间,牺牲内存提高读取速度。

guava cache的优点:

  • 线程安全,与concurrentmap类似。
  • 提供了三种基本回收方式:基于容量回收、定时回收、基于引用回收。其中基于容量回收采用LRU策略。
  • 监控缓存加载和命中情况。
  • 集成了多部操作,支持在未命中缓存时,从其他数据源(mysql、redis)获取数据。

2.服务限流实例

2.1创建SpringBoot项目和依赖

创建maven项目,添加pom.xml依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!--springboot项目一天要添加该parent属性-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>

<groupId>org.example</groupId>
<artifactId>GuavaLimiter</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!--springboot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--guava依赖-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
</dependencies>

<build>
<plugins>
<!--springboot项目打包工具-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

创建application类

1
2
3
4
5
6
7
8
9
10
11
package jia.zheng;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

2.2创建限流器

限流逻辑主要在dolimit方法中

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
package jia.zheng.limiter.impl;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import jia.zheng.limiter.Limiter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class LimiterImpl implements Limiter {

//计数缓存,key为服务名,value为限流单位时间内的已访问次数,每访问一次就加一,限流单位时间结束则归零开始下一次计数。
private static LoadingCache<String, AtomicLong> counterCache;

//时间缓存,key为服务名,value为限流单位时间的起点,经过一个限流单位时间则更新。
private static LoadingCache<String, Long> timeCache;

private static final AtomicLong ZERO = new AtomicLong(0L);
private static long timestamp = System.currentTimeMillis();

static {
//重要:maximumSize()用于设置counter最大容量为100个entry,超过则会按照LRU策略回收。
//重要:CacheLoader中的load()方法为cache未命中时从别处加载value的逻辑,还会自动将该value存入cache中。
counterCache = CacheBuilder.newBuilder().maximumSize(100L).refreshAfterWrite(24L, TimeUnit.HOURS).build(new CacheLoader<String, AtomicLong>() {
@Override
public AtomicLong load(String key) throws Exception {
return LimiterImpl.ZERO;
}
});
timeCache = CacheBuilder.newBuilder().maximumSize(100L).refreshAfterWrite(24L, TimeUnit.HOURS).build(new CacheLoader<String, Long>() {
@Override
public Long load(String key) throws Exception {
return LimiterImpl.timestamp;
}
});
}


@Override
public boolean dolimit(String serviceName) throws ExecutionException {

//重要:这两个数值就是限流设置,在timeLimit时间内限定访问countLimit次。此处我将限流参数在代码中先死,效率较低。
//常见策略为将该参数保存在redis中,然后使用定时轮询任务轮询获取该参数保存在static修饰的map中,然后本方法直接从该map中拿。
//因为限流本来就是很重视时效性,所以尽量避免本方法直接从远程redis或mysql中获取限流参数。
int countLimit = 3;
long timeLimit = 30L;

long now = System.currentTimeMillis();
if (now - timeCache.get(serviceName) < timeLimit * 1000L) {//本次请求与上次请求时间间隔小于限流单位时间
AtomicLong count = counterCache.get(serviceName);
synchronized (count) {
if (count.get() < countLimit) {//未达到访问次数限制
counterCache.put(serviceName, new AtomicLong(count.incrementAndGet()));
return true;
}else {//已达到访问次数限制
return false;
}
}
}else {//本次请求与上次请求时间间隔大于限流单位时间
counterCache.put(serviceName, new AtomicLong(1L));
timeCache.put(serviceName, now);
return true;
}
}
}
  • maximumSize()方法用于设置guava cache的最大容量为100个entry,超过则会按照LRU策略回收。guava cache还提供了很多缓存配置和操作方法,如定时回收、显式清除、设置监听器等。
  • CacheLoader中的load()方法为cache未命中时从别处加载value的逻辑,还会自动将该value存入cache中。可以将此处逻辑改为从mysql或远程redis中获取,从而真正地将guava cache作为数据缓存使用。
  • 限流一般要设置在timeLimit时间内限定访问countLimit次这两个参数,此处为了展示方便直接在代码中写死。常见策略为将该参数保存在redis中,然后使用定时轮询任务轮询获取该参数保存在static修饰的map中,然后本方法直接从该map中拿。因为限流本来就是很重视时效性,所以尽量避免本方法直接从远程redis或mysql中获取限流参数。

2.3创建工具类实现单例模式

要使用单例模式确保项目中只有一个LimiterImpl类对象在工作,使用饿汉模式或饱汉模式都可以。

spring项目中也可以直接在LimiterImpl类上添加@Component注解,借助spring实现单例bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package jia.zheng.limiter;

import jia.zheng.limiter.impl.LimiterImpl;
import java.util.concurrent.ExecutionException;

//重要:使用单例模式确保项目中只有一个LimiterImpl类对象在工作。spring项目中也可以直接在LimiterImpl类上添加@Component注解,借助spring实现单例bean。
public class LimitUtil {
private static Limiter limiter;

//重要:类的加载是私有的,别的线程无法进入。所以不用担心两个命令同时加载limitUtil类导致静态代码块执行两次。
static {
limiter = new LimiterImpl();
}

public static boolean doLimit(String serviceName) throws ExecutionException {
return limiter.dolimit(serviceName);
}
}
  • 类的加载是私有的,别的线程无法进入。所以不用担心两个命令同时加载limitUtil类导致静态代码块执行两次。

2.4创建Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package jia.zheng.controller;

import jia.zheng.limiter.LimitUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutionException;

@RestController
public class Visit {

@GetMapping("visit")
public String visit() throws ExecutionException {

boolean limit = LimitUtil.doLimit("visit");

if (limit) {
return "访问成功!visit success!";
}
return "访问限流器!visit limiting!";
}
}

2.5启动该SpringBoot项目并连续访问

连续访问3次,浏览器显示都是访问成功:

连续访问第4次及以后都是访问失败:

过了30s之后再连续访问则又能成功3次。