springboot基于Redisson实现的一套分布式锁,支持注解形式使用,key支持el表达式

1.背景

项目中最近迫切的需要一套分布式锁来解决一些接口的并发问题,在网上搜集了各种资料。并结合自己的项目场景,依赖Redis实现了一套分布式锁,使用简单,支持注解。在这里分享一下实现过程,希望能对你们有一些帮助。

 

2.项目结构

核心包:

springboot基于Redisson实现的一套分布式锁,支持注解形式使用,key支持el表达式_第1张图片

 

3.maven依赖

        
            org.springframework.boot
            spring-boot-starter-data-redis
            2.2.5.RELEASE
            
                
                    redis.clients
                    jedis
                
                
                    io.lettuce
                    lettuce-core
                
            
        

        
            org.redisson
            redisson-spring-boot-starter
            3.13.0
        

 

4.Redis配置

无需多说

spring:
    redis:
        host: xxxxx
        password: xxxxx
        timeout: 3000
        port: 6379

5.Redisson配置

redisson配置项需要单独配置一份yaml文件,在springboot的yml中配置是无法生效的。

redisson配置有两种方式:编码形式和配置式

方式一:纯编码形式无需编写yaml文件,直接对所需配置项进行配置就好:如下

@Configuration
public class RedissonConfig {

  

    @Bean
    @ConditionalOnProperty("spring.redis.host")
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.setTransportMode(TransportMode.EPOLL);
        config.useClusterServers()
                // use "rediss://" for SSL connection
                .addNodeAddress("perredis://127.0.0.1:7181");

        return  Redisson.create(config);
    }

}

配置方式二 配置式:新增redission.yaml配置文件

clusterServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveCheckInterval: 60000
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: ! {}
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 24
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 24
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  subscriptionMode: "SLAVE"
  nodeAddresses:
  - "redis://127.0.0.1:7004"
  - "redis://127.0.0.1:7001"
  - "redis://127.0.0.1:7000"
  scanInterval: 1000
  pingConnectionInterval: 0
  keepAlive: false
  tcpNoDelay: false
threads: 16
nettyThreads: 32
codec: ! {}
transportMode: "NIO"

之后编写配置类,将此配置文件引入即可(注意路径)。

Config config = Config.fromYAML(new File("redisson.yaml"));  
RedissonClient redisson = Redisson.create(config);

配置参数根据不同的redis使用场景(单机、主备、cluster等)需要自行去选择,官方文档都做了详细的讲解,具体配置参数请移步官网进行详细查看:redisso官方文档

6.编写分布式锁代码

编写注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    /**
     * 分布式锁的key值,非空。支持el表达式获取入参对象参数
     */
    String[]  keys();
    /**
     * 分布式锁key前缀,默认采用类全限定名:方法名
     */
    String  prefix() default "";
    /**
     * key与前缀的分隔符
     */
    String  separator() default ":";
    /**
     * 等待获取锁的时间
     */
    long  waitTime() default 8;
    /**
     * 锁的租期,超时后自动释放
     */
    long  leaseTime() default 3;
    /**
     * 时间单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

考虑到我们的业务场景,这里的keys我是用了数组形式,可以获取多个入参的参数。支持EL

配置切面及解析逻辑

@Aspect
@Component
@Slf4j
public class DistributedLockAspect {

    @Resource
    DistributedLocker distributedLocker;

    @Pointcut("@annotation(xxx.lock.annotation.DistributedLock)")
    public void pointCut(){}


    /**
     * 环绕增强,尝试获取锁/释放锁
     *
     * @param joinPoint 切面
     * @return Object
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        DistributedLock annotation = AnnotationUtils.findAnnotation(targetMethod, DistributedLock.class);
        assert annotation != null;
        String key=getLockKey(targetMethod,joinPoint,annotation);
        boolean lockFlag =false;
        Object proceed =null;
        try {
            lockFlag=distributedLocker.tryLock(key,annotation.timeUnit(),annotation.waitTime(),annotation.leaseTime());
            if (lockFlag) {
                log.info("success to get distributed lock with key {}",key);
                proceed = joinPoint.proceed();
            }
        } catch (Exception exception) {
            log.error("exception occurred while getting distributed lock ",exception);
            return null;
        }finally {
            if (lockFlag){
                distributedLocker.unlock(key);
                log.info("lock {} has been released",key);
            }
        }
        return proceed;
    }


    /**
     * 获取拦截到的方法,解析分布式锁key值(如果包含el表达式,则从中解析出内容)
     *
     * @param joinPoint 切点
     * @return redisKey
     */
    private String getLockKey(Method targetMethod,
                              ProceedingJoinPoint joinPoint, DistributedLock targetAnnotation) {
        Object target = joinPoint.getTarget();
        Object[] arguments = joinPoint.getArgs();
        StringBuilder stringBuilder=new StringBuilder();
        for (int i=0;i

EL解析工具(此段代码为引用工具,出处未知):

public class SpelUtil {
    public static String parse(String spel, Method method, Object[] args) {
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法参数放入SPEL上下文中
        if (paraNameArr != null) {
            for (int i = 0; i < paraNameArr.length; i++) {
                context.setVariable(paraNameArr[i], args[i]);
            }
        }
        return parser.parseExpression(spel).getValue(context, String.class);
    }

    /**
     * 支持 #p0 参数索引的表达式解析
     * @param rootObject 根对象,method 所在的对象
     * @param spel 表达式
     * @param method ,目标方法
     * @param args 方法入参
     * @return 解析后的字符串
     */
    public static String parse(Object rootObject,String spel, Method method, Object[] args) {
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
        //把方法参数放入SPEL上下文中
        if (paraNameArr != null) {
            for (int i = 0; i < paraNameArr.length; i++) {
                context.setVariable(paraNameArr[i], args[i]);
            }
        }
        return parser.parseExpression(spel).getValue(context, String.class);
    }
}

编写锁及实现

接口DistributedLocker

/**
 * 分布式锁
 *
 * @author 小菜瓜
 * @since 2020/6/8
 */
public interface DistributedLocker {

    /**
     * 以默认配置获取锁,等待锁默认5s,锁租期默认3s
     *
     * @param lockKey lockKey
     * @return RLock
     */
    RLock lock(String lockKey) ;

    /**
     * 自定义超时时间的锁,最多等待timeout秒,
     *
     * @param lockKey lockKey
     * @param waitTime 超时时间,单位为秒
     * @return lock
     */
    RLock lock(String lockKey, long waitTime);

    /**
     * 自定义超时时间和时间单位的锁,
     *
     * @param lockKey lockKey
     * @param waitTime 超时时间
     * @param unit 时间单位
     * @return lock
     */
    RLock lock(String lockKey, TimeUnit unit, long waitTime);

    /**
     * 尝试加锁。自定义时间单位和锁等待时间,租期默认3s
     *
     * @param lockKey lockKey
     * @param unit 时间单位
     * @param waitTime 超时时间
     * @return boolean
     */
    boolean tryLock(String lockKey, TimeUnit unit, long waitTime);


    /**
     *  尝试加锁,自定义时间单位和锁等待时间以及锁过期时间
     *
     * @param lockKey key
     * @param timeUnit 时间单位
     * @param waitTime 等待超时时间
     * @param leaseTime 锁租期,超过leaseTime自动释放
     * @return boolean
     */
    boolean tryLock(String lockKey,TimeUnit timeUnit, long waitTime , long leaseTime);

    /**
     * 公平锁,多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
     * 尝试加锁,最多等待waitTime后强制获取锁,上锁以后leaseTime自动解锁
     *
     * @param lockKey   锁key
     * @param unit      锁时间单位
     * @param waitTime  等到最大时间,强制获取锁
     * @param leaseTime 锁自动时间,
     * @return 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
     */
    boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);


    /**
     *  获取公平锁,单位为秒
     *
     * @param lockKey lockKey
     * @param waitTime 等待时间
     * @param leaseTime 租期
     * @return boolean
     */
    boolean fairLock(String lockKey, long waitTime,long leaseTime);

    /**
     * 解锁
     *
     * @param lockKey lockKey
     */

    void unlock(String lockKey);

    /**
     * 解锁RLock
     *
     * @param lock
     */
    void unlock(RLock lock);

}

实现类DistributedLockImpl :在这里我们依赖RedissonClient 进行锁的实现

@Slf4j
@Component
public class DistributedLockImpl implements DistributedLocker{
    /**
     * 默认时间单位:秒
     */
    public static final TimeUnit DEFAULT_TIME_UNIT= TimeUnit.SECONDS;

    /**
     * 默认锁等待超时时间
     */
    public static final int DEFAULT_TIMEOUT=8;
    /**
     * 默认锁过期时间
     */
    public static final int DEFAULT_LEASE_TIME=3;


    @Resource
    private RedissonClient redissonClient;



    @Override
    public RLock lock(String lockKey) {
        RLock lock =redissonClient.getLock(lockKey);
        try {
            lock.tryLock(DEFAULT_TIMEOUT,DEFAULT_LEASE_TIME,DEFAULT_TIME_UNIT);
        } catch (InterruptedException e) {
           log.error("get lock with key {} failed,cause ",lockKey,e);
           return null;
        }
        return lock;
    }

    @Override
    public RLock lock(String lockKey, long timeout) {
        return lock(lockKey,DEFAULT_TIME_UNIT,timeout);
    }

    @Override
    public RLock lock(String lockKey, TimeUnit unit, long timeout) {
        RLock lock =redissonClient.getLock(lockKey);
        try {
            lock.tryLock(timeout,DEFAULT_LEASE_TIME,unit);
        } catch (InterruptedException e) {
            log.error("get lock with key {} failed. cause",lockKey,e);
            return null;
        }
        return lock;
    }

    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, long timeout) {
        return  tryLock(lockKey,unit,timeout,DEFAULT_LEASE_TIME);
    }

    @Override
    public boolean tryLock(String lockKey, TimeUnit timeUnit, long waitTime, long leaseTime) {
        RLock lock=redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime,leaseTime,timeUnit);
        } catch (InterruptedException e) {
            log.error("get lock with key {} failed. cause",lockKey,e);
            return false;
        }
    }

    @Override
    public boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
        RLock lock=redissonClient.getFairLock(lockKey);
        try {
            return  lock.tryLock(waitTime,leaseTime,unit);
        } catch (InterruptedException e) {
            log.error("get lock with key {} failed. cause",lockKey,e);
            return false;
        }
    }

    @Override
    public boolean fairLock(String lockKey, long waitTime, long leaseTime) {
        return fairLock(lockKey,DEFAULT_TIME_UNIT,waitTime,leaseTime);
    }


    @Override
    public void unlock(String lockKey) {
        RLock lock=redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void unlock(RLock lock) {
        lock.unlock();
    }
}

完毕!

以上这一套分布式锁可以拿来就用,具体的 waitTime() default 8; leaseTime() default 3;等参数还需要各位根据自己的业务场景去进行调整。

如果有更好的改进方案和不对的地方还请各位大佬纠正和指教,感兴趣也可以可以一起讨论一下。

你可能感兴趣的:(技术总结,java后端,个人心得,java,分布式,分布式锁,springboot,Redisson)