How to implement SpringBoot+Redis+Lua distributed current limiting
The main advantages of Redis supporting LUA scripts
The integration of LUA scripts will create more usage scenarios for the Redis database and bring out more new advantages:
Efficiency: Reduce network overhead and delay. Multiple redis server network request operations can be completed with one request using LUA script
Data reliability: Redis will treat the entire script as a whole Execution, no other commands will be inserted in the middle.
Reusability: After the LUA script is executed, it will be permanently stored on the Redis server, and other clients can directly reuse it
Embeddability : Can be embedded in JAVA, C# and other programming languages, supports cross-platform interaction with different operating systems
Simple and powerful: small and lightweight, low resource usage, supports procedural and object-based programming Language
This is also the first time I use the language lua at work. Record it
Create the Lua file req_ratelimit.lua
local key = KEYS[1] --限流KEY local limitCount = tonumber(ARGV[1]) --限流大小 local limitTime = tonumber(ARGV[2]) --限流时间 local current = redis.call('get', key); if current then if current + 1 > limitCount then --如果超出限流大小 return 0 else redis.call("INCRBY", key,"1") return current + 1 end else redis.call("set", key,"1") redis.call("expire", key,limitTime) return 1 end
Custom annotation RateLimiter
package com.shinedata.ann; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { /** * 限流唯一标识 * @return */ String key() default "rate.limit:"; /** * 限流时间 * @return */ int time() default 1; /** * 限流次数 * @return */ int count() default 100; /** *是否限制IP,默认 否 * @return */ boolean restrictionsIp() default false; }
Definition aspect RateLimiterAspect
package com.shinedata.aop; import com.shinedata.ann.RateLimiter; import com.shinedata.config.redis.RedisUtils; import com.shinedata.exception.RateLimiterException; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; /** * @ClassName RateLimiterAspect * @Author yupanpan * @Date 2020/5/6 13:46 */ @Aspect @Component public class RateLimiterAspect { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private static ThreadLocal<String> ipThreadLocal=new ThreadLocal(); private DefaultRedisScript<Number> redisScript; @PostConstruct public void init(){ redisScript = new DefaultRedisScript<Number>(); redisScript.setResultType(Number.class); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/req_ratelimit.lua"))); } @Around("@annotation(com.shinedata.ann.RateLimiter)") public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable { try { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); RateLimiter rateLimit = method.getAnnotation(RateLimiter.class); if (rateLimit != null) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); boolean restrictionsIp = rateLimit.restrictionsIp(); if(restrictionsIp){ ipThreadLocal.set(getIpAddr(request)); } StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(rateLimit.key()); if(StringUtils.isNotBlank(ipThreadLocal.get())){ stringBuffer.append(ipThreadLocal.get()).append("-"); } stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName()); List<String> keys = Collections.singletonList(stringBuffer.toString()); Number number = RedisUtils.execute(redisScript, keys, rateLimit.count(), rateLimit.time()); if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) { logger.info("限流时间段内访问第:{} 次", number.toString()); return joinPoint.proceed(); }else { logger.error("已经到设置限流次数,当前次数:{}",number.toString()); throw new RateLimiterException("服务器繁忙,请稍后再试"); } } else { return joinPoint.proceed(); } }finally { ipThreadLocal.remove(); } } public static String getIpAddr(HttpServletRequest request) { String ipAddress = null; try { ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); } // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()= 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } } catch (Exception e) { ipAddress = ""; } return ipAddress; } }
Spring data redis provides DefaultRedisScript to interact with lua and redis. There are many articles on the Internet for specific details, here ThreadLocal is used because the IP is variable, ensuring that the IP of your own thread will not be modified by other threads. Remember to clean up ThreadLocal at the end to prevent memory leaks
RedisUtils tool class (too many methods, only execute is shown Method)
package com.shinedata.config.redis; import org.checkerframework.checker.units.qual.K; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.PostConstruct; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @ClassName RedisUtils * @Author yupanpan * @Date 2019/11/20 13:38 */ @Component public class RedisUtils { @Autowired @Qualifier("redisTemplate") private RedisTemplate<String, Object> redisTemplate; private static RedisUtils redisUtils; @PostConstruct public void init() { redisUtils = this; redisUtils.redisTemplate = this.redisTemplate; } public static Number execute(DefaultRedisScript<Number> script, List keys, Object... args) { return redisUtils.redisTemplate.execute(script, keys,args); } }
Self-configured RedisTemplate
package com.shinedata.config.redis; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import redis.clients.jedis.JedisPoolConfig; /** * @ClassName RedisConfig * @Author yupanpan * @Date 2019/11/20 13:26 */ @Configuration public class RedisConfig extends RedisProperties{ protected Logger log = LogManager.getLogger(RedisConfig.class); /** * JedisPoolConfig 连接池 * @return */ @Bean("jedisPoolConfig") public JedisPoolConfig jedisPoolConfig() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); // 最大空闲数 jedisPoolConfig.setMaxIdle(500); jedisPoolConfig.setMinIdle(100); // 连接池的最大数据库连接数 jedisPoolConfig.setMaxTotal(6000); // 最大建立连接等待时间 jedisPoolConfig.setMaxWaitMillis(5000); // 逐出连接的最小空闲时间 默认1800000毫秒(30分钟) jedisPoolConfig.setMinEvictableIdleTimeMillis(100); // 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 // jedisPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun); // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(600); // 是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个 jedisPoolConfig.setTestOnBorrow(true); // 在空闲时检查有效性, 默认false jedisPoolConfig.setTestWhileIdle(false); return jedisPoolConfig; } /** * JedisConnectionFactory * @param jedisPoolConfig */ @Bean("jedisConnectionFactory") public JedisConnectionFactory jedisConnectionFactory(@Qualifier("jedisPoolConfig")JedisPoolConfig jedisPoolConfig) { JedisConnectionFactory JedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig); // 连接池 JedisConnectionFactory.setPoolConfig(jedisPoolConfig); // IP地址 JedisConnectionFactory.setHostName(redisHost); // 端口号 JedisConnectionFactory.setPort(redisPort); // 如果Redis设置有密码 JedisConnectionFactory.setPassword(redisPassword); // 客户端超时时间单位是毫秒 JedisConnectionFactory.setTimeout(10000); return JedisConnectionFactory; } /** * 实例化 RedisTemplate 对象代替原有的RedisTemplate<String, String> * @return */ @Bean("redisTemplate") public RedisTemplate<String, Object> functionDomainRedisTemplate(@Qualifier("jedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); initDomainRedisTemplate(redisTemplate, redisConnectionFactory); return redisTemplate; } /** * 设置数据存入 redis 的序列化方式 * @param redisTemplate * @param factory */ private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) { // 如果不配置Serializer,那么存储的时候缺省使用String,比如如果用User类型存储,那么会提示错误User can't cast // to String! redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 开启事务/true必须手动释放连接,false会自动释放连接 如果调用方有用@Transactional做事务控制,可以开启事务,Spring会处理连接问题 redisTemplate.setEnableTransactionSupport(false); redisTemplate.setConnectionFactory(factory); } }
Global Controller exception handling GlobalExceptionHandler
package com.shinedata.exception; import com.fasterxml.jackson.databind.JsonMappingException; import com.shinedata.util.ResultData; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(value = RateLimiterException.class) @ResponseStatus(HttpStatus.OK) public ResultData runtimeExceptionHandler(RateLimiterException e) { logger.error("系统错误:", e); return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "处理失败"); } @ExceptionHandler(value = Exception.class) @ResponseStatus(HttpStatus.OK) public ResultData runtimeExceptionHandler(RuntimeException e) { Throwable cause = e.getCause(); logger.error("系统错误:", e); logger.error(e.getMessage()); if (cause instanceof JsonMappingException) { return ResultData.getResultError("参数错误"); } return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "处理失败"); } }
It’s very simple to use, just one annotation
Supplement: Optimized lua for
local key = KEYS[1] local limitCount = tonumber(ARGV[1]) local limitTime = tonumber(ARGV[2]) local current = redis.call('get', key); if current then redis.call("INCRBY", key,"1") return current + 1 else redis.call("set", key,"1") redis.call("expire", key,limitTime) return 1 end
The above is the detailed content of How to implement SpringBoot+Redis+Lua distributed current limiting. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

Redis cluster mode deploys Redis instances to multiple servers through sharding, improving scalability and availability. The construction steps are as follows: Create odd Redis instances with different ports; Create 3 sentinel instances, monitor Redis instances and failover; configure sentinel configuration files, add monitoring Redis instance information and failover settings; configure Redis instance configuration files, enable cluster mode and specify the cluster information file path; create nodes.conf file, containing information of each Redis instance; start the cluster, execute the create command to create a cluster and specify the number of replicas; log in to the cluster to execute the CLUSTER INFO command to verify the cluster status; make

How to clear Redis data: Use the FLUSHALL command to clear all key values. Use the FLUSHDB command to clear the key value of the currently selected database. Use SELECT to switch databases, and then use FLUSHDB to clear multiple databases. Use the DEL command to delete a specific key. Use the redis-cli tool to clear the data.

To read a queue from Redis, you need to get the queue name, read the elements using the LPOP command, and process the empty queue. The specific steps are as follows: Get the queue name: name it with the prefix of "queue:" such as "queue:my-queue". Use the LPOP command: Eject the element from the head of the queue and return its value, such as LPOP queue:my-queue. Processing empty queues: If the queue is empty, LPOP returns nil, and you can check whether the queue exists before reading the element.

Using the Redis directive requires the following steps: Open the Redis client. Enter the command (verb key value). Provides the required parameters (varies from instruction to instruction). Press Enter to execute the command. Redis returns a response indicating the result of the operation (usually OK or -ERR).

On CentOS systems, you can limit the execution time of Lua scripts by modifying Redis configuration files or using Redis commands to prevent malicious scripts from consuming too much resources. Method 1: Modify the Redis configuration file and locate the Redis configuration file: The Redis configuration file is usually located in /etc/redis/redis.conf. Edit configuration file: Open the configuration file using a text editor (such as vi or nano): sudovi/etc/redis/redis.conf Set the Lua script execution time limit: Add or modify the following lines in the configuration file to set the maximum execution time of the Lua script (unit: milliseconds)

Using Redis to lock operations requires obtaining the lock through the SETNX command, and then using the EXPIRE command to set the expiration time. The specific steps are: (1) Use the SETNX command to try to set a key-value pair; (2) Use the EXPIRE command to set the expiration time for the lock; (3) Use the DEL command to delete the lock when the lock is no longer needed.

Use the Redis command line tool (redis-cli) to manage and operate Redis through the following steps: Connect to the server, specify the address and port. Send commands to the server using the command name and parameters. Use the HELP command to view help information for a specific command. Use the QUIT command to exit the command line tool.

There are two types of Redis data expiration strategies: periodic deletion: periodic scan to delete the expired key, which can be set through expired-time-cap-remove-count and expired-time-cap-remove-delay parameters. Lazy Deletion: Check for deletion expired keys only when keys are read or written. They can be set through lazyfree-lazy-eviction, lazyfree-lazy-expire, lazyfree-lazy-user-del parameters.
