此 demo 主要演示了 Spring Boot 如何整合 redis,操作redis中的数据,并使用redis缓存数据。连接池使用 Lettuce。
Lettuce官网
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jsonartifactId>
dependency>
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
/**
*
* redis配置
*
*
*/
@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 usingClientResourcesBuilderCustomizer
. If you use Jedis,JedisClientConfigurationBuilderCustomizer
is also available. Alternatively, you can register a bean of typeRedisStandaloneConfiguration
,RedisSentinelConfiguration
, orRedisClusterConfiguration
to take full control over the configuration.
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 {
@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;
}
}
}
/**
*
* UserService
*
*
* @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);
}
}
主要测试使用
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 {
@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);
}
}
主要测试使用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 {
@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 提供的 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() 快捷方法来获取序列化器。
是spring自带的缓存,本质就是缓存方法返回的结果,下次在访问这个方法就是从缓存取.默认Spring Cache是缓存到jvm
虚拟机缓存中,这样的并不好,所有一般使用整合dataRedis
一起使用,就是缓存在Redis中!
scan
命令模糊查询key
/**
* 使用scan模糊查询key
*
* @param key
* @return java.util.Set 匹配到的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连接池生效,即使用多个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