并发控制
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
- 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
- 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
- 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
CAS
- 什么是CAS
CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。1. 比较 A 与 V 是否相等。(比较)2. 如果比较相等,将 B 写入 V。(交换) 3. 返回操作是否成功。当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。 - CAS的应用
- 自旋锁,获取锁的可以执行,没有获取锁的需要一直自旋
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
- java.util.concurrent.atomic 包下的类基本都是利用CAS 实现的,通过源码可以发现基本都是调用unsafe 类,然后Unsafe 类调用本地的native方法compareAndSwap… 来实现CAS操作,atomic包下的类并不是所有的方法都采用了CAS+自旋,要通过具体情况进行分析。一般在源码中使用了while 的基本都是加了自旋,否则不带自旋
- 令牌桶限流
限流
经常在调别人的接口的时候会发现有限制,比如微信公众平台接口、聚合API等等这样的,对方会限制每天最多调多少次或者每分钟最多调多少次,这就是限流的具体体现
令牌桶算法
- 以下直接贴出令牌桶算法代码(仿照Eurake)
public class RateLimiter {
private final AtomicInteger load = new AtomicInteger();
private final AtomicLong lastFrushTime = new AtomicLong(0L);
private final int rateType;
/**
* 初始化构造函数
*
* @param dateType 时间类型
* @throws Exception
*/
public RateLimiter(TimeUnit dateType) {
switch (dateType) {
case SECONDS:
this.rateType = 1000;
break;
case MINUTES:
this.rateType = 60 * 1000;
break;
default:
throw new IllegalArgumentException("不支持的时间参数类型");
}
this.lastFrushTime.set(System.currentTimeMillis());
}
/**
* 请求凭证
*
* @param bucketSize 凭证桶大小
* @param credentialRate 凭证产生速率
* @return true:成功,false:失败
*/
public boolean getCredential(int bucketSize, int credentialRate) {
if (bucketSize <= 0 || credentialRate <= 0) {
return true; // 如果桶大小或者凭证产生速率有任意一个小于等于0,则认为不需要限流
}
// 刷新凭证桶
refillCredential(credentialRate);
// 消费凭证
return consumCredential(bucketSize);
}
/**
* 刷新凭证桶
*
* @param credentialRate 凭证产生速率
*/
public void refillCredential(int credentialRate) {
// 获取最后一次刷新桶时间
long lastTime = this.lastFrushTime.get();
// 获取系统当前时间
long currentTimeMillis = System.currentTimeMillis();
// 根据时间差值计算出 时间差值内所需产生的令牌
long diffTime = currentTimeMillis - lastTime;
long needCredential = diffTime * credentialRate / rateType;
// 所需新产生令牌大于0 则继续往下执行
if (needCredential > 0) {
// 重新设置成功才会填充凭证
if (lastFrushTime.compareAndSet(lastTime, currentTimeMillis)) {
// 获取被消费的凭证数
int consumed = load.get();
// 计算还剩多少被消费的,最小为0
int min = (int) Math.max(0, consumed - needCredential);
// 将计算的剩余被消费的设置进去
load.compareAndSet(consumed, min);
}
}
}
/**
* 消费凭证桶
*
* @param bucketSize 凭证桶大小
* @return
*/
public boolean consumCredential(int bucketSize) {
while (true) {
int consumed = load.get();
// 判断凭证消费数量是否达到上限
if (consumed >= bucketSize) {
return false;
}
if (load.compareAndSet(consumed, consumed + 1)) {
return true;
}
}
}
/**
* 重置
*/
public void reset() {
this.load.set(0);
this.lastFrushTime.set(System.currentTimeMillis());
}
}
总结
加油噢!