分布式秒杀系统三

分布式秒杀系统三

秒杀请求限流

当单位时间内请求量很大的时候,拒绝一部分请求的手段。是解决高并发的重要手段之一。

在什么地方限流?-路由网关

根据什么来限流

  1. 如果根据请求数量来限流,因为请求是经过路由网关,那么秒杀服务会影响其他微服务。
  2. 根据ip地址来限流(单位时间每个ip只能访问几次)或根据用户ID或根据URL限流(常用)

限流的实现方案

  1. 根据压测算出大概每秒能承受多少次的请求,设置一个maxRequest=最大请求数量和count=0,每当有一个请求过来count就+1,并且每一秒count都重新置为0,当count>maxRequest时就拒绝请求。

    (问题:1. 如果路由网关集群之后,maxRequest会叠加,每一次加一台机器都要重新计算maxRequest。2. 请求不均匀:前一秒内的1W请求都集中在一秒的后半段,后一秒内的1W请求都集中在一秒的前半段,则一秒有2W的服务进入。)

  2. 漏桶算法:略

  3. 令牌桶算法⭐:路由网关中有个令牌桶数据结构(类似于集合)(将令牌桶放在redis中可以避免路由网关集群后最大令牌数成倍增加导致通过的请求数成倍增加的问题),存有许多令牌,包含当前令牌数和最大的令牌数。有一个功能会按照一定的速率往令牌桶中放令牌。当请求经过路由网关时去令牌桶中申请令牌,获得令牌则通过,没获取则拒绝或等待。

    (好处:1. 是一种滑动窗口的限流。2. 每一个请求可以申请到不同数量的令牌,可以实现不同的业务。)

    (问题:存在并发多线程的问题,线程安全问题:1. 加锁(影响性能),2. 利用redis单线程机制⭐,让redis查令牌和取令牌同时执行,利用lua脚本,在lua脚本中写上查令牌和取令牌,将lua脚本整个传给redis,可以达到无锁线程安全的目的)

    (根据Url进行限流,在redis中根据不同的url设置多个令牌桶)

在这里插入图片描述

redis + Lua脚本实现令牌桶算法

Lua脚本

因为redis的单线程特性(6.0之后io变成多线程,执行命令仍然是单线程),Lua脚本可以作为一个单元被redis执行,这个执行的过程不会被其他客户端的其他命令所打断。

Lua脚本对于redis来讲具有原子性,在实际开发过程中,往往可以借助Lua脚本原子性的特点,实现无锁化的线程安全。

语法

eval "redis.call('set','name','xiaoming')" 0

eval "return redis.call('get','name')" 0

eval "redis.call('set','KEYS[1]','ARGV[1]')" 1 name xiaohong

eval "local number = tonumber(ARGV[1]) if number % 2 == 0 then return redis.call('get','name') else return redis.call('get','age') end" 0 18

令牌桶的实现

key:Hash - 令牌桶 (key - 需要限流的关键属性)

Hash - 当前剩余令牌,令牌的最大数量,每秒产生多少令牌,下一次可以生产令牌的时间

如何添加令牌(令牌的生成方式)

  1. 通过额外的线程来按照一定的速率往令牌桶的添加令牌(redis中有许多令牌桶时需要许多的线程来生成)
  2. 当请求申请令牌时附带当前时间,该时间和令牌桶中的更新时间进行对比,算出时间差,根据每秒产生的令牌数和时间差计算出需要产生多少令牌,然后加到当前剩余令牌,并将当前时间更新到令牌桶中。(相比较线程的消耗几乎可以忽略不计)⭐

令牌的预支设置

在高并发的请求下,那些重量级的请求可以一直会得不到令牌

  1. 当请求申请令牌大于当前剩余令牌时,进行预支时要根据预支的数量和每秒生产的令牌数计算所需的时间,然后加到下一次可以生产令牌的时间之上,并将令牌数设置为0。
  2. 如果再来一个请求申请令牌数也大于当前剩余令牌,并且当前时间还不到下一次生产令牌的时间,然后计算两者差值,返回该差值告诉请求需要等待多少秒才能够预支,然后同样的要根据预支的数量和每秒生产的令牌数计算所需的时间,加到下一次可以生产令牌的时间,以供一下次请求去计算。
  3. 当前请求的预支需要下一次请求去等待。

在这里插入图片描述

脚本代码

初始化Lua脚本
--判断key是否存在,如果不存在就初始化令牌桶
--获得参数key并且用..进行拼接
local key = 'tongKey_'..KEYS[1]

--令牌桶的最大容量
local maxTokens = tonumber(ARGV[1])

--每秒产生的令牌数量
local secTokens = tonumber(ARGV[2])

--计算当前时间(微秒)
local nextTime = tonumber(ARGV[3])


--判断令牌桶是否存在
local result = redis.call('exists',key)
if result == 0 then	 	redis.call('hmset',key,'hasTokens',maxTokens,'maxTokens',maxTokens,'secTokens',secTokens,'nextTime',nextTime)
end
令牌桶的领取
--当前领取的令牌桶的key
local key = 'tongKey_'..KEYS[1]

--获取当前需要领取令牌的数量
local getTokens = tonumber(ARGV[1])

--获取令牌桶中的参数
local hasTokens = tonumber(redis.call('hget',key,'hasTokens'))

--获得最大的令牌数
local maxTokens= tonumber(redis.call('hget',key,'maxTokens'))


--每秒生产的令牌的数量
local secTokens= tonumber(redis.call('hget',key,'secTokens'))

--下一次可以生产令牌的时间(微妙)
local nextTime = tonumber(redis.call('hget',key,'nextTime'))


--当前时间(微妙值)
local nowArray = redis.call('time')
local nowTime = nowArray[1]*1000000 + nowArray[2]

--单个令牌生成的耗时
local singTokenTime = 1000000/secTokens


--获得超时时间
local timeout = tonumber(ARGV[2] or -1)

--判断超时时间
if timeout ~= -1 then
    if timeout < nextTime - nowTime then
        return -1
    end
end



--重新计算令牌
if nowTime > nextTime then
    --计算上一次生成令牌到现在的差时
    local hasTime = nowTime - nextTime
    --可以产生的令牌数
    local createTokens = hasTime/singTokenTime
    --当前总的令牌数
    hasTokens = math.min(hasTokens+createTokens,maxTokens)
    --重新设置下一次可以生成令牌的时间
    nextTime = nowTime
end


--获取令牌

--计算当前能够拿走的令牌
local canGetTokens = math.min(hasTokens,getTokens)
--计算需要预支的令牌数量
local yuzhiTokens = getTokens - canGetTokens
--计算如果预支这些令牌,需要多少时间(微秒)
local yuzhiTime = yuzhiTokens * singTokenTime
--重新设置令牌桶中的值
hasTokens = hasTokens - canGetTokens


--更新令牌桶
redis.call('hmset',key,'hasTokens',hasTokens,'nextTime',nextTime+yuzhiTime)

--返回当前请求需要等待的时间
return nextTime -nowTime

GateWay过滤器

TokenLimitFilter

@Component
public class TokenLimitFilter implements GatewayFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //令牌桶限流 -URL
        //获取当前请求的URL
        ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getPath().value();

        System.out.println("当前请求的url路径"+requestPath);

        //请求放行
        return chain.filter(exchange);
    }
}

TokenLimitFilterFactory

@Component
public class TokenLimitFilterFactory extends AbstractGatewayFilterFactory {
    @Autowired
    private TokenLimitFilter tokenLimitFilter;
    @Override
    public GatewayFilter apply(Object config) {
        return tokenLimitFilter;
    }
    @Override
    public String name(){
        return "TokenLimiter";
    }
}

问题

  1. 实际开发有没有用过多线程
  2. 线程安全问题
  3. 怎么解决

版权声明:本文为Ferko原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。