java实现通用幂等设计

记一次幂等通用设计

什么是幂等?

通俗的讲就是多次相同的请求理论上得出来的结果是一样的

如何设计

那我们该如何设计?
首先需要了解怎样保持幂等,保持幂等那么需要一个唯一确定的一组键,来表示唯一的一次请求。。 针对这组键我们需要来设计相关的幂等性。最常见的是数据库加上缓存以及加上锁来保持幂等。。

Created with Raphaël 2.2.0 开始 发送一个唯一请求 加锁 是否存在缓存中? 从缓存中拿出结果 结束 是否存在数据库中? 从数据库中拿出结果 执行目标方法 缓存结果至redis 缓存结果至数据库 yes no yes no

代码

// 设计两张表, 一张用来存储幂等相关的状态表
// 一张用来存储结果以及入参
create table t_idempotent
(
    id            bigint(11) auto_increment
        primary key,
    request_id   varchar(64)                          not null comment '请求编号',
    app_code varchar(64)                          not null comment '业务类型',
    request_desc       varchar(64)                           null comment '描述',
    business_no    varchar(255)                            null comment '业务编号',
    business_type  varchar(255)                          null comment '业务类型',
    business_desc    varchar(64)                           null comment '业务描述',
    status    tinyint(1)                           not null comment '状态0 进行中,1 成功,2 失败',
    yn            tinyint(1) default 1                 not null comment '数据有效性:0-无效,1-有效',
    created       datetime   default CURRENT_TIMESTAMP not null comment '创建时间',
    modified      datetime                             null
)
    comment '幂等表';

create table t_idempotent_body
(
    id            bigint(11) auto_increment
        primary key,
    idempotent_id   varchar(64)                          not null comment '幂等主键',
    request_param       text                           null comment '参数',
    response_body    blob                            null comment '结果',
    yn            tinyint(1) default 1                 not null comment '数据有效性:0-无效,1-有效',
    created       datetime   default CURRENT_TIMESTAMP not null comment '创建时间',
    modified      datetime                             null
)
    comment '幂等参数表';
//相关的主键以及索引根据自己的需要来

下面展示 结构 common-idempotent 就是幂等相关实现。
在这里插入图片描述

// A code block
aop模块是采用spring aop的形式进行幂等的控制,注入切面
//IdempoentKey.java
package com.wy.aop;

import java.lang.annotation.*;

/**
 * @author wangyong
 * @Classname IdempoentKey
 * @Description 幂等相关key
 * @Date 2021/5/8 17:12
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface IdempoentKey {

    /**
     * 请求编号id 支持el表达式
     *
     * @return
     */
    String requestId();

    /**
     * 请求类型编码 支持el表达式
     *
     * @return
     */
    String appCode();

    /**
     * 请求类型描述 支持el表达式
     * 主要用来告诉我是做啥的
     * 非必填
     * @return
     */
    String requestDesc() default "";

    /**
     * 需要储存的业务字段 持el表达式
     * 非必填
     * @return
     */
    String businessNo() default "";

    /**
     * 需要储存的业务字段类型 持el表达式
     * 非必填
     * @return
     */
    String businessType() default "";

    /**
     * 需要储存的业务字段类型描述 持el表达式
     * 非必填
     * @return
     */
    String businessDesc() default "";

    /**
     * 持锁时间
     * 单位毫秒,默认 5 * 1000 秒
     */
    long keeps() default 5 * 1000;

    /**
     * 超时时间(没有获得锁时的等待时间)
     * 单位毫秒,默认 15 秒,不等待
     */
    long timeout() default 15 * 1000;
}

//IdempotentAspect.java
package com.wy.aop;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.wy.IdempotentService;
import com.wy.context.AbstractIdRequest;
import com.wy.context.IdempotentBodyContext;
import com.wy.context.IdempotentContext;
import com.wy.enums.IdempotentStatusEnum;
import com.wy.exception.AssertEx;
import com.wy.exception.BaseException;
import com.wy.json.JsonUtil;
import com.wy.redis.lock.RedisLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.CharEncoding;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.session.data.redis.config.ConfigureRedisAction;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

import static com.wy.exception.RedisLockException.REQUEST_IS_PROCESS;
import static com.wy.exception.RedisLockException.REQUEST_IS_REPEAT;


/**
 * @author wangyong
 * @Classname IdempoemtAspect
 * @Description 切面
 * @Date 2021/5/8 17:22
 */
@Slf4j
@Configuration
@ComponentScan(value = "org.redisson")
@ConditionalOnClass(value = {ConfigureRedisAction.class, Redisson.class})
@Aspect
public class IdempotentAspect {

    /**
     * spring el表达式开始字符
     */
    private static final String EL_START_CHAR = "#";

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisLockUtil redisLockUtil;

    @Autowired
    private IdempotentService idempotentService;

    /**
     * AOP切入点
     */
    @Pointcut("@annotation(com.dmall.demeter.remote.common.idempotent.aop.IdempoentKey)")
    public void pointCut() {
    }

    /**
     * 切面方法
     *
     * @param point 切入点
     * @return
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        Object[] args = point.getArgs();
        IdempoentKey idempoentKey = method.getAnnotation(IdempoentKey.class);
        RLock rLock = null;
        //开始解析 相关值
        String[] parameterNames = methodSignature.getParameterNames();
        EvaluationContext context = new StandardEvaluationContext();
        ExpressionParser parser = new SpelExpressionParser();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        AssertEx.isTrue(args.length - 1 == 0, BaseException.definedException(1008,"幂等请求参数为多个,请将参数放进继承AbstractIdRequest的POJO中")) ;
        AssertEx.isTrue(args[0] instanceof AbstractIdRequest,BaseException.definedException(1008,"参数请继承AbstractIdRequest抽象类")) ;
        if (idempotentService == null) {
            log.warn("method [{}] 没有实现 IdempotentService 无法执行幂等操作", method.getName());
            return point.proceed();
        }
        String requestId = this.getElValue(point, idempoentKey.requestId(), parser, context);
        if (StringUtils.isBlank(requestId)) {
            log.warn("method [{}] requestId is null,无法执行幂等操作", method.getName());
            return point.proceed();
        }
        String appCode = this.getElValue(point, idempoentKey.appCode(), parser, context);
        if (StringUtils.isBlank(appCode)) {
            log.warn("method [{}] appCode is null,无法执行幂等操作", method.getName());
            return point.proceed();
        }
        Hessian2Output output = null;
        IdempotentContext idempotentContext = null;
        String key = requestId + ":" + appCode;
        //开始加锁
        rLock = redisLockUtil.tryLock(key, idempoentKey.timeout(), idempoentKey.keeps(), TimeUnit.SECONDS, REQUEST_IS_REPEAT);
        try {
            //序列化所用
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            output = new Hessian2Output(os);
            //只有处理正确的结果才会进行redis的存储
            if(redisTemplate.hasKey(key)){
                byte[] bytes = (byte[]) redisTemplate.opsForValue().get(key);
                ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
                Hessian2Input input = new Hessian2Input(bis);
                return input.readObject();
            }
            idempotentContext = idempotentService.queryByReqId(requestId, appCode);
            AssertEx.isTrue(idempotentContext == null || idempotentContext.getId() != null,BaseException.definedException(1098,"幂等主键未返回"));
            //状态为空或者失败则执行方法 然后存储结果5分钟
            if(idempotentContext == null || idempotentContext.getStatus().equals(IdempotentStatusEnum.FAIL)){
                Object proceed = point.proceed();
                //写入object对象中
                output.writeObject(proceed);
                output.close();
                //写入序列化后的数组
                byte[] bytes = os.toByteArray();
                redisTemplate.opsForValue().set(key,bytes, Duration.ofMinutes(60L));
                Long id = null;
                //开始插入表 插入两张表皮
                if(idempotentContext != null){
                    idempotentContext.setStatus(IdempotentStatusEnum.SUCCESS);
                    //更新状态
                    idempotentService.updateInfo(idempotentContext);
                    id = idempotentContext.getId();
                }else{
                    id = this.insertIde(point,idempoentKey,parser,context,IdempotentStatusEnum.SUCCESS);
                }
                //插入参数记录
                this.insertBody(point,bytes,id);
                return proceed;
            }
            //剩下的只有处理成功或者处理中的了
            IdempotentStatusEnum status = idempotentContext.getStatus();
            //如果正在处理中
            if(status.equals(IdempotentStatusEnum.ING)){
                throw REQUEST_IS_PROCESS.get();
            }
            //开始处理结果,如果是成功的状态则将结果直接返回,而不用重新请求
            IdempotentBodyContext contexts = idempotentService.getLatestRecordByIdeKey(idempotentContext.getId());
            if(contexts == null){
                log.warn("查询返回结果为空,requestId [{}],appCode [{}]",requestId,appCode);
                return null;
            }
            //取最新的一条
            String responseBody = contexts.getResponseBody();
            if(StringUtils.isBlank(responseBody)){
                log.warn("查询返回结果为空,requestId [{}],appCode [{}],context [{}]",requestId,appCode, JsonUtil.toJson(context));
                return null;
            }
            ByteArrayInputStream bis = new ByteArrayInputStream(contexts.getResponseBody().getBytes(CharEncoding.ISO_8859_1));
            Hessian2Input input = new Hessian2Input(bis);
            return input.readObject();
        } catch (Exception e) {
            //发生异常记录失败
            //开始插入表 插入两张表皮
            Hessian2Output outputE = null;
            try {
                //序列化所用
                ByteArrayOutputStream ose = new ByteArrayOutputStream();
                outputE = new Hessian2Output(ose);
                //写入object对象中
                outputE.writeObject(e);
                outputE.close();
                Long id = null;
                if(idempotentContext == null){
                    idempotentContext = idempotentService.queryByReqId(requestId,appCode);
                    if(idempotentContext == null){
                        //试图插入
                        id = this.insertIde(point,idempoentKey,parser,context,IdempotentStatusEnum.FAIL);
                    }
                }else{
                    id = idempotentContext.getId();
                }
                if(id == null){
                    log.error("幂等主键未返回,无法插入参数表");
                }else{
                    //插入参数记录表
                    this.insertBody(point,ose.toByteArray(),id);
                }
            }catch (Exception e1){
                log.error("插入失败数据异常",e1);
            }finally {
                try {
                    outputE.close();
                }catch (Exception ioE){
                    log.error("关闭流异常",ioE);
                }
            }
            throw e;
        }finally {
            redisLockUtil.tryUnLock(rLock);
            if(output != null){
                try {
                    output.close();
                }catch (Exception e){
                    log.error("关闭流异常",e);
                }
            }
        }
    }

    private void insertBody(ProceedingJoinPoint point, byte[] bytes,Long id) {
        try {
            idempotentService.insertBody(IdempotentBodyContext.builder()
                    .responseBody(new String(bytes,CharEncoding.ISO_8859_1))
                    .requestParam(JsonUtil.toJson(point.getArgs()))
                    .idempotentId(id)
                    .build());
        } catch (IOException ioException) {
            log.error("序列化失败无法插入",ioException);
        }
    }

    private Long insertIde(ProceedingJoinPoint point, IdempoentKey idempoentKey, ExpressionParser parser, EvaluationContext context,IdempotentStatusEnum statusEnum) {
        return idempotentService.insert(IdempotentContext.builder()
                .appCode(getElValue(point,idempoentKey.appCode(),parser,context))
                .businessDesc(getElValue(point,idempoentKey.businessDesc(),parser,context))
                .businessNo(getElValue(point,idempoentKey.businessNo(),parser,context))
                .businessType(getElValue(point,idempoentKey.businessType(),parser,context))
                .requestDesc(getElValue(point,idempoentKey.requestDesc(),parser,context))
                .requestId(getElValue(point,idempoentKey.requestId(),parser,context))
                .status(statusEnum)
                .build());
    }


    private String getElValue(ProceedingJoinPoint point, String value, ExpressionParser parser, EvaluationContext context) {
        if (!value.startsWith(EL_START_CHAR)) {
            return value;
        }
        Object[] args = point.getArgs();
        if (args != null && args.length > 0) {
            Expression expression = parser.parseExpression(value);
            String s = expression.getValue(context, String.class);
            return s;
        }
        return "";
    }

    public static void main(String[] args) {

    }

}

因为本幂等设计采用redis和数据库双重实现,所以需要配置redis的一些相关设置。。 在git上可以参考common-redis

改切面逻辑仅仅实现幂等的一系列步骤,具体的逻辑需要继承 IdempotentService

package com.wy;


import com.wy.context.IdempotentBodyContext;
import com.wy.context.IdempotentContext;

/**
 * @author wangyong
 * @Classname IServiceIdempoemt
 * @Description 幂等服务表
 * @Date 2021/5/12 17:02
 */
public interface IdempotentService {

    /**
     * 插入数据
     *
     * @param model 实体类
     * @return id
     */
    Long insert(IdempotentContext model);

    /**
     * 插入出入参
     *
     * @param model 实体类
     */
    void insertBody(IdempotentBodyContext model);

    /**
     * 更新幂等状态
     *
     * @param model 实体类
     */
    void updateInfo(IdempotentContext model);


    /**
     * 根据请求id和appCode查询
     *
     * @param requestId 请求id
     * @param appCode appCode
     * @return {@link IdempotentContext}
     */
    IdempotentContext queryByReqId(String requestId, String appCode);


    /**
     * 根据请求id和appCode查询最新的一条记录
     *
     * @param idempotentId 幂等主键
     * @return {@link IdempotentContext}
     */
    IdempotentBodyContext getLatestRecordByIdeKey(Long idempotentId);
}

上述的方法决定幂等是如何存储的。可以根据自己的需要来,本来是想通过dataSource数据源手动编写sql语句,但是这样局限性太大,不够灵活

下面展示context包内容

// AbstractIdRequest 该类为幂等实现必须。。 所有的入参必须继承这个抽象的request
package com.wy.context;

import java.io.Serializable;

/**
 * @author wangyong
 * @Classname AbstractIdRequest
 * @Description 抽象的幂等请求参数模型
 * @Date 2021/5/13 14:33
 */
public abstract class AbstractIdRequest implements Serializable {


    /**
     * 请求编号
     */
    private String requestId;

    /**
     * appCode 编码
     */
    private String appCode;

    public String getAppCode() {
        return appCode;
    }

    public void setAppCode(String appCode) {
        this.appCode = appCode;
    }

    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
}

package com.wy.context;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author wangyong
 * @Classname IdempoemtContext
 * @Description 幂等上下文
 * @Date 2021/5/12 17:03
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class IdempotentBodyContext implements Serializable {


    /**
     * 主键
     */
    private Long id;

    /**
     * 幂等主键
     *
     * @return
     */
    private Long idempotentId;


    /**
     * 请求参数
     */
    private String requestParam;

    /**
     * 请求body体
     */
    private String responseBody;

    /**
     * 创建时间
     */
    private Long createTime;
}

package com.wy.context;

import com.wy.enums.IdempotentStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author wangyong
 * @Classname IdempoemtContext
 * @Description 幂等上下文
 * @Date 2021/5/12 17:03
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class IdempotentContext implements Serializable {


    /**
     * 主键
     */
    private Long id;

    /**
     * 请求编号id
     *
     * @return
     */
    private String requestId;

    /**
     * 请求类型编码
     *
     * @return
     */
    private String appCode;

    /**
     * 请求类型描述
     * 主要用来告诉我是做啥的
     * 非必填
     * @return
     */
    private String requestDesc ;

    /**
     * 需要储存的业务字段
     * 非必填
     * @return
     */
    private String businessNo ;

    /**
     * 需要储存的业务字段类型
     * 非必填
     * @return
     */
    private String businessType ;

    /**
     * 需要储存的业务字段类型描述
     * 非必填
     * @return
     */
    private String businessDesc;

    /**
     * 状态
     */
    private IdempotentStatusEnum status;
}

下面展示幂等的几个状态

package com.wy.enums;

import com.google.common.collect.Lists;

/**
 * @author wangyong
 * @Classname IdempotentStatusEnum
 * @Description IdempotentStatusEnum
 * @Date 2021/5/12 17:18
 */
public enum IdempotentStatusEnum {

    /**
     * 进行中
     */
    ING(0),

    /**
     * 成功
     */
    SUCCESS(1),

    /**
     * 失败
     */
    FAIL(2),
    ;

    private Integer code;

    IdempotentStatusEnum(Integer code){
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }

    public static IdempotentStatusEnum getByCode(Integer code){
        return Lists.newArrayList(IdempotentStatusEnum.values())
                .stream()
                .filter(l -> l.getCode().equals(code))
                .findFirst()
                .orElse(null);
    }
}

综上是整个幂等的设计
链接: 代码地址.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值