网址
https://docs.spring.io/spring-data/redis/docs/2.4.2/reference/html/#reference
环境安装
Wndows环境
https://github.com/tporadowski/redis/releases
第一步:下载 zip 压缩包,第二步双击 redis-server.exe 启动 redis 服务。
当看到下面的截图代表启动成功
快速上手
第一步:添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步:配置文件修改
application.yml
spring:
redis:
database: 0
host: 127.0.0.1
password:
port: 6379
第三步:开发一个简单的 controller
@RequestMapping("quick")
@RestController
public class QuickController {
@Autowired
StringRedisTemplate stringRedisTemplate;
@GetMapping("hello")
public void hello() {
stringRedisTemplate.opsForValue().set("k", "v");
}
}
第四步:使用 Redis 可视化界面查看 key
StringRedisTemplate 自动装配原理
在条件注解的作用下,默认情况下会为我们创建两个可以直接注入的Bean对象,由于 @Import 导入在先,所以 LettuceConnectionConfiguration 先进行装配在容器中创建这两个 bean 对象。
当走到 Jedis 这里时,有两个条件注解不满足,第一个是缺失 Jedis 类,第二个是 容器中没有 连接工厂 这个 Bean,所以 Jedis 配置并不会生效,最终使用 Lettuce 客户端。
commons-pool2 报错原理分析
问题复现:当我们尝试配置 lettuce pool 的时候会报错。
lettuce:
pool:
min-idle: 1
为什么:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'redisCacheAop': Unsatisfied dependency expressed through field 'stringRedisTemplate'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'stringRedisTemplate' defined in class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]: Unsatisfied dependency expressed through method 'stringRedisTemplate' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisConnectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory]: Factory method 'redisConnectionFactory' threw exception; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig
首先对错误进行一个定性,NoClassDefFoundError 说明没有找到 GenericObjectPoolConfig ,没有这个类
会导致 redisConnectionFactory 创建失败,又会导致 stringRedisTemplate 创建失败,最终导致注入失败。
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
ClientResources clientResources) {
LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
getProperties().getLettuce().getPool());
return createLettuceConnectionFactory(clientConfig);
}
在 LettuceConnectionFactory 创建过程中会执行 getLettuceClientConfiguration 这个方法,一路点进去在
这个里面用到了 GenericObjectPoolConfig 这个类,因为找不到类所以最终 LettuceConnectionFactory 创建失败。
怎么解决:
- 保留池相关的配置,新增 commons-pool2 的依赖提供 GenericObjectPoolConfig 这个类。
- 删除池相关的配置。
验证:
当池配置存在的时候,getLettuceClientConfiguration 的 pool 参数正常。
当池配置不存在的时候,getLettuceClientConfiguration 的 pool 参数变为 null ;
由于在 createbuilder 中存在两种不同的实现,所以在配置池的过程中会抛出异常。
为什么 Pool 在参数配置时会产生两种效果呢,因为静态内部类只有在使用过程中初始化,所以没有配置就不会创建内部类对象。
Spring Boot 操作 Redis 的几种方式
编程式用法
StringRedisTemplate
@Autowired
StringRedisTemplate stringRedisTemplate;
默认 RedisTemplate
@Autowired
RedisTemplate<Object, Object> redisTemplate;
自定义 RedisTemplate
如果觉得RedisTemplate不好用可以自定义,例如替换序列化方式等。
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//不加会生成LinkedHashMap
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
//采用jackson
redisTemplate.setKeySerializer(serializer);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
return redisTemplate;
}
总结
我会更推荐使用 StringRedisTemplate 这种方式,存储 String ,Json 格式的内容,这样无论是 Java 还是其他语言均可以从Redis中读到内容并解析,跨语言方面更好。
声明式用法
自定义注解
第一步:声明一个注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisCache {
/**
* 过期时间
*/
int expired() default 60;
/**
* 过期时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 键
*/
String key() default "";
/**
* 是否开启异步
*/
boolean async() default false;
}
第二步:使用 AOP 拦截注解并在目标方法执行前后进行相应操作
@Aspect
@Component
public class RedisCacheAop {
private final static Logger logger = LoggerFactory.getLogger(RedisCacheAop.class);
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 空值缓存-缓存穿透
*
* @param joinPoint
* @param redisCache
* @return
*/
@Around("@annotation(redisCache)")
public Object fastjsonSerialStringRedisTemplate(ProceedingJoinPoint joinPoint, RedisCache redisCache) {
//
String key = redisCache.key();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (key == null || key.length() == 0) {
String[] split = method.getDeclaringClass().getName().split("\\.");
String className = split[split.length - 1];
String methodName = method.getName();
key = className + ":" + methodName;
}
//
String value = null;
try {
value = stringRedisTemplate.opsForValue().get(key);
} catch (Exception e) {
logger.error(e.toString());
}
Object result;
// value = null , execute method get value
if (ObjectUtils.isEmpty(value)) {
try {
//执行目标方法
result = joinPoint.proceed();
//after
value = JSON.toJSONString(result);
TimeUnit timeUnit = redisCache.timeUnit();
Integer expired = expireCheck(redisCache.expired());
stringRedisTemplate.opsForValue().set(key, value, expired, timeUnit);
} catch (Throwable throwable) {
logger.error(throwable.toString());
}
}
// value live
Class<?> returnType = method.getReturnType();
result = JSON.parseObject(value, returnType);
return result;
}
/**
* 检验是否规范
*
* @param expire
* @return
*/
private Integer expireCheck(int expire) {
if (expire < 0) {
expire = 60;
}
return expire;
}
}
第三部:测试类
@RestController
public class TestController {
@RedisCache(expired = 300)
@GetMapping("list")
public ArrayList<Integer> test() {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
return list;
}
@RedisCache(expired = 300)
@GetMapping("map")
public Map<String, String> map() {
HashMap<String, String> map = new HashMap<>();
map.put("s1", "s1");
map.put(null, "s2");
map.put("s3", null);
map.put(null, null);
return map;
}
@RedisCache(expired = 30)
@GetMapping("a")
public A a() {
A a = new A();
a.setA("sdads");
return a;
}
@Data
class A{
String a;
}
}
Spring Boot SPI Cache
https://docs.spring.io/spring-framework/docs/5.3.3/reference/html/integration.html#cache-annotations
https://docs.spring.io/spring-data/redis/docs/current/reference/html/
Spring Boot 为我们提供了一个 Cache 包,可以快速的实现缓存功能。
快速上手
第一步:修改配置文件
spring:
cache:
type: redis
第二步:编写配置类,使用 EnableCaching 注解开启缓存。
@Configuration
@EnableCaching
public class SpringCacheConfig {
}
第三步:写测试接口
@RestController
@RequestMapping("spring-cache")
public class SpringCacheController {
@GetMapping("list")
@Cacheable(cacheNames = "list")
public List<String> list() {
System.out.println("---方法返回数据---");
return Arrays.asList("abcde", "12345", "!@#$%");
}
}
多次请求接口,日志只有第一次输出结果,打开缓存工具发现已经具有了缓存功能。
不同的缓存注解
Cacheable
CachePut
CacheEvict
Caching
CacheConfig
解决乱码问题
Redisson
第一步:添加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.7</version>
</dependency>
第二步:封装为一个Bean对象
@Configuration(proxyBeanMethods = false)
public class RedissonConfig {
private static final Logger logger = LoggerFactory.getLogger(RedissonConfig.class);
@Bean
public RedissonClient redissonClient(RedisProperties properties) {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer();
serverConfig.setAddress("redis://" + properties.getHost() + ":" + properties.getPort());
String password = ObjectUtils.isEmpty(properties.getPassword())
? null
: properties.getPassword();
serverConfig.setPassword(password);
config.setCodec(new org.redisson.codec.JsonJacksonCodec());
RedissonClient redissonClient = Redisson.create(config);
logger.info("RedissonClient Create Success ...... ....... ");
return redissonClient;
}
}
发布、订阅
官方文档参考链接 https://redis.io/docs/manual/pubsub/
RedisPubSubCommands 是一个接口包含发布订阅抽象实验。
keepttl(KEEPTTL )
https://redis.io/commands/set/
KEEPTTL – Retain the time to live associated with the key.
Starting with Redis version 6.0.0: Added the KEEPTTL option.
自从6.0版本开始Redis新增命令keepttl保留上一个key的过期时间。
可以通过 stringRedisTemplate 去自定义实现,
/**
* 如果过期时间不存在,永久有效,Redis version > 6.0
*
* @param key
* @param value
* @return
*/
public Boolean setKeepTtl(byte[] key, byte[] value) {
return stringRedisTemplate.execute((connection) -> {
return connection.stringCommands().set(key, value, Expiration.keepTtl(), RedisStringCommands.SetOption.UPSERT);
}, false);
}
/**
* XX -- Only set the key if it already exist.,Redis version > 6.0
*
* @param key
* @param value
* @return
*/
public Boolean setExKeepTtl(byte[] key, byte[] value) {
return stringRedisTemplate.execute((connection) -> {
return connection.stringCommands().set(key, value, Expiration.keepTtl(), RedisStringCommands.SetOption.SET_IF_PRESENT);
}, false);
}
/**
* NX -- Only set the key if it does not already exist.,Redis version > 6.0
*
* @param key
* @param value
* @return
*/
public Boolean setNxKeepTtl(byte[] key, byte[] value) {
return stringRedisTemplate.execute((connection) -> {
return connection.stringCommands().set(key, value, Expiration.keepTtl(), RedisStringCommands.SetOption.SET_IF_ABSENT);
}, false);
}