Spring Boot整合Redis缓存(Lettuce)
创始人
2024-06-01 01:09:55
0

spring-boot-demo-cache-redis

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

Lettuce官网

pom.xml


org.springframework.bootspring-boot-starter-data-redis

org.springframework.bootspring-boot-starter-cache

org.apache.commonscommons-pool2

org.springframework.bootspring-boot-starter-json

application.yml

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

非高度配置RedisConfig.java

/*** 

* redis配置*

**/ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) @EnableCaching //开启缓存 public class RedisConfig extends CachingConfigurerSupport{/*** 自定义RedisTemplate序列化*/@Beanpublic RedisTemplate redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {RedisTemplate 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 形式*/@Beanpublic 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*/@Beanpublic LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer(){// LettuceConnectionConfiguration.java #getLettuceClientConfiguration()后置设置属性return new LettuceClientConfigurationBuilderCustomizer() {@Overridepublic 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;/*** 

* redis配置*

**/ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) @EnableConfigurationProperties(RedisProperties.class) public class RedisLettuceConfig {@Beanpublic RedisTemplate redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {// 省略, 参考上面}/*** 自定义LettuceConnectionFactory工厂** @param redisProperties* @param clientResources* @return LettuceConnectionFactory* @author: ZhiHao* @date: 2023/3/9*/@Beanpublic 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.LettucePoolingClientConfigurationBuilderbuilder = 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);}}@Dataprotected 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

/*** 

* UserService*

** @description: UserService 使用的是cache集成了redis是使用redis*/ @Service @Slf4j public class UserServiceImpl implements UserService {/*** 模拟数据库*/private static final Map 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")@Overridepublic User saveOrUpdate(User user) {DATABASES.put(user.getId(), user);log.info("保存用户【user】= {}", user);return user;}/*** 获取用户** @param id key值* @return 返回结果*/@Cacheable(value = "user", key = "#id")@Overridepublic User get(Long id) {// 我们假设从数据库读取log.info("查询用户【id】= {}", id);return DATABASES.get(id);}/*** 删除** @param id key值*/@CacheEvict(value = "user", key = "#id")@Overridepublic 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(地理位置)
/*** 

* Redis测试*

** @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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate redisCacheTemplate;/*** 测试 Redis 操作*/@Testpublic void get() {// 测试线程安全,程序结束查看redis中count的值是否为1000ExecutorService 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缓存是否起效

/*** 

* Redis - 缓存测试*

** @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 {@Autowiredprivate UserService userService;/*** 获取两次,查看日志验证缓存*/@Testpublic void getTwice() {// 模拟查询id为1的用户User user1 = userService.get(1L);log.debug("【user1】= {}", user1);// 再次查询User user2 = userService.get(1L);log.debug("【user2】= {}", user2);// 查看日志,只打印一次日志,证明缓存生效}/*** 先存,再查询,查看日志验证缓存*/@Testpublic void getAfterSave() {userService.saveOrUpdate(new User(4L, "测试中文"));User user = userService.get(4L);log.debug("【user】= {}", user);// 查看日志,只打印保存用户的日志,查询是未触发查询日志,因此缓存生效}/*** 测试删除,查看redis是否存在缓存数据*/@Testpublic 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 匹配到的Key集合* @author: ZhiHao* @date: 2021/5/13*/public  Set fuzzyQueryKey(K key) {// 需要模糊搜索的KeyString keys = String.format(key.toString(), "*");Set rawKeys = (Set) redisTemplate.execute((RedisCallback>) connection -> {Set set = new HashSet<>();Cursor 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) 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

相关内容

热门资讯

屋有蜜蜂来筑巢好不好 屋有蜜蜂来筑巢好不好当然好了察祥蔽呀,蜜蜂也是风水专家,说明你家位置很好。如果不影响你们正常生活的话...
哈尔滨空调股份有限公司2025... 证券代码:600202 证券简称:哈空调 编号:临2025-036哈尔滨空调股份有限公司2025年半...
促政策与市场双轨共振,激活产业... “十四五”规划的深化布局,为中国电子信息产业绘制了波澜壮阔的发展蓝图。作为国民经济的战略性支柱产业,...
我是做电话销售的,公司安排每人... 我是做电话销售的,公司安排每人每天轮流开一次早会,谁可以给些简单的小游戏。小故事、小话术提供一下!轰...
从世运手包到童趣闯关 世运火花... 转自:成都日报锦观从世运手包到童趣闯关 世运火花在社区点燃 本报讯 (成都日报锦观新闻记者 袁...
工地开讲“护薪”课,筑牢农民工... 为切实保障农民工合法权益,7月6日,中铁上海局七公司引汉济渭项目部联合监理单位及其他标段,共同组织开...
温江区万春镇:轨道为笔 绘就公... 转自:成都日报锦观温江区万春镇:轨道为笔 绘就公园城市乡村振兴新图景 在成都建设公园城市示范...
公开通报3起整治形式主义为基层... 转自:成都日报锦观中央层面整治形式主义为基层减负专项工作机制办公室 中央纪委办公厅公开通报3起整治形...
今起三日 成都持续晴热 转自:成都日报锦观今起三日 成都持续晴热 7月20日将进入今年的“三伏天”,但在入伏前,四川...
中国石化胜利石油工程公司:以创... 在保障国家能源安全的征程中,中国石化胜利石油工程公司以一组组亮眼的数据,交出了一份沉甸甸的“半年答卷...