Rumah > pangkalan data > Redis > Cara menggunakan SpringBoot + Redis untuk melaksanakan pengehadan semasa antara muka

Cara menggunakan SpringBoot + Redis untuk melaksanakan pengehadan semasa antara muka

PHPz
Lepaskan: 2023-05-27 15:01:19
ke hadapan
1652 orang telah melayarinya

Konfigurasi

Mula-mula kami mencipta projek Spring Boot, memperkenalkan kebergantungan Web dan Redis, dan menganggap bahawa pengehadan semasa antara muka biasanya ditanda melalui anotasi dan anotasi dihuraikan melalui AOP, jadi kami juga memerlukan Termasuk kebergantungan AOP , kebergantungan akhir adalah seperti berikut:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Salin selepas log masuk

Kemudian sediakan contoh Redis terlebih dahulu Selepas projek kami dikonfigurasikan, kami boleh mengkonfigurasi secara langsung maklumat asas Redis, seperti berikut:

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123
Salin selepas log masuk

Semasa. mengehadkan anotasi

Seterusnya kami mencipta anotasi mengehadkan semasa Kami membahagikan pengehadan semasa kepada dua situasi:

  • Penghadan arus global untuk antara muka semasa , contohnya, antara muka. boleh diakses 100 kali dalam 1 minit.

  • Had kadar untuk alamat IP tertentu, contohnya, alamat IP boleh diakses 100 kali dalam 1 minit.

Untuk kedua-dua situasi ini, kami mencipta kelas penghitungan:

public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}
Salin selepas log masuk

Seterusnya kami mencipta anotasi mengehadkan semasa:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key
     */
    String key() default "rate_limit:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
}
Salin selepas log masuk

Pertama A parameter semasa- kunci had. Ini hanyalah awalan Pada masa hadapan, kunci lengkap akan menjadi awalan ini ditambah laluan lengkap kaedah antara muka, yang bersama-sama membentuk kunci had semasa.

Tiga parameter lain mudah difahami, jadi saya tidak akan memberitahu lebih lanjut.

Baiklah, jika antara muka perlu dihadkan pada masa hadapan, cuma tambahkan anotasi @RateLimiter pada antara muka itu, kemudian konfigurasikan parameter yang berkaitan.

Customized RedisTemplate

Dalam Spring Boot, kami sebenarnya lebih terbiasa menggunakan Spring Data Redis untuk mengendalikan Redis, tetapi RedisTemplate lalai mempunyai masalah kecil, iaitu JdkSerializationRedisSerializer digunakan untuk bersiri. Saya tidak tahu Kawan, pernahkah anda perasan bahawa kunci dan nilai yang disimpan ke Redis menggunakan alat bersiri ini entah bagaimana akan mempunyai lebih banyak awalan, yang mungkin menyebabkan ralat apabila anda membacanya dengan arahan.

Contohnya, semasa menyimpan, kuncinya ialah nama dan nilainya adalah ujian, tetapi apabila anda beroperasi pada baris arahan, get name tidak boleh mendapatkan data yang anda mahukan. Sebabnya ia disimpan ke redis sebelum nama. Terdapat beberapa aksara lagi, jadi anda hanya boleh terus menggunakan RedisTemplate untuk membacanya.

Apabila kami menggunakan Redis untuk pengehadan semasa, kami akan menggunakan skrip Lua Apabila menggunakan skrip Lua, situasi yang dinyatakan di atas akan berlaku, jadi kami perlu mengubah suai skema bersiri RedisTemplate.

Sesetengah rakan mungkin bertanya mengapa tidak menggunakan StringRedisTemplate? StringRedisTemplate tidak mempunyai masalah yang dinyatakan di atas, tetapi jenis data yang boleh disimpannya tidak cukup kaya, jadi ia tidak dipertimbangkan di sini.

Ubah suai skema siri RedisTemplate, kodnya adalah seperti berikut:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}
Salin selepas log masuk

Malah, tiada apa yang boleh dikatakan tentang perkara ini Kami menggunakan kaedah siri jackson lalai dalam Spring Boot untuk menyelesaikan kunci dan nilai.

Skrip Lua

Sebenarnya, saya menyebut perkara ini dalam video vhr sebelumnya. Kami boleh menggunakan skrip Lua untuk melaksanakan beberapa operasi atom dalam Redis Untuk memanggil skrip Lua, kami mempunyai Dua idea yang berbeza.

  • Tentukan skrip Lua pada pelayan Redis, dan kemudian hitung nilai cincang Dalam kod Java, gunakan nilai cincang ini untuk mengunci skrip Lua untuk dilaksanakan.

  • Tentukan skrip Lua secara langsung dalam kod Java, dan kemudian hantar ke pelayan Redis untuk dilaksanakan.

Spring Data Redis juga menyediakan antara muka untuk mengendalikan skrip Lua, yang agak mudah, jadi kami akan menggunakan pilihan kedua di sini.

Kami mencipta folder lua baharu dalam direktori sumber khusus untuk menyimpan skrip lua Kandungan skrip adalah seperti berikut:

local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call(&#39;get&#39;, key)
if current and tonumber(current) > count then
    return tonumber(current)
end
current = redis.call(&#39;incr&#39;, key)
if tonumber(current) == 1 then
    redis.call(&#39;expire&#39;, key, time)
end
return tonumber(current)
Salin selepas log masuk

Skrip ini sebenarnya tidak sukar ia digunakan untuk sekilas pandang. KEYS dan ARGV adalah kedua-dua parameter yang dihantar semasa membuat panggilan untuk menukar redis.panggilan adalah untuk melaksanakan arahan redis khusus adalah seperti berikut:

  • . Pertama, dapatkan kunci masuk dan kiraan had semasa dan masa.

  • Dapatkan nilai yang sepadan dengan kunci ini melalui get. Nilai ini ialah bilangan kali antara muka ini boleh diakses dalam tetingkap masa semasa.

  • Sekiranya lawatan pertama, keputusan yang diperolehi pada masa ini adalah nil Jika tidak, keputusan yang diperolehi adalah nombor, jadi langkah seterusnya adalah menilai sama ada keputusan yang diperolehi Nombor, dan nombor ini lebih besar daripada kiraan, ini bermakna had trafik telah melebihi, maka hasil pertanyaan boleh dikembalikan secara langsung.

  • Jika hasil yang diperolehi adalah sifar, ini bermakna ia adalah akses pertama Pada masa ini, kunci semasa akan dinaikkan sebanyak 1, dan kemudian masa tamat tempoh akan ditetapkan.

  • Akhir sekali, hanya kembalikan nilai yang dinaikkan sebanyak 1.

Sebenarnya skrip Lua ini mudah difahami.

Seterusnya kami memuatkan skrip Lua ini dalam Bean, seperti berikut:

@Bean
public DefaultRedisScript<Long> limitScript() {
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));
    redisScript.setResultType(Long.class);
    return redisScript;
}
Salin selepas log masuk

Baiklah, skrip Lua kami kini sedia.

Analisis anotasi

Seterusnya kita perlu menyesuaikan aspek untuk menghuraikan anotasi ini Mari kita lihat definisi aspek:

@Aspect
@Component
public class RateLimiterAspect {
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    private RedisScript<Long> limitScript;
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (number==null || number.intValue() > count) {
                throw new ServiceException("访问过于频繁,请稍候再试");
            }
            log.info("限制请求&#39;{}&#39;,当前请求&#39;{}&#39;,缓存key&#39;{}&#39;", count, number.intValue(), key);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }
}
Salin selepas log masuk

Aspek ini adalah untuk memintas semua. tambahan Kaedah @RateLimiter anotasi ditambah dan anotasi diproses dalam pra-pemberitahuan.

  • 首先获取到注解中的 key、time 以及 count 三个参数。

  • 获取一个组合的 key,所谓的组合的 key,就是在注解的 key 属性基础上,再加上方法的完整路径,如果是 IP 模式的话,就再加上 IP 地址。以 IP 模式为例,最终生成的 key 类似这样:rate_limit:127.0.0.1-org.javaboy.ratelimiter.controller.HelloController-hello(如果不是 IP 模式,那么生成的 key 中就不包含 IP 地址)。

  • 将生成的 key 放到集合中。

  • 通过 redisTemplate.execute 方法取执行一个 Lua 脚本,第一个参数是脚本所封装的对象,第二个参数是 key,对应了脚本中的 KEYS,后面是可变长度的参数,对应了脚本中的 ARGV。

  • 判断 Lua 脚本执行后的结果是否超过 count,若超过则视为过载,抛出异常处理即可。

接口测试

接下来我们就进行接口的一个简单测试,如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    @RateLimiter(time = 5,count = 3,limitType = LimitType.IP)
    public String hello() {
        return "hello>>>"+new Date();
    }
}
Salin selepas log masuk

每一个 IP 地址,在 5 秒内只能访问 3 次。

这个自己手动刷新浏览器都能测试出来。

全局异常处理

由于过载的时候是抛异常出来,所以我们还需要一个全局异常处理器,如下:

@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(ServiceException.class)
    public Map<String,Object> serviceException(ServiceException e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", 500);
        map.put("message", e.getMessage());
        return map;
    }
}
Salin selepas log masuk

我将这句话重写成如下: 这个 demo 很小,所以我没有定义实体类,而是直接使用 Map 来返回 JSON。 最后我们看看过载时的测试效果:

Cara menggunakan SpringBoot + Redis untuk melaksanakan pengehadan semasa antara muka

Atas ialah kandungan terperinci Cara menggunakan SpringBoot + Redis untuk melaksanakan pengehadan semasa antara muka. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:yisu.com
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan