Spring Boot整合Redis缓存(Lettuce)

spring-boot-demo-cache-redis

此 demo 主要演示了 Spring Boot 如何整合 redis,操作redis中的数据,并使用redis缓存数据。连接池使用 Lettuce。

Lettuce官网

pom.xml

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

<!--基于spring aop的方式 为函数添加缓存的 框架-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 对象池,使用redis连接池时必须引入 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- 引入 jackson 对象json转换 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-json</artifactId>
</dependency>

application.yml

spring:
  redis:
    host: localhost 
    timeout: 10000ms # 连接超时时间(记得添加单位,Duration)
    database: 0 # Redis默认情况下有16个分片,这里配置具体使用的分片 (默认0)
    port: 6379 # Redis服务器连接端口
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-wait: -1ms
        # 连接池中的最大空闲连接 默认 8
        max-idle: 8
        # 连接池中的最小空闲连接 默认 0
        min-idle: 0
  cache:
    # 一般来说是不用配置的,Spring Cache 会根据依赖的包自行装配
    type: redis

非高度配置RedisConfig.java

/**
 * <p>
 *     redis配置
 * </p>
 *
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableCaching //开启缓存
public class RedisConfig extends CachingConfigurerSupport{

    /**
     * 自定义RedisTemplate序列化
     */
    @Bean
    public RedisTemplate<Object, Object> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
         StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置key序列化类,否则key前面会多了一些乱码
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 如果value没设置都是使用默认jdk序列化
        // 如果取value出现序列化问题,修改为使用默认jdk new JdkSerializationRedisSerializer()
        template.setConnectionFactory(redisConnectionFactory);
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 配置使用注解的时候缓存配置,默认是序列化反序列化的形式,加上此配置则为 json 形式
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 配置序列化
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
    }
    
    
     /**
     * 自定义Redis连接池其他属性
     *
     * @return LettuceClientConfigurationBuilderCustomizer
     * @author: ZhiHao
     * @date: 2023/3/9
     */
    @Bean
    public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer(){
        // LettuceConnectionConfiguration.java #getLettuceClientConfiguration()后置设置属性
       return new LettuceClientConfigurationBuilderCustomizer() {
            @Override
            public void customize(LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {
                LettucePoolingClientConfiguration build = (LettucePoolingClientConfiguration) clientConfigurationBuilder.build();
                GenericObjectPoolConfig poolConfig = build.getPoolConfig();
                poolConfig.setTestOnBorrow(Boolean.TRUE);
                poolConfig.setTestWhileIdle(Boolean.TRUE);
                // 无连接不阻塞, 进行报错
                poolConfig.setBlockWhenExhausted(Boolean.FALSE);
            }
        };
    }
}

官网自定义配置说明:

You can also register an arbitrary number of beans that implement LettuceClientConfigurationBuilderCustomizer for more advanced customizations. ClientResources can also be customized using ClientResourcesBuilderCustomizer. If you use Jedis, JedisClientConfigurationBuilderCustomizer is also available. Alternatively, you can register a bean of type RedisStandaloneConfiguration, RedisSentinelConfiguration, or RedisClusterConfiguration to take full control over the configuration.

深度自定义配置RedisConfig (工厂和池配置)

import io.lettuce.core.ClientOptions;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.resource.ClientResources;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;

/**
 * <p>
 * redis配置
 * </p>
 *
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisLettuceConfig {

    @Bean
    public RedisTemplate<Object, Object> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
        // 省略, 参考上面
    }

    /**
     * 自定义LettuceConnectionFactory工厂
     *
     * @param redisProperties
     * @param clientResources
     * @return LettuceConnectionFactory
     * @author: ZhiHao
     * @date: 2023/3/9
     */
    @Bean
    public LettuceConnectionFactory redisConnectionFactory(RedisProperties redisProperties,
                                                           ClientResources clientResources) {
        LettucePoolingClientConfiguration lettucePoolingClientConfiguration = this.getLettucePoolingClientConfiguration(redisProperties, clientResources);
        RedisStandaloneConfiguration redisStandaloneConfiguration = this.getRedisStandaloneConfiguration(redisProperties);
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolingClientConfiguration);
        // 开启使用连接前先检测, 开启性能下降默认false
        // lettuce开启一个共享的物理连接,是一个长连接,所以默认情况下是不会校验连接是否可用的
        //lettuceConnectionFactory.setValidateConnection(Boolean.TRUE);
        // 这个属性默认是true,允许多个连接公用一个物理连接。如果设置false ,
        // 每一个连接的操作都会开启和关闭socket连接。如果设置为false,会导致性能下降
        //lettuceConnectionFactory.setShareNativeConnection(Boolean.FALSE);
        return lettuceConnectionFactory;
    }

    /**
     * 自定义LettucePoolingClientConfiguration连接池配置
     *
     * @param redisProperties
     * @param clientResources
     * @return LettucePoolingClientConfiguration
     * @author: ZhiHao
     * @date: 2023/3/9
     */
    private LettucePoolingClientConfiguration getLettucePoolingClientConfiguration(RedisProperties redisProperties,
                                                                                   ClientResources clientResources) {
        RedisProperties.Lettuce lettuce = redisProperties.getLettuce();
        RedisProperties.Pool pool = lettuce.getPool();
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder
                builder = LettucePoolingClientConfiguration.builder().poolConfig(this.getPoolConfig(pool));
        if (redisProperties.isSsl()) {
            builder.useSsl();
        }
        if (redisProperties.getTimeout() != null) {
            builder.commandTimeout(redisProperties.getTimeout());
        }
        if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
            builder.shutdownTimeout(redisProperties.getLettuce().getShutdownTimeout());
        }
        if (StringUtils.hasText(redisProperties.getClientName())) {
            builder.clientName(redisProperties.getClientName());
        }
        builder.clientOptions(this.createClientOptions(redisProperties));
        builder.clientResources(clientResources);
        return builder.build();
    }

    /**
     * 自定义 RedisStandaloneConfiguration
     *
     * @param redisProperties
     * @return RedisStandaloneConfiguration
     * @author: ZhiHao
     * @date: 2023/3/9
     */
    private RedisStandaloneConfiguration getRedisStandaloneConfiguration(RedisProperties redisProperties){
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        if (StringUtils.hasText(redisProperties.getUrl())) {
            ConnectionInfo connectionInfo = parseUrl(redisProperties.getUrl());
            config.setHostName(connectionInfo.getHostName());
            config.setPort(connectionInfo.getPort());
            config.setUsername(connectionInfo.getUsername());
            config.setPassword(RedisPassword.of(connectionInfo.getPassword()));
        }
        else {
            config.setHostName(redisProperties.getHost());
            config.setPort(redisProperties.getPort());
            config.setUsername(redisProperties.getUsername());
            config.setPassword(RedisPassword.of(redisProperties.getPassword()));
        }
        config.setDatabase(redisProperties.getDatabase());
        return config;
    }


    private ClientOptions createClientOptions(RedisProperties redisProperties) {
        ClientOptions.Builder builder = this.initializeClientOptionsBuilder(redisProperties);
        Duration connectTimeout = redisProperties.getConnectTimeout();
        if (connectTimeout != null) {
            builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build());
        }
        return builder.timeoutOptions(TimeoutOptions.enabled()).build();
    }

    private ClientOptions.Builder initializeClientOptionsBuilder(RedisProperties redisProperties) {
        if (redisProperties.getCluster() != null) {
            ClusterClientOptions.Builder builder = ClusterClientOptions.builder();
            RedisProperties.Lettuce.Cluster.Refresh refreshProperties = redisProperties.getLettuce().getCluster().getRefresh();
            ClusterTopologyRefreshOptions.Builder refreshBuilder = ClusterTopologyRefreshOptions.builder()
                    .dynamicRefreshSources(refreshProperties.isDynamicRefreshSources());
            if (refreshProperties.getPeriod() != null) {
                refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod());
            }
            if (refreshProperties.isAdaptive()) {
                refreshBuilder.enableAllAdaptiveRefreshTriggers();
            }
            return builder.topologyRefreshOptions(refreshBuilder.build());
        }
        return ClientOptions.builder();
    }

    // BaseObjectPoolConfig与GenericObjectPoolConfig 还有很多连接池属性可以配置, 可以自行查看官网或者源码
    private GenericObjectPoolConfig<?> getPoolConfig(RedisProperties.Pool pool) {
        GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(pool.getMaxActive());
        config.setMaxIdle(pool.getMaxIdle());
        config.setMinIdle(pool.getMinIdle());
        config.setTestOnBorrow(Boolean.TRUE);
        if (pool.getTimeBetweenEvictionRuns() != null) {
            config.setTimeBetweenEvictionRuns(pool.getTimeBetweenEvictionRuns());
        }
        if (pool.getMaxWait() != null) {
            config.setMaxWait(pool.getMaxWait());
        }
        return config;
    }


    protected ConnectionInfo parseUrl(String url) {
        try {
            URI uri = new URI(url);
            String scheme = uri.getScheme();
            if (!"redis".equals(scheme) && !"rediss".equals(scheme)) {
                throw new RuntimeException("url异常"+url);
            }
            boolean useSsl = ("rediss".equals(scheme));
            String username = null;
            String password = null;
            if (uri.getUserInfo() != null) {
                String candidate = uri.getUserInfo();
                int index = candidate.indexOf(':');
                if (index >= 0) {
                    username = candidate.substring(0, index);
                    password = candidate.substring(index + 1);
                }
                else {
                    password = candidate;
                }
            }
            return new ConnectionInfo(uri, useSsl, username, password);
        }
        catch (URISyntaxException ex) {
            throw new RuntimeException("url异常"+url,ex);
        }
    }


   @Data
   protected static class ConnectionInfo {

        private final URI uri;

        private final boolean useSsl;

        private final String username;

        private final String password;

        ConnectionInfo(URI uri, boolean useSsl, String username, String password) {
            this.uri = uri;
            this.useSsl = useSsl;
            this.username = username;
            this.password = password;
        }

       boolean isUseSsl() {
           return this.useSsl;
       }

       String getHostName() {
           return this.uri.getHost();
       }

       int getPort() {
           return this.uri.getPort();
       }

       String getUsername() {
           return this.username;
       }

       String getPassword() {
           return this.password;
       }
    }

}

UserServiceImpl.java

/**
 * <p>
 * UserService
 * </p>
 *
 * @description: UserService 使用的是cache集成了redis是使用redis
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    /**
     * 模拟数据库
     */
    private static final Map<Long, User> DATABASES = Maps.newConcurrentMap();

    /**
     * 初始化数据
     */
    static {
        DATABASES.put(1L, new User(1L, "user1"));
        DATABASES.put(2L, new User(2L, "user2"));
        DATABASES.put(3L, new User(3L, "user3"));
    }

    /**
     * 保存或修改用户
     *
     * @param user 用户对象
     * @return 操作结果
     */
    @CachePut(value = "user", key = "#user.id")
    @Override
    public User saveOrUpdate(User user) {
        DATABASES.put(user.getId(), user);
        log.info("保存用户【user】= {}", user);
        return user;
    }

    /**
     * 获取用户
     *
     * @param id key值
     * @return 返回结果
     */
    @Cacheable(value = "user", key = "#id")
    @Override
    public User get(Long id) {
        // 我们假设从数据库读取
        log.info("查询用户【id】= {}", id);
        return DATABASES.get(id);
    }

    /**
     * 删除
     *
     * @param id key值
     */
    @CacheEvict(value = "user", key = "#id")
    @Override
    public void delete(Long id) {
        DATABASES.remove(id);
        log.info("删除用户【id】= {}", id);
    }
}

RedisTest.java

主要测试使用 RedisTemplate 操作 Redis 中的数据:

  • opsForValue:对应 String(字符串)
  • opsForZSet:对应 ZSet(有序集合)
  • opsForHash:对应 Hash(哈希)
  • opsForList:对应 List(列表)
  • opsForSet:对应 Set(集合)
  • opsForGeo:** 对应 GEO(地理位置)
/**
 * <p>
 * Redis测试
 * </p>
 *
 * @package: com.xkcoding.cache.redis
 * @description: Redis测试
 * @author: yangkai.shen
 * @date: Created in 2018/11/15 17:17
 * @copyright: Copyright (c) 2018
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Slf4j
public class RedisTest extends SpringBootDemoCacheRedisApplicationTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisTemplate<Object, Object> redisCacheTemplate;

    /**
     * 测试 Redis 操作
     */
    @Test
    public void get() {
        // 测试线程安全,程序结束查看redis中count的值是否为1000
        ExecutorService executorService = Executors.newFixedThreadPool(1000);
        IntStream.range(0, 1000).forEach(i -> executorService.execute(() -> stringRedisTemplate.opsForValue().increment("count", 1)));

        stringRedisTemplate.opsForValue().set("k1", "v1");
        String k1 = stringRedisTemplate.opsForValue().get("k1");
        log.debug("【k1】= {}", k1);

        // 以下演示整合,具体Redis命令可以参考官方文档
        String key = "xkcoding:user:1";
        redisCacheTemplate.opsForValue().set(key, new User(1L, "user1"));
        // 对应 String(字符串)
        User user = (User) redisCacheTemplate.opsForValue().get(key);
        log.debug("【user】= {}", user);
    }
}

UserServiceTest.java

主要测试使用Redis缓存是否起效

/**
 * <p>
 * Redis - 缓存测试
 * </p>
 *
 * @package: com.xkcoding.cache.redis.service
 * @description: Redis - 缓存测试
 * @author: yangkai.shen
 * @date: Created in 2018/11/15 16:53
 * @copyright: Copyright (c) 2018
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Slf4j
public class UserServiceTest extends SpringBootDemoCacheRedisApplicationTests {
    @Autowired
    private UserService userService;

    /**
     * 获取两次,查看日志验证缓存
     */
    @Test
    public void getTwice() {
        // 模拟查询id为1的用户
        User user1 = userService.get(1L);
        log.debug("【user1】= {}", user1);

        // 再次查询
        User user2 = userService.get(1L);
        log.debug("【user2】= {}", user2);
        // 查看日志,只打印一次日志,证明缓存生效
    }

    /**
     * 先存,再查询,查看日志验证缓存
     */
    @Test
    public void getAfterSave() {
        userService.saveOrUpdate(new User(4L, "测试中文"));

        User user = userService.get(4L);
        log.debug("【user】= {}", user);
        // 查看日志,只打印保存用户的日志,查询是未触发查询日志,因此缓存生效
    }

    /**
     * 测试删除,查看redis是否存在缓存数据
     */
    @Test
    public void deleteUser() {
        // 查询一次,使redis中存在缓存数据
        userService.get(1L);
        // 删除,查看redis是否存在缓存数据
        userService.delete(1L);
    }

}

参考资料

  • spring-data-redis 官方文档:https://docs.spring.io/spring-data/redis/docs/2.0.1.RELEASE/reference/html/
  • redis 文档:https://redis.io/documentation
  • redis 中文文档:http://www.redis.cn/commands.html

扩展

StringRedisTemplate和RedisTemplate的区别及使用方法

这里,总结下 Spring 提供的 4 种 RedisSerializer(Redis 序列化器):

默认情况下,RedisTemplate 使用 JdkSerializationRedisSerializer,也就是 JDK 序列化,容易产生 Redis 中保存了乱码的错觉。

通常考虑到易读性,可以设置 Key 的序列化器为 StringRedisSerializer。但直接使用 RedisSerializer.string(),相当于使用了 UTF_8 编码的 StringRedisSerializer,需要注意字符集问题。

如果希望 Value 也是使用 JSON 序列化的话,可以把 Value 序列化器设置为 Jackson2JsonRedisSerializer。默认情况下,不会把类型信息保存在 Value 中,即使我们定义 RedisTemplate 的 Value 泛型为实际类型,查询出的 Value 也只能是 LinkedHashMap 类型。如果希望直接获取真实的数据类型,你可以启用 Jackson ObjectMapper 的 activateDefaultTyping 方法,把类型信息一起序列化保存在 Value 中。

如果希望 Value 以 JSON 保存并带上类型信息,更简单的方式是,直接使用 RedisSerializer.json() 快捷方法来获取序列化器。

Cache注解:

是spring自带的缓存,本质就是缓存方法返回的结果,下次在访问这个方法就是从缓存取.默认Spring Cache是缓存到jvm虚拟机缓存中,这样的并不好,所有一般使用整合dataRedis一起使用,就是缓存在Redis中!

使用scan 命令模糊查询key

/**
     * 使用scan模糊查询key
     *
     * @param key
     * @return java.util.Set<K> 匹配到的Key集合
     * @author: ZhiHao
     * @date: 2021/5/13
     */
    public <K> Set<K> fuzzyQueryKey(K key) {
        // 需要模糊搜索的Key
        String keys = String.format(key.toString(), "*");
        Set<byte[]> rawKeys = (Set<byte[]>) redisTemplate.execute((RedisCallback<Set<byte[]>>) connection -> {
            Set<byte[]> set = new HashSet<>();
            Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(keys).count(1000L).build());
            while (cursor.hasNext()) {
                set.add(cursor.next());
            }
            return set;
        }, true);
        RedisSerializer keySerializer = redisTemplate.getKeySerializer();
        return keySerializer != null ? SerializationUtils.deserialize(rawKeys, keySerializer) : (Set<K>) rawKeys;
    }

lettuce连接池生效

要想使lettuce连接池生效,即使用多个redis物理连接。这行设置不能缺少
genericObjectPoolConfig.setTimeBetweenEvictionRunsMillis(100); 这个设置是,每隔多少毫秒,空闲线程驱逐器关闭多余的空闲连接,且保持最少空闲连接可用,这个值最好设置大一点,否者影响性能。同时 genericObjectPoolConfig.setMinIdle(minIdle); 中minldle值要大于0。
lettuce连接池属性timeBetweenEvictionRunsMillis如果不设置 默认是 -1,当该属性值为负值时,lettuce连接池要维护的最小空闲连接数的目标minIdle就不会生效 。源码中的解释如下:

/**
		 * Target for the minimum number of idle connections to maintain in the pool. This
		 * setting only has an effect if both it and time between eviction runs are
		 * positive.
		 */
		private int minIdle = 0;

1

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懵懵懂懂程序员

如果节省了你的时间, 请鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值