lua 获取外网ip_JAVA高并发 Redis+Lua限流实战

点击上方“linkoffer”,

选择关注公众号高薪职位第一时间送达

参考资料

    https://dwz.cn/Gvviwswi

    https://dwz.cn/pO9mWjhq

简介

采用了redis来作为限流器的实现

  • redis作为高性能缓存系统,性能上能够满足多机之间高并发访问的要求

  • redis有比较好的api来支持限流器令牌桶算法的实现

  • 对于我们的系统来说,通过spring data redis来操作比较简单和常见,避免了引入新的中间件带来的风险

但是我们也知道,限流器在每次请求令牌和放入令牌操作中,存在一个协同的问题,即获取令牌操作要尽可能保证原子性,否则无法保证限流器是否能正常工作。在RateLimiter的实现中使用了mutex作为互斥锁来保证操作的原子性,那么在redis中就需要一个类似于事务的机制来保证获取令牌中多重操作的原子性。 面对这样的需求,我们有几个选择:

  • 用redis实现分布式锁来保证操作的原子性,这个方案实现起来应该比较简单,分布式锁有现成的例子,然后就是把Rate Limiter的代码套用分布式锁就行了,但是这样的话效率会显得不太高,特别是在大量访问的情况下。

  • 用redis的transaction,在我查阅redis官方文档和stackoverflow之后发现redis的transaction官方并不推荐,并且有可能在未来取消事务,因此不可取。

  • 通过redis分布式锁和本地锁组成一个双层结构,每次分布式获取锁之后可以预支一部分令牌量,然后放到本地通过本地的锁来分配这些令牌,消耗完之后再到请求redis。这样的好处是相比第一个方案,网络访问延迟开销会比较好,但是实现难度和复杂程度比较难估量,而且这样的做法如果在多机不能保证均匀分配流量的情况下并不理想

  • 通过将获取锁封装到lua脚本中,提交给redis进行eval和evalsha操作来完成lua脚本的执行,由于lua脚本在redis中天然的原子性,我们的需求能够比较好的满足,问题是将业务逻辑封装在lua中,对于开发人员自身的能力和调试存在一定的问题。

经过权衡,我采用了第四种方式,通过redis和lua来编写令牌桶算法来完成分布式限流的需求。

项目实战

本项目基于SpringBoot 2.1.5,使用到 Redis + Lua 限流脚本

一. 引入依赖 pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>

xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

org.springframework.boot

spring-boot-starter-parent

2.1.5.RELEASE

com.xd

redis-lua-limit

0.0.1-SNAPSHOT

redis-lua-limit

基于Spring Boot Redis+Lua高并发限流

1.8

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-data-redis

org.springframework.boot

spring-boot-starter-aop

org.apache.commons

commons-lang3

org.springframework.boot

spring-boot-starter-test

org.projectlombok

lombok

1.18.8

org.springframework.boot

spring-boot-maven-plugin

二. application.properties

spring.application.name=spring-boot-redis-lua-limit

# Redis数据库索引 默认为0

spring.redis.database=0

# Redis地址

spring.redis.host=localhost

# Redis端口 默认6379

spring.redis.port=6379

# Redis服务器连接密码(默认为空)

spring.redis.password=

# 连接池最大连接数(使用负值表示没有限制)

spring.redis.jedis.pool.max-active=8

# 连接池最大阻塞等待时间(使用负值表示没有限制)

spring.redis.jedis.pool.max-wait=-1

# 连接池中的最大空闲连接

spring.redis.jedis.pool.max-idle=8

# 连接池中的最小空闲连接

spring.redis.jedis.pool.min-idle=0

# 连接超时时间(毫秒)

spring.redis.timeout=10000

三. Lua 脚本

--Lua脚本

--- 限流KEY资源唯一标识

local key = "rate.limit:" .. KEYS[1]

--- 时间窗最大并发数

local limit = tonumber(ARGV[1])

--- 时间窗内当前并发数

local current = tonumber(redis.call('get', key) or "0")

--如果超出限流大小

if current + 1 > limit then

return 0

else --请求数+1,并设置2秒过期

redis.call("INCRBY", key,"1")

redis.call("expire", key,"2")

return current + 1

end

--IP限流Lua脚本

--local key = "rate.limit:" .. KEYS[1]

--local limit = tonumber(ARGV[1])

--local expire_time = ARGV[2]

--

--local is_exists = redis.call("EXISTS", key)

--if is_exists == 1 then

-- if redis.call("INCR", key) > limit then

-- return 0

-- else

-- return 1

-- end

--else

-- redis.call("SET", key, 1)

-- redis.call("EXPIRE", key, expire_time)

-- return 1

--end

1、我们通过KEYS[1] 获取传入的key参数  2、通过ARGV[1]获取传入的limit参数 3、redis.call方法,从缓存中get和key相关的值,如果为nil那么就返回0  4、接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0  5、如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1

三. 注解

自定义注解的目的,是在需要限流的方法上使用

package com.xd.redislualimit.annotation;

import java.lang.annotation.*;

/**

* @Author 李号东

* @Description 限流注解

* @Date 17:49 2019-05-25

**/

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface RateLimit {

/**

* 限流唯一标示

*

* @return

*/

String key() default "";

/**

* 限流时间

*

* @return

*/

int time();

/**

* 限流次数

*

* @return

*/

int count();

}

四. 配置

package com.xd.redislualimit.config;

import org.springframework.context.annotation.Bean;

import org.springframework.core.io.ClassPathResource;

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import org.springframework.scripting.support.ResourceScriptSource;

import org.springframework.stereotype.Component;

import java.io.Serializable;

/**

* @Classname commons

* @Description 配置

* @Author 李号东 lihaodongmail@163.com

* @Date 2019-05-25 20:13

* @Version 1.0

*/

@Component

public class commons {

/**

* 读取限流脚本

*

* @return

*/

@Bean

public DefaultRedisScript<Number> redisluaScript() {

DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();

redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redisLimit.lua")));

//返回类型

redisScript.setResultType(Number.class);

return redisScript;

}

/**

* RedisTemplate

*

* @return

*/

@Bean

public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {

RedisTemplate<String, Serializable> template = new RedisTemplate<>();

template.setKeySerializer(new StringRedisSerializer());

template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

template.setConnectionFactory(redisConnectionFactory);

return template;

}

}

五. 限流注解拦截器

package com.xd.redislualimit.config;

import com.xd.redislualimit.annotation.RateLimit;

import com.xd.redislualimit.utils.IPUtil;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.reflect.MethodSignature;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

import java.io.Serializable;

import java.lang.reflect.Method;

import java.util.Collections;

import java.util.List;

import java.util.Objects;

/**

* @Classname LimitAspect

* @Description 注解拦截

* @Author 李号东 lihaodongmail@163.com

* @Date 2019-05-25 20:15

* @Version 1.0

*/

@Slf4j

@Aspect

@Configuration

public class LimitAspect {

@Autowired

private RedisTemplate<String, Serializable> redisTemplate;

@Autowired

private DefaultRedisScript<Number> redisluaScript;

//执行redis的具体方法,限制method,保证没有其他的东西进来

@Around("execution(* com.xd.redislualimit.controller ..*(..) )")

public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

Method method = signature.getMethod();

Class> targetClass = method.getDeclaringClass();

RateLimit rateLimit = method.getAnnotation(RateLimit.class);

if (rateLimit != null) {

HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

String ipAddress = IPUtil.getIp(request);

String string = ipAddress + "-" + targetClass.getName() + "- " + method.getName() + "-" + rateLimit.key();

List<String> keys = Collections.singletonList(string);

Number number = redisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());

if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {

log.info("限流时间段内访问第:{} 次", number.toString());

return joinPoint.proceed();

}

} else {

return joinPoint.proceed();

}

log.error("已经到设置限流次数");

throw new RuntimeException("已经到设置限流次数");

}

}

六. 控制层

package com.xd.redislualimit.controller;

import com.xd.redislualimit.annotation.RateLimit;

import org.apache.commons.lang3.time.DateFormatUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.support.atomic.RedisAtomicInteger;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**

* @Classname LimiterController

* @Description 测试控制层

* @Author 李号东 lihaodongmail@163.com

* @Date 2019-05-25 20:16

* @Version 1.0

*/

@RestController

public class LimiterController {

@Autowired

private RedisTemplate redisTemplate;

// 10 秒中,可以访问5次

@RateLimit(key = "test", time = 10, count = 5)

@GetMapping("/test")

public String luaLimiter() {

// 简单测试方法

RedisAtomicInteger entityIdCounter = new RedisAtomicInteger("counter", redisTemplate.getConnectionFactory());

String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");

return date + " 累计访问次数:" + entityIdCounter.getAndIncrement();

}

}

七. 测试

启动程序 打开浏览器访问 http://localhost:8080/test

连续访问5次 控制台打印

60d8040aac0df9c27a198f0746c1555d.png

限流成功!

项目地址: https://github.com/LiHaodong888/SpringBootLearn/

542b92a346e0d3c6000b015b2e4fd8fb.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本来实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本来实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值