spring boot项目10:Redis-直接使用
JAVA 8
Spring Boot 2.5.3
Redis server v=4.0.9 (单机,默认配置)
---
授人以渔:
1、Spring Boot Reference Documentation
This document is also available as Multi-page HTML, Single page HTML and PDF.
有PDF版本哦,下载下来!
无PDF,网页上可以搜索。
目录
试验1:使用CommandLineRunner测试 StringRedisTemplate
试验2:使用CommandLineRunner测试 RedisTemplate
试验3:使用RedisTemplate测试opsForValue()操作 类型为String的值
试验4:使用RedisTemplate测试opsForValue()操作 类型为Long的值
试验5:使用RedisTemplate测试opsForSet()操作 类型为String的无序集合
缓存,可以用来提高获取数据的速度,在计算机里面,就有各种缓存。
在软件工程中,也会用到各种缓存,其中,Redis是其中的佼佼者,可以有效提高用户获取数据的效率。
本文演示使用Spring Boot程序操作Redis。
在S.B.中,依赖下面的JAR包即可使用Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis 服务器 默认端口号 6379,上面的包引入后,默认使用的是 localhost:6379。
本文使用的是虚拟机的Redis服务器,因此,要做配置:
#
# Redis
# mylinux 是虚拟机的本地域名,配置到 hosts文件中
spring.redis.host=mylinux
spring.redis.port=6379
在S.B.中,还有更多以 spring.redis. 开头的配置——可以去官文查找:
# 部分 spring.redis.* 配置
spring.redis.username
spring.redis.password
spring.redis.url
spring.redis.connect-timeout # 连接超时时间
spring.redis.timeout # 读数据超时时间
spring.redis.ssl # SSL支持
spring.redis.jedis.* # 使用jedis客户端时的配置
spring.redis.lettuce.* # 使用Lettuce客户端时的配置
S.B.使用缓存时,也有一些Redis相关配置,本文暂不介绍。
添加上面依赖后,S.B.应用 会将下面的一些 和Redis相关的 Bean 添加到 Spring容器中:
# 常用
redisTemplate
stringRedisTemplate
reactiveRedisTemplate
reactiveStringRedisTemplate
# 其它
RedisAutoConfiguration
RedisProperties
redisConnectionFactory
RedisReactiveAutoConfiguration
redisCustomConversions
redisConverter
redisKeyValueAdapter
redisKeyValueTemplate
# 默认的Lettuce客户端,,可以更改为Jedis客户端
LettuceConnectionConfiguration
lettuceClientResources
一些Type的签名:StringRedisTemplate 类 继承了 RedisTemplate 类
public class RedisAccessor implements InitializingBean {
}
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
}
// RedisTemplate 的一个泛型类
public class StringRedisTemplate extends RedisTemplate<String, String> {
}
Reactive版本的类似(暂未使用):
public interface ReactiveRedisOperations<K, V> {
}
public class ReactiveRedisTemplate<K, V> implements ReactiveRedisOperations<K, V> {
}
public class ReactiveStringRedisTemplate extends ReactiveRedisTemplate<String, String> {
}
RedisTemplate类 是其中的重点:来自博客园
其中的 opsForXXX 函数用来获取 不同数据类型的操作对象——Value、Set、List、ZSet、Hash、Geo、HyperLogLog、Stream。
注,其中的 opsForCluster 用来进行集群操作,咱不清楚——不同的数据存入不同主机?。
RedisTemplate类 中还有几个 RedisSerializer属性,用来对 键、值 等进行序列化:
默认的序列化对象在使用时存在一些问题,需要做更改,后文介绍。
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer<?> defaultSerializer;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
试验1:使用CommandLineRunner测试 StringRedisTemplate
测试代码:key、value 都是 字符串String
@Component
@Order(1)
@Slf4j
class TestRunner1 implements CommandLineRunner {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void run(String... args) throws Exception {
log.info("测试 StringRedisTemplate类:stringRedisTemplate={}", stringRedisTemplate);
stringRedisTemplate.opsForValue().set("test1", "str1");
stringRedisTemplate.opsForValue().set("test2", "str2", Duration.ofSeconds(30));
stringRedisTemplate.opsForValue().set("test3", "str3", 20);
stringRedisTemplate.opsForValue().set("test4", "str4", 60, TimeUnit.SECONDS);
String s1 = (String) stringRedisTemplate.opsForValue().get("test1");
Long l1 = stringRedisTemplate.getExpire("test1");
String s2 = (String) stringRedisTemplate.opsForValue().get("test2");
Long l2 = stringRedisTemplate.getExpire("test2");
String s3 = (String) stringRedisTemplate.opsForValue().get("test3");
Long l3 = stringRedisTemplate.getExpire("test3");
String s4 = (String) stringRedisTemplate.opsForValue().get("test4");
Long l4 = stringRedisTemplate.getExpire("test4");
log.info("执行结果:");
log.info("s1={}, l1={}", s1, l1);
log.info("s2={}, l2={}", s2, l2);
log.info("s3={}, l3={}", s3, l3);
log.info("s4={}, l4={}", s4, l4);
log.info("del test1: {}", stringRedisTemplate.delete("test1"));
log.info("del test2: {}", stringRedisTemplate.delete("test2"));
log.info("del test3: {}", stringRedisTemplate.delete("test3"));
log.info("del test4: {}", stringRedisTemplate.delete("test4"));
}
}
执行结果:测试代码正常执行来自博客园
试验2:使用CommandLineRunner测试 RedisTemplate
测试代码:来自博客园
@Component
@Order(2)
@Slf4j
class TestRunner2 implements CommandLineRunner {
// @Autowired // 添加泛型参数后,不可用,需改为 @Resource
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(String... args) throws Exception {
log.info("测试 RedisTemplate类:redisTemplate={}", redisTemplate);
redisTemplate.opsForValue().set("test1", "str1");
redisTemplate.opsForValue().set("test2", "str2", Duration.ofSeconds(30));
redisTemplate.opsForValue().set("test3", "str3", 20);
redisTemplate.opsForValue().set("test4", "str4", 60, TimeUnit.SECONDS);
String s1 = (String) redisTemplate.opsForValue().get("test1");
Long l1 = redisTemplate.getExpire("test1");
String s2 = (String) redisTemplate.opsForValue().get("test2");
Long l2 = redisTemplate.getExpire("test2");
// 异常发生!
// 改造 RedisTemplate 的序列化器 后,可以执行了,见 {@link RedisConfig}
String s3 = (String) redisTemplate.opsForValue().get("test3");
Long l3 = redisTemplate.getExpire("test3");
String s4 = (String) redisTemplate.opsForValue().get("test4");
Long l4 = redisTemplate.getExpire("test4");
log.info("执行结果:");
log.info("s1={}, l1={}", s1, l1);
log.info("s2={}, l2={}", s2, l2);
log.info("s3={}, l3={}", s3, l3);
log.info("s4={}, l4={}", s4, l4);
log.info("del test1: {}", redisTemplate.delete("test1"));
log.info("del test2: {}", redisTemplate.delete("test2"));
log.info("del test3: {}", redisTemplate.delete("test3"));
log.info("del test4: {}", redisTemplate.delete("test4"));
}
执行结果1:发生异常,一个序列化问题。
# 取一行
# 执行 String s3 = (String) redisTemplate.opsForValue().get("test3"); 时
Caused by: org.springframework.core.serializer.support.SerializationFailedException: \
Failed to deserialize payload. Is the byte array a result of corresponding serialization \
for DefaultDeserializer?; nested exception is java.io.StreamCorruptedException: \
invalid stream header: 00000000
前文提到,RedisTemplate类 有几个序列化器,此时,更改 RedisTemplate类 默认的序列化器即可。
/**
* RedisTemplate配置:序列化器
* @author ben
* @date 2021-08-23 15:01:41 CST
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建模板
RedisTemplate template = new RedisTemplate();
// 2.关联redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建序列化器对象
GenericToStringSerializer serializer = new GenericToStringSerializer(Object.class);
// 4.设置 value、key 的转化格式——序列化器
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
再次执行,一切正常:
附1:序列化器类型,RedisSerializer接口 下有多个 实现类
RedisTemplate Bean默认的序列化器是什么呢?
# StringRedisTemplate 全是 StringRedisSerializer
log.info("序列化器:{}\n{}\n{}\n{}", stringRedisTemplate.getKeySerializer(), stringRedisTemplate.getValueSerializer(),
stringRedisTemplate.getHashKeySerializer(), stringRedisTemplate.getHashValueSerializer());
序列化器:org.springframework.data.redis.serializer.StringRedisSerializer@ef718de
org.springframework.data.redis.serializer.StringRedisSerializer@ef718de
org.springframework.data.redis.serializer.StringRedisSerializer@ef718de
org.springframework.data.redis.serializer.StringRedisSerializer@ef718de
# RedisTemplate 全是 JdkSerializationRedisSerializer
log.info("序列化器:{}\n{}\n{}\n{}", redisTemplate.getKeySerializer(), redisTemplate.getValueSerializer(),
redisTemplate.getHashKeySerializer(), redisTemplate.getHashValueSerializer());
序列化器:org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@67fb5025
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@67fb5025
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@67fb5025
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer@67fb5025
RedisTemplate 下的 JdkSerializationRedisSerializer 对 解析 这种带offset的发生异常。来自博客园
试验3:使用RedisTemplate测试opsForValue()操作 类型为String的值
注,先使用RedisTemplate默认序列化器。
两个接口:
/redis/value/setValueStr 设置值
/redis/value/getValueStr 获取值
public boolean setValue(@RequestParam String key, @RequestParam String value, @RequestParam Long timeout) {
}
public String getValueStr(@RequestParam String key) {
}
测试结果:一切正常。
测试key值:str1
不正常的是什么呢?使用redis-cli时,无法获取设置的 key=str1 的数据。
额,这又是一个 序列化器的问题!解决方法同试验2——配置RedisTemplate 的序列化器。
配置后,测试完使用 redis-cli 获取测试的 key=str1 的结果如下:来自博客园
符合预期了。
试验4:使用RedisTemplate测试opsForValue()操作 类型为Long的值
注,先使用RedisTemplate默认序列化器。
三个接口:来自博客园
/redis/value/setValueLong 设置值
/redis/value/getValueLong 获取值
/redis/value/incrementValueLong 增加值
public boolean setValue(@RequestParam String key, @RequestParam Long value, @RequestParam Long timeout) {
}
public Long getValueLong(@RequestParam String key) {
}
public Long incrementValueLong(@RequestParam String key, @RequestParam Long delta) {
}
测试key值:long1
测试结果:
设置值 | 正常,,但redis-cli 看到的 key值不正常(上面介绍过) |
获取值 | 不正常,没有返回值,没有key |
增加值 | 发生异常(见下方) |
增加值 时的异常信息如下:
[dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is
io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range] with root cause
io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range
at io.lettuce.core.internal.ExceptionFactory.createExecutionException(ExceptionFactory.java:137)
~[lettuce-core-6.1.4.RELEASE.jar:6.1.4.RELEASE]
是的,又是序列化器的问题。
改为 前面修改序列化器后的 RedisTemplate测试:一切符合预期。来自博客园
试验5:使用RedisTemplate测试opsForSet()操作 类型为String的无序集合
set 是 Redis支持的无需集合,除了 存取数据,还支持做 交集、并集、差集等计算。
本试验仅做了 添加、获取、pop 操作的测试。
三个接口:
/redis/set/add 添加若干个元素
/redis/set/getAll 获取所有元素
/redis/set/popOne 弹出一个元素来自博客园
测试key值:set1
测试结果:
未发生异常。
使用默认序列化器时,redis-cli 客户端看到的 键值是错误的,入前面一样更改了 序列化器即可。
更改后 redis-cli 检查结果:
注:
使用 Set<Object> set = redisTemplate.opsForSet().members(key); 无法获取 符合预期的 全体元素,
改为使用下面的 可行了:randomMembers、size 两个函数
// 第二种获取元素的方式
List<Object> list = redisTemplate.opsForSet().randomMembers(key, redisTemplate.opsForSet().size(key));
list.forEach(obj->{
retset.add((String) obj);
});
试验2、3、4、5 的各个接口的源码如下:
两个类的源码
# AddToSetDTO.java
/**
* 添加元素到缓存集合
* @author ben
* @date 2021-08-23 15:42:34 CST
*/
@Data
public class AddToSetDTO {
private String key;
private String[] values;
}
# RedisController.java
/**
* HTTP操作Redis
* @author ben
* @date 2021-08-23 13:39:07 CST
*/
@RestController
@RequestMapping(value="/redis")
@Slf4j
public class RedisController {
@Resource
private RedisTemplate<String, Object> redisTemplate;
// -------------value-------------
/**
* 前缀: /value
*/
private final static String PATH_VALUE = "/value/";
/**
* 设置String值
* @author ben
* @date 2021-08-23 14:14:33 CST
* @param key 非空
* @param value 非空
* @param timeout 必须大于0,过期秒数
* @return 成功返回true
*/
@PostMapping(value=PATH_VALUE + "/setValueStr")
public boolean setValue(@RequestParam String key, @RequestParam String value, @RequestParam Long timeout) {
if (!(StringUtils.hasText(key) && StringUtils.hasText(value) && timeout != null && timeout > 0)) {
throw new RuntimeException("参数错误");
}
log.info("key={}, value={}, timeout={}", key, value, timeout);
// 注意,第三个参数是 Duration
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeout));
return redisTemplate.hasKey(key);
}
/**
* 获取String值
* @author ben
* @date 2021-08-23 14:15:12 CST
* @param key
* @return 值
*/
@GetMapping(value=PATH_VALUE + "/getValueStr")
public String getValueStr(@RequestParam String key) {
if (!StringUtils.hasText(key)) {
throw new RuntimeException("参数错误");
}
log.info("key={}, ttl={} seconds", key, redisTemplate.getExpire(key));
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 设置长整型
* @author ben
* @date 2021-08-23 14:15:29 CST
* @param key 非空
* @param value 非null
* @param timeout 必须大于0
* @return
*/
@PostMapping(value=PATH_VALUE + "/setValueLong")
public boolean setValue(@RequestParam String key, @RequestParam Long value, @RequestParam Long timeout) {
if (!(StringUtils.hasText(key) && value != null && timeout != null && timeout > 0)) {
throw new RuntimeException("参数错误");
}
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeout));
return redisTemplate.hasKey(key);
}
/**
* 获取长整型值
* @author ben
* @date 2021-08-23 14:15:56 CST
* @param key
* @return
*/
@GetMapping(value=PATH_VALUE + "/getValueLong")
public Long getValueLong(@RequestParam String key) {
if (!StringUtils.hasText(key)) {
throw new RuntimeException("参数错误");
}
log.debug("key={}, ttl={} seconds", key, redisTemplate.getExpire(key));
Long retval = null;
Object val = redisTemplate.opsForValue().get(key);
if (String.class.equals(val.getClass())) {
retval = Long.valueOf(String.valueOf(val));
} else {
log.warn("Redis中没有key={}", key);
}
return retval;
}
/**
* 给长整型增加值
* @author ben
* @date 2021-08-23 14:16:09 CST
* @param key
* @param delta 非null,可正,可负
* @return
*/
@PostMapping(value=PATH_VALUE + "/incrementValueLong")
public Long incrementValueLong(@RequestParam String key, @RequestParam Long delta) {
if (!(StringUtils.hasText(key) && delta != null )) {
throw new RuntimeException("参数错误");
}
log.info("key={}, delta={}", key, delta);
// 发生异常:io.lettuce.core.RedisCommandExecutionException:
// ERR value is not an integer or out of range
// 改造 redisTemplate 的序列化器 见 {@link RedisConfig}
return redisTemplate.opsForValue().increment(key, delta);
}
// -------------set-------------
/**
* 前缀:set/
*/
private final static String PATH_SET = "/set/";
/**
* 添加到集合
* @author ben
* @date 2021-08-23 15:55:10 CST
* @param dto
* @return
*/
@PostMapping(value=PATH_SET + "/add")
public Long addToSet(@RequestBody AddToSetDTO dto) {
String key = dto.getKey();
String[] values = dto.getValues();
if (!StringUtils.hasText(key) || Objects.isNull(values)) {
throw new RuntimeException("参数错误");
}
if (values.length == 0) {
return 0L;
}
return redisTemplate.opsForSet().add(key, values);
}
/**
* 获取集合全部元素
* @author ben
* @date 2021-08-23 15:55:24 CST
* @param key
* @return
*/
@GetMapping(value=PATH_SET + "/getAll")
public Set<String> getAllSet(@RequestParam String key) {
Set<String> retset = new HashSet<>(32);
// 返回数据不符合预期,TODO
Set<Object> set = redisTemplate.opsForSet().members(key);
Optional.of(set).ifPresent(item -> {
// 仅执行一次
// System.out.println("item=" + item);
retset.add(String.valueOf(item));
});
return retset;
}
/**
* 删除并返回集合中一个随机元素
* @author ben
* @date 2021-08-23 15:55:31 CST
* @param key
* @return
*/
@PostMapping(value=PATH_SET + "/popOne")
public String popSet(@RequestParam String key) {
return (String) redisTemplate.opsForSet().pop(key);
}
}
说明210825-2002
还有hash、list、HyperLogLog、ZSet等,本文暂不介绍了。来自博客园
1、redis在java中设置了缓存值为什么在redis-cli获取不到
内存数据库、单线程+IO多路复用,文章中内容更精彩。
3、RedisTemplate的key、value默认序列化器问题
4、