Spring Boot集成Redis内存数据库

常规的业务数据,一般选择存储在SQL数据库中。

传统的SQL数据库基于磁盘存储,可以正常的流量需求。然而,在高并发应用场景中容易被拖垮,导致系统崩溃。

针对这种情况,我们可以通过增加缓存、使用NoSQL数据库等方式进行优化。

Redis是一款开源的内存NoSQL数据库,其稳定性高、[性能强悍](How fast is Redis? – Redis),是KV细分领域的市场占有率冠军

本节将介绍Redis与Spring Boot的集成方式。

Redis环境准备

与前文类似,我们使用Docker快速部署Redis服务器。

#!/bin/bash

NAME="redis"
PUID="1000"
PGID="1000"

VOLUME="$HOME/docker_data/redis"
mkdir -p $VOLUME 

docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    --volume "$VOLUME":/data \
    -p 6379:6379 \
    --detach \
    --restart always \
    redis:6 \
    redis-server --appendonly yes --requirepass redisdemo

在上述脚本中:

  • 使用了最新的redis 6镜像

  • 开启"appendonly"的持久化方式

  • 启用密码"redisdemo"

  • 端口暴露为6379

我们尝试连接一下:

redis-cli -h 127.0.0.1 -a redisdemo

成功!(如果你没有redis-cli的可执行文件,可以到官网下载)

Redis的缓存使用

Spring提供了内置的Cache框架,可以通过@Cache注解,轻松实现redis Cache的功能。

首先引入依赖:

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-json'
implementation 'org.apache.commons:commons-pool2:2.11.0'

上述依赖的作用分别为:

  • redis客户端:Spring Boot 2使用的是lettuce

  • json依赖:我们要使用jackson做json的序列化 / 反序列化

  • commons-pool2线程池,这里其实是data-redis没处理好,需要额外加入,按理说应该集成在starter里的

接着我们在application.yaml中定义数据源:

# redis demo
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: "redisdemo"
    lettuce:
      pool:
        max-active: 50
        min-idle: 5

接着我们需要设置自定义的Configuration:

package com.coder4.homs.demo.server.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;

/**
 * @author coder4
 */
@Configuration
@EnableCaching
public class RedisCacheCustomConfiguration extends CachingConfigurerSupport {

    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            // sb.append(target.getClass().getName());
            sb.append(target.getClass().getSimpleName());
            sb.append(":");
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
                sb.append(":");
            }
            sb.deleteCharAt(sb.length() - 1);
            return sb.toString();
        };
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, DefaultTyping.NON_FINAL);
        // use json serde
        serializer.setObjectMapper(objectMapper);
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(5)) // 5 mins ttl
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
    }

}

上述主要包含两部分:

  • KeyGenerator可以根据Class + method + 参数 生成唯一的key名字,用于Redis中存储的key

  • RedisCacheConfiguration做了2处定制:

    • 更改了序列化方式,从默认的Java(Serilization更改为Jackson(json)

    • 缓存过期时间为5分钟

接着,我们在项目中使用Cache

public interface UserRepository {

    Optional<Long> create(User user);

    @Cacheable(value = "cache")
    Optional<User> getUser(long id);

    Optional<User> getUserByName(String name);
}

这里我们用了@Cache注解,"cache"是key的前缀

访问一下:

curl http://127.0.0.1:8080/users/1

然后看一下redis

redis-cli -a redisdemo

> keys *
> "cache::UserRepository1Impl:getUser1"
> get "cache::UserRepository1Impl:getUser1"
"[\"com.coder4.homs.demo.server.model.User\",{\"id\":1,\"name\":\"user1\"}]"
> ttl "cache::UserRepository1Impl:getUser1"
> 293

数据被成功缓存在了Redis中(序列化为json),并且会自动过期。

我们使用Spring Boot集成SQL数据库2一节中的压测脚本验证性能,QPS达到860,提升达80%。

在数据发生删除、更新时,你需要更新缓存,以确保一致性。推荐你阅读[缓存更新的套路](缓存更新的套路 | 酷 壳 - CoolShell)。

在更新/删除方法上应用@CacheEvict(beforeInvocation=false),可以实现更新时删除的功能。

Redis的持久化使用

Redis不仅可以用作缓存,也可以用作持久化的存储。

首先请确认Redis已开启持久化:

127.0.0.1:6379> config get save
1) "save"
2) "3600 1 300 100 60 10000"

127.0.0.1:6379> config get appendonly
1) "appendonly"
2) "yes"

上述分别为rdb和aof的配置,有任意一个非空,即表示开启了持久化。

实际上,在我们集成Spring Data的时候,会自动配置RedisTemplte,使用它即可完成Redis的持久化读取。

不过默认配置的Template有一些缺点,我们需要做一些改造:

package com.coder4.homs.demo.server.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author coder4
 */
@Configuration
public class RedisTemplateConfiguration {

    @Autowired
    public void decorateRedisTemplate(RedisTemplate redisTemplate) {
        RedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
    }

}

如上所述,我们设置RedisTemplate的KV,分别采用String的序列化方式。

接着我们在代码中使用其存取Redis:

@Autowired
private RedisTemplate redisTemplate;


redisTemplate.boundValueOps("key").set("value");

RedisTemplate的语法稍微有些奇怪,你也可以直接使用Conn来做操作,这样更加"Lettuce"。

@Autowired
private LettuceConnectionFactory leconnFactory;

try (RedisConnection conn = leconnFactory.getConnection()) {
    conn.set("hehe".getBytes(), "haha".getBytes());
}

至此,我们已经完成了Spring Boot 与 Redis的集成。

思考题:当一个微服务需要连接多组Redis,该如何集成呢?

请自己探索,并验证其正确性。