Java並發:分布式應用限流 Redis + Lua 實踐

來源:互聯網
上載者:User

標籤:ret   aqs   .com   template   使用者服   tor   專註   分享   led   

任何限流都不是漫無目的的,也不是一個開關就可以解決的問題,常用的限流演算法有:令牌桶,漏桶。在之前的文章中,也講到過,但是那是基於單機情境來寫。

之前文章:介面限流演算法:漏桶演算法&令牌桶演算法

然而再牛逼的機器,再最佳化的設計,對於特殊情境我們也是要特殊處理的。就拿秒殺來說,可能會有百萬層級的使用者進行搶購,而商品數量遠遠小於使用者數量。如果這些請求都進入隊列或者查詢快取,對於最終結果沒有任何意義,徒增後台華麗的資料。對此,為了減少資源浪費,減輕後端壓力,我們還需要對秒殺進行限流,只需保障部分使用者服務正常即可。

就秒殺介面來說,當訪問頻率或者並發請求超過其承受範圍的時候,這時候我們就要考慮限流來保證介面的可用性,以防止非預期的請求對系統壓力過大而引起的系統癱瘓。通常的策略就是拒絕多餘的訪問,或者讓多餘的訪問排隊等待服務。

分布式限流

單機限流,可以用到 AtomicIntegerRateLimiterSemaphore 這些。但是在分布式中,就不能使用了。常用分布式限流用 Nginx 限流,但是它屬於網關層面,不能解決所有問題,例如內部服務,簡訊介面,你無法保證消費方是否會做好限流量控制,所以自己在應用程式層實現限流還是很有必要的。

本文不涉及 nginx + lua,簡單介紹 redis + lua分布式限流的實現。如果是需要在接入層限流的話,應該直接採用nginx內建的串連數限流模組和請求限流模組。

Redis + Lua 限流樣本

本次項目使用SpringBoot 2.0.4,使用到 Redis 叢集,Lua 限流指令碼

引入依賴
<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-data-redis</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-aop</artifactId>    </dependency>    <dependency>        <groupId>org.apache.commons</groupId>        <artifactId>commons-lang3</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>    </dependency></dependencies>
Redis 配置

application.properties

spring.application.name=spring-boot-limit# Redis資料庫索引spring.redis.database=0# Redis伺服器位址spring.redis.host=10.4.89.161# Redis伺服器串連連接埠spring.redis.port=6379# Redis伺服器串連密碼(預設為空白)spring.redis.password=# 串連池最大串連數(使用負值表示沒有限制)spring.redis.jedis.pool.max-active=8# 串連池最大阻塞等待時間(使用負值表示沒有限制)spring.redis.jedis.pool.max-wait=-1# 串連池中的最大空閑串連spring.redis.jedis.pool.max-idle=8# 串連池中的最小空閑串連spring.redis.jedis.pool.min-idle=0# 連線逾時時間(毫秒)spring.redis.timeout=10000
Lua 指令碼

參考: 聊聊高並發系統之限流特技
http://jinnianshilongnian.iteye.com/blog/2305117

local key = "rate.limit:" .. KEYS[1] --限流KEYlocal limit = tonumber(ARGV[1])        --限流大小local current = tonumber(redis.call(‘get‘, key) or "0")if current + 1 > limit then --如果超出限流大小  return 0else  --請求數+1,並設定2秒到期  redis.call("INCRBY", key,"1")   redis.call("expire", key,"2")   return current + 1end

1、我們通過KEYS[1] 擷取傳入的key參數
2、通過ARGV[1]擷取傳入的limit參數
3、redis.call方法,從緩衝中get和key相關的值,如果為nil那麼就返回0
4、接著判斷緩衝中記錄的數值是否會大於限制大小,如果超出表示該被限流,返回0
5、如果未超過,那麼該key的緩衝值+1,並設定到期時間為1秒鐘以後,並返回緩衝值+1

限流註解

註解的目的,是在需要限流的方法上使用

package com.souyunku.example.annotation;/** * 描述: 限流註解 * * @author yanpenglei * @create 2018-08-16 15:24 **/@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface RateLimit {    /**     * 限流唯一標示     *     * @return     */    String key() default "";    /**     * 限流時間     *     * @return     */    int time();    /**     * 限流次數     *     * @return     */    int count();}
公用配置
package com.souyunku.example.config;@Componentpublic class Commons {    /**     * 讀取限流指令碼     *     * @return     */    @Bean    public DefaultRedisScript<Number> redisluaScript() {        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimit.lua")));        redisScript.setResultType(Number.class);        return redisScript;    }    /**     * RedisTemplate     *     * @return     */    @Bean    public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {        RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();        template.setKeySerializer(new StringRedisSerializer());        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());        template.setConnectionFactory(redisConnectionFactory);        return template;    }}
攔截器

通過攔截器 攔截@RateLimit註解的方法,使用Redsi execute 方法執行我們的限流指令碼,判斷是否超過限流次數

以下下是核心代碼

package com.souyunku.example.config;/** * 描述:攔截器 * * @author yanpenglei * @create 2018-08-16 15:33 **/@Aspect@Configurationpublic class LimitAspect {    private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);    @Autowired    private RedisTemplate<String, Serializable> limitRedisTemplate;    @Autowired    private DefaultRedisScript<Number> redisluaScript;    @Around("execution(* com.souyunku.example.controller ..*(..) )")    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {        MethodSignature signature = (MethodSignature) joinPoint.getSignature();        Method method = signature.getMethod();        Class<?> targetClass = method.getDeclaringClass();        RateLimit rateLimit = method.getAnnotation(RateLimit.class);        if (rateLimit != null) {            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();            String ipAddress = getIpAddr(request);            StringBuffer stringBuffer = new StringBuffer();            stringBuffer.append(ipAddress).append("-")                    .append(targetClass.getName()).append("- ")                    .append(method.getName()).append("-")                    .append(rateLimit.key());            List<String> keys = Collections.singletonList(stringBuffer.toString());            Number number = limitRedisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {                logger.info("限流時間段內訪問第:{} 次", number.toString());                return joinPoint.proceed();            }        } else {            return joinPoint.proceed();        }        throw new RuntimeException("已經到設定限流次數");    }    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;    }}
控制層

添加 @RateLimit() 註解,會在 Redsi 中產生 10 秒中,可以訪問5次 的key

RedisAtomicLong 是為測試例子例,記錄累計訪問次數,跟限流沒有關係。

package com.souyunku.example.controller;/** * 描述: 測試頁 * * @author yanpenglei * @create 2018-08-16 15:42 **/@RestControllerpublic class LimiterController {    @Autowired    private RedisTemplate redisTemplate;    // 10 秒中,可以訪問10次    @RateLimit(key = "test", time = 10, count = 10)    @GetMapping("/test")    public String luaLimiter() {        RedisAtomicInteger entityIdCounter = new RedisAtomicInteger("entityIdCounter", redisTemplate.getConnectionFactory());        String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");        return date + " 累計訪問次數:" + entityIdCounter.getAndIncrement();    }}
啟動服務
package com.souyunku.example;@SpringBootApplicationpublic class SpringBootLimitApplication {    public static void main(String[] args) {        SpringApplication.run(SpringBootLimitApplication.class, args);    }}

啟動項目頁面訪問:http://127.0.0.1:8080/test

10 秒中,可以訪問10次,超過十次,頁面就報錯,等夠10秒,重新計算。

後台日誌

2018-08-16 18:41:08.205  INFO 18076 --- [nio-8080-exec-1] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:1 次2018-08-16 18:41:08.426  INFO 18076 --- [nio-8080-exec-3] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:2 次2018-08-16 18:41:08.611  INFO 18076 --- [nio-8080-exec-5] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:3 次2018-08-16 18:41:08.819  INFO 18076 --- [nio-8080-exec-7] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:4 次2018-08-16 18:41:09.021  INFO 18076 --- [nio-8080-exec-9] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:5 次2018-08-16 18:41:09.203  INFO 18076 --- [nio-8080-exec-1] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:6 次2018-08-16 18:41:09.406  INFO 18076 --- [nio-8080-exec-3] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:7 次2018-08-16 18:41:09.629  INFO 18076 --- [nio-8080-exec-5] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:8 次2018-08-16 18:41:09.874  INFO 18076 --- [nio-8080-exec-7] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:9 次2018-08-16 18:41:10.178  INFO 18076 --- [nio-8080-exec-9] com.souyunku.example.config.LimitAspect  : 限流時間段內訪問第:10 次2018-08-16 18:41:10.702 ERROR 18076 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 已經到設定限流次數] with root causejava.lang.RuntimeException: 已經到設定限流次數    at com.souyunku.example.config.LimitAspect.interceptor(LimitAspect.java:73) ~[classes/:na]    at sun.reflect.GeneratedMethodAccessor35.invoke(Unknown Source) ~[na:na]    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_112]    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_112]
推薦閱讀
  • Dubbo 整合 Pinpoint 做分布式服務要求跟蹤
  • 介面限流:漏桶演算法&令牌桶演算法
  • Java並發:分布式應用限流實踐
  • Java並發:Semaphore訊號量源碼分析
  • Java並發:深入淺出AQS之共用鎖定模式源碼分析
  • Java並發:深入淺出AQS之獨佔鎖模式源碼分析
  • Java並發:瞭解無鎖CAS就從源碼分析
  • Java並發:CAS原理分析
Contact
  • 鵬磊
  • 出處:http://www.ymq.io/2018/08/16/redis-lua/
  • 著作權歸作者所有,轉載請註明出處
  • Wechat:關注公眾號,搜雲庫,專註於開發技術的研究與知識分享

Java並發:分布式應用限流 Redis + Lua 實踐

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.