使用Redis進行限流

????????發表於2019-01-24

前言

通過使用註解(提供介面形式與引數形式2種方式)針對性的對介面進行限流,底層使用redis配合lua指令碼實現令牌桶。

Redis基礎實現

public class RedisRateLimiter {
	private static String REDIS_KEY = "rate:limiter:%s";
	private RedisTemplate redisTemplate;

	public RedisRateLimiter(RedisTemplate redisTemplate) {
		this.redisTemplate = redisTemplate;
	}

	public boolean tryAcquire(String flag, int maxPermits, int addRate,
			long expireSeconds) {
		String luaScript = buildLuaScript();
		String key = String.format(REDIS_KEY, flag);

		Object execute = redisTemplate.execute(new RedisCallback<Long>() {
			@Override
			public Long doInRedis(RedisConnection connection)
					throws DataAccessException {
				return connection.eval(luaScript.getBytes(), ReturnType.INTEGER,
						1, key.getBytes(),
						String.valueOf(maxPermits).getBytes(),
						String.valueOf(expireSeconds).getBytes(),
						String.valueOf(addRate).getBytes(),
						String.valueOf(System.currentTimeMillis()).getBytes());
			}
		});
		Long count = (Long) execute;
		return count != 0;
	}

	private String buildLuaScript() {
		StringBuilder lua = new StringBuilder();
		lua.append(" local key = KEYS[1]");
		lua.append("\nlocal limit = tonumber(ARGV[1])");
		lua.append("\nlocal expireSeconds = tonumber(ARGV[2])");
		lua.append("\nlocal rate = tonumber(ARGV[3])");
		lua.append("\nlocal now = tonumber(ARGV[4])");
		lua.append(
				"\nlocal rateLimiterInfo = redis.call('HMGET', key, \"count\", \"time\")");
		lua.append(
				"\nlocal currentPermits =tonumber(rateLimiterInfo[1] or \"0\")");
		lua.append(
				"\nlocal lastRecoverTime =tonumber(rateLimiterInfo[2] or \"0\")");
		lua.append("\nif lastRecoverTime > 0 then");
		lua.append(
				"\n local recoverPermits = math.floor(((now - lastRecoverTime) / 1000) * rate)");
		lua.append("\nif recoverPermits > 0 then");
		lua.append(
				"\ncurrentPermits = math.max(0, currentPermits - recoverPermits);");
		lua.append("\nend");
		lua.append("\nend");
		lua.append("\nif currentPermits + 1 > limit then");
		lua.append("\nreturn 0");
		lua.append("\nelse");
		lua.append(
				"\n redis.call(\"HMSET\", key, \"count\", currentPermits + 1, \"time\", now)");
		lua.append("\nredis.call(\"EXPIRE\", key, expireSeconds)");
		lua.append("\nreturn currentPermits + 1");
		lua.append("\nend");
		return lua.toString();
	}
}

複製程式碼

為什麼使用Hash: 使用叢集版Redis時,必須使所有key在1個slot上。

註解類

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableRedisRateLimiter {
	/**
	 * 通過key尋找配置檔案
	 * 
	 * @return
	 */
	String key() default "";
}
複製程式碼

新建配置檔案

demo.flag=
demo.params=demo
demo.maxPermits=10
demo.addRate=1
demo.expireSeconds=10
複製程式碼

此配置檔案供下文RedisRateLimiterConfigManager類使用

配置類

@Configuration
public class RedisRateLimiterConfiguration implements ApplicationContextAware {
	private ApplicationContext ctx;
	@Bean("redisRateLimiter")
	public RedisRateLimiter redisRateLimiter() {
		String beanName = "redisTemplate-rl";
		boolean containsBean = ctx.containsBean(beanName);
		if (containsBean) {
			RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean(beanName);
			return new RedisRateLimiter(redisTemplate);
		} else {
			return null;
		}
	}
	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.ctx = applicationContext;
	}
}
複製程式碼

RedisTemplate類注入程式碼省略,整合jedis。

Aspect

@Aspect
@Component
public class RedisRateLimiterAspect {

	private final static String FLAG_SUFFIX = ".flag";//根據flag指定限流
	private final static String PARAMS_SUFFIX = ".params";//根據引數指定限流,如果根據引數指定了則不必填flag
	private final static String MAX_PERMITS_SUFFIX = ".maxPermits";//最大令牌數
	private final static String ADD_RATE_SUFFIX = ".addRate";//每秒增加的令牌數
	private final static String EXPIRE_SECONDS_SUFFIX = ".expireSeconds";//過期時間

	@Autowired
	private RedisRateLimiter redisRateLimiter;

	@Before("execution(* 此處根據自己環境填寫路徑")
	public void beforeMethod(JoinPoint joinPoint) {
		Signature signature = joinPoint.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		EnableRedisRateLimiter annotation = methodSignature.getMethod()
				.getAnnotation(EnableRedisRateLimiter.class);
		if (annotation != null) {
			String key = annotation.key();
			if (!StringUtil.isNullOrEmpty(key)) {
				String flag = RedisRateLimiterConfigManager
						.getProperty(combineKey(key, FLAG_SUFFIX));
				if (StringUtil.isNullOrEmpty(flag)) {
					String paramName = RedisRateLimiterConfigManager
							.getProperty(combineKey(key, PARAMS_SUFFIX));
					if (StringUtil.isNullOrEmpty(paramName)) {
						return;
					}
					String[] paramNames = paramName.split(",");
					String[] argNames = methodSignature.getParameterNames(); // 引數名
					Object[] args = joinPoint.getArgs();// 引數值
					flag = Arrays.stream(paramNames).map(p -> {
						for (int i = 0; i < argNames.length; i++) {
							if (argNames[i].equals(p)) {
								try {
									return args[i].toString();
								} catch (Exception e) {
									return null;
								}
							}
						}
						return null;
					}).filter(Objects::nonNull)
							.collect(Collectors.joining(":"));
				}
				if (StringUtil.isNullOrEmpty(flag)) {
					return;
				}
				String strMaxPermits = RedisRateLimiterConfigManager
						.getProperty(combineKey(key, MAX_PERMITS_SUFFIX));
				int maxPermits = StringUtil.isNullOrEmpty(strMaxPermits) ? 0
						: Integer.parseInt(strMaxPermits);
				String strAddRate = RedisRateLimiterConfigManager
						.getProperty(combineKey(key, ADD_RATE_SUFFIX));
				int addRate = StringUtil.isNullOrEmpty(strAddRate) ? 0
						: Integer.parseInt(strAddRate);
				String strExpireSeconds = RedisRateLimiterConfigManager
						.getProperty(combineKey(key, EXPIRE_SECONDS_SUFFIX));
				long expireSeconds = StringUtil.isNullOrEmpty(strExpireSeconds)
						? 0
						: Integer.parseInt(strExpireSeconds);
				String finalFlag = key + ":" + flag;
				boolean success = redisRateLimiter.tryAcquire(finalFlag,
						maxPermits, addRate, expireSeconds);
				if (!success) {
					throw error;
				}
			}
		}
	}

	public String combineKey(String key, String suffix) {
		return key + suffix;
	}
}

複製程式碼

上述程式碼中RedisRateLimiterConfigManager程式碼省略,內部實現為通過combineKey方法後的值去獲取對應的配置檔案中value。

結尾

上述程式碼都完成後,只需在需要限流的介面上註上@EnableRedisRateLimiter(key = "demo")即可,如果有更好的實現方式可以留言探討。

相關文章