记一次幂等通用设计
什么是幂等?
通俗的讲就是多次相同的请求理论上得出来的结果是一样的
如何设计
那我们该如何设计?
首先需要了解怎样保持幂等,保持幂等那么需要一个唯一确定的一组键,来表示唯一的一次请求。。 针对这组键我们需要来设计相关的幂等性。最常见的是数据库加上缓存以及加上锁来保持幂等。。
代码
// 设计两张表, 一张用来存储幂等相关的状态表
// 一张用来存储结果以及入参
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);
}
}
综上是整个幂等的设计
链接: 代码地址.