Guava并发使用学习
前言:
在开发高并发系统,有三把利器用来保护系统:缓存,降级,限流。
在开放api接口时候,有时也需要用限流来控制,防止并发过高,系统奔溃。
1.Maven配置:
<!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency>
2.基本类方法
//0.5代表一秒最多多少个 RateLimiter rateLimiter = RateLimiter.create(0.5); //rateLimiter.acquire()该方法会阻塞线程,直到令牌桶中能取到令牌为止才继续向下执行,并返回等待的时间。 rateLimiter.acquire() //从RateLimiter 获取许可如果该许可可以在不超过timeout的时间内获取得到的话, //或者如果无法在timeout 过期之前获取得到许可的话,那么立即返回false(无需等待) tryAcquire(long timeout, TimeUnit unit)
3. 限流
//5代表一秒最多5个 RateLimiter rateLimiter = RateLimiter.create(5); @RequestMapping(value = "/miaosha") @ResponseBody public String miaosha(int count, String code) { System.out.println("等待时间:" + rateLimiter.acquire()); if (update(code, count) > 0) { return "购买成功"; } return "购买失败"; }
请求过来时,调用RateLimiter.acquire,如果每秒超过了5个请求,就阻塞等待
jmeter演示:
等待时间:0.0
等待时间:0.0
等待时间:0.0
等待时间:0.0
等待时间:0.0
等待时间:0.175592
等待时间:0.375583
等待时间:0.575578
等待时间:0.775507
等待时间:0.975484
等待时间:1.175457
等待时间:1.375442
等待时间:1.575435
等待时间:1.775429
等待时间:1.975424
结论:前5个请求无需等待直接成功,后面的开始被1秒5次限流了,基本上每0.2秒放行一个。
4. 降级
RateLimiter rateLimiter = RateLimiter.create(10); @RequestMapping("/buy") @ResponseBody public String buy(int count, String code) { //判断能否在1秒内得到令牌,如果不能则立即返回false,不会阻塞程序 if (!rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) { System.out.println("短期无法获取令牌,真不幸,排队也瞎排"); return "失败"; } if (update(code, count) > 0) { System.out.println("购买成功"); return "成功"; } System.out.println("数据不足,失败"); return "失败"; }
tryAcquire(long timeout, TimeUnit unit)
* 从RateLimiter 获取许可如果该许可可以在不超过timeout的时间内获取得到的话,
* 或者如果无法在timeout 过期之前获取得到许可的话,那么立即返回false(无需等待)
jmeter演示(100个请求):
购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 购买成功
* 短期无法获取令牌,真不幸,排队也瞎排
* 短期无法获取令牌,真不幸,排队也瞎排
* 短期无法获取令牌,真不幸,排队也瞎排
* 短期无法获取令牌,真不幸,排队也瞎排
* 短期无法获取令牌,真不幸,排队也瞎排
* 短期无法获取令牌,真不幸,排队也瞎排
* 短期无法获取令牌,真不幸,排队也瞎排
* 购买成功
* 短期无法获取令牌,真不幸,排队也瞎排
* ...
基本上就是前10个成功,后面的就开始按照固定的速率而成功了。
这种场景更符合实际的应用场景,按照固定的单位时间进行分割,每个单位时间产生一个令牌,可供购买。
5. guava缓存
创建:
public static com.google.common.cache.CacheLoader<String, String> createCacheLoader() { return new com.google.common.cache.CacheLoader<String, String>() { @Override public String load(String key) throws Exception { System.out.println( "加载创建key:" + key); return key+"+value"; } }; }
//CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(1000)
//设置写缓存后30L过期
.expireAfterAccess(30L, TimeUnit.MILLISECONDS)
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(createCacheLoader());
//放入缓存 cache.put(key,value);
// 移除缓存 cache.invalidate(key);
//批量删除缓存 cache.invalidateAll(keys); List<String> keys
//会重新加载创建cache cache.getUnchecked(key);
//不会重新加载创建cache cache.getIfPresent(key);
//获取,会抛出异常 cache.get(key);
//任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
// 个别清除:Cache.invalidate(key)
//批量清除:Cache.invalidateAll(keys)
// 清除所有缓存项:Cache.invalidateAll()
//正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
//expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
//expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回 收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。