springboot is a set of distributed locks based on Redisson, which supports annotation and el expression

1. Background

Recently, there is an urgent need for a set of distributed locks in the project to solve the concurrency problem of some interfaces, and various data are collected on the Internet. Combined with its own project scenario, it relies on Redis to implement a set of distributed locks, which are easy to use and support annotation. Let's share the implementation process here. I hope it can help you.

 

2. Project structure

Core package:

 

3.maven dependence

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.2.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>redis.clients</groupId>
                    <artifactId>jedis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.0</version>
        </dependency>

 

4.Redis configuration

No need to say more

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

5.Redisson configuration

The redisson configuration item needs to be configured with a separate yaml file. Configuration in yml of springboot cannot take effect.

There are two ways to configure redisson: coding and configuration

Mode 1: in pure encoding mode, you don't need to write yaml file, just configure the required configuration items directly as follows:

@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);
    }

}

Configuration mode 2 configuration mode: New redission.yaml configuration file

clusterServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  failedSlaveReconnectionInterval: 3000
  failedSlaveCheckInterval: 60000
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  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: !<org.redisson.codec.FstCodec> {}
transportMode: "NIO"

Then write the configuration class and introduce the configuration file (note the path).

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

The configuration parameters are selected according to different redis use scenarios (stand-alone, active / standby, cluster, etc.) and are explained in detail in the official documents. For specific configuration parameters, please go to the official website for details: Official document of redisso

6. Write distributed lock code

Write notes

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

    /**
     * key value of distributed lock, non empty. Support el expression to get parameters of input parameter object
     */
    String[]  keys();
    /**
     * Distributed lock key prefix, the default is class fully qualified name: method name
     */
    String  prefix() default "";
    /**
     * key Separator from prefix
     */
    String  separator() default ":";
    /**
     * Waiting time to acquire lock
     */
    long  waitTime() default 8;
    /**
     * Lease term of lock, automatically released after timeout
     */
    long  leaseTime() default 3;
    /**
     * Time unit
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

Considering our business scenario, I use the array form of keys here to get multiple parameters. EL support

Configuration section and analysis logic

@Aspect
@Component
@Slf4j
public class DistributedLockAspect {

    @Resource
    DistributedLocker distributedLocker;

    @Pointcut("@annotation(com.huawei.apiloadtest.lock.annotation.DistributedLock)")
    public void pointCut(){}


    /**
     * Surround enhancement, try to acquire lock / release lock
     *
     * @param joinPoint section
     * @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;
    }


    /**
     * Get the intercepted method, and parse the distributed lock key value (if it contains el expression, the content will be parsed from it)
     *
     * @param joinPoint Tangent point
     * @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<targetAnnotation.keys().length;i++){
            String subKey=targetAnnotation.keys()[i];
            if (StringUtils.isNotBlank(subKey) && StringUtils.contains(subKey, Constants.Symbol.SHARP)) {
                stringBuilder.append(SpelUtil.parse(target, subKey , targetMethod, arguments));
            }else {
                stringBuilder.append(subKey);
            }
        }
        if (StringUtils.isNotBlank(targetAnnotation.prefix())){
            return StringUtil.concat(targetAnnotation.prefix(),targetAnnotation.separator(),stringBuilder);
        }else{
            return StringUtil.concat(target.getClass().getName(),targetAnnotation.separator(),targetMethod.getName(),targetAnnotation.separator(),stringBuilder);
        }
    }
}

EL parsing tool (this code is a reference tool, the source is unknown):

public class SpelUtil {
    public static String parse(String spel, Method method, Object[] args) {
        //Get the list of intercepted method parameter names (using Spring support class library)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //Using SPEL to resolve key
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL context
        StandardEvaluationContext context = new StandardEvaluationContext();
        //Put method parameters in SPEL context
        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);
    }

    /**
     * Support expression parsing of ා p0 parameter index
     * @param rootObject Root object, object of method
     * @param spel expression
     * @param method ,Target method
     * @param args Method reference
     * @return Parsed string
     */
    public static String parse(Object rootObject,String spel, Method method, Object[] args) {
        //Get the list of intercepted method parameter names (using Spring support class library)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        //Using SPEL to resolve key
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL context
        StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
        //Put method parameters in SPEL context
        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);
    }
}

Write lock and Implementation

Interface DistributedLocker

/**
 * Distributed lock
 *
 * @author Cauliflower
 * @since 2020/6/8
 */
public interface DistributedLocker {

    /**
     * Obtain the lock with the default configuration, wait for the lock for 5s by default, and lock lease period for 3s by default
     *
     * @param lockKey lockKey
     * @return RLock
     */
    RLock lock(String lockKey) ;

    /**
     * For custom timeout locks, wait for timeout seconds at most,
     *
     * @param lockKey lockKey
     * @param waitTime Timeout in seconds
     * @return lock
     */
    RLock lock(String lockKey, long waitTime);

    /**
     * Lock of custom timeout and time unit,
     *
     * @param lockKey lockKey
     * @param waitTime Timeout
     * @param unit Time unit
     * @return lock
     */
    RLock lock(String lockKey, TimeUnit unit, long waitTime);

    /**
     * Try to lock. Custom time unit and lock waiting time, lease term is 3s by default
     *
     * @param lockKey lockKey
     * @param unit Time unit
     * @param waitTime Timeout
     * @return boolean
     */
    boolean tryLock(String lockKey, TimeUnit unit, long waitTime);


    /**
     *  Try to lock, customize the time unit, lock waiting time and lock expiration time
     *
     * @param lockKey key
     * @param timeUnit Time unit
     * @param waitTime Wait timeout
     * @param leaseTime Lock lease period, release automatically after leaseTime
     * @return boolean
     */
    boolean tryLock(String lockKey,TimeUnit timeUnit, long waitTime , long leaseTime);

    /**
     * Fair lock: when multiple Redisson client threads request to lock at the same time, the priority is given to the thread that makes the request first.
     * Try to lock, wait for waitTime at most, and then force to acquire the lock. After locking, leaseTime will unlock automatically
     *
     * @param lockKey   Lock key
     * @param unit      Lock time unit
     * @param waitTime  Wait until the maximum time to force lock acquisition
     * @param leaseTime Lock auto time,
     * @return Returns true if the acquisition is successful or false if the acquisition fails (i.e. the lock has been acquired by another thread)
     */
    boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);


    /**
     *  Get fair lock in seconds
     *
     * @param lockKey lockKey
     * @param waitTime waiting time
     * @param leaseTime lease term
     * @return boolean
     */
    boolean fairLock(String lockKey, long waitTime,long leaseTime);

    /**
     * Unlock
     *
     * @param lockKey lockKey
     */

    void unlock(String lockKey);

    /**
     * Unlock RLock
     *
     * @param lock
     */
    void unlock(RLock lock);

}

Implementation class DistributedLockImpl: Here we rely on redisson client to implement the lock

@Slf4j
@Component
public class DistributedLockImpl implements DistributedLocker{
    /**
     * Default time unit: Second
     */
    public static final TimeUnit DEFAULT_TIME_UNIT= TimeUnit.SECONDS;

    /**
     * Default lock wait timeout
     */
    public static final int DEFAULT_TIMEOUT=8;
    /**
     * Default lock expiration time
     */
    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();
    }
}

complete!

The above set of distributed locks can be used as soon as possible. Specific parameters such as waitTime() default 8; leaseTime() default 3; need to be adjusted according to your own business scenarios.

If there is a better improvement plan and something wrong, please correct and give advice. If you are interested, you can discuss it together.

Tags: Redis Spring codec Maven

Posted on Sun, 14 Jun 2020 23:38:45 -0400 by Reviresco