PIG框架学习3——Redisson 实现业务接口幂等

news2024/12/26 15:26:02

零、前言

​ 业务接口幂等问题是在开发中遇到的,如果对业务接口代码不进行幂等控制,并且在前端没有对请求进行限制的情况下,可能会出现多次对接口调用,导致错误异常的发生。就上述情况,对PIGX自带的业务接口幂等实现进行了相关的学习,相关具体内容可以参考官方文档,本文章只是作为学习笔记。

一、接口幂等概念

幂等性原本是数学上的概念,在数学中表示对同一操作的多次执行,产生的结果与仅执行一次的结果相同。

用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。 调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。

实现接口幂等性通常需要考虑以下几个方面:

①请求参数:确保接口请求中的所有参数都是唯一的,不能重复。比如,在生成订单编号时可以使用时间戳或随机数等方式来保证唯一性

②接口设计:接口应该设计为“幂等性操作”,即无论调用多少次,结果都应该相同。例如,修改密码的接口应该设计为只能修改一次,多次修改的请求会被忽略。

③数据库操作:如果接口需要对数据库进行操作,需要确保每次操作都是幂等的。可以通过添加唯一索引或使用乐观锁等方式来避免重复操作

④错误处理:当请求出现异常时,接口应该处理异常并返回相应的错误信息。例如,当订单已经存在时,接口应该返回订单已存在的错误信息,而不是尝试再次创建订单

二、PIGX中具体实现方式

1、依赖的引入

情况①:

如果是其他项目,可以从中央仓库中引入PIGX提供的实现接口幂等的依赖

 <dependency>
     <groupId>com.pig4cloud.plugin</groupId>
     <artifactId>idempotent-spring-boot-starter</artifactId>
     <version>0.4.0</version>
 </dependency>

引入后的包内容如下所示
在这里插入图片描述

情况②:

如果项目中原先使用的就是pigx框架,那么在基础平台中的pigx-common-idempotent就已经自带
在这里插入图片描述

我们只需要引入pigx-common服务,就可以将实现接口幂等的依赖引入

2、Redis的配置

在配置文件中进行Redis的配置,设置redis存为我们的缓存中间件

通过如下配置,Spring框架会自动启用Redis作为缓存,并将Redis服务器的连接信息配置为指定的主机名和端口号

spring:
  cache:
    type: redis
  data:
    redis:
      host: 127.0.0.1
      port: 6379

spring.cache 对应的配置类是CacheProperties,这个配置项用来指定缓存的类型。在上述示例中,我们将缓存类型设置为Redis,表示使用Redis作为缓存。

spring.data.redis:这个配置项用来指定连接Redis所需的参数。在上述示例中,我们指定了Redis服务器的主机名为127.0.0.1,端口号为`6379

3、具体使用示例

pigx主要是通过AOP去实现业务接口幂等,其最核心的就是其Idempotent注解,其使用示例如下:

@Idempotent(key = "#demo.username", expireTime = 3, info = "请勿重复查询")
@GetMapping("/test")
public String test(Demo demo) {
    return "success";
}

通过使用@Idempotent注解,当多次请求进入test方法时,框架会根据key生成的唯一标识符判断是否已经执行过相同的请求。如果已经执行过,则直接返回之前的结果;如果没有执行过,则继续执行业务逻辑。

这样可以避免重复查询或操作带来的额外开销,并确保系统在重复请求时的一致性和可靠性。

三、原理分析

1、目录结构说明

对于PIGX自带的幂等插件idempotent的目录结构如下所示:

在这里插入图片描述

各目录内容说明如下:

- annotation     // 关于@Idempotent注解的声明
	- @Idempotent
- aspect		 // 关于AOP的切面方法
	- IdempotentAspect
- exception		 // 自定义异常类
	- IdempotentException
- expression	 // sqel表达式的解析工具
	- ExpressionResolver
	- KeyResolver
- IdempotentAutoConfiguration   //幂等插件配置文件

- org.springframework.boot.autoconfigure.AutoConfiguration.imports   //springboot自动配置文件
2、@idempotent 注解 配置详细说明
2.1、@idempotent注解源码
@Inherited
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Idempotent {

	/**
	 * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
	 * @return Spring-EL expression
	 */
	String key() default "";

	/**
	 * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
	 * @return expireTime
	 */
	int expireTime() default 1;

	/**
	 * 时间单位 默认:s
	 * @return TimeUnit
	 */
	TimeUnit timeUnit() default TimeUnit.SECONDS;

	/**
	 * 提示信息,可自定义
	 * @return String
	 */
	String info() default "重复请求,请稍后重试";

	/**
	 * 是否在业务完成后删除key true:删除 false:不删除
	 * @return boolean
	 */
	boolean delKey() default false;

}

key

key参数用于生成幂等性的唯一标识符,用于识别不同的请求或操作.具体来说,当多个请求或操作需要进行幂等性处理时,每个请求或操作需要有一个唯一的标识符来区分。这个标识符可以是请求参数、请求头信息、用户ID等等,根据实际场景而定。

在示例中,我们使用了SpEL表达式#demo.username作为唯一标识符,表示使用Demo对象的username属性作为标识符。也就是说,当多个请求的demo对象的username属性相同时,这些请求被认为是重复的,并且只有第一个请求会被执行,后续的请求会直接返回上次执行的结果。

expireTime

幂等性键过期时间,单位为秒

timeUnit

用于设置幂等性键过期时间的单位,默认是秒

info

用于描述幂等性操作的提示信息,在某些情况下,当出现重复请求时,我们需要向客户端返回一些提示信息,告知客户端该请求已经被处理过,不能重复提交。因此,通过设置info参数,我们可以自定义提示信息,以便更好地向客户端反馈信息。

delKey

是否在业务完成后删除key true:删除 false:不删除, 默认为false。

3、IdempotentAspect
3.1、IdempotentAspect源码及相关解析:
/**
 * The Idempotent Aspect
 *
 * @author ITyunqing
 */

//Spring框架中用于声明切面(Aspect)的注解
//通过在类上添加@Aspect注解,将其标识为一个切面类。切面类中可以包含各种通知(advice)和切点(pointcut),用于定义切面的具体行为和拦截规则
@Aspect
public class IdempotentAspect {
	//通过slf4j的LoggerFactory(日志工厂)创建一个Logger对象,用于在IdempotentAspect类中记录日志信息
	private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);

    //创建一个名为THREAD_CACHE的私有的静态常量ThreadLocal对象
    //指定了泛型类型为Map<String, Object>,表示该ThreadLocal对象存储的值是一个键值对的映射
    //通过ThreadLocal类的withInitial方法,传入一个Supplier接口实例,该Supplier接口用于提供初始值。在这里,使用HashMap::new作为Supplier接口的实现,创建一个新的HashMap作为初始值。
	private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);

    //定义了一个常量字符串变量RMAPCACHE_KEY,表示缓存中的键名,用于在缓存中存储幂等性相关的数据
	private static final String RMAPCACHE_KEY = "idempotent";

    //定义了一个常量字符串变量KEY,表示幂等性数据的键名,用于在幂等性相关的数据中标识唯一的幂等性请求
	private static final String KEY = "key";

    //定义了一个常量字符串变量DELKEY,表示删除缓存数据的键名,用于在缓存中标识需要删除的幂等性数据
	private static final String DELKEY = "delKey";

    //通过spring的自动装配功能,将Redisson对象注入到当前类中
	@Autowired
	private Redisson redisson;

    //通过spring的自动装配功能,将KeyResolver对象注入到当前类中
	@Autowired
	private KeyResolver keyResolver;

    //通过spring的自动装配功能,将一个Optional<KeyStrResolver>对象注入到当前类中
	//KeyStrResolver的一个可选注入项,它可以包含一个非空的值,也可以为空
    @Autowired
	private Optional<KeyStrResolver> keyStrResolverOptional;

    //该注解表示定义一个切入点,使用@annotation表达式来匹配被指定注解标记的方法。在这里,切入点匹配的条件是被com.pig4cloud.pigx.common.idempotent.annotation.Idempotent注解标记的方法
	@Pointcut("@annotation(com.pig4cloud.pigx.common.idempotent.annotation.Idempotent)")
	//这是一个空方法体,作为切入点的定义。它没有任何实际的逻辑操作,只是用来定义一个切入点的名称
    public void pointCut() {
	}

    
    //切面的前置通知(@Before):它在切入点方法执行之前被调用
	@Before("pointCut()")
	public void beforePointCut(JoinPoint joinPoint) {
		//通过RequestContextHolder类可以获取当前线程的RequestAttributes对象
        //将获取到的RequestAttributes对象强制转换为ServletRequestAttributes类型。ServletRequestAttributes是RequestAttributes的子接口,扩展了一些与Servlet请求相关的方法和属性
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
				.getRequestAttributes();
        
        //获取当前请求的 HttpServletRequest 对象
		HttpServletRequest request = requestAttributes.getRequest();
	
        //将 joinPoint 转换为 MethodSignature 对象
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		//通过 MethodSignature 对象获取到当前方法的 Method 对象
        Method method = signature.getMethod();
        //如果当前方法上没有使用注解@Idempotent,则return返回
		if (!method.isAnnotationPresent(Idempotent.class)) {
			return;
		}
        //如果当前方法上有使用注解@Idempotent,获得该注解信息
        //通过调用 getAnnotation() 方法并传入 Idempotent.class 作为参数,可以获取到该方法上的 Idempotent 注解的实例对象
		Idempotent idempotent = method.getAnnotation(Idempotent.class);
		
		String key;

		// 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分(作为生成幂等性的唯一标识符)
		if (!StringUtils.hasLength(idempotent.key())) {
			String url = request.getRequestURL().toString();
			String argString = Arrays.asList(joinPoint.getArgs()).toString();
			key = url + argString;
		}
		else {
			// 使用jstl 规则区分(具体会在expression中进行讲解)
			key = keyResolver.resolver(idempotent, joinPoint);
		}

        //StrUtil.COLON:字符串常量:冒号 {@code ":"}
        //如果keyStrResolverOptional存在,则设置key的值为KeyStrResolver调用extract(key, StrUtil.COLON)方法的结果值
        //即在原先key的基础的前面加上租户id:
		if (keyStrResolverOptional.isPresent()) {
			key = keyStrResolverOptional.get().extract(key, StrUtil.COLON);
		}

        //获取注解idempotent的有效期
		long expireTime = idempotent.expireTime();
        //获取注解idempotent的提示信息
		String info = idempotent.info();
        //获取注解idempotent的时间单位
		TimeUnit timeUnit = idempotent.timeUnit();
        //获取注解idempotent的是否在业务完成后删除key
		boolean delKey = idempotent.delKey();

		// do not need check null
        //RMapCache 是 Redisson 提供的一个接口,用于表示一个带有过期时间的分布式 Map 缓存,RMapCache 继承自 RMap 接口,提供了一些额外的方法,可以设置和获取缓存的过期时间,以及进行相关的缓存操作
        //通过 redisson 对象调用 getMapCache(RMAPCACHE_KEY) 方法,传入参数 RMAPCACHE_KEY 来获取对应的缓存对象
        //RMAPCACHE_KEY 是表示缓存的键值,可能是一个字符串或其他适当的类型
		RMapCache<String, Object> rMapCache = redisson.getMapCache(RMAPCACHE_KEY);
		//获取当前系统的时间,并将其转化为字符串,将其中的T替换为“ ”
        String value = LocalDateTime.now().toString().replace("T", " ");
		
        Object v1;
        //如果当前对应的键key已经有缓存对象,则抛出异常,异常信息为"[idempotent]:" + info
		if (null != rMapCache.get(key)) {
			// had stored
			throw new IdempotentException("[idempotent]:" + info);
		}
        //使用 synchronized 关键字来对代码块进行同步,确保在多线程环境下的安全性
        //通过 synchronized(this) 来锁定当前对象,以保证同一时间只有一个线程可以进入这个代码块
		synchronized (this) {
        	//将键值对放入缓存中
            //该方法会返回之前关联到 key 上的值,如果该值为 null,则表示插入成功
			v1 = rMapCache.putIfAbsent(key, value, expireTime, timeUnit);
			//如果 v1 不为 null,即 putIfAbsent 返回了一个非空值,说明之前已经存在相同的键值对,此时抛出一个 IdempotentException 异常,表示幂等性校验失败
            if (null != v1) {
				throw new IdempotentException("[idempotent]:" + info);
			}
            //如果 v1 为 null,表示 putIfAbsent 成功插入了新的键值对,记录一条日志信息,并打印当前存储的 key、value、过期时间等信息
			else {
				LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime,
						timeUnit, LocalDateTime.now().toString());
			}
		}
        
        //在当前线程中使用 ThreadLocal 来创建一个线程本地的 Map 对象,并将 key 和 delKey 存储到其中
		//通过 THREAD_CACHE.get() 方法获取当前线程绑定的 Map<String, Object> 对象,存储到 map 变量中
		Map<String, Object> map = THREAD_CACHE.get();
		//将键值对 KEY 和对应的变量 key 存储到 map 中
        map.put(KEY, key);
        //将键值对 DELKEY 和对应的变量 delKey 存储到 map 中
		map.put(DELKEY, delKey);
	}

    //切面的后置通知(@After):它在切入点方法执行之后被调用
	@After("pointCut()")
	public void afterPointCut(JoinPoint joinPoint) {
        //从当前线程中获取之前存储的 map 对象,并将其存储到 map 变量中
		Map<String, Object> map = THREAD_CACHE.get();
		
        //判断 map 是否为空,如果为空,则直接返回,不执行后续操作
        if (CollectionUtils.isEmpty(map)) {
			return;
		}
		//获取一个名为 RMAPCACHE_KEY 的 RMapCache<Object, Object> 实例
		RMapCache<Object, Object> mapCache = redisson.getMapCache(RMAPCACHE_KEY);
		//判断该 RMapCache 是否为空。如果为空,则直接返回,不执行后续操作
        if (mapCache.size() == 0) {
			return;
		}
		//从 map 中获取 key ,并将 key 转换为字符串类型保存到 key 变量中
		String key = map.get(KEY).toString();
        //从 map 中获取 delKey, 将 delKey 强制转换为布尔型保存到 delKey 变量中
		boolean delKey = (boolean) map.get(DELKEY);
		//delKey为真,则调用 mapCache.fastRemove(key) 方法从 RMapCache 中删除指定的键值对,并记录一条日志信息
		if (delKey) {
			mapCache.fastRemove(key);
			LOGGER.info("[idempotent]:has removed key={}", key);
		}
        //从当前线程中删除 map 对象。这样做是为了避免在下次请求时出现数据混淆的情况
		THREAD_CACHE.remove();
	}

}

3.2、相关内容补充

① ThreadLocal

ThreadLocal 是一个 Java 类,它提供了线程局部变量的支持。每个线程都可以独立地访问和修改自己的线程局部变量,而不会干扰其他线程的变量。

使用 ThreadLocal 可以解决多线程并发访问共享变量时可能出现的线程安全问题。通过将数据存储在 ThreadLocal 对象中,每个线程都有自己独立的副本,线程之间的数据互不干扰。

在使用 ThreadLocal 时,通常的做法是创建一个 ThreadLocal 对象,并使用 set() 方法来设置当前线程的变量值,使用 get() 方法来获取当前线程的变量值。可以通过 remove() 方法来清除当前线程的变量值。

② Redisson

Redisson是一个Java Redis客户端,提供了简单易用的API接口,支持分布式锁、分布式集合、分布式对象等功能。它基于Redis客户端(Jedis、Lettuce)封装了一系列的分布式操作接口,使得Java开发者可以方便地使用Redis实现分布式应用程序。

Redisson的主要特点如下:

  • 对Redis的高效封装:Redisson对Redis客户端进行了高效的封装,提供了简单易用的API接口,使得Java开发者可以方便地实现复杂的分布式应用程序。
  • 分布式锁:Redisson提供了分布式锁的实现,支持可重入锁、公平锁、联锁、红锁等多种类型的锁,并且保证了锁的正确性和高可用性。
  • 分布式集合:Redisson支持分布式Set、List、Queue、Deque、Map、SortedSet等集合类型的操作,可以方便地实现分布式计算、消息队列等功能。
  • 分布式对象:Redisson支持分布式ExecutorService、ScheduledExecutorService、Semaphore、CountDownLatch、ReadWriteLock等对象类型的操作,可以方便地实现分布式任务调度、并发控制等功能。

③MethodSignature

在 Spring AOP 中,joinPoint 是一个连接点(即程序执行过程中能够插入额外代码的点),它封装了当前方法的相关信息,包括方法名、参数等。

MethodSignature 是 JoinPoint 的一个子类,它用于表示方法签名,即方法名和参数类型等信息。通过 MethodSignature 对象,我们可以获取到当前方法的 Method 对象,进而获取该方法的返回值类型、参数列表等信息。

④KeyStrResolver

其是一个接口,源码如下

public interface KeyStrResolver {

	/**
	 * 字符串加工
	 * @param in 输入字符串
	 * @param split 分割符
	 * @return 输出字符串
	 */
	String extract(String in, String split);

	/**
	 * 字符串获取
	 * @return 模块返回字符串
	 */
	String key();
}

其具体实现类TenantKeyStrResolver的源码如下所示

/**
 * @author lengleng
 * @date 2020/9/29
 * <p>
 * 租户字符串处理(方便其他模块获取)
 */
public class TenantKeyStrResolver implements KeyStrResolver {

	/**
	 * 传入字符串增加 租户编号:in
	 * @param in 输入字符串
	 * @param split 分割符
	 * @return
	 */
	@Override
	public String extract(String in, String split) {
		return TenantContextHolder.getTenantId() + split + in;
	}

	/**
	 * 返回当前租户ID
	 * @return
	 */
	@Override
	public String key() {
		return String.valueOf(TenantContextHolder.getTenantId());
	}

}
4、IdempotentException自定义异常类
4.1、IdempotentException自定义异常类源码
//RuntimeException 运行时异常的实现类
public class IdempotentException extends RuntimeException {

    //无参构造器
	public IdempotentException() {
		super();
	}

    //有参构造器1
	public IdempotentException(String message) {
		super(message);
	}

    //有参构造器2
	public IdempotentException(String message, Throwable cause) {
		super(message, cause);
	}

    //有参构造器3
	public IdempotentException(Throwable cause) {
		super(cause);
	}

    //有参构造器4
	protected IdempotentException(String message, Throwable cause, boolean enableSuppression,
			boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
	}

}

5、唯一标志处理器KeyResolver
5.1、 KeyResolver源码及解析

其中定义了一个解析处理key的抽象方法

public interface KeyResolver {

	/**
	 * 解析处理 key
	 * @param idempotent 接口注解标识
	 * @param point 接口切点信息
	 * @return 处理结果
	 */
	String resolver(Idempotent idempotent, JoinPoint point);

}

5.2、ExpressionResolver源码及其解析

ExpressionResolverKeyResolver的实现类,其主要的作用就是进行key的抽取,优先根据 spel 处理

/**
 * @author lengleng
 * <p>
 * 默认key 抽取, 优先根据 spel 处理
 * @date 2020-09-25
 */
public class ExpressionResolver implements KeyResolver {
	//创建一个 SpEL 表达式解析器实例SpelExpressionParser
	private static final SpelExpressionParser PARSER = new SpelExpressionParser();

    //创建一个 LocalVariableTableParameterNameDiscoverer 对象,用于获取方法参数的名称
	private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    //重写父类keyResolver的resolver方法
	@Override
	public String resolver(Idempotent idempotent, JoinPoint point) {
        //通过 point.getArgs() 获取方法的参数数组
		Object[] arguments = point.getArgs();
        // 获取方法参数的名称数组
		String[] params = DISCOVERER.getParameterNames(getMethod(point));
        //创建一个 StandardEvaluationContext 对象,用于提供表达式求值的上下文环境
		StandardEvaluationContext context = new StandardEvaluationContext();
		//如果方法参数的名称数组不为空且长度大于 0,则遍历参数名称数组,将参数名和对应的参数值存储到 context 中
		if (params != null && params.length > 0) {
			for (int len = 0; len < params.length; len++) {
				context.setVariable(params[len], arguments[len]);
			}
		}
		
        //使用 PARSER.parseExpression(idempotent.key()) 解析 idempotent.key() 字符串为一个 SpEL 表达式,并返回一个 Expression 对象
		Expression expression = PARSER.parseExpression(idempotent.key());
		//调用 Expression 对象的 getValue() 方法,传入 context 和期望的结果类型 String.class,以获取表达式的求值结果
        return expression.getValue(context, String.class);
	}

	/**
	 * 根据切点解析方法信息
	 * @param joinPoint 切点信息
	 * @return Method 原信息
	 */
	private Method getMethod(JoinPoint joinPoint) {
        //通过 joinPoint.getSignature() 获取切入点的方法签名,然后将其转换为 MethodSignature 类型,并赋值给 signature 变量
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		//从方法签名中获取切入点方法,并赋值给 method 变量
        Method method = signature.getMethod();
		//判断切入点方法所在的类是否是一个接口。如果是接口,则执行以下代码块
        if (method.getDeclaringClass().isInterface()) {
			try {
                //通过 joinPoint.getTarget().getClass() 获取切入点目标对象的类,并使用该类调用 getDeclaredMethod() 方法来获取具体实现类中与切入点方法相同名称和参数类型的方法,并将得到的方法赋值给 method 变量
				method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
						method.getParameterTypes());
			}
			catch (SecurityException | NoSuchMethodException e) {
                //如果在获取具体实现类中的方法时抛出了 SecurityException 或 NoSuchMethodException 异常,会捕获并抛出一个具有该异常的 RuntimeException 异常
				throw new RuntimeException(e);
			}
		}
        //返回获取到的方法对象
		return method;
	}

}
6、幂等插件配置文件IdempotentAutoConfiguration
6.1、配置文件源码
//标记该类为配置类
//proxyBeanMethods = false,表示Spring 将不会为配置类的方法生成代理对象,这样可以避免额外的代理开销
@Configuration(proxyBeanMethods = false)
//指定在特定自动配置类RedisAutoConfiguration之后进行自动配置
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class IdempotentAutoConfiguration {

	/**
	 * 生成IdempotentAspect切面类的bean实例
	 * 切面 拦截处理所有 @Idempotent
	 * @return Aspect
	 */
	@Bean
	public IdempotentAspect idempotentAspect() {
		return new IdempotentAspect();
	}

	/**
	 * 生成key 解析器的bean实例
	 * key 解析器
	 * @return KeyResolver
	 */
	@Bean
	@ConditionalOnMissingBean(KeyResolver.class)
	public KeyResolver keyResolver() {
		return new ExpressionResolver();
	}

}
7、AutoConfiguration.imports
7.1、源码
com.pig4cloud.pigx.common.idempotent.IdempotentAutoConfiguration
7.2、作用

org.springframework.boot.autoconfigure.AutoConfiguration.imports 是一个 Spring Boot 的属性,用于指定需要在自动配置类中导入的其他配置类。

在 Spring Boot 中,自动配置是一种机制,它根据应用程序的依赖和配置信息,自动创建并配置相应的 Bean 实例。而 AutoConfiguration.imports 属性则提供了一种扩展机制,允许开发人员在自动配置类中导入其他配置类,以实现更为灵活和定制化的自动配置。

具体来说,AutoConfiguration.imports 属性是一个字符串数组,每个元素表示一个需要导入的配置类的全限定名。在自动配置过程中,Spring Boot 会自动加载这些配置类,并将它们合并到当前上下文中,以实现更全面的自动配置。

8、补充

1.请求开始前,根据key查询 查到结果:报错 | 未查到结果:存入key-value-expireTime

2.请求结束后,直接删除key ,不管key是否存在,直接删除 是否删除,可配置

3.expireTime过期时间,防止一个请求卡死,会一直阻塞,超过过期时间,自动删除

过期时间要大于业务执行时间,需要大概评估下;

4.此方案直接切的是接口请求层面。

5.过期时间需要大于业务执行时间,否则业务请求1进来还在执行中,前端未做遮罩,或者用户跳转页面后再回来做重复请求2,在业务层面上看,结果依旧是不符合预期的。

6.建议delKey = false。即使业务执行完,也不删除key,强制锁expireTime的时间。预防5的情况发生。

7.实现思路:同一个请求ip和接口,相同参数的请求,在expireTime内多次请求,只允许成功一次。

8.页面做遮罩,数据库层面的唯一索引,先查询再添加,等处理方式应该都处理下。

9.此注解只用于幂等,不用于锁,100个并发这种压测,会出现问题,在这种场景下也没有意义,实际中用户也不会出现1s或者3s内手动发送了50个或者100个重复请求,或者弱网下有100个重复请求;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1402496.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sql数据库的相关概念与底层介绍

本文中的数据库指的是磁盘数据库。如果有sql语言&#xff08;CRUD&#xff0c;增删改查&#xff09;的使用经验会更容易理解本文的知识点。 数据库与redis的区别 数据库&#xff1a;数据存储长期在磁盘中&#xff0c;小部分频繁需要的数据会被临时提取在内存中。 Redis&…

数组oj 移除元素

思路用双指针更简单&#xff0c;直接用一个数组&#xff0c;开始src和dst都为0&#xff0c;一起往后走(先走dst)&#xff0c;当dst遇到val就停下&#xff0c;src接着走&#xff0c;走到不是val的地方&#xff0c;然后把src赋值给dst&#xff0c;在一起&#xff0c;往后面走。结…

启发式教学是什么

学生们在上课时看似认真听讲&#xff0c;但是在下课后却一片茫然&#xff0c;不知道你讲了什么内容&#xff1f;这是因为你可能使用了传统的教学方法&#xff0c;而不是启发式教学。 启发式教学是指老师在教育教学中&#xff0c;采用引导、启示、激发等手段&#xff0c;调动学…

【操作系统】实验二 Proc文件系统

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的很重要&…

以Python程序为例介绍算法复杂度的估算

文章目录 概念介绍时间复杂度估算空间复杂度估算 引自同一作者百家号文章&#xff1a;「Python语言进阶」算法复杂度是什么&#xff1f;如何估算&#xff1f; 概念介绍 算法&#xff0c;可以理解为解决问题的方法和思路&#xff0c;不是一定得有代码的才叫算法&#xff0c;它…

每日汇评:黄金的市场注意力转向央行和地缘政治

金价从2000美元附近的数周低点反弹&#xff1b; 到目前为止&#xff0c;黄金的进一步巩固当前价格是当前的主题&#xff1b; 地缘政治、宏观数据和央行是本周的主导市场情绪&#xff1b; 黄金价格设法恢复了平衡&#xff0c;扭转了本周开始时的负面走势&#xff0c;从关键争夺区…

AI数据模型支撑下,鼎捷「预测透镜」如何助力汽配厂商浙江华工提质增效?

关于【浙江华工】 浙江华工汽车零部件有限公司&#xff08;以下简称“华工”&#xff09;成立于1987年&#xff0c;位于“中国汽摩配之都”浙江省瑞安塘下镇鲍七工业区&#xff0c;是一家集汽车发动机用冷却水泵和机油泵研发、制造、销售于一体的民营股份制企业。 近些年来&…

六种pdf在线转换工具了解一下-轻松应对pdf转换

PDF文件是我们工作中经常需要使用到的一种文件格式。然而&#xff0c;有时候我们可能需要将PDF文件转换成其他格式以满足特定的需求。为了帮助大家解决这个问题&#xff0c;本文将介绍六种实用的PDF在线转换工具&#xff0c;让你轻松完成各种文件格式的转换。 一、迅捷PDF转换器…

【代码随想录算法训练营第二十四天|回溯算法的理论基础、77. 组合】

代码随想录算法训练营第二十四天|回溯算法的理论基础、77. 组合 回溯算法的理论基础77. 组合 回溯算法的理论基础 这里我觉得《代码随想录》和y总的课都比较好了 《代码随想录》 &#xff1a; https://programmercarl.com/0077.%E7%BB%84%E5%90%88%E4%BC%98%E5%8C%96.html#%E5…

代理IP助力云函数实现更高效的网络通信

一、前言 云计算的出现使得无服务器计算成为可能&#xff0c;例如云函数。云函数是一种无需管理服务器的计算服务&#xff0c;能够根据需求自动伸缩&#xff0c;并能处理网络请求。然而&#xff0c;云函数常常受限于网络通信速度问题&#xff0c;特别是面对大量请求时。为了提…

【JavaEE进阶】 关于⽇志框架(SLF4J)

文章目录 &#x1f333;SLF4j&#x1f332;⻔⾯模式(外观模式)&#x1f6a9;⻔⾯模式的定义&#x1f6a9;⻔⾯模式的优点 &#x1f343;关于SLF4J框架&#x1f6a9;不引⼊⽇志⻔⾯&#x1f6a9;引⼊⽇志⻔⾯ ⭕总结 &#x1f333;SLF4j SLF4J不同于其他⽇志框架,它不是⼀个真正…

MySQL TINYINT(1)和TINYINT(2)有什么区别?

文章目录 1.直接建表2.查询数据3.总结 身为程序员&#xff0c;拿事实说话拿代码说话最直观了&#xff0c;show the code 1.直接建表 CREATE TABLE tinyinttest (id int NOT NULL,a TINYINT(1) NOT NULL DEFAULT 0,b TINYINT(2) NOT NULL DEFAULT 0,c TINYINT(1) ZEROFILL NOT…

【Docker】部署和运行青龙面板:一个支持python3、javaScript、shell、typescript 的定时任务管理面板

引言 青龙面板是一个支持python3、javaScript、shell、typescript 的定时任务管理面板。 步骤 拉取镜像 从 Docker Hub 上拉取最新的 “qinglong” 镜像。 docker pull whyour/qinglong:latest启动容器 使用刚刚拉取的镜像来启动一个新的 Docker 容器。 docker run -dit \-v…

基于SpringBoot的高校学科竞赛平台管理系统

基于SpringBoot的高校学科竞赛平台管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatis工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 前台界面 管理员界面 教师界面 学生界面 摘要 本文详细介绍了一款基于…

【BERT】详解

BERT 简介 BERT 是谷歌在 2018 年时提出的一种基于 Transformer 的双向编码器的表示学习模型&#xff0c;它在多个 NLP 任务上刷新了记录。它利用了大量的无标注文本进行预训练&#xff0c;预训练任务有掩码语言模型和下一句预测&#xff0c;掩码语言模型指的是随机地替换文本中…

项目管理流程

优质博文 IT-BLOG-CN 一、简介 项目是为提供某项独特产品【独特指:创造出与以往不同或者多个方面与以往有所区别产品或服务&#xff0c;所以日复一日重复的工作就不属于项目】、服务或成果所做的临时性【临时性指:项目有明确的开始时间和明确的结束时间&#xff0c;不会无限期…

线框图是什么?怎么画?看这篇文章就够了

产品设计中&#xff0c;简洁清晰传达出复杂概念是件难度不小的事&#xff0c;但也是很重要的事&#xff0c;这不仅关系设计的准确性&#xff0c;更关乎整个项目的顺利进行。在这种情境下&#xff0c;线框图的作用不容小觑。它作为设计思路的初步可视化手段&#xff0c;不仅是产…

GIS项目实战06:超详细Node.js安装及系统环境配置

简单的说 Node.js 就是运行在服务端的 JavaScript。 Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。 Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境&#xff0c;基于 Google 的 V8 引擎&#xff0c;V8 引擎执行 Javascript 的速度非常快&#xff0c;性能…

HCIA——20应用层:C/S、P2P、peer

学习目标&#xff1a; 计算机网络 1.掌握计算机网络的基本概念、基本原理和基本方法。 2.掌握计算机网络的体系结构和典型网络协议&#xff0c;了解典型网络设备的组成和特点&#xff0c;理解典型网络设备的工作原理。 3.能够运用计算机网络的基本概念、基本原理和基本方法进行…

LabVIEW扫描探针显微镜系统开发

在纳米技术对高精度材料特性测量的需求日益增长。介绍了基于LabVIEW开发的扫描探针显微镜&#xff08;SPM&#xff09;系统。该系统不仅可以高效地测量材料的热物性&#xff0c;还能在纳米尺度上探究热电性质&#xff0c;为材料研究提供了强大的工具。 系统基于扫描探针显微技…