Technology Sharing

SpringBoot uses Redisson to operate Redis and actual usage scenarios

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Preface

existSpringBoot uses RedisTemplate and StringRedisTemplate to operate RedisIn the article, we introduced RedisTemplate and how SpringBoot operates Redis through RedisTemplate and StringRedisTemplate.
RedisTemplate的好处就是基于SpringBoot自动装配的原理,使得整合redis时比较简单。

Since SrpingBoot can operate Redis through RedisTemplate, why does Redisson appear?Rddisson Chinese Documentation
Reddissin也是一个redis客户端,其在提供了redis基本操作的同时,还具备其他客户端一些不具备的高精功能,例如:分布式锁+看门狗、分布式限流、远程调用等等。Reddissin的缺点是api抽象,学习成本高。

I. Overview

Starting from spring-boot 2.x version, spring-boot-data-redis uses the Lettuce client to operate data by default.

1.1 Lettuce

SpringBoot2之后,默认就采用了lettuce。
It is an advanced Redis client, an event-driven communication layer based on the Netty framework, for thread-safe synchronous, asynchronous and reactive use, supporting clusters, Sentinel, pipelines and encoders.
The Lettuce API is thread-safe and can operate a single Lettuce connection to complete various operations. The connection instance (StatefulRedisConnection) can be accessed concurrently by multiple threads.

1.2 Reddisson

An event-driven communication layer based on the Netty framework. The methods are asynchronous, the API is thread-safe, and a single Redisson connection can be operated to complete various operations.
It implements distributed and scalable Java data structures, but does not support string operations, sorting, transactions, pipelines, partitions and other Redis features.
It provides many distributed-related operation services, such as distributed locks, distributed collections, and can support delayed queues through Redis.

Summarize:Lettuce is used first. If you need advanced distributed features such as distributed locks and distributed collections, add Redisson for combined use.

2. Spring-Boot integrates Redisson

2.1 Introducing Dependencies

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.6</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

Notice: After introducing this dependency, there is no need to introduce it againspring-boot-starter-data-redis,Thatredisson-spring-boot-starterIt has been introduced internally, and Luttuce and Jedis clients of Redis have been excluded. Therefore, the configuration of Luttuce and Jedis in application.yaml will not take effect.
insert image description here

When using Redisson in a project, we usually use RedissonClient for data operations, but some friends may find RedissonClient inconvenient to operate, or prefer to use RedisTemplate for operations. In fact, the two can coexist. We only need to define the configuration class of RedisTemplate.SpringBoot uses RedisTemplate and StringRedisTemplate to operate Redis

It was found that after Redisson was introduced into the project, the connection factory used by the underlying RedisTemplate was also Redisson.
insert image description here

2.2 Configuration File

Add redis configuration information in application.yaml.

spring:
  data:
    redis:
      mode: master
      # 地址
      host: 30.46.34.190
      # 端口,默认为6379
      port: 6379
      # 密码,没有不填
      password: ''
      # 几号库
      database: 1
      sentinel:
        master: master
        nodes: 30.46.34.190
      cluster:
        nodes: 30.46.34.190
      lettuce:
        pool:
          # 连接池的最大数据库连接数
          max-active: 200
          # 连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 连接池中的最大空闲连接
          max-idle: 50
          # 连接池中的最小空闲连接
          min-idle: 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

2.3 Configuration Class

@Configuration
@EnableConfigurationProperties({RedisProperties.class})
public class RedissonConfig {

    private static final String REDIS_PROTOCOL_PREFIX = "redis://";

    @Value("${spring.data.redis.mode}")
    private String redisMode;

    private final RedisProperties redisProperties;

    public RedissonConfig(RedisProperties redisProperties) {
        this.redisProperties = redisProperties;
    }

    /**
     * 逻辑参考 RedissonAutoConfiguration#redisson()
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson(@Autowired(required = false) List<RedissonAutoConfigurationCustomizer> redissonAutoConfigurationCustomizers) throws IOException {
        Config config = new Config();
        config.setCheckLockSyncedSlaves(false);

        int max = redisProperties.getLettuce().getPool().getMaxActive();
        int min = redisProperties.getLettuce().getPool().getMinIdle();

        switch (redisMode) {
            case "master": {
                SingleServerConfig singleConfig = config.useSingleServer()
                        .setAddress(REDIS_PROTOCOL_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort())
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    singleConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                singleConfig.setConnectionPoolSize(max);
                singleConfig.setConnectionMinimumIdleSize(min);
            }
            break;
            case "sentinel": {
                String[] nodes = convert(redisProperties.getSentinel().getNodes());

                SentinelServersConfig sentinelConfig = config.useSentinelServers()
                        .setMasterName(redisProperties.getSentinel().getMaster())
                        .addSentinelAddress(nodes)
                        .setDatabase(redisProperties.getDatabase())
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    sentinelConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                sentinelConfig.setMasterConnectionPoolSize(max);
                sentinelConfig.setMasterConnectionMinimumIdleSize(min);
                sentinelConfig.setSlaveConnectionPoolSize(max);
                sentinelConfig.setSlaveConnectionMinimumIdleSize(min);
            }
            break;
            case "cluster": {
                String[] nodes = convert(redisProperties.getCluster().getNodes());

                ClusterServersConfig clusterConfig = config.useClusterServers()
                        .addNodeAddress(nodes)
                        .setPassword(redisProperties.getPassword());

                if (redisProperties.getConnectTimeout() != null) {
                    clusterConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
                }

                clusterConfig.setMasterConnectionMinimumIdleSize(min);
                clusterConfig.setMasterConnectionPoolSize(max);
                clusterConfig.setSlaveConnectionMinimumIdleSize(min);
                clusterConfig.setSlaveConnectionPoolSize(max);
            }
            break;
            default:
                throw new IllegalArgumentException("无效的redis mode配置");
        }

        if (redissonAutoConfigurationCustomizers != null) {
            for (RedissonAutoConfigurationCustomizer customizer : redissonAutoConfigurationCustomizers) {
                customizer.customize(config);
            }
        }

        return Redisson.create(config);

    }

    private String[] convert(List<String> nodesObject) {
        List<String> nodes = new ArrayList<String>(nodesObject.size());
        for (String node : nodesObject) {
            if (!node.startsWith(REDIS_PROTOCOL_PREFIX)) {
                nodes.add(REDIS_PROTOCOL_PREFIX + node);
            } else {
                nodes.add(node);
            }
        }
        return nodes.toArray(new String[0]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

2.4 Usage

@Component
public class RedissonService {
	@Resource
    protected RedissonClient redissonClient;

	public void redissonExists(String key){
		RBucket<String> rBucketValue = redissonClient.getBucket(key, StringCodec.INSTANCE);
		if (rBucketValue.isExists()){
            String value = rBucketValue.get();
            // doSomething
        } else {
           // doElseSomething
        }
	}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

2.5 Practical Scenarios

2.5.1 Distributed Locks

Students with some experience think of redis when they mention the use of distributed locks. So how does redis implement distributed locks?

The essential goal of distributed locks is to occupy a pit in Redis (in simple terms, it is the principle of carrots occupying pits). When other processes also want to occupy the pit, if they find that there is already a big carrot in the pit, they have to give up or try again later.

Common methods of distributed locks

1. Use the setNx command
The detailed description of this command is (set if not exists). If the specified key does not exist, it is set (successfully occupying the pit). After the business execution is completed, call the del command to delete the key (release the pit). For example:

# set 锁名 值
setnx distribution-lock  locked

// dosoming

del  distribution-lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

However, there is a problem with this command. If there is a problem in the execution logic, the del instruction may not be executed, and the lock will become a deadlock.
Maybe some of you have thought of this thoughtfully. We can set an expiration time for this key. For example:

setnx distribution-lock  locked

expire distribution-lock  10

// dosoming

del  distribution-batch
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Even after this operation, the logic still has problems. Since setnx and expire are two commands, if the redis server hangs between setnx and expire, expire will not be executed, and the expiration time setting will fail, and the lock will still become a deadlock.

The root cause is that the setnx and expire commands are not atomic commands.

Moreover, redis transactions cannot solve the problem of setnx and expire, because expire depends on the execution result of setnx. If setnx is not successful, expire should not be executed. Transactions cannot perform if else judgments, so the setnx+expire method to implement distributed locks is not an optimal solution.

2. Use the setNx Ex command
The problem of setNx+expire has been mentioned above. In order to solve this problem, Redis officially introduced an extended parameter of the set command in version 2.8, so that the setnx and expire commands can be executed together. For example:

# set 锁名 值 ex 过期时间(单位:秒) nx
set distribution-lock locked ex 5 nx

// doSomthing

del distribution-lock
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Logically speaking, setNx Ex is an optimal solution and will not cause the distributed lock to deadlock.

But problems may still arise during our development. Why?
Since we set an expiration time for this lock at the beginning, what if our business logic execution takes longer than the set expiration time? There will be a situation where one thread has not completed execution and the second thread may hold this distributed lock.
Therefore, if you use the setNx Ex combination, you must ensure that your lock timeout is greater than the business execution time after the lock is occupied.

3. Use Lua script + watch dog automatic extension mechanism
There are a lot of solutions for this on the Internet, so I won’t go into details here.

Redisson implements distributed lock

The setNx and setNx Ex commands introduced above are both native commands provided by the Redis server. They also have some problems. To solve the problem that the business logic of the setNx Ex command is greater than the lock timeout, Redisson provides a watchdog to monitor the lock. Its function is to continuously extend the validity period of the lock before the Redisson instance is closed. By default, the timeout period for the watchdog to check the lock is 30 seconds (that is, 30s renewal), which can also be specified by modifying Config.lockWatchdogTimeout. The initial expiration time of the lock is also 30s by default.

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
@Resource
RedissonClient redissonClient;

@GetMapping("/testDistributionLock")
public BaseResponse<String> testRedission(){
    RLock lock = redissonClient.getLock("redis:distributionLock");
    try {
        boolean locked = lock.tryLock(10, 3, TimeUnit.SECONDS);
        if(locked){
            log.info("获取锁成功");
            Thread.sleep(100);
            return ResultUtils.success("ok" );
        }else{
            log.error("获取锁失败");
            return ResultUtils.error(ErrorCode.SYSTEM_ERROR);
        }
    } catch (InterruptedException e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR,"出异常了");
    } finally {
        lock.unlock();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

2.5.2 Current Limitation

We are facing the problem of limiting the flow of interfaces or business logic under high concurrency. We can use the RateLimiter implemented by Guaua. In fact, Redisssion also has a similar flow limiting function.

RateLimiter is called token bucket current limiting. This type of current limiting first defines a token bucket, specifies how many tokens are generated within a certain period of time, and obtains a specified number of tokens from the token bucket each time a visit is made. If the acquisition is successful, it is set as a valid access.