一、为什么要做计数器:
在唯品会的工作的时候,为了更好的推动系统向智慧供应链发展,理性分析供应商和商助对产品的使用频率,追溯服务历史调用情况,于是,开发了一款轻量级计数器,供各团队使用。
二、难点:
1、调用量大
2、各团队需要保证极端情况下,计数器服务不可用时,不影响各团队业务正常运行
3、需要使用简单
4、个别需要统一业务日志
5、不能有额外的定时器一直轮询
三、优势:
1、简单 @LogCounter 对应的类或者方法上 即可开启计数器功能
2、可配置 日志记录可选择开启和关闭、可选择记录到本地、可选择记录到远程服务器
3、统一日志 开启日志功能后,即可对方法的入参和出参进行记录
4、采用了线程池隔离
5、设计了漏斗策略
四、代码:
https://gitee.com/sunrisexq/counter
package com.javaxiaobang.counter.aspect.log;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 功 能:日志注解标签
* 作 者:java潇邦
* 时 间:2018-05-19
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface LogCounter {
String counterName() default "";//计数器名称
boolean useLogFile() default true;//true时记录log日志
boolean useRemote() default true;//true时记录counter
boolean useLocal() default false;//true时开启本地counter
}
package com.javaxiaobang.counter.aspect.log;
import com.alibaba.fastjson.JSON;
import com.javaxiaobang.counter.exception.CounterException;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 功 能:日志AOP
* 作 者:java潇邦
* 时 间:2018-05-19
*/
@Order
@Aspect
@Component("commonLogCounterAop")
public class LogCounterAop {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
@Autowired
private LogCounterLocalBusiness logCounterLocalBusiness;
@Autowired
private LogCounterRemoteBusiness logCounterRemoteBusiness;
//切入点
@Pointcut("@annotation(com.javaxiaobang.counter.aspect.log.LogCounter)")
public void anyMethod() {
}
//方法执行之前
@Before("anyMethod()")
public void before(final JoinPoint joinPoint) {
try {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null && method.isAnnotationPresent(LogCounter.class)) {
String methodName = method.getName();
final LogCounter aspect = method.getAnnotation(LogCounter.class);
if (aspect.useLogFile()) {
logger.info("{}{}方法的参数值:{}", aspect.counterName(), methodName, JSON.toJSONString(joinPoint.getArgs()));
}
final String counterName = StringUtils.isBlank(aspect.counterName()) ? methodName : aspect.counterName();
if (aspect.useLocal()) {
pool.submit(new Runnable() {
@Override
public void run() {
logCounterLocalBusiness.execute(counterName);
}
});
}
if (aspect.useRemote()) {
pool.submit(new Runnable() {
@Override
public void run() {
logCounterRemoteBusiness.execute(counterName);
}
});
}
}
} catch (Exception e) {
logger.debug("before切面异常:", e);
}
}
//方法执行之后
@AfterReturning(pointcut = "anyMethod()", returning = "retVal")
public void after(JoinPoint joinPoint, Object retVal) {
try {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
String methodName = method.getName();
LogCounter aspect = method.getAnnotation(LogCounter.class);
if (aspect != null && aspect.useLogFile()) {
logger.info("{}{}方法的返回值:{}", aspect.counterName(), methodName, JSON.toJSONString(retVal));
}
}
} catch (Exception e) {
logger.debug("after切面异常:", e);
}
}
//方法执行之后异常日志
@AfterThrowing(pointcut = "anyMethod()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) throws CounterException {
//不需要再记录ServiceException,因为在service异常切面中已经记录过
if (e instanceof CounterException) {
throw (CounterException) e;
} else {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
String methodName = method.getName();
LogCounter aspect = method.getAnnotation(LogCounter.class);
logger.warn("{}{}方法异常:", aspect.counterName(), methodName, e);
throw new CounterException();
}
}
}
}
package com.javaxiaobang.counter.aspect.log;
import com.javaxiaobang.counter.util.LogCounterUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
/**
* 功 能:远程计数器-调用远程服务保存数据
* 作 者:java潇邦
* 时 间:2018-05-19
*/
@Component("logCounterRemoteBusiness")
public class LogCounterRemoteBusiness {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static ReentrantLock lock = new ReentrantLock();
private static String domain = LogCounterUtil.getDomain();
private static long tryLockTime = 2;//尝试2秒获取锁的时间
private static long refreshTime = 30000L;//30秒刷新一次
private static ConcurrentHashMap<String, AtomicLong> countMap = new ConcurrentHashMap<String, AtomicLong>(32);//并发计数
private static ConcurrentHashMap<String, Long> timeMap = new ConcurrentHashMap<String, Long>(32);//并发计时
private static long detailRefreshTime = 10 * 60 * 1000L;//默认10分钟刷新数据
private static ConcurrentHashMap<String, AtomicLong> detailCountMap = new ConcurrentHashMap<String, AtomicLong>(32);//并发计数
private static ConcurrentHashMap<String, Long> detailTimeMap = new ConcurrentHashMap<String, Long>(32);//并发计时
// private CounterServiceHelper.CounterServiceClient client = new CounterServiceHelper.CounterServiceClient();//唯品会counter服务-对应两张表
/**
* 计数器逻辑处理
*/
public void execute(String counterName) {
commonMethod("total", counterName, refreshTime, countMap, timeMap);//计数器头表
commonMethod("detail", counterName, detailRefreshTime, detailCountMap, detailTimeMap);//计数器明细表
}
private void commonMethod(String type, String counterName, long refreshTime, ConcurrentHashMap<String, AtomicLong> countMap, ConcurrentHashMap<String, Long> timeMap) {
try {
String key = counterName + domain;//唯一key
AtomicLong atomicLong = countMap.get(key);
if (null == atomicLong) {//原子计数器
synchronized (lock) {//并发控制
atomicLong = countMap.get(key);
if (null == atomicLong) {
countMap.put(key, new AtomicLong(1));
timeMap.put(key, System.currentTimeMillis());
return;
}
}
}
atomicLong.incrementAndGet();//原子计数器加1
boolean flag = (System.currentTimeMillis() - timeMap.get(key) > refreshTime) && (atomicLong.get() > 0);//超过N秒 并且 次数大于0
if (flag) {//满足条件的漏斗
if (lock.tryLock(tryLockTime, TimeUnit.SECONDS)) {
try {
flag = (System.currentTimeMillis() - timeMap.get(key) > refreshTime) && (atomicLong.get() > 0);//避免错误更新
if (flag) {
long counter = countMap.get(key).get();
timeMap.put(key, System.currentTimeMillis());//重新计时
atomicLong.set(0);
countMap.put(key, atomicLong);//重新计数
if ("detail".equals(type)) {
incrementDetailCounter(counterName, domain, counter, refreshTime);
} else {
incrementCounter(counterName, domain, counter);
}
}
} finally {
lock.unlock();
}
} else {
logger.warn("2秒内没有获取到锁则放弃,下次再调用远程保存数据的服务:{}", key);
}
}
} catch (Exception e) {
logger.debug("remote计数器异常:", e);
}
}
/**
* 保存到counter_info头表
*/
public void incrementCounter(String counterName, String domain, long counter) {
//调用远程方法
// try {
// CounterInfo counterInfo = new CounterInfo();
// counterInfo.setCounterName(counterName);
// counterInfo.setDomain(domain);
// counterInfo.setCounter(counter);
// client.incrementCounter(counterInfo);
// } catch (Exception e) {
// logger.debug("incrementCounter:", e);
// }
}
/**
* 保存到counter_detail明细表
*/
public void incrementDetailCounter(String counterName, String domain, long counter, long interval) {
//调用远程方法
// try {
// CounterInfo counterInfo = new CounterInfo();
// counterInfo.setCounterName(counterName);
// counterInfo.setDomain(domain);
// counterInfo.setCounter(counter);
// counterInfo.setInterval(interval);
// client.incrementDetailCounter(counterInfo);
// } catch (Exception e) {
// logger.debug("incrementDetailCounter:", e);
// }
}
public static long getRefreshTime() {
return refreshTime;
}
public static void setRefreshTime(long refreshTime) {
LogCounterRemoteBusiness.refreshTime = refreshTime;
}
public static long getDetailRefreshTime() {
return detailRefreshTime;
}
public static void setDetailRefreshTime(long detailRefreshTime) {
LogCounterRemoteBusiness.detailRefreshTime = detailRefreshTime;
}
public static void setDomain(String domain) {
LogCounterRemoteBusiness.domain = domain;
}
public static String getDomain() {
return domain;
}
}
package com.javaxiaobang.counter.aspect.log;
import com.javaxiaobang.counter.aspect.delay.LogCounterDelayInterface;
import com.javaxiaobang.counter.util.LogCounterUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
/**
* 功 能:本地计数器-实现LogCounterDelayInterface接口-保存到本地数据库
* 作 者:java潇邦
* 时 间:2018-05-19
*/
@Component("logCounterLocalBusiness")
public class LogCounterLocalBusiness {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static ReentrantLock lock = new ReentrantLock();
private static String domain = LogCounterUtil.getDomain();
private static long tryLockTime = 2;//尝试2秒获取锁的时间
private static long refreshTime = 30000L;//30秒刷新一次
private static ConcurrentHashMap<String, AtomicLong> countMap = new ConcurrentHashMap<String, AtomicLong>(32);//并发计数
private static ConcurrentHashMap<String, Long> timeMap = new ConcurrentHashMap<String, Long>(32);//并发计时
private static long detailRefreshTime = 10 * 60 * 1000L;//默认10分钟刷新数据
private static ConcurrentHashMap<String, AtomicLong> detailCountMap = new ConcurrentHashMap<String, AtomicLong>(32);//并发计数
private static ConcurrentHashMap<String, Long> detailTimeMap = new ConcurrentHashMap<String, Long>(32);//并发计时
//懒加载
@Autowired(required = false)
private LogCounterDelayInterface logCounterDelayInterface;
/**
* 计数器逻辑处理
*/
public void execute(String counterName) {
commonMethod("total", counterName, refreshTime, countMap, timeMap);
commonMethod("detail", counterName, detailRefreshTime, detailCountMap, detailTimeMap);
}
private void commonMethod(String type, String counterName, long refreshTime, ConcurrentHashMap<String, AtomicLong> countMap, ConcurrentHashMap<String, Long> timeMap) {
try {
String key = counterName + domain;//唯一key
AtomicLong atomicLong = countMap.get(key);
if (null == atomicLong) {//原子计数器
synchronized (lock) {//并发控制
atomicLong = countMap.get(key);
if (null == atomicLong) {
countMap.put(key, new AtomicLong(1));
timeMap.put(key, System.currentTimeMillis());
return;
}
}
}
atomicLong.incrementAndGet();//原子计数器加1
boolean flag = (System.currentTimeMillis() - timeMap.get(key) > refreshTime) && (atomicLong.get() > 0);//超过N秒 并且 次数大于0
if (flag) {//满足条件的漏斗
if (lock.tryLock(tryLockTime, TimeUnit.SECONDS)) {
try {
flag = (System.currentTimeMillis() - timeMap.get(key) > refreshTime) && (atomicLong.get() > 0);//避免错误更新
if (flag) {
long counter = countMap.get(key).get();
timeMap.put(key, System.currentTimeMillis());//重新计时
atomicLong.set(0);
countMap.put(key, atomicLong);//重新计数
if ("detail".equals(type)) {
logCounterDelayInterface.incrementDetailCounter(counterName, domain, counter, refreshTime);
} else {
logCounterDelayInterface.incrementCounter(counterName, domain, counter);
}
}
} finally {
lock.unlock();
}
} else {
logger.warn("2秒内没有获取到锁则放弃,下次再调用保存数据的服务:{}", key);
}
}
} catch (Exception e) {
logger.info("local计数器异常:", e);
}
}
public static void setDomain(String domain) {
LogCounterLocalBusiness.domain = domain;
}
public static String getDomain() {
return domain;
}
public static long getRefreshTime() {
return refreshTime;
}
public static void setRefreshTime(long refreshTime) {
LogCounterLocalBusiness.refreshTime = refreshTime;
}
public static long getDetailRefreshTime() {
return detailRefreshTime;
}
public static void setDetailRefreshTime(long detailRefreshTime) {
LogCounterLocalBusiness.detailRefreshTime = detailRefreshTime;
}
}