方案一 控制并发数
利用信号量机制(java的Semaphore类可实现)来控制并发数量,这个信号量机制类似于线程锁机制,比如服务允许线程为50,可以利用信号量机制,控制线程执行只维持20个线程同时执行。具体代码如下
第8行获得信号量,14行释放信号量,类似锁机制。此方案就是简单,无需引入任何jar就能实现代码逻辑,只需要java的Semaphore类。
方案二 控制访问速率
在订单服务添加请求速率控制器,过滤请求,请求溢出服务承受能力,跳转资源紧张页面或排队页。
速率控制使用Google的RateLimiter,RateLimiter利用令牌桶算法实现流量控制,
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
令牌桶也能控制数据流读取速率,当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
如图,桶中按一定速率生成令牌,可以实现按请求发放令牌,当桶中令牌为空时,可以实现请求堵塞,或者直接返回false,跳转警告页。
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率.
RateLimiter的主要功能就是提供一个稳定的速率,实现方式就是通过限制请求流入的速度。
代码逻辑范例
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); // 也许需要等待 ,如果不阻塞,可使用tryAcquire方法,直接跳转页面
executor.execute(task);
}
}
代码逻辑大意就是,每秒生成2个令牌,每次请求过来都去领取一个令牌,如果令牌桶中令牌为空,请求阻塞或返回false做页面跳转。
在web中实现可以利用拦截器实现RateLimiter 功能,拦截过载请求。
问题点:功能不算复杂,但是存在分布式问题,RateLimiter多是针对于单机实现,多机实现RateLimiter会创建多个令牌桶,拦截数会变成节点数*令牌数导致限流失败,看是否能够部署的时候做成多机单桶调用。
单机Web项目可使用过滤器实现,要初始化令牌桶,过滤器init()方法中实现令牌桶初始化。
多机要考虑我们现有框架令牌桶如何初始化。
方案三 控制单位时间窗口内请求数(可实现分布式限流)
redis + Lua做访问流量控制,此方案是针对分布式系统的限流方案,并且能实现对各个不同维度不同颗粒度的流量限制,比如,某个ip在短时间内频繁访问页面(如爬虫),可以控制其单位时间内对某一接口或者整个服务的访问次数,也可以实现对某个服务接口的单位时间的流量限制。
主要脚本及代码如下,key取的参数不同以及限制数的不同就能实现是针对用户ip或者服务端口的限流,理论上还可以实现更细化的更多维度的流量控流。
此方案无论部署和代码量都不是很复杂,它是利用计数器实现限流,但做不到速率流量限制,可以考虑在业务量更大的时候和方案二配合使用。
实现
使用拦截器实现,redis+lua限流功能单独实现,与其他服务解耦实现。
(是否需要添加开关功能待定)
功能描述:
接口项 | 说明 |
功能 | Redis+Lua限流 |
应用场景 | 大请求量的限流拦截功能 |
接口模块 | eshop-app |
拦截器名称 | requestLimiterByRedisLua |
关键变量说明:
变量名称 | 变量类型 | 详细说明 |
limiteKey | String | Key的拼接规则:Key+time(时间维度的颗粒度) 存储的key不同可以针对用户限制还是服务限制,key拼接的时间,限制到秒就是每秒限流,限制到分就是每分钟限制 |
limit | String | 限流大小,为数字字符串 |
limitType | String | 常量字段,1:针对用户限流,2针对服务端口限流,后期可扩展 |
limitIsRun | Boolean | 开关字段,是否启用限流(是否需要待定) |
一些其他想法
这只是在应用层解决了限流服务,解决大体量请求的方案还有其他,可以根据现有的请求量,成本和技术慢慢演化成型,后期可以在服务上抽象出一个接入层,来实现nginx+lua拦截,这是小米抢购系统初期实现的一种方式,利用lua扩展nginx的功能模块,可以在请求进入java服务层之前将请求隔离,nginx处理并发的能力比java强,这样能够更好的避免伤及服务。
数据库端也可以做优化,因为秒杀这种都是实时热点数据,短时间内会占用大量的数据库连接,数据库并发连接越多,整体处理请求能力越差,所以一般会将秒杀单独分库处理,以免让1%的热点数据影响到99%的普通业务,还可以通过合理的排队机制来减少单位时间内的数据库并发连接。