自定义分布式锁实现接口幂等性

1 自定义注解

package com.grm.annotation;


import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * desc: 自定义分布式锁注解,实现接口重复提交(幂等性)
 *
 * @author gaorimao
 * @since 2021-04-30
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLockAnnotation {
    //redis锁前缀
    String prefix() default "";

    //redis锁过期时间
    int expire() default 5;

    //redis锁过期时间单位
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    //redis  key分隔符
    String delimiter() default ":";
}
import java.lang.annotation.*;

/**
 * desc: 自定义key规则注解
 * 由于redis的key可能是多层级结构,例如 redistest:demo1:token:kkk这种形式,因此需要自定义key的规则。
 *
 * @author gaorimao
 * @since 2021-04-30
 */
@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParamAnnotation{
    String name() default "";
}

2 定义key生成策略

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * desc: 定义key生成策略接口
 *
 * @author gaorimao
 * @since 2021-04-30
 */
public interface CacheKeyGeneratorService {
    /**
     * 获取AOP参数,生成指定缓存Key
     *
     * @param joinPoint 切点
     * @return lockKey
     */
    String getLockKey(ProceedingJoinPoint joinPoint);
}

实现层

package com.grm.service.impl;


import com.grm.annotation.CacheLockAnnotation;
import com.grm.annotation.CacheParamAnnotation;
import com.grm.service.CacheKeyGeneratorService;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/**
 * desc: 获取缓存key
 *
 * @author gaorimao
 * @since 2021-04-30
 */
@Service
public class CacheKeyGeneratorServiceImpl implements CacheKeyGeneratorService {
    @Override
    public String getLockKey(ProceedingJoinPoint joinPoint) {
        //获取连接点的方法签名对象
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //Method对象
        Method method = methodSignature.getMethod();
        //获取Method对象上的注解对象
        CacheLockAnnotation cacheLock = method.getAnnotation(CacheLockAnnotation.class);
        //获取方法参数
        Object[] args = joinPoint.getArgs();
        //获取Method对象上所有的注解
        Parameter[] parameters = method.getParameters();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parameters.length; i++) {
            CacheParamAnnotation cacheParams = parameters[i].getAnnotation(CacheParamAnnotation.class);
            //如果属性不是CacheParamAnnotation注解,则不处理
            if (cacheParams == null) {
                continue;
            }
            //如果属性是CacheParamAnnotation注解,则拼接 连接符(:)+ CacheParam
            sb.append(cacheLock.delimiter()).append(args[i]);
        }
        //如果方法上没有加CacheParamAnnotation注解
        if (StringUtils.isEmpty(sb.toString())) {
            //获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            //循环注解
            for (int i = 0; i < parameterAnnotations.length; i++) {
                Object object = args[i];
                //获取注解类中所有的属性字段
                Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    //判断字段上是否有CacheParamAnnotation注解
                    CacheParamAnnotation annotation = field.getAnnotation(CacheParamAnnotation.class);
                    //如果没有,跳过
                    if (annotation == null) {
                        continue;
                    }
                    //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)
                    field.setAccessible(true);
                    //如果属性是CacheParamAnnotation注解,则拼接 连接符(:)+ CacheParam
                    sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field, object));
                }
            }
        }
        //返回指定前缀的key
        return cacheLock.prefix() + sb.toString();
    }
}

3 AOP切面

package com.grm.aspect;

import com.grm.annotation.CacheLockAnnotation;
import com.grm.enums.ErrorEnum;
import com.grm.exception.BusinessException;
import com.grm.service.CacheKeyGeneratorService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.lang.reflect.Method;

/**
 * @author gaorimao
 */
@Aspect
@Configuration
@Slf4j
public class CacheLockAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private CacheKeyGeneratorService cacheKeyGenerator;

    @Pointcut("@annotation(com.grm.annotation.CacheLockAnnotation)")
    public void pointcut() {}

    @Around("pointcut()")
    public Object cacheLockAspect(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CacheLockAnnotation cacheLock = method.getAnnotation(CacheLockAnnotation.class);
        if (StringUtils.isEmpty(cacheLock.prefix())) {
            throw new BusinessException(ErrorEnum.PREFIX_CAN_NOT_EMPTY);
        }
        //获取自定义key
        String lockKey = cacheKeyGenerator.getLockKey(joinPoint);
        log.info("[CacheLockAspect] lockKey={}",lockKey);
        Boolean success = stringRedisTemplate.execute(
                /*
                 * 基于redis的分布式同步锁版本,保证了加锁与设置超时时间两个动作的原子性
                 * @param key 使用key来当锁,唯一
                 * @param value 解锁依据
                 * @param time 超时时间,时间过后,key将会自动删除,避免死锁
                 * @param RedisStringCommands.SetOption 枚举 为NX,XX设置命令参数。
                 *   SET_IF_ABSENT----------NX-------SETNX命令(SET if Not exists)----当且仅当key不存在,将key的值设为value,并返回1;若给定的key已经存在,则SETNX不做任何动作,并返回0
                 *   SET_IF_PRESENT---------XX-------SETXX命令----仅当key存在时,set才会生效
                 *   UPSERT-----------------不要设置任何其他命令参数。
                 * Boolean set(byte[] key, byte[] value, Expiration time, RedisStringCommands.SetOption var4);
                 */
                (RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit())
                        , RedisStringCommands.SetOption.SET_IF_ABSENT));
        if (!success) {
            throw new BusinessException(ErrorEnum.MORE_REPEAT_REQUEST);
        }
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            throw new BusinessException(ErrorEnum.SEVER_ERROR);
        }
    }
}

4 测试


    /**
     * 没有加前缀prefix,测试会报错
     *
     * @param token
     * @return
     */
    @PostMapping(value = "/cacheLock")
    @CacheLockAnnotation()
    public Result cacheLock(String token) {
        return Result.success("sucess=====" + token);
    }

    @PostMapping(value = "/cacheLock1")
    @CacheLockAnnotation(prefix = "redisLock.test", expire = 20)
    public Result cacheLock1(String token) {
        return Result.success("sucess=====" + token);
    }

    @PostMapping(value = "/cacheLock2")
    @CacheLockAnnotation(prefix = "redisLock.test", expire = 20)
    public Result cacheLock2(@CacheParamAnnotation(name = "token") String token) {
        return Result.success("sucess=====" + token);
    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值