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版本哦,下载下来!

2、Spring Data Redis

无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获取不到

2、为什么要用Redis?Redis为什么这么快?

内存数据库、单线程+IO多路复用,文章中内容更精彩。

3、RedisTemplate的key、value默认序列化器问题

4、

 

posted @ 2021-08-25 20:09  快乐的欧阳天美1114  阅读(414)  评论(0编辑  收藏  举报