利用aop、redis、注解等实现简单的熔断

利用aop、redis、注解来实现超时熔断机制

author:陈镇坤27

创建时间:2022年3月31日

创作不易,转载请注明来源

摘要:利用AOP切面、Reids、自定义注解等知识,实现简单的服务超时熔断。


——————————————————————————————

前言

c项目还有半小时要上线了,临时给我加个紧急任务。c服务调用弹窗广告时,需要用到某个三方api接口,该接口调用在某段时间内会频繁超时, 导致c服务一直不响应,连锁影响到Nginx认定该服务宕机,导致服务压力全部给到了其他机子。其他机子又出现这个问题,又给到别的机子,颇有我方唱罢你登场,你下场来我来唱的感觉。

计划使用超时熔断,但因为是ssm架构的旧项目,没有引入openFegin组件,用不了hystrix的熔断机制。

意外的是,项目里面有个陈年的注解,结合aop、redis进行了一个比较简单的熔断控制。

用了下,结果原始代码里面一堆bug,熔断功能根本不生效。一番操作下来,解决了几个bug,并且进行了一定的优化。

接下来给大家介绍下一个比较简单的,超时熔断控制机制。

核心思路

利用aop环切注解配置的方法,执行方法前后计算时长,若超时则以方法全路径名组合的key增值+1,该key设置存活时长。

当该key的值大小超过指定的大小,则存储一个熔断激活标识,标识设置存活时长。

此时,再存储一个恢复访问标识,该标识会在区间时间之后固定往redis存储一个不断增值的恢复熔断尝试key。

此后,其他的请求,都会向判断是否有开启熔断,有开启恢复访问标识,在标识启动的情况下,会减值访问,记录指定时间区间的非超时的请求次数。

在达到非超时成功次数后,会关闭熔断标识,关闭恢复访问标识。服务正常访问。

正文

废话不多说,贴代码

PS:需要注意,CacheService是项目二次封装的服务,把这些地方代码替换为对应的redis api或对应服务方法即可。

@Component
@Aspect
public class CircuitBreakerInterceptor {


	@Autowired
	private CacheService cacheService;

	private static final int criterionMillOfTimeOut =  30 * 1000;
	private static final int millWhenStartRecover = 20 * 60 * 1000;

	private static final ReentrantLock lock = new ReentrantLock();
	
	private static final String BREAKER_KEY = "breaker";
	private static final String RECOVER_KEY = "recover";
	private static final String FAILED_COUNTS_KEY = "failed_counts";
	private static final String TIMEOUT_COUNTS_KEY = "timeout_counts";
	private static final String RECOVER_COUNTS = "recover_counts";
	private static final String SUCCESS_COUNTS = "success_counts";

	@Pointcut("@annotation(com.emodor.attendance.annotation.CircuitBreaker)")
	private void circuitBreake() {
	}

	// 声明环绕通知
	@Around("circuitBreake()")
	public void doAroundCircuitBreake(ProceedingJoinPoint pjp) throws Throwable {

		MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
		Method targetMethod = AopUtils.getMostSpecificMethod(methodSignature.getMethod(), pjp.getTarget().getClass());
		String targetName = pjp.getTarget().getClass().getName();
		String methodName = pjp.getSignature().getName();

		CircuitBreaker circuitBreaker = targetMethod.getAnnotation(CircuitBreaker.class);

		long startTime = System.currentTimeMillis();

		String redisKey = getRedisKey(targetName, methodName);
		String breakerFlag = cacheService.getAsString(redisKey + BREAKER_KEY);
		String recoverFlag = cacheService.getAsString(redisKey + RECOVER_KEY);
		// 如果开启了熔断
		if (!"on".equals(breakerFlag) || "on".equals(recoverFlag)) {
			try {
				if ("on".equals(recoverFlag)) {
					//	这里应该加锁,保证查询与减值是原子操作。留待各位优化
					String recoverCountsStr = cacheService.getAsString(redisKey + RECOVER_COUNTS);
					long recoverCounts = 0;
					if (StringUtils.isNotBlank(recoverCountsStr)) {
						recoverCounts = Long.parseLong(recoverCountsStr);
					}
					if (recoverCounts > 0) {
						recoverCounts = cacheService.decrement(redisKey + RECOVER_COUNTS);
						pjp.proceed();
						if (System.currentTimeMillis() - startTime <= criterionMillOfTimeOut) {
							long successCounts = cacheService.increment(redisKey + SUCCESS_COUNTS, 30 * 60 * 1000L);
							// 30分钟内成功次数达到规定值可以关闭熔断
							if (successCounts >= circuitBreaker.successCount()) {
								// 关闭熔断,清除相关key
								cacheService.delete(redisKey + BREAKER_KEY);
								cacheService.delete(redisKey + FAILED_COUNTS_KEY);
								cacheService.delete(redisKey + TIMEOUT_COUNTS_KEY);
								cacheService.delete(redisKey + RECOVER_KEY);
								cacheService.delete(redisKey + SUCCESS_COUNTS);
								logger.info("CircuitBreaker,服务器已经关闭熔断,熔断的方法为:"  + redisKey);
							}
						}
					}
				} else {
					pjp.proceed();
				}

			} catch (Exception ex) {
				logger.info("调用失败" + redisKey,ex);
				long errorCounts = cacheService.increment(redisKey + FAILED_COUNTS_KEY, circuitBreaker.timeRange() * 1000);
				logger.info("调用失败次数" + redisKey + "," + errorCounts);
				// 如果错误超过阀值则进行熔断降级
				if (errorCounts >= circuitBreaker.failCount()) {
					cacheService.put(redisKey + BREAKER_KEY, "on", 24 * 60 * 60 * 1000L);
					logger.info("CircuitBreaker,服务器已经开启熔断,熔断的方法为:"  + redisKey);
				}
				throw ex;
			}
		}
		long endTime = System.currentTimeMillis();

		// 如果执行超过0.5分钟
		if (endTime - startTime > criterionMillOfTimeOut) {
			long timeoutCounts = cacheService.increment(redisKey + TIMEOUT_COUNTS_KEY, circuitBreaker.timeRange() * 1000);
			// 如果超时超过阀值则进行熔断降级
			if (timeoutCounts >= circuitBreaker.failCount() && !"on".equals(breakerFlag)) {
				//熔断有效时间24小时
				cacheService.put(redisKey + BREAKER_KEY, "on", 24 * 60 * 60 * 1000L);
				logger.info("CircuitBreaker,服务器已经开启熔断,熔断的方法为:"  + redisKey);
			}
		}
		
		// 熔断进行恢复half_open
		if ("on".equals(breakerFlag) && !"on".equals(recoverFlag)) {
			lock.lock();
			if (!"on".equals(recoverFlag)) {
				cacheService.put(redisKey + RECOVER_KEY, "on",24 * 60 * 60 * 1000L);
				new Thread() {
					@Override
					public void run() {
						//20分钟之后开启熔断恢复halfOpen
						try {
							Thread.sleep(millWhenStartRecover);
							logger.info("开启半熔断恢复----------------------------------------");
						}catch(Exception e) {
						}
						String breakerFlag = cacheService.getAsString(redisKey + BREAKER_KEY);
						while ("on".equals(breakerFlag)) {
							cacheService.delete(redisKey + RECOVER_COUNTS);
							// 每60秒放10个进去
							for (int i=0;i<10;i++) {
								cacheService.increment(redisKey + RECOVER_COUNTS, 60 * 1000L);
							}
							try {
								Thread.sleep(60 * 1000L);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							breakerFlag = cacheService.getAsString(redisKey + BREAKER_KEY);
						}

					}
				}.start();
			}
			lock.unlock();
		}
	}

	/**
	 * 获取缓存的key值
	 */
	private String getRedisKey(String targetName, String methodName) {
		StringBuilder sb = new StringBuilder("");
		sb.append("emodor.circuit.breaker.").append(targetName).append(".").append(methodName).append("_");
		return sb.toString();
	}
}

注解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface CircuitBreaker {
	/**
     * 失败次数进行熔断,默认值MAX_VALUE
     */
    long failCount() default Long.MAX_VALUE;
    /**
     * 成功次数关闭熔断 默认值 20
     * @return
     */
    long successCount() default 20;
    /**
     * 时间段,单位秒,默认每秒限流大小
     */
    long timeRange() default 1;
}

逻辑演示

环切指定的方法;

熔断标识和恢复访问标识初始为null,直接执行pjp.proceed()方法。

计算执行时长,大于30分钟则累加超时次数。

仅开启熔断标识时刻,会启动恢复访问标识,并在配置时间后创建一个线程,区间时间循环累加测试访问标识次数。

其他请求访问时,若有测试次数,则再执行访问,并记录非超时访问次数。若无测试次数可用,则继续熔断。

遗留的问题

该超时机制最大的问题,在于没有强制控制该方法超时断开连接。

举个例子:

设置100分钟内,超时100个的话,进行熔断。

认为大于30s即为超时。

此时若超时时长是2分钟,则Nginx的宕机判定仍然会被触发。

这个不足暂时没想到思路解决,后面如果学习到的话,会跟大家分享。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈镇坤27

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值