前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JAVA高并发 Redis+Lua限流实战

JAVA高并发 Redis+Lua限流实战

作者头像
小东啊
发布2019-06-26 15:13:41
3.2K0
发布2019-06-26 15:13:41
举报
文章被收录于专栏:李浩东的博客李浩东的博客

参考资料

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文件
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?><project 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">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.1.5.RELEASE</version>        <relativePath/>    </parent>    <groupId>com.xd</groupId>    <artifactId>redis-lua-limit</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>redis-lua-limit</name>    <description>基于Spring Boot Redis+Lua高并发限流</description>
    <properties>        <java.version>1.8</java.version>    </properties>
    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-redis</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-aop</artifactId>        </dependency>        <dependency>            <groupId>org.apache.commons</groupId>            <artifactId>commons-lang3</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>1.18.8</version>        </dependency>    </dependencies>
    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build>
</project>
二. application.properties
代码语言:javascript
复制
spring.application.name=spring-boot-redis-lua-limit
# Redis数据库索引 默认为0spring.redis.database=0# Redis地址spring.redis.host=localhost# Redis端口 默认6379spring.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 脚本
代码语言:javascript
复制
--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 0else  --请求数+1,并设置2秒过期  redis.call("INCRBY", key,"1")   redis.call("expire", key,"2")   return current + 1end


--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

三. 注解

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

代码语言:javascript
复制
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)@Documentedpublic @interface RateLimit {
    /**     * 限流唯一标示     *     * @return     */    String key() default "";
    /**     * 限流时间     *     * @return     */    int time();
    /**     * 限流次数     *     * @return     */    int count();}
四. 配置
代码语言:javascript
复制
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 */@Componentpublic 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;    }}
五. 限流注解拦截器
代码语言:javascript
复制
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@Configurationpublic 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("已经到设置限流次数");    }
}
六. 控制层
代码语言:javascript
复制
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 */@RestControllerpublic 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次 控制台打印

限流成功!

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

点好看,送你小花花~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 李浩东的博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 项目实战
    • 一. 引入依赖 pom.xml文件
      • 二. application.properties
        • 三. Lua 脚本
          • 三. 注解
            • 四. 配置
              • 五. 限流注解拦截器
                • 六. 控制层
                  • 七. 测试
                  相关产品与服务
                  云数据库 Redis
                  腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档