Spring Boot利用AOP切面实现接口监控

一、实现功能

服务的每次接口调用,对于超过标准执行时长(单位:毫秒)的接口,需要记录到缓存中;

1.记录到缓存的操作需要进行异步处理;
2.第二天凌晨2.50执行定时任务,将数据插入到数据库t_slow_api_record表中;
3.执行时长阈值从字典表获取,如果没有获取到,则默认为5000ms,从字典表获取的阈值数据需要缓存半小时;

分析:要实现该功能,需要知道服务 当前正被调用的接口是什么?被调用的接口的执行开始时间,以及结束时间,两者取差值,便是该接口的执行时长;再与阈值进行比较,大于阈值的,便是超过标准执行时长的接口,进行记录,以便后续对接口性能进行优化。

二、知识储备

1.如何获取当前正在调用的接口?

在Spring Boot中,获取当前Request(内含接口路径/api/xxx、接口方法POST、PUT、GET、DELETE等属性)实例的方法

HttpServletRequest request =
        ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // String apiPath=request.getServletPath(); /api/xxx
        // String method=request.getMethod(); POST|PUT|GET|DELETE|…

2.AOP切面

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。是Spring框架中的重要内容。

它的主要操作对象就是“切面”,通过注解@Aspect在类上进行声明;通知(advice)、切入点(pointcut)和连接点(JoinPoint)共同组成了切面(aspect)。

使用切面的好处,不需要在核心实现代码中添加冗余的代码。

AspectJ 支持 5 种类型的通知注解:

@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行
@AfterReturning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行

后置通知:不论目标方法是否发生异常都会执行,若无异常,则执行顺序在返回通知之后

返回通知:当核心业务代码执行完成后执行,发生异常不执行

贯穿于方法之中。相当于是将我们原本一条线执行的程序在各个部位切开,加入一些辅助操作。

切入点
用来确定哪些类需要代理
一个pointcut由两部分组成:
Pointcut signature:pointcut签名,类似于方法签名,但该方法返回值必须是void
Pointcut expression:@Pointcut注解中的模式定义表达式

常用的execution,用于匹配指定方法

execution(public * *(..))	//任意public方法 
execution(* set*(..))	//任意名称以set开头的方法 
execution(* com.example.service.AccountService.*(..))	//com.example.service.AccountService接口中定义的任意方法 
execution(* com.example.service.*.*(..))	//com.example.service包中定义的任意方法 
execution(* com.example.service..*.*(..))	//com.example.service包中及其子包中定义的任意方法

连接点
通过JoinPoint可以获取被代理方法的各种信息,如方法参数,方法所在类的class对象,然后执行反射操作。

	@Before("declareJoinPointerExpression()")
    public void beforeMethod(JoinPoint joinPoint){
        System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
        System.out.println("目标方法所属类的简单类名:" +        joinPoint.getSignature().getDeclaringType().getSimpleName());
        System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
        //获取传入目标方法的参数
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            System.out.println("第" + (i+1) + "个参数为:" + args[i]);
        }
        System.out.println("被代理的对象:" + joinPoint.getTarget());
        System.out.println("被代理的对象类名:" + joinPoint.getTarget().getClass().getName(););
        System.out.println("代理对象自己:" + joinPoint.getThis());
    }

简易模版:

@Slf4j
@Aspect
@Component
public class ApiMonitorAdvice {

  /** 定义切面 */
  @Pointcut(
      "execution(* com.boc.org.controller..*.*(..)) || execution(* com.boc.org.controller3rd..*.*(..)) ")
      // 为此类内一个空方法上面的注解。可以把拦截的地址表达式表示为方法签名,利于使用起来方便
  public void pointCut() {}

  @Before("pointCut()")
  public void doBefore(JoinPoint joinPoint) throws Throwable {
    // 执行前
  }
  
  @Around("pointCut()")
  public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
    // 执行中
  }

  /** 只有正常返回才会执行此方法 */
  @AfterReturning(returning = "returnVal", pointcut = "pointCut()")
  public void doAfterReturning(JoinPoint joinPoint, Object returnVal) {
    // 执行后
  }

  @AfterThrowing(pointcut = "pointCut()")
  public void doAfterThrowing(JoinPoint joinPoint) {
    // 执行后
  }
}

3.异步处理的实现

通过注解@Async+executor/TaskExecutor来实现;
@Async官方说明:
指定异步操作的限定符值。
可以用来确定执行异步操作时要使用的目标执行程序,匹配特定executor或TaskExecutor bean定义的限定符值(或bean名称)。
当在类级别的@Async注释上指定时,表示给定的执行器应该用于类中的所有方法。方法级使用async#值总是覆盖在类级设置的任何值。

executor: 配置线程池

@Configuration
@EnableAsync
public class ThreadUtils {
    private static final int CORE_POOL_SIZE = 5;

    private static final int MAX_POOL_SIZE = 10;

    private static final int QUEUE_CAPACITY = 1000;

    public static final String SENTIMENT_EVENT_EXECUTOR = "test_executor";

    @Bean(SENTIMENT_EVENT_EXECUTOR)
    public Executor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setKeepAliveSeconds(60);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}


同步调用:是指程序按预定顺序一步步执行,每一步必须等到上一步执行完后才能执行;
异步调用:则无需等待上一步程序执行完即可执行。【新开了一个线程】

log.info(System.currentTimeMillis()); // ①
doSync();//  ②
doAsync();// ③
log.info("finish") // ④

……

void doSync(){
	try{
		Thread.sleep(3000);//延迟3s
		log.info("同步方法")
	}catch(Exception e){
	log.warn(e.printLocalMessage());
	}

}

@Async(ThreadUtils.SENTIMENT_EVENT_EXECUTOR)//使用方式
void doAsync(){
	try{
		log.info("异步方法")
	}catch(Exception e){
	log.warn(e.printLocalMessage());
	}

}

打印输出:
2023-04-03 09:40:49.095 [http-nio-8084-exec-2] INFO com.example.congee.common.advice.ApiMonitorAdvice - 1680486049095
2023-04-03 09:40:49.099 [http-nio-8084-exec-2] INFO com.example.congee.common.advice.ApiMonitorAdvice - 异步
2023-04-03 09:40:52.140 [http-nio-8084-exec-2] INFO com.example.congee.common.advice.ApiMonitorAdvice - 同步
2023-04-03 09:40:52.260 [http-nio-8084-exec-2] INFO com.example.congee.common.advice.ApiMonitorAdvice - finish

可见③在②的前面执行,②的打印时间延迟了3s,doAsync方法没有等待doSync()执行完毕便执行了。

异步调用时,调用者会在调用时立即返回,而被调用方法的实际执行是交给 Spring 的 TaskExecutor 来完成的。所以被注解的方法被调用的时候,会在新的线程中执行,而调用它的方法会在原线程中执行,这样可以避免阻塞,以及保证任务的实时性。

4.缓存

缓存使用Redis进行存取,网上可以找到很多封装好的RedisTemplate。下为一种。

package com.example.congee.tool.redis;

import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @version 1.0.0 @ClassName BaseRedisOperator.java @Description BaseRedisOperator
 * @createTime 2020年06月03日 15:05:00
 */
@Component
public class BaseRedisOperator<H, HK, HV> {
  @Resource private RedisTemplate redisTemplate;
  /**************************************************** key 相关操作 *************************************************/

  /**
   * 实现命令 : KEYS pattern 查找所有符合 pattern 模式的 key ? 匹配单个字符 * 匹配0到多个字符 [a-c] 匹配a和c [ac] 匹配a到c [^a]
   * 匹配除了a以外的字符
   *
   * @param pattern redis pattern 表达式
   * @return
   */
  public Set<String> keys(String pattern) {
    return redisTemplate.keys(pattern);
  }

  /**
   * 实现命令 : DEL key1 [key2 ...] 删除一个或多个key
   *
   * @param keys
   * @return
   */
  public Long del(String... keys) {
    Set<String> keySet = Stream.of(keys).collect(Collectors.toSet());
    return redisTemplate.delete(keySet);
  }

  /**
   * 实现命令 : UNLINK key1 [key2 ...] 删除一个或多个key
   *
   * @param keys
   * @return
   */
  public Long unlink(String... keys) {
    Set<String> keySet = Stream.of(keys).collect(Collectors.toSet());
    return redisTemplate.unlink(keySet);
  }

  /**
   * 实现命令 : EXISTS key1 [key2 ...] 查看 key 是否存在,返回存在 key 的个数
   *
   * @param keys
   * @return
   */
  public Long exists(String... keys) {
    Set<String> keySet = Stream.of(keys).collect(Collectors.toSet());
    return redisTemplate.countExistingKeys(keySet);
  }

  /**
   * 实现命令 : 查看 key 是否存在
   *
   * @param key
   * @return
   */
  public Boolean hasKey(Object key) {
    return redisTemplate.hasKey(key);
  }

  /**
   * 实现命令 : TYPE key 查看 key 的 value 的类型
   *
   * @param key
   * @return
   */
  public String type(String key) {
    DataType dataType = redisTemplate.type(key);
    if (dataType == null) {
      return "";
    }
    return dataType.code();
  }

  /**
   * 实现命令 : PERSIST key 取消 key 的超时时间,持久化 key
   *
   * @param key
   * @return
   */
  public boolean persist(String key) {
    Boolean result = redisTemplate.persist(key);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : TTL key 返回给定 key 的剩余生存时间,key不存在返回 null 单位: 秒
   *
   * @param key
   * @return
   */
  public Long ttl(String key) {
    return redisTemplate.getExpire(key);
  }

  /**
   * 实现命令 : PTTL key 返回给定 key 的剩余生存时间,key不存在返回 null 单位: 毫秒
   *
   * @param key
   * @return
   */
  public Long pTtl(String key) {
    return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
  }

  /**
   * 实现命令 : EXPIRE key 秒 设置key 的生存时间 单位 : 秒
   *
   * @param key
   * @return
   */
  public boolean expire(String key, int ttl) {
    Boolean result = redisTemplate.expire(key, ttl, TimeUnit.SECONDS);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : PEXPIRE key 毫秒 设置key 的生存时间 单位 : 毫秒
   *
   * @param key
   * @return
   */
  public boolean pExpire(String key, Long ttl) {
    Boolean result = redisTemplate.expire(key, ttl, TimeUnit.MILLISECONDS);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : EXPIREAT key Unix时间戳(自1970年1月1日以来的秒数) 设置key 的过期时间
   *
   * @param key
   * @param date
   * @return
   */
  public boolean expireAt(String key, Date date) {
    Boolean result = redisTemplate.expireAt(key, date);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : RENAME key newkey 重命名key,如果newKey已经存在,则newKey的原值被覆盖
   *
   * @param oldKey
   * @param newKey
   */
  public void rename(String oldKey, String newKey) {
    redisTemplate.rename(oldKey, newKey);
  }

  /**
   * 实现命令 : RENAMENX key newkey 安全重命名key,newKey不存在时才重命名
   *
   * @param oldKey
   * @param newKey
   * @return
   */
  public boolean renameNx(String oldKey, String newKey) {
    Boolean result = redisTemplate.renameIfAbsent(oldKey, newKey);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**************************************************** key 相关操作 *************************************************/

  /************************************************* String 相关操作 *************************************************/

  /**
   * 实现命令 : SET key value 添加一个持久化的 String 类型的键值对
   *
   * @param key
   * @param value
   */
  public void set(String key, Object value) {
    redisTemplate.opsForValue().set(key, value);
  }

  /**
   * 实现命令 : SET key value EX 秒、 setex key value 秒 添加一个 String 类型的键值对,并设置生存时间
   *
   * @param key
   * @param value
   * @param ttl key 的生存时间,单位:秒
   */
  public void set(String key, Object value, int ttl) {
    redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
  }

  /**
   * 实现命令 : SET key value PX 毫秒 、 psetex key value 毫秒 添加一个 String 类型的键值对,并设置生存时间
   *
   * @param key
   * @param value
   * @param ttl key 的生存时间,单位:毫秒
   */
  public void set(String key, Object value, long ttl) {
    redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.MILLISECONDS);
  }

  /**
   * 实现命令 : SET key value [EX 秒|PX 毫秒] [NX|XX] 添加一个 String 类型的键值对, ttl、timeUnit 不为 null 时设置生存时间
   * keyIfExist 不为 null 时,设置 NX 或 XX 模式
   *
   * @param key
   * @param value
   * @param ttl 生存时间
   * @param timeUnit 生存时间的单位
   * @param keyIfExist true 表示 xx,key 存在时才添加. false 表示 nx,key 不存在时才添加
   */
  public boolean set(String key, Object value, Long ttl, TimeUnit timeUnit, Boolean keyIfExist) {
    Boolean result = false;

    if ((ttl == null || timeUnit == null) && (keyIfExist == null)) {
      // SET key value
      redisTemplate.opsForValue().set(key, value);
      result = true;
    }

    if (ttl != null && timeUnit != null && keyIfExist == null) {
      // SET key value [EX 秒|PX 毫秒]
      redisTemplate.opsForValue().set(key, value, ttl, timeUnit);
      result = true;
    }

    if ((ttl == null || timeUnit == null) && (keyIfExist != null) && keyIfExist) {
      // SET key value XX
      result = redisTemplate.opsForValue().setIfPresent(key, value);
    }

    if (ttl != null && timeUnit != null && keyIfExist != null && keyIfExist) {
      // SET key value [EX 秒|PX 毫秒] XX
      result = redisTemplate.opsForValue().setIfPresent(key, value, ttl, timeUnit);
    }

    if ((ttl == null || timeUnit == null) && (keyIfExist != null) && (!keyIfExist)) {
      // SET key value NX
      result = redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    if (ttl != null && timeUnit != null && keyIfExist != null && (!keyIfExist)) {
      // SET key value [EX 秒|PX 毫秒] NX
      result = redisTemplate.opsForValue().setIfAbsent(key, value, ttl, timeUnit);
    }

    if (result == null) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : 存放set集合
   *
   * @param key
   * @param
   */
  public void set(String key, Object... values) {
    redisTemplate.opsForSet().add(key, values);
  }

  /**
   * 实现命令 : 获取set集合
   *
   * @param key
   * @param
   */
  public Set<Object> members(String key) {
    return redisTemplate.opsForSet().members(key);
  }

  /**
   * 实现命令 : 批量移出
   *
   * @param key
   * @param
   */
  public void remove(String key, Object... values) {
    redisTemplate.opsForSet().remove(key, values);
  }

  /**
   * 实现命令 : MSET key1 value1 [key2 value2...] 安全批量添加键值对,只要有一个 key 已存在,所有的键值对都不会插入
   *
   * @param keyValueMap
   */
  public void mSet(Map<String, Object> keyValueMap) {
    redisTemplate.opsForValue().multiSetIfAbsent(keyValueMap);
  }

  /**
   * 实现命令 : MSETNX key1 value1 [key2 value2...] 批量添加键值对
   *
   * @param keyValueMap
   */
  public void mSetNx(Map<String, Object> keyValueMap) {
    redisTemplate.opsForValue().multiSet(keyValueMap);
  }

  /**
   * 实现命令 : SETRANGE key 下标 str 覆盖 原始 value 的一部分,从指定的下标开始覆盖, 覆盖的长度为指定的字符串的长度。
   *
   * @param key
   * @param str 字符串
   * @param offset 开始覆盖的位置,包括开始位置,下标从0开始
   */
  public void setRange(String key, Object str, int offset) {
    redisTemplate.opsForValue().set(key, str, offset);
  }

  /**
   * 实现命令 : APPEND key value 在原始 value 末尾追加字符串
   *
   * @param key
   * @param str 要追加的字符串
   */
  public void append(String key, String str) {
    redisTemplate.opsForValue().append(key, str);
  }

  /**
   * 实现命令 : GETSET key value 设置 key 的 value 并返回旧 value
   *
   * @param key
   * @param value
   */
  public Object getSet(String key, Object value) {
    return redisTemplate.opsForValue().getAndSet(key, value);
  }

  /**
   * 实现命令 : GET key 获取一个key的value
   *
   * @param key
   * @return value
   */
  public Object get(String key) {
    return redisTemplate.opsForValue().get(key);
  }

  /**
   * 实现命令 : MGET key1 [key2...] 获取多个key的value
   *
   * @param keys
   * @return value
   */
  public List<Object> mGet(String... keys) {
    Set<String> keySet = Stream.of(keys).collect(Collectors.toSet());
    return redisTemplate.opsForValue().multiGet(keySet);
  }

  /**
   * 实现命令 : GETRANGE key 开始下标 结束下标 获取指定key的value的子串,下标从0开始,包括开始下标,也包括结束下标。
   *
   * @param key
   * @param start 开始下标
   * @param end 结束下标
   * @return
   */
  public String getRange(String key, int start, int end) {
    return redisTemplate.opsForValue().get(key, start, end);
  }

  /**
   * 实现命令 : STRLEN key 获取 key 对应 value 的字符串长度
   *
   * @param key
   * @return
   */
  public Long strLen(String key) {
    return redisTemplate.opsForValue().size(key);
  }

  /**
   * 实现命令 : INCR key 给 value 加 1,value 必须是整数
   *
   * @param key
   * @return
   */
  public Long inCr(String key) {
    return redisTemplate.opsForValue().increment(key);
  }

  /**
   * 实现命令 : INCRBY key 整数 给 value 加 上一个整数,value 必须是整数
   *
   * @param key
   * @return
   */
  public Long inCrBy(String key, Long number) {
    return redisTemplate.opsForValue().increment(key, number);
  }

  /**
   * 实现命令 : INCRBYFLOAT key 数 给 value 加上一个小数,value 必须是数
   *
   * @param key
   * @return
   */
  public Double inCrByFloat(String key, double number) {
    return redisTemplate.opsForValue().increment(key, number);
  }

  /**
   * 实现命令 : DECR key 给 value 减去 1,value 必须是整数
   *
   * @param key
   * @return
   */
  public Long deCr(String key) {
    return redisTemplate.opsForValue().decrement(key);
  }

  /**
   * 实现命令 : DECRBY key 整数 给 value 减去一个整数,value 必须是整数
   *
   * @param key
   * @return
   */
  public Long deCcrBy(String key, Long number) {
    return redisTemplate.opsForValue().decrement(key, number);
  }

  /************************************************* String 相关操作 *************************************************/

  /************************************************* Hash 相关操作 ***************************************************/

  /**
   * 实现hash的scan命令
   *
   * @param key
   * @param options
   * @return
   */
  public Cursor<Map.Entry<HK, HV>> hscan(H key, ScanOptions options) {
    return redisTemplate.opsForHash().scan(key, options);
  }

  /**
   * 实现命令 : HSET key field value 添加 hash 类型的键值对,如果字段已经存在,则将其覆盖。
   *
   * @param key
   * @param field
   * @param value
   */
  public void hSet(String key, String field, Object value) {
    redisTemplate.opsForHash().put(key, field, value);
  }

  /**
   * 实现命令 : HSET key field1 value1 [field2 value2 ...] 添加 hash 类型的键值对,如果字段已经存在,则将其覆盖。
   *
   * @param key
   * @param map
   */
  public void hSet(String key, Map<String, Object> map) {
    redisTemplate.opsForHash().putAll(key, map);
  }

  /**
   * 实现命令 : HSETNX key field value 添加 hash 类型的键值对,如果字段不存在,才添加
   *
   * @param key
   * @param field
   * @param value
   */
  public boolean hSetNx(String key, String field, Object value) {
    Boolean result = redisTemplate.opsForHash().putIfAbsent(key, field, value);
    if (result == null) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : HGET key field 返回 field 对应的值
   *
   * @param key
   * @param field
   * @return
   */
  public Object hGet(String key, String field) {
    return redisTemplate.opsForHash().get(key, field);
  }

  /**
   * 实现命令 : HMGET key field1 [field2 ...] 返回 多个 field 对应的值
   *
   * @param key
   * @param fields
   * @return
   */
  public List<Object> hGet(String key, String... fields) {
    Set<Object> fieldSet = Stream.of(fields).collect(Collectors.toSet());
    return redisTemplate.opsForHash().multiGet(key, fieldSet);
  }

  /**
   * 实现命令 : HGETALL key 返回所以的键值对
   *
   * @param key
   * @return
   */
  public Map<Object, Object> hGetAll(String key) {
    return redisTemplate.opsForHash().entries(key);
  }

  /**
   * 实现命令 : HKEYS key 获取所有的 field
   *
   * @param key
   * @return
   */
  public Set<Object> hKeys(String key) {
    return redisTemplate.opsForHash().keys(key);
  }

  /**
   * 实现命令 : HVALS key 获取所有的 value
   *
   * @param key
   * @return
   */
  public List<Object> hValue(String key) {
    return redisTemplate.opsForHash().values(key);
  }

  /**
   * 实现命令 : HDEL key field [field ...] 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
   *
   * @param key
   * @param fields
   */
  public Long hDel(String key, Object... fields) {
    return redisTemplate.opsForHash().delete(key, fields);
  }

  /**
   * 实现命令 : HEXISTS key field 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
   *
   * @param key
   * @param field
   */
  public boolean hExists(String key, String field) {
    Boolean result = redisTemplate.opsForHash().hasKey(key, field);
    if (result == null) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : HLEN key 获取 hash 中字段:值对的数量
   *
   * @param key
   */
  public Long hLen(String key) {
    return redisTemplate.opsForHash().size(key);
  }

  /**
   * 实现命令 : HSTRLEN key field 获取字段对应值的长度
   *
   * @param key
   * @param field
   */
  public Long hStrLen(String key, String field) {
    return redisTemplate.opsForHash().lengthOfValue(key, field);
  }

  /**
   * 实现命令 : HINCRBY key field 整数 给字段的值加上一个整数
   *
   * @param key
   * @param field
   * @return 运算后的值
   */
  public Long hInCrBy(String key, String field, long number) {
    return redisTemplate.opsForHash().increment(key, field, number);
  }

  /**
   * 实现命令 : HINCRBYFLOAT key field 浮点数 给字段的值加上一个浮点数
   *
   * @param key
   * @param field
   * @return 运算后的值
   */
  public Double hInCrByFloat(String key, String field, Double number) {
    return redisTemplate.opsForHash().increment(key, field, number);
  }

  /************************************************* Hash 相关操作 ***************************************************/

  /************************************************* List 相关操作 ***************************************************/

  /**
   * 实现命令 : LPUSH key 元素1 [元素2 ...] 在最左端推入元素
   *
   * @param key
   * @param values
   * @return 执行 LPUSH 命令后,列表的长度。
   */
  public Long lPush(String key, Object... values) {
    return redisTemplate.opsForList().leftPushAll(key, values);
  }

  /**
   * 实现命令 : RPUSH key 元素1 [元素2 ...] 在最右端推入元素
   *
   * @param key
   * @param values
   * @return 执行 RPUSH 命令后,列表的长度。
   */
  public Long rPush(String key, Object... values) {
    return redisTemplate.opsForList().rightPushAll(key, values);
  }

  /**
   * 实现命令 : LPOP key 弹出最左端的元素
   *
   * @param key
   * @return 弹出的元素
   */
  public Object lPop(String key) {
    return redisTemplate.opsForList().leftPop(key);
  }

  /**
   * 实现命令 : RPOP key 弹出最右端的元素
   *
   * @param key
   * @return 弹出的元素
   */
  public Object rPop(String key) {
    return redisTemplate.opsForList().rightPop(key);
  }

  /**
   * 实现命令 : BLPOP key (阻塞式)弹出最左端的元素,如果 key 中没有元素,将一直等待直到有元素或超时为止
   *
   * @param key
   * @param timeout 等待的时间,单位秒
   * @return 弹出的元素
   */
  public Object bLPop(String key, int timeout) {
    return redisTemplate.opsForList().leftPop(key, timeout, TimeUnit.SECONDS);
  }

  /**
   * 实现命令 : BRPOP key (阻塞式)弹出最右端的元素,将一直等待直到有元素或超时为止
   *
   * @param key
   * @return 弹出的元素
   */
  public Object bRPop(String key, int timeout) {
    return redisTemplate.opsForList().rightPop(key, timeout, TimeUnit.SECONDS);
  }

  /**
   * 实现命令 : LINDEX key index 返回指定下标处的元素,下标从0开始
   *
   * @param key
   * @param index
   * @return 指定下标处的元素
   */
  public Object lIndex(String key, int index) {
    return redisTemplate.opsForList().index(key, index);
  }

  /**
   * 实现命令 : LRANGE key 开始下标 结束下标 获取指定范围的元素,下标从0开始,包括开始下标,也包括结束下标(待验证)
   *
   * @param key
   * @param start
   * @param end
   * @return
   */
  public List<Object> lRange(String key, int start, int end) {
    return redisTemplate.opsForList().range(key, start, end);
  }

  /**
   * 实现命令 : LLEN key 获取 list 的长度
   *
   * @param key
   * @return
   */
  public Long lLen(String key) {
    return redisTemplate.opsForList().size(key);
  }

  /**
   * 实现命令 : LREM key count 元素 删除 count 个指定元素,
   *
   * @param key
   * @param count count > 0: 从头往尾移除指定元素。count < 0: 从尾往头移除指定元素。count = 0: 移除列表所有的指定元素。(未验证)
   * @param value
   * @return
   */
  public Long lLen(String key, int count, Object value) {
    return redisTemplate.opsForList().remove(key, count, value);
  }

  /**
   * 实现命令 : LSET key index 新值 更新指定下标的值,下标从 0 开始,支持负下标,-1表示最右端的元素(未验证)
   *
   * @param key
   * @param index
   * @param value
   * @return
   */
  public void lSet(String key, int index, Object value) {
    redisTemplate.opsForList().set(key, index, value);
  }

  /**
   * 实现命令 : LTRIM key 开始下标 结束下标 裁剪 list。[01234] 的 `LTRIM key 1 -2` 的结果为 [123]
   *
   * @param key
   * @param start
   * @param end
   * @return
   */
  public void lTrim(String key, int start, int end) {
    redisTemplate.opsForList().trim(key, start, end);
  }

  /**
   * 实现命令 : RPOPLPUSH 源list 目标list 将 源list 的最右端元素弹出,推入到 目标list 的最左端,
   *
   * @param sourceKey 源list
   * @param targetKey 目标list
   * @return 弹出的元素
   */
  public Object rPopLPush(String sourceKey, String targetKey) {
    return redisTemplate.opsForList().rightPopAndLeftPush(sourceKey, targetKey);
  }

  /**
   * 实现命令 : BRPOPLPUSH 源list 目标list timeout (阻塞式)将 源list 的最右端元素弹出,推入到 目标list 的最左端,如果 源list
   * 没有元素,将一直等待直到有元素或超时为止
   *
   * @param sourceKey 源list
   * @param targetKey 目标list
   * @param timeout 超时时间,单位秒, 0表示无限阻塞
   * @return 弹出的元素
   */
  public Object bRPopLPush(String sourceKey, String targetKey, int timeout) {
    return redisTemplate
        .opsForList()
        .rightPopAndLeftPush(sourceKey, targetKey, timeout, TimeUnit.SECONDS);
  }

  /************************************************* List 相关操作 ***************************************************/

  /************************************************** SET 相关操作 ***************************************************/

  /**
   * 实现命令 : SADD key member1 [member2 ...] 添加成员
   *
   * @param key
   * @param values
   * @return 添加成功的个数
   */
  public Long sAdd(String key, Object... values) {
    return redisTemplate.opsForSet().add(key, values);
  }

  /**
   * 实现命令 : SREM key member1 [member2 ...] 删除指定的成员
   *
   * @param key
   * @param values
   * @return 删除成功的个数
   */
  public Long sRem(String key, Object... values) {
    return redisTemplate.opsForSet().remove(key, values);
  }

  /**
   * 实现命令 : SCARD key 获取set中的成员数量
   *
   * @param key
   * @return
   */
  public Long sCard(String key) {
    return redisTemplate.opsForSet().size(key);
  }

  /**
   * 实现命令 : SISMEMBER key member 查看成员是否存在
   *
   * @param key
   * @param values
   * @return
   */
  public boolean sIsMember(String key, Object... values) {
    Boolean result = redisTemplate.opsForSet().isMember(key, values);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : SMEMBERS key 获取所有的成员
   *
   * @param key
   * @return
   */
  public Set<Object> sMembers(String key) {
    return redisTemplate.opsForSet().members(key);
  }

  /**
   * 实现命令 : SMOVE 源key 目标key member 移动成员到另一个集合
   *
   * @param sourceKey 源key
   * @param targetKey 目标key
   * @param value
   * @return
   */
  public boolean sMove(String sourceKey, String targetKey, Object value) {
    Boolean result = redisTemplate.opsForSet().move(sourceKey, value, targetKey);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : SDIFF key [otherKey ...] 求 key 的差集
   *
   * @param key
   * @param otherKeys
   * @return
   */
  public Set<Object> sDiff(String key, String... otherKeys) {
    List<String> otherKeyList = Stream.of(otherKeys).collect(Collectors.toList());
    return redisTemplate.opsForSet().difference(key, otherKeyList);
  }

  /**
   * 实现命令 : SDIFFSTORE 目标key key [otherKey ...] 存储 key 的差集
   *
   * @param targetKey 目标key
   * @param key
   * @param otherKeys
   * @return
   */
  public Long sDiffStore(String targetKey, String key, String... otherKeys) {
    List<String> otherKeyList = Stream.of(otherKeys).collect(Collectors.toList());
    return redisTemplate.opsForSet().differenceAndStore(key, otherKeyList, targetKey);
  }

  /**
   * 实现命令 : SINTER key [otherKey ...] 求 key 的交集
   *
   * @param key
   * @param otherKeys
   * @return
   */
  public Set<Object> sInter(String key, String... otherKeys) {
    List<String> otherKeyList = Stream.of(otherKeys).collect(Collectors.toList());
    return redisTemplate.opsForSet().intersect(key, otherKeyList);
  }

  /**
   * 实现命令 : SINTERSTORE 目标key key [otherKey ...] 存储 key 的交集
   *
   * @param targetKey 目标key
   * @param key
   * @param otherKeys
   * @return
   */
  public Long sInterStore(String targetKey, String key, String... otherKeys) {
    List<String> otherKeyList = Stream.of(otherKeys).collect(Collectors.toList());
    return redisTemplate.opsForSet().intersectAndStore(key, otherKeyList, targetKey);
  }

  /**
   * 实现命令 : SUNION key [otherKey ...] 求 key 的并集
   *
   * @param key
   * @param otherKeys
   * @return
   */
  public Set<Object> sUnion(String key, String... otherKeys) {
    List<String> otherKeyList = Stream.of(otherKeys).collect(Collectors.toList());
    return redisTemplate.opsForSet().union(key, otherKeyList);
  }

  /**
   * 实现命令 : SUNIONSTORE 目标key key [otherKey ...] 存储 key 的并集
   *
   * @param targetKey 目标key
   * @param key
   * @param otherKeys
   * @return
   */
  public Long sUnionStore(String targetKey, String key, String... otherKeys) {
    List<String> otherKeyList = Stream.of(otherKeys).collect(Collectors.toList());
    return redisTemplate.opsForSet().unionAndStore(key, otherKeyList, targetKey);
  }

  /**
   * 实现命令 : SPOP key [count] 随机删除(弹出)指定个数的成员
   *
   * @param key
   * @return
   */
  public Object sPop(String key) {
    return redisTemplate.opsForSet().pop(key);
  }

  /**
   * 实现命令 : SPOP key [count] 随机删除(弹出)指定个数的成员
   *
   * @param key
   * @param count 个数
   * @return
   */
  public List<Object> sPop(String key, int count) {
    return redisTemplate.opsForSet().pop(key, count);
  }

  /**
   * 实现命令 : SRANDMEMBER key [count] 随机返回指定个数的成员
   *
   * @param key
   * @return
   */
  public Object sRandMember(String key) {
    return redisTemplate.opsForSet().randomMember(key);
  }

  /**
   * 实现命令 : SRANDMEMBER key [count] 随机返回指定个数的成员 如果 count 为正数,随机返回 count 个不同成员 如果 count 为负数,随机选择 1
   * 个成员,返回 count 个
   *
   * @param key
   * @param count 个数
   * @return
   */
  public List<Object> sRandMember(String key, int count) {
    return redisTemplate.opsForSet().randomMembers(key, count);
  }

  /************************************************** SET 相关操作 ***************************************************/

  /*********************************************** Sorted SET 相关操作 ***********************************************/

  /**
   * 实现命令 : ZADD key score member 添加一个 成员/分数 对
   *
   * @param key
   * @param value 成员
   * @param score 分数
   * @return
   */
  public boolean zAdd(String key, double score, Object value) {
    Boolean result = redisTemplate.opsForZSet().add(key, value, score);
    if (result == null) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : ZREM key member [member ...] 删除成员
   *
   * @param key
   * @param values
   * @return
   */
  public Long zRem(String key, Object... values) {
    return redisTemplate.opsForZSet().remove(key, values);
  }

  /**
   * 实现命令 : ZREMRANGEBYRANK key start stop 删除 start下标 和 end下标间的所有成员
   * 下标从0开始,支持负下标,-1表示最右端成员,包括开始下标也包括结束下标 (未验证)
   *
   * @param key
   * @param start 开始下标
   * @param end 结束下标
   * @return
   */
  public Long zRemRangeByRank(String key, int start, int end) {
    return redisTemplate.opsForZSet().removeRange(key, start, end);
  }

  /**
   * 实现命令 : ZREMRANGEBYSCORE key start stop 删除分数段内的所有成员 包括min也包括max (未验证)
   *
   * @param key
   * @param min 小分数
   * @param max 大分数
   * @return
   */
  public Long zRemRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
  }

  /**
   * 实现命令 : ZSCORE key member 获取成员的分数
   *
   * @param key
   * @param value
   * @return
   */
  public Double zScore(String key, Object value) {
    return redisTemplate.opsForZSet().score(key, value);
  }

  /**
   * 实现命令 : ZINCRBY key 带符号的双精度浮点数 member 增减成员的分数
   *
   * @param key
   * @param value
   * @param delta 带符号的双精度浮点数
   * @return
   */
  public Double zInCrBy(String key, Object value, double delta) {
    return redisTemplate.opsForZSet().incrementScore(key, value, delta);
  }

  /**
   * 实现命令 : ZCARD key 获取集合中成员的个数
   *
   * @param key
   * @return
   */
  public Long zCard(String key) {
    return redisTemplate.opsForZSet().size(key);
  }

  /**
   * 实现命令 : ZCOUNT key min max 获取某个分数范围内的成员个数,包括min也包括max (未验证)
   *
   * @param key
   * @param min 小分数
   * @param max 大分数
   * @return
   */
  public Long zCount(String key, double min, double max) {
    return redisTemplate.opsForZSet().count(key, min, max);
  }

  /**
   * 实现命令 : ZRANK key member 按分数从小到大获取成员在有序集合中的排名
   *
   * @param key
   * @param value
   * @return
   */
  public Long zRank(String key, Object value) {
    return redisTemplate.opsForZSet().rank(key, value);
  }

  /**
   * 实现命令 : ZREVRANK key member 按分数从大到小获取成员在有序集合中的排名
   *
   * @param key
   * @param value
   * @return
   */
  public Long zRevRank(String key, Object value) {
    return redisTemplate.opsForZSet().reverseRank(key, value);
  }

  /**
   * 实现命令 : ZRANGE key start end 获取 start下标到 end下标之间到成员,并按分数从小到大返回
   * 下标从0开始,支持负下标,-1表示最后一个成员,包括开始下标,也包括结束下标(未验证)
   *
   * @param key
   * @param start 开始下标
   * @param end 结束下标
   * @return
   */
  public Set<Object> zRange(String key, int start, int end) {
    return redisTemplate.opsForZSet().range(key, start, end);
  }

  /**
   * 实现命令 : ZREVRANGE key start end 获取 start下标到 end下标之间到成员,并按分数从小到大返回
   * 下标从0开始,支持负下标,-1表示最后一个成员,包括开始下标,也包括结束下标(未验证)
   *
   * @param key
   * @param start 开始下标
   * @param end 结束下标
   * @return
   */
  public Set<Object> zRevRange(String key, int start, int end) {
    return redisTemplate.opsForZSet().reverseRange(key, start, end);
  }

  /**
   * 实现命令 : ZRANGEBYSCORE key min max 获取分数范围内的成员并按从小到大返回 (未验证)
   *
   * @param key
   * @param min 小分数
   * @param max 大分数
   * @return
   */
  public Set<Object> zRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().rangeByScore(key, min, max);
  }

  /**
   * 实现命令 : ZRANGEBYSCORE key min max LIMIT offset count 分页获取分数范围内的成员并按从小到大返回 包括min也包括max(未验证)
   *
   * @param key
   * @param min 小分数
   * @param max 大分数
   * @param offset 开始下标,下标从0开始
   * @param count 取多少条
   * @return
   */
  public Set<Object> zRangeByScore(String key, double min, double max, int offset, int count) {
    return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
  }

  /**
   * 实现命令 : ZREVRANGEBYSCORE key min max 获取分数范围内的成员并按从大到小返回 (未验证)
   *
   * @param key
   * @param min 小分数
   * @param max 大分数
   * @return
   */
  public Set<Object> zRevRangeByScore(String key, double min, double max) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
  }

  /**
   * 实现命令 : ZREVRANGEBYSCORE key min max LIMIT offset count 分页获取分数范围内的成员并按从大到小返回 包括min也包括max(未验证)
   *
   * @param key
   * @param min 小分数
   * @param max 大分数
   * @param offset 开始下标,下标从0开始
   * @param count 取多少条
   * @return
   */
  public Set<Object> zRevRangeByScore(String key, double min, double max, int offset, int count) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max, offset, count);
  }

  /*********************************************** Sorted SET 相关操作 ***********************************************/
}

继承的RedisOperator:

package com.example.congee.tool.redis;

import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Component
public class RedisOperator extends BaseRedisOperator {
  @Resource private RedisTemplate<String, Object> redisTemplate;

  /**************************************************** key 相关操作
   *************************************************/

  /**
   * 实现命令:DEL key1 [key2 ...] 删除任意多个 key
   *
   * @param keys
   * @return
   */
  public Long del(Collection<String> keys) {
    Set<String> keySet = new HashSet<>(keys);
    return redisTemplate.delete(keySet);
  }

  /**
   * 实现命令:UNLINK key1 [key2 ...] 删除任意多个 key
   *
   * @param keys
   * @return
   */
  public Long unlink(Collection<String> keys) {
    Set<String> keySet = new HashSet<>(keys);
    return redisTemplate.unlink(keySet);
  }

  /**
   * 设置缓存过期时间
   *
   * @param key
   * @param value
   * @param timeout
   * @param unit
   */
  public void setCacheObject(String key, String value, long timeout, TimeUnit unit) {
    redisTemplate.opsForValue().set(key, value, timeout, unit);
  }

  /**
   * 实现命令:EXISTS key1 [key2 ...] key去重后,查看 key 是否存在,返回存在 key 的个数
   *
   * @param keys
   * @return
   */
  public Long exists(Collection<String> keys) {
    Set<String> keySet = new HashSet<>(keys);
    return redisTemplate.countExistingKeys(keySet);
  }

  /**
   * 实现命令:EXPIREAT key Unix时间戳(自1970年1月1日以来的秒数) 设置key 的过期时间
   *
   * @param key
   * @param ttl 存活时间 单位秒
   * @return
   */
  public boolean expireAt(String key, int ttl) {
    Date date = new Date(System.currentTimeMillis() + ttl * 1000);
    Boolean result = redisTemplate.expireAt(key, date);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令:PEXPIREAT key 自1970年1月1日以来的毫秒数 设置key 的过期时间
   *
   * @param key
   * @param ttl 存活时间 单位毫秒
   * @return
   */
  public boolean pExpireAt(String key, long ttl) {
    Date date = new Date(System.currentTimeMillis() + ttl);
    Boolean result = redisTemplate.expireAt(key, date);
    if (null == result) {
      return false;
    }
    return result;
  }

  /**
   * 实现命令 : PEXPIREAT key 自1970年1月1日以来的毫秒数 设置key 的过期时间
   *
   * @param key
   * @param date
   * @return
   */
  public boolean pExpireAt(String key, Date date) {
    Boolean result = redisTemplate.expireAt(key, date);
    if (null == result) {
      return false;
    }
    return result;
  }

  public void release(String... key) {
    if (key != null && key.length > 0) {
      if (key.length == 1) {
        redisTemplate.delete(key[0]);
      } else {
        redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
      }
    }
  }

  /**************************************************** key 相关操作
   *************************************************/

  /************************************************* String 相关操作
   *************************************************/

  /**
   * 实现命令 : MGET key1 [key2...] key去重后,获取多个key的value
   *
   * @param keys
   * @return value
   */
  public List<Object> mGet(Collection<String> keys) {
    Set<String> keySet = new HashSet<>(keys);
    return redisTemplate.opsForValue().multiGet(keySet);
  }

  /************************************************* String 相关操作
   *************************************************/

  /************************************************* Hash 相关操作
   ***************************************************/

  /**
   * 实现命令 : HMGET key field1 [field2 ...] 返回 多个 field 对应的值
   *
   * @param key
   * @param fields
   * @return
   */
  public List<Object> hGet(String key, Collection<Object> fields) {
    return redisTemplate.opsForHash().multiGet(key, fields);
  }

  /**
   * 实现命令 : HDEL key field [field ...] 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
   *
   * @param key
   * @param fields
   */
  public Long hDel(String key, Collection<Object> fields) {
    Object[] objects = fields.toArray();
    return redisTemplate.opsForHash().delete(key, objects);
  }

  /************************************************* Hash 相关操作
   ***************************************************/

  /************************************************* List 相关操作
   ***************************************************/

  /**
   * 实现命令 : LPUSH key 元素1 [元素2 ...] 在最左端推入元素
   *
   * @param key
   * @param values
   * @return 执行 LPUSH命令后,列表的长度。
   */
  public Long lPush(String key, Collection<Object> values) {
    return redisTemplate.opsForList().leftPushAll(key, values);
  }

  /**
   * 实现命令 : LPUSHX key 元素 key 存在时在,最左端推入元素
   *
   * @param key
   * @param value
   * @return 执行 LPUSHX 命令后,列表的长度。
   */
  public Long lPushX(String key, Object value) {
    return redisTemplate.opsForList().leftPushIfPresent(key, value);
  }

  /**
   * 实现命令 : RPUSH key 元素1 [元素2 ...] 在最右端推入元素
   *
   * @param key
   * @param values
   * @return 执行 RPUSH 命令后,列表的长度。
   */
  public Long rPush(String key, Collection<Object> values) {
    return redisTemplate.opsForList().rightPushAll(key, values);
  }

  /**
   * 实现命令 : RPUSHX key 元素 key 存在时,在最右端推入元素
   *
   * @param key
   * @param value
   * @return 执行 RPUSHX 命令后,列表的长度。
   */
  public Long rPushX(String key, Object value) {
    return redisTemplate.opsForList().rightPushIfPresent(key, value);
  }

  /************************************************* List 相关操作
   ***************************************************/

  /************************************************** SET 相关操作
   ***************************************************/

  /**
   * 实现命令 : SADD key member1 [member2 ...] 添加成员
   *
   * @param key
   * @param values
   * @return 添加成功的个数
   */
  public Object rPopLpush(String key, Collection<Object> values) {
    Object[] members = values.toArray();
    return redisTemplate.opsForSet().add(key, members);
  }

  /**
   * 实现命令 : SREM key member1 [member2 ...] 删除指定的成员
   *
   * @param key
   * @param values
   * @return 删除成功的个数
   */
  public Long sRem(String key, Collection<Object> values) {
    Object[] members = values.toArray();
    return redisTemplate.opsForSet().remove(key, members);
  }

  /**
   * 实现命令 : SDIFF key [otherKey ...] 求 key 的差集
   *
   * @param key
   * @param otherKeys
   * @return
   */
  public Set<Object> sDiff(String key, List<String> otherKeys) {
    return redisTemplate.opsForSet().difference(key, otherKeys);
  }

  /**
   * 实现命令 : SDIFFSTORE 目标key key [otherKey ...] 存储 key 的差集
   *
   * @param targetKey 目标key
   * @param key
   * @param otherKeys
   * @return
   */
  public Long sDiffStore(String targetKey, String key, List<String> otherKeys) {
    return redisTemplate.opsForSet().differenceAndStore(key, otherKeys, targetKey);
  }

  /**
   * 实现命令 : SINTER key [otherKey ...] 求 key 的交集
   *
   * @param key
   * @param otherKeys
   * @return
   */
  public Set<Object> sInter(String key, List<String> otherKeys) {
    return redisTemplate.opsForSet().intersect(key, otherKeys);
  }

  /**
   * 实现命令 : SINTERSTORE 目标key key [otherKey ...] 存储 key 的交集
   *
   * @param targetKey 目标key
   * @param key
   * @param otherKeys
   * @return
   */
  public Long sInterStore(String targetKey, String key, List<String> otherKeys) {
    return redisTemplate.opsForSet().intersectAndStore(key, otherKeys, targetKey);
  }

  /**
   * 实现命令 : SUNION key [otherKey ...] 求 key 的并集
   *
   * @param key
   * @param otherKeys
   * @return
   */
  public Set<Object> sUnion(String key, List<String> otherKeys) {
    return redisTemplate.opsForSet().union(key, otherKeys);
  }

  /**
   * 实现命令 : SUNIONSTORE 目标key key [otherKey ...] 存储 key 的并集
   *
   * @param targetKey 目标key
   * @param key
   * @param otherKeys
   * @return
   */
  public Long sUnionStore(String targetKey, String key, List<String> otherKeys) {
    return redisTemplate.opsForSet().unionAndStore(key, otherKeys, targetKey);
  }

  /************************************************** SET 相关操作
   ***************************************************/

  /*********************************************** Sorted SET 相关操作
   ***********************************************/

  /**
   * 实现命令 : ZADD key score1 member1 [score2 member2 ...] 批量添加 成员/分数 对
   *
   * @param key
   * @param tuples
   * @return
   */
  public Long zAdd(String key, Set<ZSetOperations.TypedTuple<Object>> tuples) {
    return redisTemplate.opsForZSet().add(key, tuples);
  }

  /**
   * 实现命令 : ZREM key member [member ...] 删除成员
   *
   * @param key
   * @param values
   * @return
   */
  public Long zRem(String key, Collection<Object> values) {
    Object[] members = values.toArray();
    return redisTemplate.opsForZSet().remove(key, members);
  }

  /**
   * 实现命令 : ZUNIONSTORE destination numkeys key1 [key2 ...]
   * 计算指定key集合与otherKey集合的并集,并保存到targetKey集合,aggregat 默认为 SUM
   *
   * @param targetKey 目标集合
   * @param key 指定集合
   * @param otherKey 其他集合
   * @return
   */
  public Long zUnionStore(String targetKey, String key, String otherKey) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKey, targetKey);
  }

  /**
   * 实现命令 : ZUNIONSTORE destination numkeys key1 [key2 ...]
   * 计算指定key集合与otherKey集合的并集,并保存到targetKey集合,aggregat 默认为 SUM
   *
   * @param targetKey 目标集合
   * @param key 指定集合
   * @param otherKeys 其他集合
   * @return
   */
  public Long zUnionStore(String targetKey, String key, Collection<String> otherKeys) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKeys, targetKey);
  }

  /**
   * 实现命令 : ZUNIONSTORE destination numkeys key1 [key2 ...][AGGREGATE SUM | MIN | MAX]
   * 计算指定key集合与otherKey集合的并集,并保存到targetKey集合
   *
   * @param targetKey 目标集合
   * @param key 指定集合
   * @param otherKeys 其他集合
   * @param aggregat SUM 将同一成员的分数相加, MIN 取同一成员中分数最小的, MAX 取同一成员中分数最大的
   * @return
   */
  public Long zUnionStore(
      String targetKey,
      String key,
      Collection<String> otherKeys,
      RedisZSetCommands.Aggregate aggregat) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKeys, targetKey, aggregat);
  }

  /**
   * 实现命令 : ZINTERSTORE destination numkeys key1 [key2 ...]
   * 计算指定key集合与otherKey集合的交集,并保存到targetKey集合,aggregat 默认为 SUM
   *
   * @param targetKey 目标集合
   * @param key 指定集合
   * @param otherKey 其他集合
   * @return
   */
  public Long zInterStore(String targetKey, String key, String otherKey) {
    return redisTemplate.opsForZSet().intersectAndStore(key, otherKey, targetKey);
  }

  /**
   * 实现命令 : ZINTERSTORE destination numkeys key1 [key2 ...]
   * 计算指定key集合与otherKey集合的交集,并保存到targetKey集合,aggregat 默认为 SUM
   *
   * @param targetKey 目标集合
   * @param key 指定集合
   * @param otherKeys 其他集合
   * @return
   */
  public Long zInterStore(String targetKey, String key, Collection<String> otherKeys) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKeys, targetKey);
  }

  /**
   * 实现命令 : ZINTERSTORE destination numkeys key1 [key2 ...][AGGREGATE SUM | MIN | MAX]
   * 计算指定key集合与otherKey集合的交集,并保存到targetKey集合
   *
   * @param targetKey 目标集合
   * @param key 指定集合
   * @param otherKeys 其他集合
   * @param aggregat SUM 将同一成员的分数相加, MIN 取同一成员中分数最小的, MAX 取同一成员中分数最大的
   * @return
   */
  public Long zInterStore(
      String targetKey,
      String key,
      Collection<String> otherKeys,
      RedisZSetCommands.Aggregate aggregat) {
    return redisTemplate.opsForZSet().unionAndStore(key, otherKeys, targetKey, aggregat);
  }

  /*********************************************** Sorted SET 相关操作
   ***********************************************/

}

使用时,直接用RedisOperator,没有的功能可以进行补充。

5.定时任务

在主启动类上添加@EnableScheduling注解
在要进行定时执行的任务上添加 @Scheduled(cron = "0 50 2 * * ?")注解,被注解的方法不能有参数,不能有返回值

三、功能实现

1. 定义AOP切面

package com.demo.congee.common.advice;

import com.demo.congee.aop.async.ApiMonitorUtil;
import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @author congee
 * @date 2023/3/28 09:50
 * @description 接口监控
 */
@Slf4j
@Aspect
@Component
public class ApiMonitorAdvice {
  @Autowired private ApiMonitorUtil apiMonitorUtil;

  ThreadLocal<Long> startTime = new ThreadLocal<>();
  ThreadLocal<Map<String, String>> requestThreadLocal = new ThreadLocal<>();

  /** 定义切面 */
  @Pointcut(
      "execution(* com.boc.org.controller..*.*(..)) || execution(* com.boc.org.controller3rd..*.*(..)) ")
  public void pointCut() {}

  @Before("pointCut()")
  public void doBefore(JoinPoint joinPoint) throws Throwable {
    startTime.set(System.currentTimeMillis());
    HttpServletRequest request =
        ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    requestThreadLocal.set(
        ImmutableMap.of("servletPath", request.getServletPath(), "method", request.getMethod()));
  }

  /** 只有正常返回才会执行此方法 */
  @AfterReturning(returning = "returnVal", pointcut = "pointCut()")
  public void doAfterReturning(JoinPoint joinPoint, Object returnVal) {
    long reqTime = System.currentTimeMillis() - startTime.get();
    apiMonitorUtil.updateResTime(reqTime, requestThreadLocal.get());
  }

  @AfterThrowing(pointcut = "pointCut()")
  public void doAfterThrowing(JoinPoint joinPoint) {
    long reqTime = System.currentTimeMillis() - startTime.get();
    apiMonitorUtil.updateResTime(reqTime, requestThreadLocal.get());
  }
}

2. 记录数据到缓存

@Slf4j
@Component
public class ApiMonitorUtil {
  @Autowired private RedisOperator redisOperator;
  @Resource private MonitorV2ServiceImpl monitorV2Service;

  /** 接口处理时长默认阈值 5000 单位:ms */
  public static final String API_DURATION_THRESHOLD = "5000";

  private static final String DICT_KEY = "api_duration_threshold";
  private static final String DICT_LABEL = "api_duration_threshold";

  @Async(ThreadUtils.SENTIMENT_EVENT_EXECUTOR)
  public void updateResTime(Long reqTime, Map<String, String> request) {
    Integer apiDurationThreshold = Integer.parseInt(getDurationThresholdStr());
    if (reqTime > apiDurationThreshold) {
      // 记录数据到缓存中,统计日期、服务名称(org、monitor、edu)、接口路径、接口方法、超标次数、最大执行时长
      String apiPath = request.get("servletPath");
      String method = request.get("method");

      log.info("API:{} EXEC TIME: [{}]ms", apiPath + "::" + method, reqTime);

      String key = keyPrefix(LocalDate.now()) + "Api";
      String exceedApi = apiPath + "::" + method;
      Map<String, Monitor> value = (Map<String, Monitor>) redisOperator.get(key);
      // 准备初始化数据
      Monitor monitor = new Monitor();
      monitor.setAriseTimes(1);
      monitor.setMaxDuration(Math.toIntExact(reqTime));
      // 赋值,更新
      if (MapUtils.isNotEmpty(value)) {
        if (value.containsKey(exceedApi)) {
          Integer oldTimes = value.get(exceedApi).getAriseTimes();
          value.get(exceedApi).setAriseTimes(oldTimes + 1);

          Integer oldReq = value.get(exceedApi).getMaxDuration();
          if (oldReq < reqTime) {
            // 更新最大超时时间
            value.get(exceedApi).setMaxDuration(Math.toIntExact(reqTime));
          }
        } else {
          value.put(exceedApi, monitor);
        }
        redisOperator.set(key, value, 3 * 24 * 60 * 60L, TimeUnit.SECONDS, true);
      } else {
        value = new HashMap<>();
        value.put(exceedApi, monitor);
        redisOperator.set(key, value, 3 * 24 * 60 * 60L, TimeUnit.SECONDS, false);
      }
    }
  }

  public static String keyPrefix(LocalDate statsDate) {
    if (statsDate == null) {
      throw new RuntimeException("统计日期不能为空");
    }
    String dateStr = statsDate.format(DateTimeFormatter.ofPattern(EConst.DATE_FORMAT_NO_DASH));
    return String.format("OPERATION_STATS::%s::", dateStr);
  }

  /**
   * 获取接口监控设定的耗时阈值
   *
   * @return
   */
  public String getDurationThresholdStr() {

    String durationThresholdStr = "";

    // 1.尝试从redis中获取设定的阈值字典列表
    if (!redisOperator.hasKey(DICT_LABEL)) {
      // 2.如果未从redis中获取到,则到表中查询对应的字典信息列表
      List<DictListRes> dictList = monitorV2Service.getDictListByDictType(DICT_KEY);
      if (CollectionUtils.isNotEmpty(dictList)) {
        Map<String, String> dictMap =
            dictList.stream()
                .collect(Collectors.toMap(DictListRes::getDictLabel, DictListRes::getDictValue));
        if (MapUtils.isNotEmpty(dictMap) && dictMap.containsKey(DICT_LABEL)) {
          durationThresholdStr = dictMap.get(DICT_LABEL);
          // 3.缓存到redis,并设置过期时间为半小时
          redisOperator.setCacheObject(DICT_LABEL, durationThresholdStr, 30L, TimeUnit.MINUTES);
        }
      }
    } else {
      durationThresholdStr = (String) redisOperator.get(DICT_LABEL);
    }

    if (StringUtils.isBlank(durationThresholdStr)) {
      durationThresholdStr = API_DURATION_THRESHOLD;
    }

    return durationThresholdStr;
  }
}

redis中的数据形式:

{
  "@class": "java.util.HashMap",
  "/test/msg::POST": {
    "@class": "com.example.congee.dto.Monitor",
    "ariseTimes": 23,
    "maxDuration": 5849
  },
  "/test/msg::GET": {
    "@class": "com.example.congee.dto.Monitor",
    "ariseTimes": 18,
    "maxDuration": 10267
  }
}

3. 定时任务将前一天的数据插入到数据库

@Slf4j
@Component
public class ScheduleTaskForApiMonitor extends AbstractScheduleTask {

  @Autowired private RedisOperator redisOperator;
  @Autowired private SlowApiRecordService recordService;

  /** 每日凌晨2:50触发,将数据插入到表t_slow_api_record表中 */
  @TaskLock(lockName = "addApiMonitorData")
  @Scheduled(cron = "0 50 2 * * ?")
  @Transactional(rollbackFor = Exception.class)
  public void addApiMonitorData() {
    log.info("开始统计接口监控数据.....");
    if (lock()) {
      try {
        String key = keyPrefix(LocalDate.now().minusDays(1)) + "Api";
        if (redisOperator.hasKey(key)) {
          List<SlowApiRecordAddDto> exceedTimeApis = new ArrayList<>();
          Map<String, Monitor> value = (Map<String, Monitor>) redisOperator.get(key);
          for (Map.Entry<String, Monitor> entry : value.entrySet()) {
            SlowApiRecordAddDto dto = new SlowApiRecordAddDto();
            dto.setApiPath(entry.getKey().split("::")[0]);
            dto.setApiMethod(entry.getKey().split("::")[1]);
            dto.setAriseTimes(entry.getValue().getAriseTimes());
            dto.setMaxDuration(entry.getValue().getMaxDuration());
            dto.setStatsDate(LocalDate.now().minusDays(1));
            dto.setServiceName("Api");
            exceedTimeApis.add(dto);
          }
          // 将数据插入到t_slow_api_record表中
          SlowApiRecordAddParam param = new SlowApiRecordAddParam();
          param.setSlowApiRecordList(exceedTimeApis);
          recordService.addSlowApiRecord(param);
        }
      } catch (Exception e) {
        opStatsUtil.incr(EConst.IDX_SCHEDULE_TASK_FAILED_TIMES);
        log.warn("统计接口监控数据时出现异常:{}", e.getLocalizedMessage());
      } finally {
        unlock();
      }
    }
    log.info("统计接口监控数据执行结束.....");
  }

  public static String keyPrefix(LocalDate statsDate) {
    if (statsDate == null) {
      throw new RuntimeException("统计日期不能为空");
    }
    String dateStr = statsDate.format(DateTimeFormatter.ofPattern(EConst.DATE_FORMAT_NO_DASH));
    return String.format("OPERATION_STATS::%s::", dateStr);
  }
}

四、问题记录

1. 异步注解不生效

  1. 异步方法使用 static 修饰;
  2. 异步类没有使用 @Component 注解(或其他注解)导致 Spring 无法扫描到异步类;
  3. 异步方法不能与被调用的异步方法在同一个类中;
  4. 类中需要使用 @Autowired 或 @Resource 等注解自动注入,不能手动 new 对象;
  5. 如果使用 Spring Boot 框架必须在启动类/线程池配置类(此例中的ThreadUtils)中增加 @EnableAsync 注解,主要是为了扫描范围包下的所有 @Async 注解。

原因:@Cacheable、@Transcation、@Async等注解是使用AOP 代理实现的 ,通过创建内部类来代理缓存方法(JDK动态代理、CGLib代理),这样就会导致一个问题,类内部的方法调用类内部的缓存方法不会走代理,不会走代理,就不能正常使用。

所以在此例中,新建了一个ApiMonitorUtil类来实现异步的功能。

2.为什么调用updateResTime方法时,不直接传入Request?

因为实际测试中发现,会报空指针异常null,在切面里面有值,在异步方法里调用时值为null。即异步调用的方法(子线程)无法准确获取父线程的Request数据

原因:一个请求到达容器后,Spring会把该请求Request实例通过setRequestAttributes方法 放入该请求线程内ThreadLocalMap中,然后就可以通过静态方法取到。但Spring所使用的默认的ThreadLocal不能让子线程继承ThreadLocalMap信息。

网上查询得到的InherbritableThreadLocalsetRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) 、给线程池配装饰器TaskDecorator都没有能够完全解决问题【个人能力不足】,所以最后直接使用Map来进行值的传递。

ThreadLocal<Map<String, String>> requestThreadLocal = new ThreadLocal<>();
 ……
 HttpServletRequest request =
        ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    requestThreadLocal.set(
        ImmutableMap.of("servletPath", request.getServletPath(), "method", request.getMethod()));

3.为什么记录到缓存中选择异步?异步会带来哪些问题?

因为记录到缓存中的操作也需要时间,且实现接口监控是为了查看哪些接口性能差,好进行提升,设定为同步的话,会影响到核心代码的执行。

问题:如果同一时间test这个接口被调用两次,两次均超时,那么因为使用了异步,在比较大小修改时,可能修改后的最大超时时间是这两次调用中的较小值。

解决方案:使用消息队列。也是异步进行,但是异步的线程会在消息队列中排队。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值