springboot中使用lua脚本+aop作限流访问案例代码
文章目錄
- 1.限流注解
- 2.redis配置
- 3.aop配置
- 4.controller層測試
- 拓展:Atomic類的學習
- lua腳本學習
1.限流注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Limit {// 資源名稱,用于描述接口功能String name() default "";// 資源 keyString key() default "";// key 前綴String prefix() default "";// 時間,單位秒int period();// 限制訪問次數int count();// 限制類型LimitType limitType() default LimitType.CUSTOMER;}2.redis配置
spring:redis:#數據庫索引database: 0host: ....port: 6379password:jedis:pool:max-active: 8 # 連接池最大連接數(使用負值表示沒有限制)max-wait: -1ms # 連接池最大阻塞等待時間(使用負值表示沒有限制)max-idle: 8 # 連接池中的最大空閑連接min-idle: 0 # 連接池中的最小空閑連接#連接超時時間timeout: 5000 @Slf4j @Configuration @EnableCaching @ConditionalOnClass(RedisOperations.class) //Spring工程中引用了redis相關的包 才會構建這個bean @EnableConfigurationProperties(RedisProperties.class) public class RedisConfig extends CachingConfigurerSupport {/*** 設置 redis 數據默認過期時間,默認2小時* 設置@cacheable 序列化方式*/@Beanpublic RedisCacheConfiguration redisCacheConfiguration() {FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(2));return configuration;}@SuppressWarnings("all")@Bean(name = "redisTemplate")@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();//序列化FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);// value值的序列化采用fastJsonRedisSerializertemplate.setValueSerializer(fastJsonRedisSerializer);template.setHashValueSerializer(fastJsonRedisSerializer);// 全局開啟AutoType,這里方便開發,使用全局的方式ParserConfig.getGlobalInstance().setAutoTypeSupport(true);// 建議使用這種方式,小范圍指定白名單// ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain");// key的序列化采用StringRedisSerializertemplate.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());template.setConnectionFactory(redisConnectionFactory);return template;}/*** 自定義緩存key生成策略,默認將使用該策略*/@Bean@Overridepublic KeyGenerator keyGenerator() {return (target, method, params) -> {Map<String, Object> container = new HashMap<>(3);Class<?> targetClassClass = target.getClass();// 類地址container.put("class", targetClassClass.toGenericString());// 方法名稱container.put("methodName", method.getName());// 包名稱container.put("package", targetClassClass.getPackage());// 參數列表for (int i = 0; i < params.length; i++) {container.put(String.valueOf(i), params[i]);}// 轉為JSON字符串String jsonString = JSON.toJSONString(container);// 做SHA256 Hash計算,得到一個SHA256摘要作為Keyreturn DigestUtils.sha256Hex(jsonString);};}@Bean@Overridepublic CacheErrorHandler errorHandler() {// 異常處理,當Redis發生異常時,打印日志,但是程序正常走log.info("初始化 -> [{}]", "Redis CacheErrorHandler");return new CacheErrorHandler() {@Overridepublic void handleCacheGetError(RuntimeException e, Cache cache, Object key) {log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);}@Overridepublic void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);}@Overridepublic void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);}@Overridepublic void handleCacheClearError(RuntimeException e, Cache cache) {log.error("Redis occur handleCacheClearError:", e);}};}}/*** Value 序列化** @author /* @param <T>*/ class FastJsonRedisSerializer<T> implements RedisSerializer<T> {private final Class<T> clazz;FastJsonRedisSerializer(Class<T> clazz) {super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) {if (t == null) {return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8);}@Overridepublic T deserialize(byte[] bytes) {if (bytes == null || bytes.length <= 0) {return null;}String str = new String(bytes, StandardCharsets.UTF_8);return JSON.parseObject(str, clazz);}}/*** 重寫序列化器** @author /*/ class StringRedisSerializer implements RedisSerializer<Object> {private final Charset charset;StringRedisSerializer() {this(StandardCharsets.UTF_8);}private StringRedisSerializer(Charset charset) {Assert.notNull(charset, "Charset must not be null!");this.charset = charset;}@Overridepublic String deserialize(byte[] bytes) {return (bytes == null ? null : new String(bytes, charset));}@Overridepublic byte[] serialize(Object object) {String string = JSON.toJSONString(object);if (StringUtils.isBlank(string)) {return null;}string = string.replace("\"", "");return string.getBytes(charset);} }3.aop配置
@Aspect @Component public class LimitAspect {private final RedisTemplate<Object, Object> redisTemplate;private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);public LimitAspect(RedisTemplate<Object, Object> redisTemplate) {this.redisTemplate = redisTemplate;}@Pointcut("@annotation(co.yixiang.annotation.Limit)")public void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {HttpServletRequest request = RequestHolder.getHttpServletRequest();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method signatureMethod = signature.getMethod();Limit limit = signatureMethod.getAnnotation(Limit.class);LimitType limitType = limit.limitType();String key = limit.key();if (StringUtils.isEmpty(key)) {if (limitType == LimitType.IP) {key = StringUtils.getIp(request);} else {key = signatureMethod.getName();}} //ImmutableList是一個不可變、線程安全的列表集合,它只會獲取傳入對象的一個副本,而不會影響到原來的變量或者對象// //獲取一個有兩個元素的不可變集合對象// ImmutableList<String> list3 = ImmutableList .<String>of("12","23");// // limit.prefix():key prefixImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(), "_", key, "_", request.getRequestURI().replaceAll("/", "_")));String luaScript = buildLuaScript();RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());if (null != count && count.intValue() <= limit.count()) {logger.info("第{}次訪問key為 {},描述為 [{}] 的接口", count, keys, limit.name());//count, keys, limit.name()為參數return joinPoint.proceed();} else {throw new BadRequestException("訪問次數受限制");}}/*** 限流腳本*/private String buildLuaScript() {return "local c" +"\nc = redis.call('get',KEYS[1])" +"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +"\nreturn c;" +"\nend" +"\nc = redis.call('incr',KEYS[1])" +"\nif tonumber(c) == 1 then" +"\nredis.call('expire',KEYS[1],ARGV[2])" +"\nend" +"\nreturn c;";} }4.controller層測試
/*** @author /* 接口限流測試類*/ @RestController @RequestMapping("/api/limit") @Api(tags = "系統:限流測試管理") public class LimitController {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();/*** 測試限流注解,下面配置說明該接口 60秒內最多只能訪問 10次,保存到redis的鍵名為 limit_test,*/@GetMapping@AnonymousAccess@ApiOperation("測試")@Limit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")public int testLimit() {return ATOMIC_INTEGER.incrementAndGet();} }拓展:Atomic類的學習
JUC包提供了一系列的原子性操作類,這些類都是使用非阻塞算法CAS實現的,相比使用鎖實現原子性操作這在性能上有很大提高。
JUC并發包中包含有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作類,它們的原理類似。AtomicLong是原子性遞增或者遞減類,其內部使用Unsafe來實現,我們看下面的代碼。
public class AtomicLong extends Number implements java.io.Serializable {private static final long serialVersionUID = 1927816293512124184L;//判斷jvm是否支持long類型無鎖CASstatic final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();/*** Returns whether underlying JVM supports lockless CompareAndSet* for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.*/private static native boolean VMSupportsCS8();private static final Unsafe U = Unsafe.getUnsafe();private static final long VALUE= U.objectFieldOffset(AtomicLong.class, "value");private volatile long value;//實際變量值//value被聲明為volatile的,這是為了在多線程下保證內存可見性,value是具體存放計數的變量。....}注意:private static final Unsafe U = Unsafe.getUnsafe();為何能通過Unsafe.getUnsafe()方法獲取到Unsafe類的實例?其實這是因為AtomicLong類也是在rt.jar包下面的,AtomicLong類就是通過BootStarp類加載器進行加載的。
jdk8中的getAndAddLong方法:
可以看到,JDK 7的AtomicLong中的循環邏輯已經被JDK 8中的原子操作類UNsafe內置了,之所以內置應該是考慮到這個函數在其他地方也會用到,而內置可以提高復用性。
下面通過一個多線程使用AtomicLong統計0的個數的例子來加深對AtomicLong的理解。
輸出count0:7
如上代碼中的兩個線程各自統計自己所持數據中0的個數,每當找到一個0就會調用AtomicLong的原子性遞增方法。 在沒有原子類的情況下,實現計數器需要使用一定的同步措施,比如使用synchronized關鍵字等,但是這些都是阻塞算法,對性能有一定損耗,而這些原子操作類都使用CAS非阻塞算法,性能更好。但是在高并發情況下AtomicLong還會存在性能問題。實際上JDK 8提供了一個在高并發下性能更好的LongAdder類
lua腳本學習
關于lua腳本參考了《redis入門指南》
使用腳本的好處如下:
1.減少網絡開銷:本來5次網絡請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis服務器上完成。使用腳本,減少了網絡往返時延。
2.原子操作:Redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。
3.復用:客戶端發送的腳本會永久存儲在Redis中,意味著其他客戶端可以復用這一腳本而不需要使用代碼完成同樣的邏輯。
示例:
寫一個.lua腳本
在redis客戶端機器上,如何測試這個腳本呢?如下:
redis-cli --eval ratelimiting.lua rate.limitingl:127.0.0.1 , 10 3
–eval參數是告訴redis-cli讀取并運行后面的Lua腳本,ratelimiting.lua是腳本的位置,后面跟著是傳給Lua腳本的參數。其中",“前的rate.limiting:127.0.0.1是要操作的鍵,可以再腳本中用KEYS[1]獲取,”,“后面的10和3是參數,在腳本中能夠使用ARGV[1]和ARGV[2]獲得。注:”,"兩邊的空格不能省略,否則會出錯
結合腳本的內容可知這行命令的作用是將訪問頻率限制為每10秒最多3次,所以在終端中不斷的運行此命令會發現當訪問頻率在10秒內小于或等于3次時返回1,否則返回0。
總結
以上是生活随笔為你收集整理的springboot中使用lua脚本+aop作限流访问案例代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【学习笔记】redis一些配置文件参数详
- 下一篇: 【学习笔记】springboot的Aut