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>
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
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 }
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; }
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.
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; } }
Malah, tiada apa yang boleh dikatakan tentang perkara ini Kami menggunakan kaedah siri jackson lalai dalam Spring Boot untuk menyelesaikan kunci dan nilai.
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('get', key) if current and tonumber(current) > count then return tonumber(current) end current = redis.call('incr', key) if tonumber(current) == 1 then redis.call('expire', key, time) end return tonumber(current)
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; }
Baiklah, skrip Lua kami kini sedia.
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("限制请求'{}',当前请求'{}',缓存key'{}'", 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(); } }
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(); } }
每一个 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; } }
我将这句话重写成如下: 这个 demo 很小,所以我没有定义实体类,而是直接使用 Map 来返回 JSON。 最后我们看看过载时的测试效果:
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!