当前位置 : 首页 » 文章分类 :  开发  »  Google-Guava 使用笔记

Google-Guava 使用笔记

Google Java类库Guava使用笔记

google/guava 官网
https://github.com/google/guava

官方User Guide
https://github.com/google/guava/wiki

官方API文档
https://google.github.io/guava/releases/snapshot-jre/api/docs/


RateLimiter

Google 的开源项目 guava 提供了 RateLimiter 类,实现了单点的令牌桶限流。

create() 创建限流器

public static RateLimiter create(double permitsPerSecond);
创建一个稳定输出令牌的 RateLimiter,保证了平均每秒不超过 permitsPerSecond 个请求
当请求到来的速度超过了 permitsPerSecond,保证每秒只处理 permitsPerSecond 个请求
当这个 RateLimiter 使用不足(即请求到来速度小于 permitsPerSecond),会囤积最多 permitsPerSecond 个请求

public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);
根据指定的稳定吞吐率和预热期来创建 RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少个请求量),在这段预热时间内,RateLimiter 每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)
同样地,如果 RateLimiter 在 warmupPeriod 时间内闲置不用,它将会逐步地返回冷却状态。也就是说,它会像它第一次被创建般经历同样的预热期。返回的 RateLimiter 主要用于那些需要预热期的资源,而不是在稳定(最大)的速率下可以立即被访问的资源。返回的 RateLimiter 在冷却状态下启动(即预热期将会紧跟着发生),并且如果被长期闲置不用,它将回到冷却状态。

Guava有两种限流模式:
一种为稳定模式(SmoothBursty:令牌生成速度恒定)
一种为渐进模式(SmoothWarmingUp:令牌生成速度缓慢提升直到维持在一个稳定值)

在调用create接口时,实际实例化的为SmoothBursty类

public static RateLimiter create(double permitsPerSecond) {
    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}

static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
}

关键变量:
maxPermits 最大许可数
storedPermits 当前令牌数,数量不能超过最大许可数
stableIntervalMicros 添加令牌时间间隔 ,毫秒, 等于 1/permitsPerSecond
nextFreeTicketMicros 表示下一次允许补充许可的时间(时刻)。

acquire() 获取令牌

public double acquire();
public double acquire(int permits);

acquire() 会阻塞当前线程直到许可证可用后获取该许可证
提供了两个获取令牌的方法,不带参数表示获取一个令牌。如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0

注意:RateLimiter 并不提供公平性的保证

tryAcquire() 尝试获取令牌

尝试获取令牌,分为待超时时间和不带超时时间两种:

//尝试获取一个令牌,立即返回
public boolean tryAcquire();
public boolean tryAcquire(int permits);

//尝试获取permits个令牌,带超时时间,在超时时间内能获取到则返回true,否则返回false
public boolean tryAcquire(long timeout, TimeUnit unit);
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);

惰性计算(延迟计算)

RateLimiter 中采取的是 惰性计算方式:在每次请求进来的时候先去计算上次请求和本次请求之间应该生成多少个令牌
具体实现是通过 resync() 方法来做的
该函数会在每次获取令牌之前调用,其实现思路为,若当前时间晚于 nextFreeTicketMicros ,则计算该段时间内可以生成多少令牌,将生成的令牌加入令牌桶中并更新数据。这样一来,只需要在获取令牌时计算一次即可。

void resync(long nowMicros) {
  // if nextFreeTicket is in the past, resync to now
  if (nowMicros > nextFreeTicketMicros) {
    double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
    storedPermits = min(maxPermits, storedPermits + newPermits);
    nextFreeTicketMicros = nowMicros;
  }
}

如果当前时间(不是时刻,而是自创建起所流经的时间,下同)超过了上一次所设定的nextFreeTicketMicros时间,则会重新进行同步:
通过计算上一次设定nextFreeTicketMicros到当前时刻的时间差获取新增的可用许可数;
计算可用的许可数:如果新增的许可数+原有的许可数小于最大许可数,则存储的许可数增加新增的数量,否则同步为最大许可数;
同步下一次允许补充许可时间为当前时间

Guava RateLimiter源码解析
https://segmentfault.com/a/1190000012875897

热身

RateLimiter 的 SmoothWarmingUp 是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。
比如
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
创建一个平均分发令牌速率为2,预热期为3秒钟的限流器。
由于设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。这种功能适合系统刚启动需要一点时间来“热身”的场景。

*          ^ throttling
*          |
*    cold  +                  /
* interval |                 /.
*          |                / .
*          |               /  .   ← "warmup period" is the area of the trapezoid between
*          |              /   .     thresholdPermits and maxPermits
*          |             /    .
*          |            /     .
*          |           /      .
*   stable +----------/  WARM .
* interval |          .   UP  .
*          |          . PERIOD.
*          |          .       .
*        0 +----------+-------+--------------→ storedPermits
*          0 thresholdPermits maxPermits

上图中横坐标是当前令牌桶中的令牌 storedPermits, SmoothWarmingUp 将 storedPermits 分为两个区间:[0, thresholdPermits) 和 [thresholdPermits, maxPermits]。
纵坐标是请求的间隔时间,stableInterval 就是 1 / QPS,例如设置的 QPS 为5,则 stableInterval 就是200ms,coldInterval = stableInterval * coldFactor,这里的 coldFactor “hard-code”写死的是3。

SmoothWarmingUp 实现预热缓冲的关键在于其分发令牌的速率会随时间和令牌数而改变,速率会先慢后快。
令牌刷新的时间间隔由长逐渐变短。等存储令牌数从maxPermits到达thresholdPermits时,发放令牌的时间价格也由coldInterval降低到了正常的stableInterval。

当系统进入 cold 阶段时,图像会向右移,直到 storedPermits 等于 maxPermits;
当系统请求增多,图像会像左移动,直到 storedPermits 为0。

超详细的Guava RateLimiter限流原理解析
https://zhuanlan.zhihu.com/p/60979444

Guava官方文档-RateLimiter类
http://ifeve.com/guava-ratelimiter/


Guava Cache

maximumSize()设置最大缓存数

Guava Cache 可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当 Cache 中的记录数量达到最大值后再调用put方法向其中添加对象,Guava会先从当前缓存的对象记录中选择一条删除掉,腾出空间后再将新的对象存储到Cache中。

expireAfterAccess()设置过期时间

在构建 Cache 对象时,可以通过 CacheBuilder 类的 expireAfterAccessexpireAfterWrite 两个方法为缓存中的对象指定过期时间,过期的对象将会被缓存自动删除。其中,expireAfterWrite 方法指定对象被写入到缓存后多久过期,expireAfterAccess 指定对象多久没有被访问后过期。

get()方法传入Callable缓存生成方法

Cache的 get 方法有两个参数,第一个参数是要从 Cache 中获取记录的 key,第二个记录是一个 Callable 对象。当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法,call方法的返回值会作为key对应的值被存储到缓存中,并且被get方法返回。

Guava缓存的get方法如何解决并发回源的击穿问题?

在高并发场景下,被动更新(先从缓存获取,没有则回源取,再放回缓存)的回源是要格外小心的,如果有太多请求在同一时间回源,可能导致后端服务无法支撑这么高并发而宕机,这就是所谓的缓存击穿问题。

Guava Cache 里的 CacheLoader 在回源的 load 方法上加了控制,对于同一个key,只让一个请求回源load,其他线程阻塞等待结果。

recordStats()开启命中率等统计信息

可以对Cache的命中率、加载数据时间等信息进行统计。在构建Cache对象时,可以通过 CacheBuilder 的 recordStats() 方法开启统计信息的开关。开关开启后Cache会自动对缓存的各种操作进行统计,调用Cache的stats方法可以查看统计后的信息。

@Autowired
private NameService nameService;

private Cache<String, String> nameCache = CacheBuilder.newBuilder().maximumSize(100).expireAfterAccess(3, TimeUnit.MINUTES).build();

// 根据 code 查 name
public String getNameByCode(String code) {
     try {
         return nameCache.get(code, () -> nameService.GetByCode(code));
     } catch (Exception e) {
         return null;
     }
 }

Guava Cache用法介绍
https://segmentfault.com/a/1190000011105644

集中式内存缓存Guava Cache
https://www.jianshu.com/p/64b0df87e51b


Preconditions入参检查

package com.google.common.base;

@GwtCompatible
public final class Preconditions {
  public static void checkArgument(boolean expression, @Nullable Object errorMessage) {
      if (!expression) {
          throw new IllegalArgumentException(String.valueOf(errorMessage));
      }
  }

  public static void checkState(boolean expression, @Nullable Object errorMessage) {
      if (!expression) {
          throw new IllegalStateException(String.valueOf(errorMessage));
      }
  }
}

由于Preconditions中所有方法都是static的,使用的时候一般静态导入即可,比如:

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Preconditions.checkArgument;

private void method(long userId, String orderId, Request request) {
    checkArgument(userId > 0, "Required long param userId is invalid");
    checkState(StringUtils.isNotBlank(orderId), "Required String param orderId is not present");
    checkState(request != null, "Required param request is not present");
    ... ...
}

Splitter

package com.google.common.base;
@GwtCompatible(emulated = true)
public final class Splitter {
}

on() 指定分隔符

public static Splitter on(char separator)
public static Splitter on(String separator)

split()

public Iterable<String> split(CharSequence sequence)

例如

Splitter.on(',').split("foo,bar,qux")

splitToList()

public List<String> splitToList(CharSequence sequence)

splitToStream()

public Stream<String> splitToStream(CharSequence sequence)

trimResults()去掉前后空白

public Splitter trimResults()

omitEmptyStrings()去掉空串

public Splitter omitEmptyStrings()

withKeyValueSeparator()

public Splitter.MapSplitter withKeyValueSeparator(String separator)
返回一个 Splitter.MapSplitter ,基于此 Splitter 以 k/v 分隔符 separator 分隔

逗号分隔字符串转List

String userIds = "11,22,aa,bb"
List<Long> userIdList = Splitter.on(',').splitToList(userIds).stream().filter(StringUtils::isNumeric).map(Long::parseLong).collect(Collectors.toList());

Splitter.on(‘,’).split(“ foo,,, bar ,”) 结果为: [“ foo”, “”, “”, “ bar “, “”]
其中包含了空的字符串,这可能不是我们需要的,可以通过Splitter类中的 trimResults() 方法去掉子串的空格,
List<String> result = Splitter.on(",").trimResults().splitToList(str);

以及 omitEmptyStrings() 方法去掉空的子串。运行之后的结果为[“foo”, “bar”]。
Splitter.on(‘,’).limit(3).omitEmptyStrings().split(“a,,,b,,,c,d”) 结果为 [“a”, “b”, “c,d”]

Splitter类还提供了limit(int limit)方法,当分割的子字符串达到了limit个时,则停止分割,如下:
Splitter.on(‘,’).limit(3).split(“a,b,c,d”) 结果为[“a”, “b”, “c,d”]

Splitter解析url查询参数

MapSplitter
对url中的查询字符串”id=123&name=green”进行分割
利用Guava的MapSplitter的代码如下:

final Map<String, String> join = Splitter.on("&").withKeyValueSeparator("=").split("id=123&name=green");

这里同样利用on传入字符串的第一分隔符,withKeyValueSeparator传入项的分隔符,产生map的key/value项,其结果是一个{id=123, name=green}的Map对象。


Joiner

如果不指定 skipNulls()useForNull(String) , 当要连接的元素有 null 值时会抛出空指针异常。

useForNull

public Joiner useForNull(final String nullText)
对于 null 元素,使用 nullText 串代替

Joiner.on("|").useForNull(" ").join(stringList);

对于MapJoinner和MapSplitter的最好案例就是url的param编码。

Joiner拼接url查询参数

MapJoinner
生产一个查询id: 123, name: green的学生信息的url。
利用Guava的MapJoinner的代码如下:

Joiner.on("&").withKeyValueSeparator("=").join(ImmutableMap.of("id", "123", "name", "green"));

这里采用了on传入map item之间分隔符,以及withKeyValueSeparator传递map项key/value之间的分隔符。所以能够很简单的实现,不用我们在去实现一个的for循环代码。

String baseUrl = "/api/v1/users/%s/hello";
Map<String, String> body = Maps.newHashMap();
body.put("city_id", "11010000");
body.put("name", RandomStringUtils.randomAlphabetic(10));
body.put("mobile", "98762000970");

Long userId = 1001;
String url = String.format(baseUrl, userId);
doPOST(url, Joiner.on('&').withKeyValueSeparator("=").join(body))
                .andExpect(MockMvcResultMatchers.status().isOk())

guava之Joiner 和 Splitter
https://www.cnblogs.com/whitewolf/p/4214749.html

List转逗号分隔字符串

public static String joinByGuava(List stringList, String delimiter) {
  return Joiner.on(delimiter).skipNulls().join(stringList);
}

当然我们也可以用common-lang来很简单的完成:StringUtils.join(stringList, delimiter).但是个人推荐尽量使用Guava替代common-lange,因为Guava还有更多的有用方法,后续会陆续介绍,还有就是Guava的API相对更有语意一点。


Maps

package com.google.common.collect;
@GwtCompatible( emulated = true )
public final class Maps

GwtCompatible 表示该类型兼容Google Web Toolkit

newHashMap()创建HashMap

public static <K,V> HashMap<K,V> newHashMap()

例如:

Map<String, Map<String, String>> mobileMessageMap = Maps.newHashMap();
Map<String, String> messageMap = Maps.newHashMap();
messageMap.put("messageKey", "messageBody");
mobileMessageMap.put("18688888888", messageMap);

uniqueIndex()

Maps.uniqueIndex(Iterable,Function)通常针对的场景是:有一组对象,它们在某个属性上分别有独一无二的值,而我们希望能够按照这个属性值查找对象

这个方法返回一个Map,键为Function返回的属性值,值为Iterable中相应的元素,因此我们可以反复用这个Map进行查找操作。

比方说,我们有一堆字符串,这些字符串的长度都是独一无二的,而我们希望能够按照特定长度查找字符串:

ImmutableMap<Integer, String> stringsByIndex = Maps.uniqueIndex(strings,
    new Function<String, Integer> () {
        public Integer apply(String string) {
            return string.length();
        }
    });

根据User集合构建userId <-> User 映射

List<User> users = userMapper.queryUsersByIds(userIds);
Map<Long, User> userIdMap = Maps.uniqueIndex(users, User::getId);

如果索引值不是独一无二的,请参见下面的Multimaps.index方法。

[Google Guava] 2.3-强大的集合工具类:java.util.Collections中未包含的集合工具
http://ifeve.com/google-guava-collectionutilities/


Multimaps

index

通常针对的场景是:有一组对象,它们有共同的特定属性,我们希望按照这个属性的值查询对象,但属性值不一定是独一无二的。

比方说,我们想把字符串按长度分组:

ImmutableSet digits = ImmutableSet.of("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine");
Function<String, Integer> lengthFunction = new Function<String, Integer>() {
    public Integer apply(String string) {
        return string.length();
    }
};

ImmutableListMultimap<Integer, String> digitsByLength= Multimaps.index(digits, lengthFunction);
/*
*  digitsByLength maps:
*  3 => {"one", "two", "six"}
*  4 => {"zero", "four", "five", "nine"}
*  5 => {"three", "seven", "eight"}
*/

根据用户标签集合(一个用户可能多个标签)构建 userId <-> 标签集合 映射

List<UserTag> userTags = userMap.queryUserTagsByUserIds(userIds);
// UserId <--> UserTag Map
Multimap<Long, UserTag> userIdMultimap = Multimaps.index(userTags, UserTag::getUserId());
Map<Long, Collection<UserTag>> userIdMap = userIdMultimap.asMap();

用户收货地址,一个用户可能有多个收货地址:

// 批量查询地址信息
List<Long> userIds = Lists.newArrayList(12L, 23L, 34L);
List<UserAddress> userAddresses = userDao.queryUserAddressesByUserId(userIds);
Map<Long, Collection<UserAddress>> userIdAddressMap = Multimaps.index(userAddresses, UserAddress::getUserId).asMap();

[Google Guava] 2.3-强大的集合工具类:java.util.Collections中未包含的集合工具
http://ifeve.com/google-guava-collectionutilities/


ImmutableMap 不可变map

builder()

快速构造一个 map

public static final Map map = ImmutableMap.builder()
        .put("key", "value")
        .put("key2", "valuew")
        .build();

Sets

package com.google.common.collect;

@GwtCompatible( emulated = true )
public final class Sets {
  ...
}

newHashSet()创建HashSet

public static <E> HashSet<E> newHashSet()

例如:

Set<String> emails = Sets.newHashSet();
emails.add("aa@aaa.com");
emails.add("bbb@bb.com");

Set<String> emails = Sets.newHashSet("aa@aaa.com","bbb@bb.com");

intersection()求交集

public static <E> Sets.SetView<E> intersection(Set<E> set1, Set<?> set2)
返回 set1和set2中共同存在的元素

difference()求差集

public static <E> Sets.SetView<E> difference(Set<E> set1, Set<?> set2)
返回 在set1中但不在set2中 的元素,只在set2中不在set1中的元素会被忽略。

union()求并集

public static <E> Sets.SetView<E> union(Set<? extends E> set1, Set<? extends E> set2)
返回 set1 和 set2 的并集。

symmetricDifference()对称差

public static <E> Sets.SetView<E> symmetricDifference(Set<? extends E> set1, Set<? extends E> set2)
返回 在set1或set2中,但不在两者交集中的元素。
等于 set1 和 set2 的并集减去交集。

求集合的交集/差集/并集

package com.uwo9.test08;

import java.util.Set;

import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;

/**
 * 集合的操作:交集、差集、并集
 * Sets.intersection()交集
 * Sets.difference()差集
 * Sets.union()并集
 */
public class Test04 {

    public static void main(String[] args) {
        Set<Integer> sets = Sets.newHashSet(1, 2, 3, 4, 5, 6);
        Set<Integer> sets2 = Sets.newHashSet(3, 4, 5, 6, 7, 8, 9);
        // 交集
        System.out.println("交集为:");
        SetView<Integer> intersection = Sets.intersection(sets, sets2);
        for (Integer temp : intersection) {
            System.out.println(temp);
        }
        // 差集
        System.out.println("差集为:");
        SetView<Integer> diff = Sets.difference(sets, sets2);
        for (Integer temp : diff) {
            System.out.println(temp);
        }
        // 并集
        System.out.println("并集为:");
        SetView<Integer> union = Sets.union(sets, sets2);
        for (Integer temp : union) {
            System.out.println(temp);
        }
    }

}

java guava 集合的操作:交集、差集、并集
https://blog.csdn.net/huanyinghanlang/article/details/78774652

求List的差集(先转Set求差集再转回List)

List<Long> userIds1 = Lists.newArrayList(12L, 23L, 34L);
List<Long> userIds2 = Lists.newArrayList(12L, 23L);

// 差集
List<Long> userIdsDifference = Sets.difference(Sets.newHashSet(userIds1), Sets.newHashSet(userIds2)).stream()
        .collect(Collectors.toList());

Lists

package com.google.common.collect;

@GwtCompatible( emulated = true )
public final class Lists {
  ...
}

partition()分割List

public static <T> List<List<T>> partition(List<T> list, int size)
将入参list分割为n个list,每个长度为size,最后一个长度可能小于size,返回一个list集合。

List<Long> ids = Lists.newArrayList(1L, 2L, 3L, 4L, 5L);
List<List<Long>> partition = Lists.partition(ids, 2);

例如,把一个较大的user ids列表为分100个一组,分别对100个调用服务

Lists.partition(userIds, 100).forEach(userIdList -> getSomeInfos(userIdList, resultMap));

上一篇 Postman使用笔记

下一篇 Apache-Commons-Collections使用笔记

阅读
评论
4,073
阅读预计18分钟
创建日期 2018-06-29
修改日期 2020-08-22
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论