Spring boot + redis realizes the record of article views

Posted by StumpDK on Wed, 26 Jan 2022 18:47:21 +0100

Spring boot + redis realizes the record of article views

Previously, the article visits in personal blog websites were stored in MySQL. Every time you visit, you need to check the database through the article id and operate + 1 after obtaining the number of views. After updating the database, in order to display the article views in real time, you have to read the database again. A large number of accesses brings thread safety problems: two threads obtain the number at the same time and update after + 1 respectively. This access is problematic. In this case, we used synchronized to lock the operation roughly. This brings a problem. Multiple threads can only have one thread to obtain article information, and other threads are blocked,. Later, I came up with another solution. The increase of browsing volume is independent and not bound with reading article information. After each article information is read, an interface is called to increase the number. One drawback is that the number of article visits displayed is not real-time.

Finally, it is decided to use redis to store the visits of articles. The key statement of redis is incr key. The function is value+1 of key; The key is bound to the article id. Because redis is single threaded, the execution of a single statement is thread safe.

The ideal is very plump, but there are still many problems in operation.

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

Set redis configuration

redis:
    database: 0
    # Write your ip address in the Redis server address
    host: XXX.XX.XX.XXX
    # Redis server connection port
    port: Your port number
    # Redis server connection password (blank by default)
    password: Your password
    timeout: 6000
    lettcue:
      pool:
        # The maximum free connection in the connection pool. The default value is 8.
        max-idle: 100
        # The minimum free connection in the connection pool. The default value is 0.
        min-idle: 10
        # If the value is - 1, it means there is no limit; If the pool has allocated maxActive jedis instances, the status of the pool is exhausted
        max-active: 8
        # The maximum time to wait for an available connection, in milliseconds. The default value is - 1, which means never timeout. If the waiting time is exceeded, the JedisConnectionException will be thrown directly
        max-wait: 2000

If the connection pool attribute of redis is not set, there is only one connection by default. If multiple threads use redisTemplate, it will report "redis connection exception".

Set redisTemplate class template

@Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
        // Configure redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());//key serialization
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//value serialization
        return redisTemplate;
    }    

JUnit test class:

 	@Resource
    RedisUtils redisUtils;
    @Test
    public void redisTest(){
        try {
            Runnable runnable = new Runnable(){
                @SneakyThrows
                @Override
                public void run() {
                    for(int i=0;i<500;i++){
                        Thread.sleep(1);
                        System.out.println(Thread.currentThread().getName()+": "+addReadNums());
                    }
                }
            };
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            executorService.execute(runnable);//Thread 1
            executorService.execute(runnable);//Thread 2
            System.out.println("@Test Thread execution completed");
        } catch (Exception e) {
            System.out.println(e);
        }
    }
    public Long addReadNums(){
        return redisUtils.incrBy("test", 1);
    }

An error was reported during the multithreading simulation test. There was a problem with the lettuce thread pool under multithreading. After searching the data, they all said that Jedis was used as the redis thread pool.

After modification

Modified dependency: remove lettuce thread pool in spring boot starter data redis and import jedis

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>

Note that the version of Jedis is different from that of spring boot starter data redis. If the version is inconsistent, the creation of redisTemplate will fail.

reference resources: [exception] nested exception is Java lang.NoClassDefFoundError: redis/clients/jedis/util/SafeEncoder

You should also modify the creation method of redisTemplate class

@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory connectionFactory) {
    // Configure redisTemplate
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    redisTemplate.setKeySerializer(new StringRedisSerializer());//key serialization
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));//value serialization
    return redisTemplate;
}

At this time, the test found that the thread will print after running for a while: Shutting down ExecutorService 'applicationtask executor'. Then the thread stops. Discovery is the pit of the JUnit unit test: it will call the relevant System. after the main thread ends. The exit () method closes the JVM, so the child thread hangs passively.

reference resources: Multithreading in JUnit unit testing

Modified test class:

@Test
public void redisTest(){
    try {
        Runnable runnable = new Runnable(){
            @SneakyThrows
            @Override
            public void run() {
                for(int i=0;i<500;i++){
                    Thread.sleep(1);
                    System.out.println(Thread.currentThread().getName()+": "+addReadNums());
                }
            }
        };
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(runnable);//Thread 1
        executorService.execute(runnable);//Thread 2
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("@Test Thread execution completed");
    } catch (Exception e) {
        System.out.println(e);
    }
}
public Long addReadNums(){
    return redisUtils.incrBy("test", 1);
}

It is found that there is no problem adding 500 under the two threads.

Look, redis is really 1000.

Finally, for article views, as long as the key passes in the article id. If you are worried that the key is repeated with other contents, you can add an identifier to distinguish it.

Topics: Database Redis Spring Boot