SpringBoot接口幂等性设计

MVC方案
  多版本并发控制,该策略主要使用 update with condition(更新带条件来防止)来保证多次外部请求调用对系统的影响是一致的。在系统设计的过程中,合理的使用乐观锁,通过 version 或者 updateTime(timestamp)等其他条件,来做乐观锁的判断条件,这样保证更新操作即使在并发的情况下,也不会有太大的问题。例如
 select * from tablename where condition=#condition# // 取出要跟新的对象,带有版本 versoin
update tableName set name=#name#,version=version+1 where version=#version#
在更新的过程中利用 version 来防止,其他操作对对象的并发更新,导致更新丢失。为了避免失败,通常需要一定的重试机制。

Token机制,防止页面重复提交
业务要求:页面的数据只能被点击提交一次。
发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交
解决办法:
集群环境:采用 token 加 redis(redis 单线程的,处理需要排队)
单 JVM 环境:采用 token 加 redis 或 token 加 jvm 内存
处理流程:
数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间
提交后后台校验 token,同时删除 token,生成新的 token 返回
token 特点:要申请,一次有效性,可以限流

基于Token方式防止API接口幂等
客户端每次在调用接口的时候,需要在请求头中,传递令牌参数,每次令牌只能用一次。
一旦使用之后,就会被删除,这样可以有效防止重复提交。
步骤:

  1. 生成令牌接口
  2. 接口中获取令牌验证

实战教程
要用到aop跟Redis , 所以在pom中添加

    <!-- Redis-Jedis -->
            <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
    <!-- springboot-aop 技术 -->
    	<dependency>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-aop</artifactId>
    	</dependency>

这是通过注解实现接口幂等性,先写Redis逻辑

@Component
public class BaseRedisService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public void setString(String key, Object data, Long timeout) {
        if (data instanceof String) {
            String value = (String) data;
            // 往Redis存值
            stringRedisTemplate.opsForValue().set(key, value);
        }
        if (timeout != null) {
            // 带时间缓存
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    /**
     * 查看是否有值
     * @param key 值
     * @return
     */
    public Object getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除Redis
     * @param key 值
     */
    public void delKey(String key) {
        stringRedisTemplate.delete(key);
    }
}

然后写怎么生成token,保证每个token只用一次

@Component
public class RedisToken {
	
    @Autowired
    private BaseRedisService baseRedisService;
    
    /** 缓存指定时间200秒 */
    private static final long TOKENTIMEOUT = 200;
    
    /**
     * 生成Token
     */
    public String getToken(){
        String token = UUID.randomUUID().toString();
        // 将token放到Redis中,用UUID保证唯一性
        baseRedisService.setString(token, token, TOKENTIMEOUT);
        return token; 
    }
    
    public synchronized boolean findToken(String tokenKey) {
        String tokenValue = (String) baseRedisService.getString(tokenKey);
        
        // 如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
        if (StringUtils.isEmpty(tokenValue)) {
            return false;
        }
        // 保证每个接口对应的token 只能访问一次,保证接口幂等性问题,用完直接删掉
        baseRedisService.delKey(tokenValue);
        return true;
    }
    
}

写一个工具类 请求是通过http请求还是from提交过来的,大部分都是form提交来的

public interface ConstantUtils {

    /**
     * http 中携带的请求
     */
    static final String EXTAPIHEAD = "head";

    /**
     * from 中提交的请求
     */
    static final String EXTAPIFROM = "from";
    
}

写好了 现在就写我们的注解了,没带参数的是前后端不分离,直接跳页面,获取到token,带参数前后端不分离的

带参数的

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
    String value();
}

不带参数的

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {

}

写好这个 要是aop切点,要把注解切入进去

@Aspect
@Component
public class ExtApiAopIdempotent {

    @Autowired
    private RedisToken redisToken;
    
    // 1.使用AOP环绕通知拦截所有访问(controller)
    @Pointcut("execution(public * com.yuyi.controller.*.*(..))")
    public void rlAop() {
    
    }
    
    /**
     * 封装数据
     */
    public HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request;
    }
    
    /**
     * 前置通知
     */
    @Before("rlAop()")
    public void before(JoinPoint point) {
        // 获取被增强的方法相关信息 - 查看方法上是否有次注解
        
        MethodSignature signature = (MethodSignature) point.getSignature();
        ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);
        if (extApiToken != null) {
            // 可以放入到AOP代码 前置通知
            getRequest().setAttribute("token", redisToken.getToken());
        }
    }
    
    /**
     * 环绕通知
     */
    @Around("rlAop()")
    public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 获取被增强的方法相关信息 - 查看方法上是否有次注解
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        ExtApiIdempotent declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
        if (declaredAnnotation != null) {
            String values = declaredAnnotation.value();
            String token = null;
            HttpServletRequest request = getRequest();
            if (values.equals(ConstantUtils.EXTAPIHEAD)) {
                token = request.getHeader("token");
            } else {
                token = request.getParameter("token");
            }
            
            // 获取不到token
            if (StringUtils.isEmpty(token)) {
                return ResultTool.error(ExceptionNume.PARAMETER_ERROR);
            }
            
            // 接口获取对应的令牌,如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑
            boolean isToken = redisToken.findToken(token);
            // 接口获取对应的令牌,如果获取不到该令牌 直接返回请勿重复提交
            if (!isToken) {
                return ResultTool.error(ExceptionNume.REPEATED_SUBMISSION);
            }
        }
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }

}

controller层 大家可以测一下

@Autowired
    private OrderInfoDAO infoDAO;
    @Autowired
    private RedisToken token;

    // @Autowired
    // private RedisTokenUtils redisTokenUtils;
    //
    // 从redis中获取Token
    @RequestMapping("/redisToken")
    public String getRedisToken() {
        return token.getToken();
    }
    
    @RequestMapping("/addOrderExtApiIdempotent")
    @ExtApiIdempotent(ConstantUtils.EXTAPIFROM)
    public ResultBO<?> addOrderExtApiIdempotent(
            @RequestParam String orderName,
            @RequestParam String orderDes
            ) {
        int result = infoDAO.addOrderInfo(orderName, orderDes);
        return ResultTool.success(result);
    }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

保证了只能请求一次。前后端没有分离的,@ExtApiToken带上注解会自动吧token携带过去

在这里插入图片描述

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
接口幂等是指同一个请求多次发送,最终的结果都是一致的。在设计Spring Boot接口时,需要考虑如何保证接口幂等性。 以下是一些实现接口幂等性的方法: 1. 使用唯一标识符:客户端每次发送请求时,需要提供唯一标识符。服务端在处理请求时,先检查该标识符是否已经存在,如果存在,则直接返回上一次的结果,否则执行请求。 2. 使用Token:客户端在第一次请求时,服务端生成一个Token,并将该Token返回给客户端。客户端每次请求时,都需要携带该Token。服务端在处理请求时,先检查该Token是否有效,如果有效,则返回上一次的结果,否则执行请求。 3. 使用数据库事务:在处理请求时,使用数据库事务来保证幂等性。在执行操作前,先检查数据是否存在,如果存在,则直接返回上一次的结果,否则执行操作。 4. 使用消息队列:将请求放入消息队列中,每次处理请求时,先检查队列中是否存在该请求,如果存在,则直接返回上一次的结果,否则执行请求。 5. 使用时间戳:客户端在请求时,携带一个时间戳。服务端在处理请求时,先检查该时间戳是否与上一次请求相同,如果相同,则直接返回上一次的结果,否则执行请求。 需要根据具体的业务场景选择合适的方法来保证接口幂等性。无论使用哪种方法,都需要注意并发请求可能会带来的问题,需要进行合理的并发控制。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值