日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

springboot中使用lua脚本+aop作限流访问案例代码

發(fā)布時間:2024/9/30 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 springboot中使用lua脚本+aop作限流访问案例代码 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

  • 1.限流注解
  • 2.redis配置
  • 3.aop配置
  • 4.controller層測試
    • 拓展:Atomic類的學(xué)習(xí)
    • lua腳本學(xué)習(xí)


1.限流注解

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Limit {// 資源名稱,用于描述接口功能String name() default "";// 資源 keyString key() default "";// key 前綴String prefix() default "";// 時間,單位秒int period();// 限制訪問次數(shù)int count();// 限制類型LimitType limitType() default LimitType.CUSTOMER;}

2.redis配置

spring:redis:#數(shù)據(jù)庫索引database: 0host: ....port: 6379password:jedis:pool:max-active: 8 # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制)max-wait: -1ms # 連接池最大阻塞等待時間(使用負(fù)值表示沒有限制)max-idle: 8 # 連接池中的最大空閑連接min-idle: 0 # 連接池中的最小空閑連接#連接超時時間timeout: 5000 @Slf4j @Configuration @EnableCaching @ConditionalOnClass(RedisOperations.class) //Spring工程中引用了redis相關(guān)的包 才會構(gòu)建這個bean @EnableConfigurationProperties(RedisProperties.class) public class RedisConfig extends CachingConfigurerSupport {/*** 設(shè)置 redis 數(shù)據(jù)默認(rèn)過期時間,默認(rèn)2小時* 設(shè)置@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,這里方便開發(fā),使用全局的方式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生成策略,默認(rèn)將使用該策略*/@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());// 參數(shù)列表for (int i = 0; i < params.length; i++) {container.put(String.valueOf(i), params[i]);}// 轉(zhuǎn)為JSON字符串String jsonString = JSON.toJSONString(container);// 做SHA256 Hash計算,得到一個SHA256摘要作為Keyreturn DigestUtils.sha256Hex(jsonString);};}@Bean@Overridepublic CacheErrorHandler errorHandler() {// 異常處理,當(dāng)Redis發(fā)生異常時,打印日志,但是程序正常走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()為參數(shù)return joinPoint.proceed();} else {throw new BadRequestException("訪問次數(shù)受限制");}}/*** 限流腳本*/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 = "系統(tǒng):限流測試管理") public class LimitController {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();/*** 測試限流注解,下面配置說明該接口 60秒內(nèi)最多只能訪問 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類的學(xué)習(xí)

JUC包提供了一系列的原子性操作類,這些類都是使用非阻塞算法CAS實現(xiàn)的,相比使用鎖實現(xiàn)原子性操作這在性能上有很大提高。

JUC并發(fā)包中包含有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作類,它們的原理類似。AtomicLong是原子性遞增或者遞減類,其內(nèi)部使用Unsafe來實現(xiàn),我們看下面的代碼。

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的,這是為了在多線程下保證內(nèi)存可見性,value是具體存放計數(shù)的變量。....}

注意:private static final Unsafe U = Unsafe.getUnsafe();為何能通過Unsafe.getUnsafe()方法獲取到Unsafe類的實例?其實這是因為AtomicLong類也是在rt.jar包下面的,AtomicLong類就是通過BootStarp類加載器進(jìn)行加載的。
jdk8中的getAndAddLong方法:

public final getAndAddLong(Object paramObject,long paramLong1,long paramlong2) { long l; do{ l = getLongvolatile(paramObject,paramLong); }while(!compareAndSwapLong(paramObject,paramLong1,1,1 + paramLong2)); return l; }

可以看到,JDK 7的AtomicLong中的循環(huán)邏輯已經(jīng)被JDK 8中的原子操作類UNsafe內(nèi)置了,之所以內(nèi)置應(yīng)該是考慮到這個函數(shù)在其他地方也會用到,而內(nèi)置可以提高復(fù)用性。
下面通過一個多線程使用AtomicLong統(tǒng)計0的個數(shù)的例子來加深對AtomicLong的理解。

/** 統(tǒng)計0的個數(shù) */ public class Atomic { //(10)創(chuàng)建Long型原子計數(shù)器 private static AtomicLong atomicLong=new AtomicLong(); //(11)創(chuàng)建數(shù)據(jù)源 private static Integer[]arrayone=new Integer[]{01230560560}; private static Integer[]arrayTwo=new Integer[]{101230560560}; public static void main(String[]args)throws InterruptedException { //(12)線程one統(tǒng)計數(shù)組array0ne中0的個數(shù) Thread threadOne=new Threadnew Runnable(){ @Override public void run(){ int size=arrayone.length; for(inti=0;i<size;++i){ if(arrayone[i].intValue()==0{ atomicLong.incrementAndGet(); } }} }); //(13)線程two統(tǒng)計數(shù)組arrayTwo中0的個數(shù) Thread threadTwo=new Threadnew Runnable(){ @override public void run(){ int size=arrayTwo.length; for(inti=0;i<size;++i){ if(arrayTwo[i].intValue()=0{ atomicLong.incrementAndGet(); } } } }); //(14)啟動子線程 threadone.start(); threadTwo.start(); //(15)等待線程執(zhí)行完畢 threadone.join(); threadTwo.join(); System.out.println("count0:"+atomicLong.get()); }

輸出count0:7
如上代碼中的兩個線程各自統(tǒng)計自己所持?jǐn)?shù)據(jù)中0的個數(shù),每當(dāng)找到一個0就會調(diào)用AtomicLong的原子性遞增方法。 在沒有原子類的情況下,實現(xiàn)計數(shù)器需要使用一定的同步措施,比如使用synchronized關(guān)鍵字等,但是這些都是阻塞算法,對性能有一定損耗,而這些原子操作類都使用CAS非阻塞算法,性能更好。但是在高并發(fā)情況下AtomicLong還會存在性能問題。實際上JDK 8提供了一個在高并發(fā)下性能更好的LongAdder類

lua腳本學(xué)習(xí)

關(guān)于lua腳本參考了《redis入門指南》
使用腳本的好處如下:

1.減少網(wǎng)絡(luò)開銷:本來5次網(wǎng)絡(luò)請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis服務(wù)器上完成。使用腳本,減少了網(wǎng)絡(luò)往返時延。
2.原子操作:Redis會將整個腳本作為一個整體執(zhí)行,中間不會被其他命令插入。
3.復(fù)用:客戶端發(fā)送的腳本會永久存儲在Redis中,意味著其他客戶端可以復(fù)用這一腳本而不需要使用代碼完成同樣的邏輯。

示例:
寫一個.lua腳本

local times = redis.call('incr',KEYS[1])if times == 1 thenredis.call('expire',KEYS[1], ARGV[1]) endif times > tonumber(ARGV[2]) thenreturn 0 end return 1

在redis客戶端機(jī)器上,如何測試這個腳本呢?如下:

redis-cli --eval ratelimiting.lua rate.limitingl:127.0.0.1 , 10 3
–eval參數(shù)是告訴redis-cli讀取并運(yùn)行后面的Lua腳本,ratelimiting.lua是腳本的位置,后面跟著是傳給Lua腳本的參數(shù)。其中",“前的rate.limiting:127.0.0.1是要操作的鍵,可以再腳本中用KEYS[1]獲取,”,“后面的10和3是參數(shù),在腳本中能夠使用ARGV[1]和ARGV[2]獲得。注:”,"兩邊的空格不能省略,否則會出錯

結(jié)合腳本的內(nèi)容可知這行命令的作用是將訪問頻率限制為每10秒最多3次,所以在終端中不斷的運(yùn)行此命令會發(fā)現(xiàn)當(dāng)訪問頻率在10秒內(nèi)小于或等于3次時返回1,否則返回0。

總結(jié)

以上是生活随笔為你收集整理的springboot中使用lua脚本+aop作限流访问案例代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。