Token+Redis实现接口幂等性

一.什么是 幂等性

在编程中,幂等性的特点就是其任意多次执行的效果和一次执行的效果所产生的影响是一样的。

二.Token+Redis的实现思路

1.数据提交前要向服务的申请 token(用户登录时可以获取),token 放到 redis 或 jvm 内存,token 有效时间;
2. 提交后后台校验 token,同时删除 token,生成新的 token 返回。
注意:Redis要用删除操作来判断是否操作成功,删除成功代表校验成功。

三.具体实现

1.首先导入Redis的pom依赖:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

2.设置切面

package com.apps.idempotent.aspect;

import com.apps.bcodemsg.MsgResponse;
import com.apps.redis.service.RedisService;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@Order(2)
@Component
@Aspect
public class IdempotentAspect {

    @Autowired
    private RedisService redisService;

    @Pointcut("@annotation(com.apps.annotation.Token)")
    public void tokenIdempotent(){}

    @Around(value = "tokenIdempotent()")
    public MsgResponse before(ProceedingJoinPoint jp) {
        System.out.println("================================IdempotentAspect==============================");

        MsgResponse response = new MsgResponse();

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();

        String token = request.getHeader("token");
        System.out.println("=====================接收到的参数token:"+token);
        if(StringUtils.isEmpty(token)){
            response.fail("key.err");
            response.setMsg("请勿重复点击!");
            return response;
        }
        boolean contains = redisService.contains(token);
        if(!contains){
            response.fail("key.err");
            response.setMsg("请勿重复点击!");
            return response;
        }
        Object stringToken = redisService.get(token);
        if(stringToken!=null){
            boolean flag = redisService.del(token);
            if(!flag){
                response.fail("key.err");
                response.setMsg("请勿重复点击!");
                return response;
            }
            System.out.println("==========拦截成功,成功删除redis中的token,避免重复提交。");
        }


        try {
            response = (MsgResponse) jp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
        return response;
    }
}

注意:

1.因为之前有一个日志切面,如果不使用@Order注解标明切面执行顺序就会报错,当然如果没有其他切面就不需要添加这个@Order注解

2.其中MsgResponse类是一个回复类

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.apps.bcodemsg;

import com.apps.asysfinal.SysFinal;
import com.apps.msgconfig.MyProperties;
import com.github.pagehelper.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.List;

@ApiModel(
    value = "数据模型",
    description = "数据模型"
)
public class MsgResponse extends SysFinal {
    @ApiModelProperty(
        example = "状态码,成功200,失败400"
    )
    private int code;
    @ApiModelProperty(
        example = "错误和成功信息"
    )
    private String msg;
    @ApiModelProperty(
        example = "交互提示"
    )
    private String msgText;
    @ApiModelProperty(
        example = "每页行数"
    )
    private Integer pageNum;
    @ApiModelProperty(
        example = "当前页数"
    )
    private Integer pageSize;
    @ApiModelProperty(
        example = "总行数"
    )
    private Long pageTotal;
    @ApiModelProperty(
        example = "总页数"
    )
    private Integer pages;
    @ApiModelProperty(
        example = "开始行"
    )
    private Integer startRow;
    @ApiModelProperty(
        example = "结束行"
    )
    private Integer endRow;
    @ApiModelProperty(
        example = "返回数据"
    )
    private Object data;

    public MsgResponse() {
    }

    public void success(Object obj, String key) {
        this.setCode(200);
        this.setMsg("业务处理成功!");
        this.setMsgText(MyProperties.getPropertiesText(key));
        this.setData(obj);
    }

    public void success(String key) {
        this.setCode(200);
        this.setMsg("业务处理成功!");
        this.setMsgText(MyProperties.getPropertiesText(key));
    }

    public void fail(Object obj, String key) {
        this.setCode(400);
        this.setMsg("业务处理失败!");
        this.setMsgText(MyProperties.getPropertiesText(key));
        this.setData(obj);
    }

    public void fail(String key) {
        this.setCode(400);
        this.setMsg("业务处理失败!");
        this.setMsgText(MyProperties.getPropertiesText(key));
    }

    public MsgResponse add(Object value) {
        this.setData(value);
        return this;
    }

    public int getCode() {
        return this.code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return this.msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return this.data;
    }

    public void setData(Object data) {
        if (data instanceof Page) {
            Page page = (Page)data;
            this.setPageNum(page.getPageNum());
            this.setPageSize(page.getPageSize());
            this.setPageTotal(page.getTotal());
            this.setPages(page.getPages());
            this.setStartRow(page.getStartRow());
            this.setEndRow(page.getEndRow());
        }

        this.data = data;
    }

    public Object returnJson() {
        return this;
    }

    public String getMsgText() {
        return this.msgText;
    }

    public void setMsgText(String msgText) {
        this.msgText = msgText;
    }

    public Integer getPageNum() {
        return this.pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPageSize() {
        return this.pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public Long getPageTotal() {
        return this.pageTotal;
    }

    public void setPageTotal(Long pageTotal) {
        this.pageTotal = pageTotal;
    }

    public Integer getPages() {
        return this.pages;
    }

    public void setPages(Integer pages) {
        this.pages = pages;
    }

    public Integer getStartRow() {
        return this.startRow;
    }

    public void setStartRow(Integer startRow) {
        this.startRow = startRow;
    }

    public Integer getEndRow() {
        return this.endRow;
    }

    public void setEndRow(Integer endRow) {
        this.endRow = endRow;
    }

    public String toString() {
        if (!(this.data instanceof List)) {
            return "MsgResponse{code=" + this.code + ", msg=‘" + this.msg + ‘\‘‘ + ", msgText=‘" + this.msgText + ‘\‘‘ + ", pageNum=" + this.pageNum + ", pageSize=" + this.pageSize + ", pageTotal=" + this.pageTotal + ", pages=" + this.pages + ", startRow=" + this.startRow + ", endRow=" + this.endRow + ", data=" + this.data + ‘}‘;
        } else {
            List list = (List)this.data;
            StringBuffer stringBuffer = new StringBuffer();

            for(int i = 0; i < list.size(); ++i) {
                stringBuffer.append(list.get(i));
            }

            return "MsgResponse{code=" + this.code + ", msg=‘" + this.msg + ‘\‘‘ + ", msgText=‘" + this.msgText + ‘\‘‘ + ", pageNum=" + this.pageNum + ", pageSize=" + this.pageSize + ", pageTotal=" + this.pageTotal + ", pages=" + this.pages + ", startRow=" + this.startRow + ", endRow=" + this.endRow + ", data=" + stringBuffer + ‘}‘;
        }
    }
}

3.添加相关的业务代码和控制器

package com.apps.controller.Idempotent;

import com.apps.annotation.Token;
import com.apps.bcodemsg.MsgResponse;
import com.apps.redis.service.RedisService;
import com.apps.service.Idempotent.IdempotentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.math.BigDecimal;

@Controller
@RequestMapping("/idempotent")
@Api(value = "IdempotentController",tags = {"幂等测试controller"})
public class IdempotentController {

    @Autowired
    private IdempotentService idempotentService;
    @Autowired
    private RedisService redisService;

    @RequestMapping(value = "/create/token",method = RequestMethod.POST)
    @ResponseBody
    @ApiOperation("创建token")
    public MsgResponse createToken(){
        MsgResponse response = idempotentService.createToken();
        return response;
    }

    @RequestMapping(value = "/balance",method = RequestMethod.POST)
    @ResponseBody
    @ApiOperation("进行业务操作")
    @Token
    public MsgResponse subTract( BigDecimal count){
        MsgResponse response = idempotentService.subtract(count);
        return response;
    }

    @RequestMapping(value = "/get/token",method = RequestMethod.POST)
    @ApiOperation("判断token是否还有效")
    public void getToken(String key){
        boolean contains = redisService.contains(key);
        System.out.println("==========redis是否存在:"+contains);
    }

    @RequestMapping(value = "/del/token",method = RequestMethod.POST)
    @ApiOperation("删除token")
    public void delToken(String key){
        boolean contains = redisService.contains(key);
        System.out.println("==========redis是否存在:"+contains);
        boolean del = redisService.del(key);
        if(del){
            System.out.println("===========key删除成功!");
        }else{
            System.out.println("==============key删除失败");
        }
    }


}
package com.apps.service.Idempotent.impl;

import com.apps.bcodemsg.MsgResponse;
import com.apps.redis.service.RedisService;
import com.apps.service.Idempotent.IdempotentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.UUID;

@Service
public class IdempotentImpl implements IdempotentService {

    private static BigDecimal account = new BigDecimal(10000);

    @Autowired
    private RedisService redisService;

    @Override
    public MsgResponse createToken() {

        MsgResponse msgResponse = new MsgResponse();

        try{
            String token = UUID.randomUUID().toString();
            System.out.println("============token: "+token);
            boolean isSuccess = redisService.set(token, token, 10*60*1000);
            if(isSuccess){
                System.out.println("============token成功添加到Redis中。");
                msgResponse.success("key.msg");
                msgResponse.setData(token);
                msgResponse.setMsg("创建token成功!");
            }else{
                System.out.println("=============token添加到Redis中失败!");
                msgResponse.success("key.err");
                msgResponse.setMsg("创建token失败!");
            }
        }catch(Exception ex){
            System.out.println("发生异常的接口:createToken()");
            ex.printStackTrace();
        }finally {
            return msgResponse;
        }
    }

    @Override
    public MsgResponse subtract(BigDecimal count) {

        MsgResponse response = new MsgResponse();

        try{
            System.out.println("===============当前数量:"+account);
            BigDecimal result = account.subtract(count);
            account = result;
            if(account.setScale(2).compareTo(BigDecimal.ZERO)<=0){
                response.fail("key.err");
                response.setMsg("余额已经小于0,无法继续操作!");
                return response;
            }
            System.out.println("=================扣除成功,你很棒棒哒啊!");
        }catch(Exception ex){
            System.out.println("异常接口为:subtract(Integer count)");
            ex.printStackTrace();
        }finally {
            return response;
        }
    }
}

四.使用Jmeter进行测试

会看到只有一个请求能够成功处理业务,其余请求无法修改数据。