|
|
@@ -0,0 +1,236 @@
|
|
|
+package shop.alien.gateway.config;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
|
|
+import com.fasterxml.jackson.annotation.PropertyAccessor;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.context.annotation.Bean;
|
|
|
+import org.springframework.context.annotation.Configuration;
|
|
|
+import org.springframework.data.redis.connection.RedisClusterConfiguration;
|
|
|
+import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
|
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
|
|
+import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
|
|
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
+import org.springframework.data.redis.core.script.DefaultRedisScript;
|
|
|
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
|
|
+import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|
|
+
|
|
|
+import java.time.Duration;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Redis配置类
|
|
|
+ * 支持集群和单机模式,配置Lua脚本
|
|
|
+ *
|
|
|
+ * @author ssk
|
|
|
+ * @version 1.0
|
|
|
+ * @date 2025/01/21
|
|
|
+ */
|
|
|
+@Configuration
|
|
|
+public class RedisConfig {
|
|
|
+
|
|
|
+ @Value("${spring.redis.host:localhost}")
|
|
|
+ private String redisHost;
|
|
|
+
|
|
|
+ @Value("${spring.redis.port:6379}")
|
|
|
+ private int redisPort;
|
|
|
+
|
|
|
+ @Value("${spring.redis.password:}")
|
|
|
+ private String redisPassword;
|
|
|
+
|
|
|
+ @Value("${spring.redis.database:0}")
|
|
|
+ private int redisDatabase;
|
|
|
+
|
|
|
+ @Value("${spring.redis.cluster.nodes:}")
|
|
|
+ private String clusterNodes;
|
|
|
+
|
|
|
+ @Value("${spring.redis.cluster.enabled:false}")
|
|
|
+ private boolean clusterEnabled;
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ public RedisConnectionFactory redisConnectionFactory() {
|
|
|
+ LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
|
|
|
+ .commandTimeout(Duration.ofMillis(100))
|
|
|
+ .shutdownTimeout(Duration.ofSeconds(2))
|
|
|
+ .build();
|
|
|
+
|
|
|
+ if (clusterEnabled) {
|
|
|
+ // 集群模式
|
|
|
+ List<String> nodes = Arrays.asList(clusterNodes.split(","));
|
|
|
+ RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(nodes);
|
|
|
+ if (!redisPassword.isEmpty()) {
|
|
|
+ clusterConfig.setPassword(redisPassword);
|
|
|
+ }
|
|
|
+ return new LettuceConnectionFactory(clusterConfig, clientConfig);
|
|
|
+ } else {
|
|
|
+ // 单机模式
|
|
|
+ RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration();
|
|
|
+ standaloneConfig.setHostName(redisHost);
|
|
|
+ standaloneConfig.setPort(redisPort);
|
|
|
+ standaloneConfig.setDatabase(redisDatabase);
|
|
|
+ if (!redisPassword.isEmpty()) {
|
|
|
+ standaloneConfig.setPassword(redisPassword);
|
|
|
+ }
|
|
|
+ return new LettuceConnectionFactory(standaloneConfig, clientConfig);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
|
|
+ RedisTemplate<String, Object> template = new RedisTemplate<>();
|
|
|
+ template.setConnectionFactory(connectionFactory);
|
|
|
+
|
|
|
+ // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
|
|
|
+ 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);
|
|
|
+
|
|
|
+ StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
|
|
+ // key采用String的序列化方式
|
|
|
+ template.setKeySerializer(stringRedisSerializer);
|
|
|
+ // hash的key也采用String的序列化方式
|
|
|
+ template.setHashKeySerializer(stringRedisSerializer);
|
|
|
+ // value序列化方式采用jackson
|
|
|
+ template.setValueSerializer(jackson2JsonRedisSerializer);
|
|
|
+ // hash的value序列化方式采用jackson
|
|
|
+ template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
|
|
+ template.afterPropertiesSet();
|
|
|
+
|
|
|
+ return template;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
|
|
+ StringRedisTemplate template = new StringRedisTemplate();
|
|
|
+ template.setConnectionFactory(connectionFactory);
|
|
|
+ return template;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 全局限流Lua脚本
|
|
|
+ */
|
|
|
+ @Bean
|
|
|
+ public DefaultRedisScript<Long> globalRateLimitScript() {
|
|
|
+ DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
|
|
+ script.setScriptText(
|
|
|
+ "local key = KEYS[1]\n" +
|
|
|
+ "local limit = tonumber(ARGV[1])\n" +
|
|
|
+ "local window = tonumber(ARGV[2])\n" +
|
|
|
+ "local current = tonumber(redis.call('get', key) or \"0\")\n" +
|
|
|
+ "if current + 1 > limit then\n" +
|
|
|
+ " return 0\n" +
|
|
|
+ "else\n" +
|
|
|
+ " redis.call('incr', key)\n" +
|
|
|
+ " redis.call('expire', key, window)\n" +
|
|
|
+ " return 1\n" +
|
|
|
+ "end"
|
|
|
+ );
|
|
|
+ script.setResultType(Long.class);
|
|
|
+ return script;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * IP限流Lua脚本
|
|
|
+ */
|
|
|
+ @Bean
|
|
|
+ public DefaultRedisScript<Long> ipRateLimitScript() {
|
|
|
+ DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
|
|
+ script.setScriptText(
|
|
|
+ "local key = KEYS[1]\n" +
|
|
|
+ "local limit = tonumber(ARGV[1])\n" +
|
|
|
+ "local window = tonumber(ARGV[2])\n" +
|
|
|
+ "local current = tonumber(redis.call('get', key) or \"0\")\n" +
|
|
|
+ "if current + 1 > limit then\n" +
|
|
|
+ " return 0\n" +
|
|
|
+ "else\n" +
|
|
|
+ " redis.call('incr', key)\n" +
|
|
|
+ " redis.call('expire', key, window)\n" +
|
|
|
+ " return 1\n" +
|
|
|
+ "end"
|
|
|
+ );
|
|
|
+ script.setResultType(Long.class);
|
|
|
+ return script;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 用户限流Lua脚本
|
|
|
+ */
|
|
|
+ @Bean
|
|
|
+ public DefaultRedisScript<Long> userRateLimitScript() {
|
|
|
+ DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
|
|
+ script.setScriptText(
|
|
|
+ "local key = KEYS[1]\n" +
|
|
|
+ "local limit = tonumber(ARGV[1])\n" +
|
|
|
+ "local window = tonumber(ARGV[2])\n" +
|
|
|
+ "local current = tonumber(redis.call('get', key) or \"0\")\n" +
|
|
|
+ "if current + 1 > limit then\n" +
|
|
|
+ " return 0\n" +
|
|
|
+ "else\n" +
|
|
|
+ " redis.call('incr', key)\n" +
|
|
|
+ " redis.call('expire', key, window)\n" +
|
|
|
+ " return 1\n" +
|
|
|
+ "end"
|
|
|
+ );
|
|
|
+ script.setResultType(Long.class);
|
|
|
+ return script;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 接口限流Lua脚本
|
|
|
+ */
|
|
|
+ @Bean
|
|
|
+ public DefaultRedisScript<Long> apiRateLimitScript() {
|
|
|
+ DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
|
|
+ script.setScriptText(
|
|
|
+ "local key = KEYS[1]\n" +
|
|
|
+ "local limit = tonumber(ARGV[1])\n" +
|
|
|
+ "local window = tonumber(ARGV[2])\n" +
|
|
|
+ "local current = tonumber(redis.call('get', key) or \"0\")\n" +
|
|
|
+ "if current + 1 > limit then\n" +
|
|
|
+ " return 0\n" +
|
|
|
+ "else\n" +
|
|
|
+ " redis.call('incr', key)\n" +
|
|
|
+ " redis.call('expire', key, window)\n" +
|
|
|
+ " return 1\n" +
|
|
|
+ "end"
|
|
|
+ );
|
|
|
+ script.setResultType(Long.class);
|
|
|
+ return script;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 综合限流Lua脚本(支持多种限流策略组合)
|
|
|
+ */
|
|
|
+ @Bean
|
|
|
+ public DefaultRedisScript<List> combinedRateLimitScript() {
|
|
|
+ DefaultRedisScript<List> script = new DefaultRedisScript<>();
|
|
|
+ script.setScriptText(
|
|
|
+ "local results = {}\n" +
|
|
|
+ "for i = 1, #KEYS do\n" +
|
|
|
+ " if KEYS[i] ~= \"\" then\n" +
|
|
|
+ " local key = KEYS[i]\n" +
|
|
|
+ " local limit = tonumber(ARGV[i])\n" +
|
|
|
+ " local window = tonumber(ARGV[#ARGV])\n" +
|
|
|
+ " local current = tonumber(redis.call('get', key) or \"0\")\n" +
|
|
|
+ " if current + 1 > limit then\n" +
|
|
|
+ " results[i] = 0\n" +
|
|
|
+ " else\n" +
|
|
|
+ " redis.call('incr', key)\n" +
|
|
|
+ " redis.call('expire', key, window)\n" +
|
|
|
+ " results[i] = 1\n" +
|
|
|
+ " end\n" +
|
|
|
+ " else\n" +
|
|
|
+ " results[i] = 1\n" +
|
|
|
+ " end\n" +
|
|
|
+ "end\n" +
|
|
|
+ "return results"
|
|
|
+ );
|
|
|
+ script.setResultType(List.class);
|
|
|
+ return script;
|
|
|
+ }
|
|
|
+}
|