redis:使用AOP和lua实现分布式限流

!!! 这个是 原文链接

按照原文链接操作了下,发现最后有报错,原先以为是lua脚本的问题,后来查了半天是redisTemplate序列化的问题,记录一下

ERR Error running script (call to f_c79b3e806a8d1920de018f895a7c77b74651efe1): @user_script:3: user_script:3: attempt to compare number with nil

pom依赖

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.1-jre</version>
        </dependency>

注解

package com.example.demo.annotation;

import com.example.demo.enums.LimitType;

import java.lang.annotation.*;


@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit {
    // 资源名称
    String name() default "";
    // 资源key
    String key() default "";
    // 前缀
    String prefix() default "";
    // 时间
    int period();
    // 最多访问次数
    int count();
    // 类型
    LimitType limitType() default LimitType.CUSTOMER;
    // 提示信息
    String msg() default "系统繁忙,请稍后再试";
}

切面类

package com.example.demo.aspect;

import com.example.demo.annotation.RedisLimit;
import com.example.demo.enums.LimitType;
import com.example.demo.exception.LimitException;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;


@Slf4j
@Aspect
@Configuration
public class RedisLimitAspect {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    @Around("@annotation(com.example.demo.annotation.RedisLimit)")
    public Object around(ProceedingJoinPoint pjp) throws LimitException {
        MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
        Method method = methodSignature.getMethod();
        RedisLimit annotation = method.getAnnotation(RedisLimit.class);
        LimitType limitType = annotation.limitType();
        String name = annotation.name();
        String key;
        int period = annotation.period();
        int count = annotation.count();
        switch (limitType){
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                key = annotation.key();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(annotation.prefix(), key));
        try {
            String luaScript = buildLuaScript();
            DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number number = redisTemplate.execute(redisScript, keys, count, period);
            log.info("Access try count is {} for name = {} and key = {}", number, name, key);
            if(number != null && number.intValue() == 1){
                return pjp.proceed();
            }
            throw new LimitException(annotation.msg());
        }catch (Throwable e){
            if(e instanceof LimitException){
                log.debug("令牌桶={},获取令牌失败",key);
                throw new LimitException(e.getLocalizedMessage());
            }
            e.printStackTrace();
            throw new RuntimeException("服务器异常");
        }
    }
    public String buildLuaScript(){
        return "redis.replicate_commands(); local listLen,time" +
                "\nlistLen = redis.call('LLEN', KEYS[1])" +
                // 不超过最大值,则直接写入时间
                "\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then" +
                "\nlocal a = redis.call('TIME');" +
                "\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
                "\nelse" +
                // 取出现存的最早的那个时间,和当前时间比较,看是小于时间间隔
                "\ntime = redis.call('LINDEX', KEYS[1], -1)" +
                "\nlocal a = redis.call('TIME');" +
                "\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then" +
                // 访问频率超过了限制,返回0表示失败
                "\nreturn 0;" +
                "\nelse" +
                "\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
                "\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)" +
                "\nend" +
                "\nend" +
                "\nreturn 1;";
    }
    public String getIpAddress(){
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String ip = request.getHeader("x-forwarded-for");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ip = request.getHeader("Proxy-Client-IP");
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ip = request.getHeader("WL-Client-IP");
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

枚举类-ip还是自定义限流

package com.example.demo.enums;

public enum LimitType {

    CUSTOMER,IP;
}

异常类

package com.example.demo.exception;

public class LimitException extends Exception {

    public LimitException(String message) {
        super(message);
    }
}

controller接口

注意:注解要加在方法上,不然不生效

package com.example.demo.controller;

import com.example.demo.domain.Student;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

public interface DemoController {

    @GetMapping("/demo/test")
    String demoTest();
}

package com.example.demo.controller;

import com.alibaba.fastjson.JSON;
import com.example.demo.annotation.RedisLimit;
import com.example.demo.domain.Student;
import com.example.demo.service.DemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Slf4j
public class DemoControllerImpl implements DemoController {

    @Autowired
    private DemoService demoService;

    @Override
    @RedisLimit(key = "cachingTest", count = 1, period = 1, msg = "当前排队人数较多,请稍后再试!")
    public String demoTest() {
        log.info("this is a test log");
        try {
            Thread.sleep(3);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "test";
    }
}

问题

如果上述操作完后,启动运行报错, 要对redis添加序列化设置
在这里插入图片描述

package com.example.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;

@SpringBootConfiguration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(redisSerializer());
        //设置key序列化方式String
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置hash key序列化方式String
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //设置hash value序列化json
        redisTemplate.setHashValueSerializer(redisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    public RedisSerializer<Object> redisSerializer() {
        //创建JSON序列化器
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //必须设置,否则无法序列化实体类对象
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}

测试

使用apipost调用1次:
在这里插入图片描述

在这里插入图片描述

使用apipost并发调用3次:

在这里插入图片描述
在这里插入图片描述

一、本课题的目的和意义 本课题会开发一个计算机协会社团信息管理系统,从而实现计算机协会社团信息管理系统内部各种繁琐事务的管理。追求低碳生活,摆脱以前纸质化的办公模式,节约人力物力从而达到提高办事效率的目的。随着计算机技术的飞速发展,计算机在企业、高校等管理中应用的普及,利用计算机实现高效、智能的管理势在必行。对于知识创新的主体,高校教育来说,更应该着重实际,从身边做起,利用高科技解决实际问题,适应现代要求、推动管理走向科学化、规范化,走在科技的前沿。 二、课题的国内外开发动态 大学生活丰富多彩,校园中存在的很多的社团,为学生的全面发展提供了平台,同时也给他们一个展现自我的机会。随着社团的增加,入社成员的增加,社团管理工作的复杂性也随之增加,但现在国内大部分的高校还只是基于校园社联会这个平台来进行一小部分的社团信息管理,但是本系统能够对社团的信息进行系统化的整理与管理,能够大大的节省整理工作的时间成本还有提高工作效率。本系统运用Web页面进行前台信息展示,后台偏重于信息管理,两者相互结合,可以使学生在网上进行入社、退社,查询相关社团信息等操作;可以让社团负责人管理自己的社员,在线进行社团活动管理等;可以在线实现社团的管理等。一系列的无纸化操作,大大地节省了财力物力,使社团各种事项管理高效化。在此我对该系统进行了初步设计工作,希望它能够在校园的社团管理工作中发挥便捷、高效的作用,更好地推进各社团的发展。本文采用Servlet+Jsp+SQL Server 2005+PowerDesigner作为开发技术,以Java为编程语言开发一个基于Servlet的计算机协会社团信息管理系统。该系统可以对社团工作进行快速、高效的管理,为社团之间和社团会员之间提供一个良好的信息交流的平台,让社团成员可以展示自己的风采,及时了解社团的新动态,营造一个现代化的学习生活环境。 三、课题的基本内容 计算机协会社团信息管理系统作为一款管理计算机协会社团信息工作的系统,它将复杂的工作便捷化、高效化。本系统按功能分为以下几个模块: 1. 协会活动管理模块:实现协会活动信息的发布,修改,删除等功能。 2. 会员管理模块:实现对会员的添加和删除以及对会员信息的修改等功能。 3. 协会会费管理模块:实现对协会会费的使用情况以及对协会会费信息统计等功能。 4. 登录管理模块:实现对登录信息的管理的功能。 四、拟解决的主要问题 1. 用户的登录问题:不同的用户如(普通用户、各支社团管理员、系统管理员)所能实现的操作以及显示不同的操作界面的划分。 2. 界面开发:利用Dreamweaver开发界面,使用Photoshop进行图片处理,使界面看起来简洁、友好。 3. 数据表的设计问题:系统数据表之间必须保证一致性、完整性,且要做到表内冗余小,表间关系明确,才能进行有效的管理。 此外、利用SQL 2000建立好关系数据库和建好客户端和服务器之间的连接又是另一个难点。建立良好的数据库要从科学性、安全性、规范性、结构性等各个方面进行考虑。客户端和服务器之间的连接要配置好数据库服务器等。 五、课题设计的实现方案 1. 本系统开发语言的选择 本系统使用的开发语言是Java语言,Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言,Java 技术具有卓越的通用性、高效性、平台移植性和安全性,广泛应用于个人PC、数据中心、游戏控制台、科学超级计算机、移动电话和互联网,同时拥有全球最大的开发者专业社群。在全球云计算和移动互联网的产业环境下,Java更具备了显著优势和广阔前景。而对于信息管理系统来说,java能够实现前台和后台的信息交互,并能够对前台所提出的请求处理数据,因此在开发系统时我把它作为本系统开发语言。 2. 本系统开发工具的选择 本系统前台使用的开发工具是MyEclipse。MyEclipse企业级工作平台MyEclipse Enterprise Workbench ,简称MyEclipse. 是对EclipseIDE的扩展,利用它我们可以在数据库和JavaEE的开发、发布以及应用程序服务器的整合方面极大的提高工作效率。它是功能丰富的JavaEE集成开发环境,包括了完备的编码、调试、测试和发布功能,完整支持HTML, Struts, JSP, CSS, Javascript, Spring, SQL, Hibernate。 MyEclipse 是一个十分优秀的用于开发Java, J2EE的 Eclipse 插件集合,MyEclipse的功能非常强大,支持也十分广泛,尤其是对各种开源产品的支持十分不错。MyEclipse目前支持Java Servlet,AJAX, JSP, JSF, Struts,Spring, Hibernate,EJB3,JDBC数据库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值