文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Redis分布式限流组件设计与使用实例

2024-04-02 19:55

关注

本文主要讲解基于 自定义注解+Aop+反射+Redis+Lua表达式 实现的限流设计方案。实现的限流设计与实际使用。

1.背景

在互联网开发中经常遇到需要限流的场景一般分为两种

一般我们衡量系统处理能力的指标是每秒的QPS或者TPS,假设系统每秒的流量阈值是2000,
理论上第2001个请求进来时,那么这个请求就需要被限流。

本文演示项目使用的是 SpringBoot 项目,项目构建以及其他配置,这里不做演示。文末附限流Demo源码

2.Redis计数器限流设计

本文演示项目使用的是 SpringBoot 项目,这里仅挑选了重点实现代码展示,
项目构建以及其他配置,这里不做演示,详细配置请参考源码demo工程。

2.1Lua脚本

Lua 是一种轻量小巧的脚本语言可以理解为就是一组命令。
使用Redis的计数器达到限流的效果,表面上Redis自带命令多个组合也可以支持了,那为什么还要用Lua呢?
因为要保证原子性,这也是使用redis+Lua表达式原因,一组命令要么全成功,要么全失败。
相比Redis事务,Lua脚本的优点:

实现限流Lua脚本示例


# 定义计数变量
local count
# 获取调用脚本时传入的第一个key值(用作限流的 key)
count = redis.call('get',KEYS[1])
# 限流最大值比较,若超过最大值,则直接返回
if count and tonumber(count) > tonumber(ARGV[1]) then
return count;
end
# incr 命令 执行计算器累加
count = redis.call('incr',KEYS[1])
# 从第一次调用开始限流,并设置失效时间
if tonumber(count) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
end
return count;

参数说明

2.2自定义注解

支持范围:任意接口



@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    
    String key();

    
    int time() default 60;

    
    int count();

    
    String keyField() default "";

    
    String msg() default "over the max request times please try again";

}

属性介绍

这样生成的key为参数中userId的值。一般与key属性组合使用。不支持java基本类型参数,
仅支持参数是一个对象的接口。

msg - 超过限流的提示内容

示例:


@RateLimit(key = "limit-phone-key", time = 300, count = 10, keyField = "phone", msg = "5分钟内,验证码最多发送10次")

含义 - 5分钟内根据手机号限流10次
RedisKey- limit-phone-key:后面拼接的是参数中phone的值。

2.3限流组件

这里用的是jedis客户端,配置就不列在这里的,详见源码,文末附源码地址



@Component
public class RedisRateLimitComponent {
    private static final Logger logger = LoggerFactory.getLogger(RedisRateLimitComponent.class);

    private JedisPool jedisPool;

    @Autowired
    public RedisRateLimitComponent(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    
    public Long rateLimit(String redisKey, Integer time, Integer rateLimitCount) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Object obj = jedis.evalsha(jedis.scriptLoad(this.buildLuaScript()), Collections.singletonList(redisKey),
                    Arrays.asList(String.valueOf(rateLimitCount), String.valueOf(time)));
            return Long.valueOf(obj.toString());
        } catch (JedisException ex) {
            logger.error("[ executeLua ] >> messages:{}", ex.getMessage(), ex);
            throw new RateLimitException("[ RedisRateLimitComponent ] >> jedis run lua script exception" + ex.getMessage());
        } finally {
            if (jedis != null) {
                if (jedis.isConnected()) {
                    jedis.close();
                }
            }
        }
    }

    
    private String buildLuaScript() {
        StringBuilder luaBuilder = new StringBuilder();
        //定义变量
        luaBuilder.append("local count");
        //获取调用脚本时传入的第一个key值(用作限流的 key)
        luaBuilder.append("\ncount = redis.call('get',KEYS[1])");
        // 获取调用脚本时传入的第一个参数值(限流大小)-- 调用不超过最大值,则直接返回
        luaBuilder.append("\nif count and tonumber(count) > tonumber(ARGV[1]) then");
        luaBuilder.append("\nreturn count;");
        luaBuilder.append("\nend");
        //执行计算器自增
        luaBuilder.append("\ncount = redis.call('incr',KEYS[1])");
        //从第一次调用开始限流
        luaBuilder.append("\nif tonumber(count) == 1 then");
        //设置过期时间
        luaBuilder.append("\nredis.call('expire',KEYS[1],ARGV[2])");
        luaBuilder.append("\nend");
        luaBuilder.append("\nreturn count;");
        return luaBuilder.toString();
    }
}

2.4限流切面实现



@Aspect
@Configuration
public class RateLimitAspect {
    private static final Logger logger = LoggerFactory.getLogger(RateLimitAspect.class);

    private RedisRateLimitComponent redisRateLimitComponent;

    @Autowired
    public RateLimitAspect(RedisRateLimitComponent redisRateLimitComponent) {
        this.redisRateLimitComponent = redisRateLimitComponent;
    }

    
    @Pointcut("@annotation(com.example.ratelimit.annotation.RateLimit)")
    public void pointCut() {
    }

    @Around("pointCut()&&@annotation(rateLimit)")
    public Object logAround(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getMethod().getName();

        //组装限流key
        String rateLimitKey = this.getRateLimitKey(joinPoint, rateLimit);

        //限流组件-通过计数方式限流
        Long count = redisRateLimitComponent.rateLimit(rateLimitKey, rateLimit.time(), rateLimit.count());
        logger.debug("[ RateLimit ] method={},rateLimitKey={},count={}", methodName, rateLimitKey, count);

        if (null != count && count.intValue() <= rateLimit.count()) {
            //未超过限流次数-执行业务方法
            return joinPoint.proceed();
        } else {
            //超过限流次数
            logger.info("[ RateLimit ] >> over the max request times method={},rateLimitKey={},currentCount={},rateLimitCount={}",
                    methodName, rateLimitKey, count, rateLimit.count());
            throw new RateLimitException(rateLimit.msg());
        }
    }

    
    private String getRateLimitKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
        String fieldName = rateLimit.keyField();
        if ("".equals(fieldName)) {
            return rateLimit.key();
        }

        //处理自定义-参数名-动态属性key
        StringBuilder rateLimitKeyBuilder = new StringBuilder(rateLimit.key());
        for (Object obj : joinPoint.getArgs()) {
            if (null == obj) {
                continue;
            }
            //过滤基本类型参数
            if (ReflectionUtil.isBaseType(obj.getClass())) {
                continue;
            }
            //属性值
            Object fieldValue = ReflectionUtil.getFieldByClazz(fieldName, obj);
            if (null != fieldValue) {
                rateLimitKeyBuilder.append(":").append(fieldValue.toString());
                break;
            }
        }
        return rateLimitKeyBuilder.toString();
    }
}

由于演示项目中做了统一异常处理
在限流切面这里未做异常捕获,若超过最大限流次数会抛出自定义限流异常。可以根据业务自行处理。



public class ReflectionUtil {

    private static final Logger logger = LoggerFactory.getLogger(ReflectionUtil.class);

    
    public static Object getFieldByClazz(String fieldName, Object object) {
        Field field = null;
        Class<?> clazz = object.getClass();
        try {
            for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
                try {
                    //子类中查询不到属性-继续向父类查
                    field = clazz.getDeclaredField(fieldName);
                } catch (NoSuchFieldException ignored) {
                }
            }
            if (null == field) {
                return null;
            }
            field.setAccessible(true);
            return field.get(object);
        } catch (Exception e) {
            //通过反射获取 属性值失败
            logger.error("[ ReflectionUtil ] >> [getFieldByClazz] fieldName:{} ", fieldName, e);
        }
        return null;
    }

    
    public static boolean isBaseType(Class clazz) {
        if (null == clazz) {
            return false;
        }
        //基本类型
        if (clazz.isPrimitive()) {
            return true;
        }
        //String
        if (clazz.equals(String.class)) {
            return true;
        }
        //Integer
        if (clazz.equals(Integer.class)) {
            return true;
        }
        //Boolean
        if (clazz.equals(Boolean.class)) {
            return true;
        }
        //BigDecimal
        if (clazz.equals(BigDecimal.class)) {
            return true;
        }
        //Byte
        if (clazz.equals(Byte.class)) {
            return true;
        }
        //Long
        if (clazz.equals(Long.class)) {
            return true;
        }
        //Double
        if (clazz.equals(Double.class)) {
            return true;
        }
        //Float
        if (clazz.equals(Float.class)) {
            return true;
        }
        //Character
        if (clazz.equals(Character.class)) {
            return true;
        }
        //Short
        return clazz.equals(Short.class);
    }
}

3.测试一下

基本属性已经配置好了,写个接口测试一下。

3.1方法限流示例


  
  private static final AtomicInteger COUNTER = new AtomicInteger();    

  
  @RequestMapping("/limitTest")
  @RateLimit(key = "limit-test-key", time = 30, count = 10)
  public Response limitTest() {
      Map<String, Object> dataMap = new HashMap<>();
      dataMap.put("date", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
      dataMap.put("times", COUNTER.incrementAndGet());
      return Response.success(dataMap);
  }

在这里插入图片描述

3.2动态入参限流示例

3.2.1场景一:5分钟内,方法最多访问10次,根据入参手机号限流

入参类


public class UserPhoneCaptchaRateParam implements Serializable {

    private static final long serialVersionUID = -1L;

    private String phone;
    //省略 get/set
}

  private static final Map<String, AtomicInteger> COUNT_PHONE_MAP = new HashMap<>();


  
  @RequestMapping("/limitByPhone")
  @RateLimit(key = "limit-phone-key", time = 300, count = 10, keyField = "phone", msg = "5分钟内,验证码最多发送10次")
  public Response limitByPhone(UserPhoneCaptchaRateParam param) {
      Map<String, Object> dataMap = new HashMap<>();
      dataMap.put("date", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
      if (COUNT_PHONE_MAP.containsKey(param.getPhone())) {
          COUNT_PHONE_MAP.get(param.getPhone()).incrementAndGet();
      } else {
          COUNT_PHONE_MAP.put(param.getPhone(), new AtomicInteger(1));
      }
      dataMap.put("times", COUNT_PHONE_MAP.get(param.getPhone()).intValue());
      dataMap.put("reqParam", param);
      return Response.success(dataMap);
  }

在这里插入图片描述

3.2.2场景二:根据订单ID限流

入参类


@Data
public class OrderRateParam implements Serializable {

    private static final long serialVersionUID = -1L;

    private String orderId;
    //省略 get\set
}

  private static final Map<String, AtomicInteger> COUNT_ORDER_MAP = new HashMap<>();

  
  @RequestMapping("/limitByOrderId")
  @RateLimit(key = "limit-order-key", time = 300, count = 10, keyField = "orderId", msg = "订单飞走了,请稍后再试!")
  public Response limitByOrderId(OrderRateParam param) {
      Map<String, Object> dataMap = new HashMap<>();
      dataMap.put("date", DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
      if (COUNT_ORDER_MAP.containsKey(param.getOrderId())) {
          COUNT_ORDER_MAP.get(param.getOrderId()).incrementAndGet();
      } else {
          COUNT_ORDER_MAP.put(param.getOrderId(), new AtomicInteger(1));
      }
      dataMap.put("times", COUNT_ORDER_MAP.get(param.getOrderId()).intValue());
      dataMap.put("reqParam", param);
      return Response.success(dataMap);
  }

在这里插入图片描述

4.其它扩展

根据ip限流

在key中拼接IP即可;

5.源码地址

传送门

到此这篇关于Redis分布式限流组件设计与使用实例的文章就介绍到这了,更多相关Redis分布式限流内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯