The use of redis distributed lock in springboot project

Posted by dionsweet on Tue, 28 Apr 2020 16:15:39 +0200

Now, the microservice projects developed use more distributed locks, so some records are made for the self-determined implementation of distributed locks.
*In the actual project, you can use the RLock lock of the Redisson framework directly.

Create a springboot redis project
    springboot-redis-demo

Add dependency package

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Add configuration item application.properties

spring.cache.type=redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=111111
spring.redis.timeout=0
spring.redis.ssl=false
spring.redis.jedis.pool.max-active=300
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=300
spring.redis.jedis.pool.min-idle=1
#spring.redis.cluster.nodes=127.0.0.1:6080
#spring.redis.cluster.max-redirects=10
#spring.redis.sentinel.master=127.0.0.1:6080
#spring.redis.sentinel.nodes=192.168.1.248:26379,192.168.1.249:26379,192.168.1.250:26379
#spring.data.redis.repositories.enabled=true

Create redis configuration class
Add @ EnableFeignClients annotation to configuration class

@Configuration
@EnableCaching
public class RediseConfig extends CachingConfigurerSupport {

    @Autowired
    private RedisProperties redisProperties;
    @Autowired
    private RedisProperties.Pool redisPropertiesPool;

    @Bean
    @ConditionalOnMissingBean(name = "poolConfig")
    public JedisPoolConfig poolConfig() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setBlockWhenExhausted(true);
        jedisPoolConfig.setTestOnBorrow(true);
        jedisPoolConfig.setMaxTotal(redisPropertiesPool.getMaxActive());
        jedisPoolConfig.setMaxIdle(redisPropertiesPool.getMaxIdle());
        jedisPoolConfig.setMinIdle(redisPropertiesPool.getMinIdle());
        jedisPoolConfig.setMaxWaitMillis(redisPropertiesPool.getMaxWait());// 100000

        jedisPoolConfig.setTestOnBorrow(true);
        jedisPoolConfig.setTestOnReturn(true);
        //Connection scan on Idle
        jedisPoolConfig.setTestWhileIdle(true);
        //Indicates the number of milliseconds to sleep between two scans of idle object monitor
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
        //Represents the maximum number of objects per scan for idle object monitor
        jedisPoolConfig.setNumTestsPerEvictionRun(10);
        //Indicates the minimum time for an object to stay in the idle state before it can be scanned and expelled by the idle object monitor; this item only makes sense when the time between evaluation runsmillis is greater than 0
        jedisPoolConfig.setMinEvictableIdleTimeMillis(60000);
        return jedisPoolConfig;
    }

    @Bean
    @ConditionalOnMissingBean(name = "jedisPool")
    public JedisPool jedisPool(JedisPoolConfig poolConfig) {
        return new JedisPool(poolConfig,
                redisProperties.getHost(),
                redisProperties.getPort(),
                redisProperties.getTimeout().getNano(),
                redisProperties.getPassword());
    }

    @Bean
    @ConditionalOnMissingBean(name = "redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig poolConfig) {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        //Set the host or ip address of the redis server
        redisStandaloneConfiguration.setHostName(redisProperties.getHost());
        redisStandaloneConfiguration.setPort(redisProperties.getPort());
        redisStandaloneConfiguration.setPassword(redisProperties.getPassword());
        redisStandaloneConfiguration.setDatabase(redisProperties.getDatabase());
        //Get the default connection pool construction
        //It should be noted here that editconnectionfactoryj has no (RedisStandaloneConfiguration, JedisPoolConfig) constructor for the Standalone mode, for which
        //We instantiate a constructor with the builder method of the JedisClientConfiguration interface, and we have to convert the type
        JedisClientConfiguration.JedisPoolingClientConfigurationBuilder jpcf = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
        //Modify our connection pool configuration
        jpcf.poolConfig(poolConfig);
        //Constructing jedis client configuration through a constructor
        JedisClientConfiguration jedisClientConfiguration = jpcf.build();
        return new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration);
    }

    @Primary
    @Bean
    public CacheManager cacheManager(@Qualifier("redisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        //. entryTtl(Duration.ofHours(1)) / / set the cache validity for one hour
        //. disablecacheingnullvalues(); / / do not cache null values
        return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheConfiguration).build();
    }

    /**
     * Instantiate RedisTemplate object
     * @return
     */
    @Bean(name = "redisTemplate")
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = this.jackson2JsonRedisSerializer();

        template.setKeySerializer(redisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        template.setHashKeySerializer(redisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }

    @Bean(name = "stringRedisTemplate")
    @ConditionalOnMissingBean(name = "stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = this.jackson2JsonRedisSerializer();

        template.setKeySerializer(redisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        template.setHashKeySerializer(redisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }

    private Jackson2JsonRedisSerializer jackson2JsonRedisSerializer() {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }

    /**
     * Data operation on hash type
     * @param redisTemplate
     * @return
     */
    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    /**
     * Data operation on redis string type
     * @param redisTemplate
     * @return
     */
    @Bean
    public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    /**
     * Data operation on linked list type
     * @param redisTemplate
     * @return
     */
    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    /**
     * Data operations on unordered collection types
     * @param redisTemplate
     * @return
     */
    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    /**
     * Data operations on ordered collection types
     * @param redisTemplate
     * @return
     */
    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }

    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    @Bean
    public RedisLockRegistry redisLockRegistry(@Qualifier("redisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        return new RedisLockRegistry(redisConnectionFactory, "redis_locks", 6000L);
    }

    @Bean
    public Jedis jedis(@Qualifier("jedisPool") JedisPool jedisPool) {
        return jedisPool.getResource();
    }
}

Create RedisLockUtil tool class

@Slf4j
public class RedisLockUtil implements Lock {

    private Jedis jedis = SpringUtils.getBean("jedis");
    private static String REDIS_LOCK_KEY = "REDIS_LOCK_KEY_";
    private static String REQUEST_ID = "redis_lock_id_";
    /**
     * Lock timeout to prevent threads from waiting indefinitely after entering the lock
     */
    private int EXPIRE_TIME_MSECS = 60 * 1000;
    private static final Long RELEASE_SUCCESS = 1L;


    public RedisLockUtil(String key){
        this.REDIS_LOCK_KEY = this.REDIS_LOCK_KEY + key;
    }
    public RedisLockUtil(String key, String val){
        this.REDIS_LOCK_KEY = this.REDIS_LOCK_KEY + key;
        this.REQUEST_ID = this.REQUEST_ID  + val;
    }

    @Override
    public boolean tryLock() {
        try {
            if (jedis.exists(REDIS_LOCK_KEY)) {
                Long setNx = jedis.setnx(REDIS_LOCK_KEY, REQUEST_ID);
                if (setNx == 1) {
                    return Boolean.TRUE;
                }
            }
        } catch (Exception e) {
            log.error("redis tryLock Exception", e);
        }
        return Boolean.FALSE;
    }

    @Override
    public void lock() {
        try {
            if (!tryLock()) {
                SetParams params = new SetParams();
                params.px(EXPIRE_TIME_MSECS).nx();
                jedis.set(REDIS_LOCK_KEY, REQUEST_ID, params);
                log.info(">>>>>>redis lock: {}", jedis.get(REDIS_LOCK_KEY));
            }
        } catch (Exception e) {
            log.error("redis lock Exception", e);
        }
    }

    @Override
    public boolean unlock() {
        try {
            if (jedis.exists(REDIS_LOCK_KEY)) {
                String delLock = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                Object result = jedis.eval(delLock, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(REQUEST_ID));
                if (RELEASE_SUCCESS.equals(result)) {
                    return true;
                }
            } else {
                return true;
            }
        } catch (Exception e) {
            log.error("redis unlock Exception", e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }
}

To ensure that the automatic release time for adding locks when a lock is acquired is atomic
    SetParams params = new SetParams();
    params.px(EXPIRE_TIME_MSECS).nx();
    jedis.set(REDIS_LOCK_KEY, REQUEST_ID, params);
xn: it means SET IF NOT EXIST, that is, when the key does not exist, set operation will be performed; if the key already exists, no operation will be performed;
px: it means to add an expiration time of MSECS to the key.

Create unit test test class

@SpringBootTest(classes = SpringbootRedisDemoApplication.class)
public class RedisTests {

    @Autowired
    private RedisUtils redisUtil;
    
    @Test
    public void redisLockUtilTest() throws InterruptedException {
        RedisLockUtil redisLockUtil = new RedisLockUtil("testLock1", "1");
        redisLockUtil.lock();
        TimeUnit.SECONDS.sleep(10);
        redisLockUtil.unlock();
    }
}

test result
You can see the status of the lock in redis when it is locked

 

Source code example: https://gitee.com/lion123/springboot-redis-demo

 

*What happens when the redis distributed lock expires but the business is not finished?
Directly use the RLock of the Redisson framework. After the successful acquisition of the redisson RLock lock, a scheduled task (a background thread) will be registered to check the expiration time of the lock every 10 seconds (the default life time of the locked key is 30 seconds). If the current thread still holds the lock key, the life time of the lock key will be continuously extended.

Topics: Programming Redis Spring Jedis SpringBoot