Redis实现接口幂等

13 篇文章 0 订阅
10 篇文章 0 订阅

最近自己在做一套spring开发脚手架,期间做了一个幂等工具。今天分享一下吧。也请大家给提提意见。看看有哪些问题。

 

实现思路大概就是一个声明式的方式,通过注解进入切面,实现对目标方法的环切。利用redis的单线程特性。实现接口幂等。

 

不多说了,直接上代码,现阶段还不是很完善。后续如果整个项目完善了,到时候再发上来吧。

 

先看一下注解:

/**
 * 幂等注解
 * 用于controller层方法
 *
 * @author: 王锰
 * @date: 2018/8/18
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    /**
     * @return key超时时间;默认10秒超时
     */
    long timeout() default 10;

    /**
     * @return 幂等策略
     */
    IdempotentStrategy strategy() default IdempotentStrategy.BASE_IDEMPOTENT_DTO;

}

幂等注解,标记controller层方法。里面有两个属性,一个是设置的超时时间;一个是幂等策略。其实还不是很完善,比如还可以自定义时间单位,设定等待时间等等一些其他操作。这里因为也是一个初步方案,就简单实现了。

/**
 * 幂等字段注解
 * 用于标注dto中的字段
 * 要配合@Idempotent使用
 *
 * @author: 王锰
 * @date: 2018/8/18
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IdempotentField {

}

幂等字段注解,只有当开启了幂等才能起作用。不然该注解无效。目前是一个空接口,后续有需要的地方可以加入相应的配置。

 

上面提到了幂等策略,这里我用了一个策略模式,根据声明的不同策略进行不同幂等key的操作。

/**
 * 幂等策略枚举
 *
 * @author: 王锰
 * @date: 2018/08/20
 */
@Getter
public enum IdempotentStrategy {

    /**
     * 使用继承与BaseDto的类,如果dto中有指定字段,使用指定字段;否则使用dto整体做幂等
     */
    BASE_IDEMPOTENT_DTO,

    /**
     * 使用实现了IdempotentInterface接口的类做幂等;如果dto中有指定字段,使用指定字段;否则使用dto整体做幂等
     */
    IDEMPOTENT_INTERFACE,

    /**
     * 使用整个参数列表做幂等,无关@idempotentfield
     */
    LIST_PARAMETER,
    ;

}

目前我们现在都是使用http+json形式的rest风格接口,其中的get和delete天生具有幂等性,所以幂等主要用于post和put,而put一般不做计算也是幂等的,况且一些公司会规定只能用post请求。所以我就做了一个策略模式,上面提到3个策略,一个是使用继承BaseIdempotentDto;一个是实现IdempotentInterface接口;一个是使用方法的参数列表;

所以引申出一个类,一个接口;

@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(value = "BaseIdempotentDto对象", description = "需要使用messageId实现幂等的dto对象基类")
public class BaseIdempotentDto extends BaseDto {

    @IdempotentField
    @ApiModelProperty(value = "消息ID,用于实现幂等操作", name = "messageId", dataType = "String")
    protected String messageId;

}

继承这个类,默认使用messageId进行幂等操作;为什么要做这个呢?是因为后面我有一个消息代理项目,做一个分布式消息可靠性投递,那时每个消息会有自己的一个唯一ID也就是messageId。所以通过broker发送的消息,会直接通过messageId做幂等。

 

/**
 * 一个空接口,用于标注该类需要幂等
 *
 * @author: 王锰
 * @date: 2018/8/20
 */
public interface IdempotentInterface {
}

一个空接口,主要用于标记此DTO是需要被用来做幂等的。其实也是后续可以提供很多默认方法,如果有必要的话。这里还是一个最初方案。所以一切以实现为目标。拓展性的东西,后续再补充吧。毕竟先做对,再做好。

 

好,基础的东西都说完了,后面该实现我们的幂等策略了。这里之前我写过一个spring下实现策略模式。有兴趣的可以去看看。

这里直接上代码

既然是策略模式,必须现有一个幂等策略功能接口:

/**
 * 接口幂等keyStr实现策略
 *
 * @author: 王锰
 * @date: 2018/8/20
 */
public interface IdempotentStrategyInterface {

    /**
     * 根据不同策略获取key值
     *
     * @return key值
     */
    String process(ProceedingJoinPoint pjp) throws IllegalAccessException;

}

先做一个策略转换器模板类

/**
 * 策略模板
 *
 * @author 王锰
 * @date 20:02 2019/7/8
 */
@Data
public abstract class AbstractStrategyContext<R> {

    @Autowired
    Map<String, R> map;

    /**
     * 根据type获取对应的策略实例
     *
     * @param type 策略名称
     * @return 策略实例
     */
    R getStrategy(String type) {
        return Optional.ofNullable(getMap().get(type)).orElseThrow(() -> new RuntimeException("类型:" + type + "未定义"));
    }

}

然后实现我们当前幂等功能的策略转换器

/**
 * 幂等策略转换器
 *
 * @author 王锰
 * @date 20:03 2019/7/8
 */
@Component
public class IdempotentStrategyContext extends AbstractStrategyContext<IdempotentStrategyInterface> {

    /**
     * 策略转换方法
     */
    public <T> String accept(IdempotentStrategy idempotentStrategy, ProceedingJoinPoint pjp) throws IllegalAccessException {
        return getStrategy(idempotentStrategy.name()).process(pjp);
    }
}

好了,基本的框架写完了。后面就剩实现AOP拦截和具体策略了。

我们先看一下AOP吧。很简单,

/**
 * 接口幂等切面
 *
 * @author: 王锰
 * @date: 2019/8/18
 */
@Aspect
@Component
@Slf4j
public class IdempotentAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IdempotentStrategyContext idempotentStrategyContext;

    /**
     * 切点,标注了@Idempotent的controller方法
     */
    @Pointcut(value = "@annotation(org.wmframework.idempotent.annotations.Idempotent)")
    public void idempotent() {
    }

    @Around("idempotent()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("-----idempotent aspect start-----");
        Object[] args = pjp.getArgs();
        if (null == args || args.length == 0) {
            log.error("args is null,skip idempotent,execute target class");
            return pjp.proceed();
        }
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Idempotent idempotent = methodSignature.getMethod().getAnnotation(Idempotent.class);
        long timeout = idempotent.timeout();
        IdempotentStrategy strategy = idempotent.strategy();
        String keyStr = idempotentStrategyContext.accept(strategy, pjp);
        if (StringUtils.isEmpty(keyStr)) {
            log.error("keyStr is null,skip idempotent,execute target class");
            return pjp.proceed();
        }
        String key = new Md5Hash(JSON.toJSONString(keyStr)).toHex();
        log.info("redis key:{}", key);
        boolean setNxRes = setNx(key, "1", timeout);
        if (setNxRes) {
            log.info("try lock success,execute target class");
            Object processResult = pjp.proceed();
            String targetRes = JSONObject.toJSONString(processResult);
            log.info("target result:{}", targetRes);
            setEx(key, targetRes, timeout);
            return processResult;
        } else {
            log.info("try lock failed");
            String value = redisTemplate.opsForValue().get(key);
            if ("1".equals(value)) {
                log.error("same request executing");
                throw new BizException("请求正在处理。。。。。。");
            } else {
                log.info("same request already be executed,return success result");
                //第一次已经处理完成,但未过超时时间,所以后续同样请求使用同一个返回结果
                return JSONObject.parseObject(value, Resp.class);
            }

        }
    }

    /**
     * 使用StringRedisTemplate实现setnx
     *
     * @param key    redis key
     * @param value  redis value  1
     * @param expire 设置的超时时间
     * @return true:setnx成功,false:失败或者执行结果为null
     */
    private boolean setNx(String key, String value, Long expire) {
        try {
            RedisCallback<Boolean> callback = (connection) ->
                    connection.set(key.getBytes(), value.getBytes(), Expiration.seconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
            Boolean result = redisTemplate.execute(callback);
            return Optional.ofNullable(result).orElse(false);
        } catch (Exception e) {
            log.error("setNx redis occured an exception", e);
            return false;
        }
    }

    /**
     * 使用StringRedisTemplate实现setex
     *
     * @param key    redis key
     * @param value  redis value
     * @param expire 设置的超时时间
     * @return true:setex成功,false:失败或者执行结果为null
     */
    private boolean setEx(String key, String value, Long expire) {
        try {
            RedisCallback<Boolean> callback = (connection) ->
                    connection.set(key.getBytes(), value.getBytes(), Expiration.seconds(expire), RedisStringCommands.SetOption.SET_IF_PRESENT);
            Boolean result = redisTemplate.execute(callback);
            return Optional.ofNullable(result).orElse(false);
        } catch (Exception e) {
            log.error("setEx redis occured an exception", e);
            return false;
        }
    }


}

1,注解应该都比较清晰,大概思路就是定义切点为Idempotent。

2,进入环切

3,获取参数列表,如果为空,不使用幂等。一般也不会写这种方法。写也不会需要幂等。起码我没遇到过。

4,获取当前Idempotent注解。

5,注入策略转换器,根据不用策略返回keystr。

6,判断keyStr是否为空,为空不走幂等。

7,将keystr做MD5加密。进行redis的setnx操作。

8,setnx key值为1

        8.1,成功,获取到锁,同样的消息将不可再次进入。执行目标方法,执行后,将结果setex到redis。

        8.2,失败,没获取到锁。判断当前key对应的值。

                8.2.1,value=1   请求正在处理

                8.2.2,value!=1  请求已经处理成功过。直接获取到成功报文返回。

 

这里面的setnx和setex我是用的是stringredistemplate模板来做的,因为直接使用setif方法不能setnx时设置超时时间。所以使用rediscalllback来做。

我看了一下源码,rediscallback的入参是redisconnection,这个接口的父接口是rediscommods。而他又继承了很多接口,比如我们这里用到的redisStringCommands里的set方法。可以通过

	Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);

来进行和jedis同样的setnx设置超时时间的操作。

到这里,还剩下3个策略。

下面直接上代码吧。没什么难度。

/**
 * BASE_IDEMPOTENT_DTO策略
 * 不允许参数列表为空;
 * DTO必须继承于BaseIdempotentDto
 * DTO中如果存在IdempotentField表及字段;使用字段幂等;否则使用DTO幂等
 *
 * @author: 王锰
 * @date: 2018/8/20
 */
@Component("BASE_IDEMPOTENT_DTO")
@Slf4j
public class BaseDtoIdempotentStrategy implements IdempotentStrategyInterface {

    public static final String PREFIX = "base_idempotent_dto_";

    @Override
    public String process(ProceedingJoinPoint pjp) throws IllegalAccessException {
        log.info("idempotent strategy:{}", IdempotentStrategy.BASE_IDEMPOTENT_DTO.name());
        Object[] args = pjp.getArgs();
        Object dto = null;
        for (Object arg : args) {
            boolean assignableFrom = BaseIdempotentDto.class.isAssignableFrom(arg.getClass());
            if (assignableFrom) {
                dto = arg;
                break;
            }
        }
        if (dto == null) {
            log.info("not class extends of BaseIdempotentDto in list of parameter");
            return null;
        }
        StringBuilder keyStr = new StringBuilder(PREFIX);
        List<Field> fields = BeanUtils.recursive(new ArrayList<>(), dto.getClass());
        for (Field field : fields) {
            field.setAccessible(true);
            IdempotentField idempotentField = field.getAnnotation(IdempotentField.class);
            if (null == idempotentField) {
                continue;
            }
            keyStr.append(field.get(dto)).append("_");
        }
        if (keyStr.toString().equals(PREFIX)) {
            log.info("use dto");
            keyStr.append(JSONObject.toJSONString(dto));
        } else {
            log.info("user idempotentField");
            return keyStr.substring(0, keyStr.length() - 1);
        }
        return keyStr.toString();
    }
}
/**
 * IDEMPOTENT_INTERFACE策略
 * 不允许参数列表为空;
 * DTO必须实现IdempotentInterface接口;
 * DTO中如果存在IdempotentField表及字段;使用字段幂等;否则使用DTO幂等
 *
 * @author: 王锰
 * @date: 2018/8/20
 */
@Component("IDEMPOTENT_INTERFACE")
@Slf4j
public class InterfacesIdempotentStrategy implements IdempotentStrategyInterface {

    public static final String PREFIX = "idempotent_interface_";

    @Override
    public String process(ProceedingJoinPoint pjp) throws IllegalAccessException {
        log.info("idempotent strategy:{}", IdempotentStrategy.IDEMPOTENT_INTERFACE.name());
        Object[] args = pjp.getArgs();
        Object dto = null;
        for (Object arg : args) {
            boolean assignableFrom = IdempotentInterface.class.isAssignableFrom(arg.getClass());
            if (assignableFrom) {
                dto = arg;
                break;
            }
        }
        if (dto == null) {
            log.info("no class implements of IdempotentStrategyInterface in list of parameter");
            return null;
        }
        StringBuilder keyStr = new StringBuilder(PREFIX);
        Field[] fields = dto.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            IdempotentField idempotentField = field.getAnnotation(IdempotentField.class);
            if (null == idempotentField) {
                continue;
            }
            keyStr.append(field.get(dto)).append("_");
        }
        if (keyStr.toString().equals(PREFIX)) {
            log.info("use dto");
            keyStr.append(JSONObject.toJSONString(dto));
        } else {
            log.info("use idempotentField");
            keyStr.substring(0, keyStr.length() - 1);
        }
        return keyStr.toString();
    }
}
/**
 * LIST_PARAMETER策略
 * 不允许参数列表为空
 *
 * @author: 王锰
 * @date: 2018/8/20
 */
@Component("LIST_PARAMETER")
@Slf4j
public class ListParameterIdempotentStrategy implements IdempotentStrategyInterface {

    public static final String PREFIX = "list_parameter_";

    @Override
    public String process(ProceedingJoinPoint pjp) {
        log.info("idempotent strategy:{}", IdempotentStrategy.LIST_PARAMETER.name());
        Object[] args = pjp.getArgs();
        StringBuilder keyStr = new StringBuilder(PREFIX);
        keyStr.append(JSONObject.toJSONString(Arrays.asList(args)));
        return keyStr.toString();
    }
}

做策略时有个默认约定就是有问题了返回null。所以在AOP中进行了null值判断。

好了,到这里已经完事了。

 

我又做了一个demo项目,写了一个controller

    @PostMapping("/post1")
    @Idempotent(timeout = 600,strategy = IdempotentStrategy.BASE_IDEMPOTENT_DTO)
    public Resp<User> post1(@RequestBody User user) throws InterruptedException {
        log.info("user:{}", JSONObject.toJSONString(user));
        Thread.sleep(3000);
        User user1 = new User();
        user1.setId(1234);
        user1.setName("wm");
        return Resp.ok(user1);
    }

设置超时时间600秒,策略,继承dto。测试结果不发了。是没什么问题的。第一次请求进来,会请求到方法,第二次进来,就直接返回了。不同的请求不会互相影响。

如果有什么问题或者性能优化的点。可以指出大家共同探讨。就到这吧。

 

哦,对了。我这里其实是做到了一个脚手架里。demo直接引入的jar包就可以用了。后续再完善好了把脚手架发上来吧。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值