redis distributed lock

preface

redis and zookeeper are the most widely used choices of distributed locks in the market. These two implementations have their own advantages.

zookeeper: high availability, low performance and low concurrency.

redis: high performance, medium availability and high concurrency

Select the appropriate technology implementation according to the business scenario. Next, realize the distributed lock by applying the characteristics of redis.

Requirements related to business development

  1. Low coupling with business code and low intrusion.

  2. Easy to use and simple

  3. Component development

Implementation principle

Using the serial operation characteristics of redis, set the relevant lock to the key and set the expiration time for the key. Write it as a luna script to realize the integrated operation.

redislock related open source

redisson is an open source redis client that provides rich data operations. The implementation of distributed lock is also provided. If you already have relevant open source projects, don't build wheels again.

Code related implementation

According to the relevant business development requirements, based on the characteristics of spring aop, it is implemented by annotation. The development of components refers to the implementation of relevant open source projects and integrates their own implementation

Refer to the open source git address: https://github.com/kekingcn/spring-boot-klock-starter

The lock assembly involves functions

  1. Lock type implementation: Reentry lock, fair lock
  2. Add unlock timeout, exception handling
  3. Custom error handling
  4. The watchdog function is enabled
  5. Custom local parameter incoming

@RedisLock facet class

package com.xxx.component.redislock.annotation;

import com.xxx.component.redislock.model.LockRange;
import com.xxx.component.redislock.model.LockType;
import com.xxx.component.redislock.strategy.LockExceptionStrategy;
import com.xxx.component.redislock.strategy.LockTimeOutStrategy;
import com.xxx.component.redislock.strategy.ReleaseExceptionStrategy;
import com.xxx.component.redislock.strategy.ReleaseTimeOutStrategy;
import com.xxx.component.redislock.util.Constant;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RedisLock {
    /**
     * Lock prefix
     * @return
     */
    String preFix() default "";


    /**
     * Lock range
     *
     * @return
     */
    LockRange lockRange() default LockRange.normal;

    /**
     * Lock suffix
     *
     * @return
     */
    String postFix() default "";

    /**
     * Parameter key value splicer
     * @return
     */
    String separator() default ".";
    /**
     * Lock name
     * @return name
     */
    String name() default "";
    /**
     * Lock type, reentrant lock by default
     * @return lockType
     */
    LockType lockType() default LockType.Reentrant;
    /**
     * Try to lock, maximum waiting time
     * @return waitTime
     */
    long waitTime() default Constant.waitTime;
    /**
     *Automatically unlock xxx seconds after locking
     * @return leaseTime
     */
    long leaseTime() default Constant.leaseTime;

    /**
     * Custom business key
     * @return keys
     */
     String[] keys() default {};

     /**
      * Lock exception handling strategy
     * @return lockTimeoutStrategy
     */
     LockExceptionStrategy lockExceptionStrategy() default LockExceptionStrategy.FAIL_FAST;

     /**
      * Lock release exception handling strategy
     * @return releaseTimeoutStrategy
     */
     ReleaseExceptionStrategy releaseExceptionStrategy() default ReleaseExceptionStrategy.FAIL_FAST;


    /**
     * Lock timeout policy
     *
     * @return lockTimeoutStrategy
     */
    LockTimeOutStrategy lockTimeoutStrategy() default LockTimeOutStrategy.FAIL_FAST;


    /**
     * Processing policy for timeout when releasing lock
     *
     * @return releaseTimeoutStrategy
     */
    ReleaseTimeOutStrategy releaseTimeoutStrategy() default ReleaseTimeOutStrategy.NO_OPERATION;

    /**
     * Local parameter variable
     *
     * @return
     */
    String[] localArgs() default {};

}

Custom error handling @ CustomLockExceptionStrategy

In case of timeout or other exceptions, user-defined error handling is required. eg: record lock error data, etc. I personally suggest asynchronous processing here.

package com.xxx.component.redislock.annotation;

import java.lang.annotation.*;

/**
 * @Author Buck Wang
 * @Description Custom reisson locking exception handling class
 * @Date 2021/4/7 15:06
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomLockExceptionStrategy {
    //Processing class that implements CustomLockExceptionStrategyInter
    Class name();
    //Custom business type
    String busType() default "";

}

aop facet class of RedisLockAspect

@Aspect
@Component
@Order(0)
public class RedisLockAspect {

    private static final Logger logger = LoggerFactory.getLogger(RedisLockAspect.class);

    @Autowired
    LockFactory lockFactory;

    @Autowired
    private LockInfoProvider lockInfoProvider;


    @Around(value = "@annotation(redisLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {

        LockInfo lockInfo = lockInfoProvider.get(joinPoint, redisLock);
        LockRes lockResCurrent = new LockRes(lockInfo, LockResEnum.LockDealStatusEnum.EXCEPTION.getKey());
        Lock lock = lockFactory.getLock(lockInfo);
        lockResCurrent.setLock(lock);
        //Lock
        lock(lockResCurrent);

        try {
            return joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
            throw e;
        } finally {
            try {
                //Release lock
                releaseLock(lockResCurrent);
            } catch (Throwable e) {
                throw e;

            } finally {
                LockThreadLocalUtil.removeLockRes(lockResCurrent);
            }


        }


    }

}

Attention

The Order execution level of aop needs to be raised. The smaller the value, the earlier the execution. The value of the transaction involved in the implementation of the @ Order.lock method is smaller than that of the aop method. Therefore, the value of the transaction involved in the implementation of the @ Order.lock method is also smaller than that of the aop method LOWEST_ PRECEDENCE=2147483647

Instantiate the redundancy client

  1. code implementation

  2. xml configuration file

At present, the framework based on spring MVC in the project is version 3.2.5. Considering redis exceptions and automatic offline error nodes, redis adopts sentinel mode.

xml configuration of spring

<?xml version="1.0" encoding="UTF-8"?>
<beans default-lazy-init="true"
	   xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
	   http://www.springframework.org/schema/context
	   http://www.springframework.org/schema/context/spring-context.xsd
	   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
	   http://redisson.org/schema/redisson
	   http://redisson.org/schema/redisson/redisson.xsd"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xmlns:redisson="http://redisson.org/schema/redisson">


	<aop:aspectj-autoproxy expose-proxy="true"/>
	<context:component-scan base-package="com.xxx.component.redislock"></context:component-scan>
	<redisson:client id="redisLockComponent" >
		<redisson:sentinel-servers master-name="${redislock.sentinelmaster}"
								   slave-connection-pool-size="500"
								   master-connection-pool-size="500"
								   idle-connection-timeout="60000"
								   slave-connection-minimum-idle-size="32"
								   master-connection-minimum-idle-size="32"
								   connect-timeout="10000"
								   timeout="3000"
								   ping-timeout="1000">
			<redisson:sentinel-address value="${redislock.addr1}" />
			<redisson:sentinel-address value="${redislock.addr2}" />
			<redisson:sentinel-address value="${redislock.addr3}" />
		</redisson:sentinel-servers>
	</redisson:client>
</beans>

springboot

For spring boot, redisson has corresponding jar s for code implementation. Of course, you can also implement it in your own code. Here is the code.

Config config = Config.fromYAML("yaml Content of");
or
Resource resource = ctx.getResource(redisLockConfig.getFile());
InputStream is = resource.getInputStream();   
Config config = Config.fromYAML(is);
Redisson.create(config);

Configuration of redisson yaml

sentinelServersConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  subscriptionsPerConnection: 5
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  slaveSubscriptionConnectionMinimumIdleSize: 1
  slaveSubscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 32
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 32
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  sentinelAddresses:
    - "redis://10.xxx.32.94:26379"
    - "redis://10.xxx.32.95:26379"
    - "redis://10.xxx.32.106:26379"
  masterName: "mymaster"
  database: 0
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"

Attention

#redislock, sentry mode configuration
redislock.sentinelmaster=mymaster
redislock.addr1=redis://10.xx.32.xx:26379
26379 This is the Sentinel's port

Enable watchdog

org.redisson.RedissonLock#scheduleExpirationRenewal

private void scheduleExpirationRenewal(final long threadId) {
        if (expirationRenewalMap.containsKey(getEntryName())) {
            return;
        }

        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                
                future.addListener(new FutureListener<Boolean>() {
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        
                        if (future.getNow()) {
                            // reschedule itself
                            scheduleExpirationRenewal(threadId);
                        }
                    }
                });
            }

        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
            task.cancel();
        }
    }

Set the value of leaseTime to - 1, schedule tasks, internalLockLeaseTime / 3, and execute automatic renewal once. The renewal time is internalLockLeaseTime. If the internalLockLeaseTime is not set, the default is 30000ms. If you want to see the automatic delay, be careful not to use the debug breakpoint execution method, so that the execution of scheduled task renewal will get stuck. You can use the thread sleep method to see whether the automatic delay is determined by observing the change of the remaining expiration time of the key.

The key of redisson setting lock is hashmap type.

#Check whether the key exists. If it does not exist, the return value is 0
HGETALL key
#Second, check the remaining time of the key. Return - 1, which means the expiration time is not set, and - 2, which means the key has expired.
ttl key
#Check the remaining time of the key in milliseconds
Pttl key

Problems encountered in development

  1. Note not valid

    The main reason is that the aop function is not enabled, which makes it ineffective.

    <aop:aspectj-autoproxy expose-proxy="true"/>

  2. Nested calls, aop does not take effect

    Expose proxy = "true", the attribute must be true. eg: for non aop annotated methods in the same class, to call aop annotated methods, you need to get the proxy of this class first and call it later.

    OrderService has method1 and method2 annotated aop methods. method1->method2

    OrderService orderService=(OrderService) AopContext.currentProxy();
    orderService.method2()
    

Keywords: Java Redis Spring Distributed lock redisson

Added by cab on Fri, 18 Feb 2022 16:19:16 +0200