导语
在之前的博客中介绍过关于Memcache的使用,这篇文章中主要介绍关于Redis的有关概念及如何与SpringBoot整合使用。
首先Redis是目前使用最为广泛的缓存中间件,相比较Memcache来说,Redis支持更多的数据结构而且对于这些数据结构的操作也很多,当然Redis也支持非常丰富的高可用集群解决方案。下面就来看一下关于Redis相关操作。
文章目录
Redis介绍
Redis是一个高速非关系型数据库也就是常说的No-SQL数据库,它可以存储键值对,支持五种不同Value的存储,可以将存储在内存的键值对持久化到硬盘中,可以使用复制的特点扩展其可读性,还可以使用客户端进行分片来进行扩展。
为了高性能,Redis也采用的是内存数据集,根据使用场景,可以通过每隔一段时间转存数据到磁盘,或者追加每条命令到日志来维持持久化操作。当然持久化也可以被禁用,如果只是需要一个功能则可以不用持久化。
数据模型
Redis数据模型不仅仅与关系型数据库不同,也不同于其他的NoSQL键值对存储。Redis数据类型类似于编程中的基本数据类型,可以通过这个基本的数据类型做其他的高级操作。主要支持的数据类型包括如下一些
- String
- Hash
- List
- Set
- ZSet
优势
Redis是使用C语言来编程的,直接操作内存在速度上有了优势,其次它支持的基本数据类型丰富、支持原子性的操作、对于所有语言的通用性等等。
- 高性能:每秒可以执行10w个set以及10w个get操作
- 数据类型丰富:Redis对于多数开发人员已知的数据类型提供了原生的支持
- 原子性:Redis操作都是原子性的,也就是多个客户端并发访问Redis服务器,获取到的值一定是最新的。
- 其他特性:Redis所适用的场景有很多,包括缓存、消息队列(Redis原生支持)、短期的应用数据存储(例如Session、Web命中数据等)等。
spring-boot-starter-data-redis
作为一个优秀的企业级解决方案。SpringBoot提供了与Redis集成的组件包,spring-boot-starter-date-redis, 它依赖与spring-data-redis 和 lettuce。在SpringBoot1.0默认使用的是Jedis客户端,2.0的时候被替换成了Lettuce,如果使用的是1.5.x的SpringBoot是感觉不到差异的,因为SpringBoot隔离了其中的一些差异性。
- Lettuce:一个可伸缩并且线程安全的Redis客户端,多个线程同时共享了一个RedisConnection,利用了Netty NIO作为IO连接来高效的管理多个连接。
- SpringData: Spring中的提供数据存储解决方案的框架,包括支持关系型数据库、非关系型数据库、Map-Reduce框架、云数据服务等。
- SpringData Redis: 是SpringData提供的一个关于Redis的解决方案,实现了对于Redis客户端API的高度整合。
关系如下
如上图所示,其实SpringDataRedis和Lettuce具备的功能,在SpringBoot Redis的启动器中都会存在。
快速开发
第一步、引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nh.redis</groupId>
<artifactId>redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意
这里说明一下为什么要使用commons-pool2是因为Lettuce需要使用commons-pool2来创建连接池。
第二步、application的配置
# Redis 数据库索引
spring.redis.database=0
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数据 默认为 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞时间 默认为 -1
spring.redis.lettuce.pool.max-wait= -1
# 连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0
第三步、缓存配置
这里是为了更好的整合Redis而设置一些全局的配置,例如key的生成策略,设置默认参数等等操作。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public KeyGenerator keyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj: params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}
第四步、单元测试
在单元测试中注入一个RedisTemplate,对于这个RedisTemplate与JDBCTemplate的作用是相同的。而String是以最长用的数据类型,普通的KV存储都可以使用String类型,当然这个是不一定的,也是使用其他的。
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void contextLoads() {
redisTemplate.opsForValue().set("nihui","hello");
Assert.assertEquals("hello",redisTemplate.opsForValue().get("nihui"));
}
}
从上面的流程来看只需要使用spring-boot-starter-data-redis只需要三步就可以与Redis进行集成。下面就来介绍一下Redis对于其他数据类型的操作。
Redis对于其他数据类型的操作
Redis在SpringBoot中支持的数据类型有,实体、哈希、列表、集合、有序集合。这些类型在SpringBoot中怎么去使用,下面就来看看各个数据类型怎么使用。
1、实体类型
首先来看看Redis对于Pojo的支持,首先来创建一个User对象,将其放入到缓存中,然后再取出来,测试操作如下
@Test
public void testObj(){
User user = new User("nihui",23,"[email protected]");
ValueOperations<String,User> operations = redisTemplate.opsForValue();
operations.set("user",user);
User u = operations.get("user");
System.out.println("user : "+ u.toString());
}
2、超时失效
Redis对于每个存储到其中的数据都可以设置一个超时时间,过了这个时间就会自动删除数据,这种特点就适合进行阶段性的数据缓存操作。
@Test
public void testExpire() throws InterruptedException {
User user = new User("nihui",23,"[email protected]");
ValueOperations<String,User> operations = redisTemplate.opsForValue();
operations.set("nihui",user,100,TimeUnit.MILLISECONDS);
Thread.sleep(1000);
boolean exists = redisTemplate.hasKey("nihui");
if (exists){
System.out.println("exists is true");
}else {
System.out.println("exists is false");
}
}
从测试结果来看Redis已经不能存在User对象了,也就是说这个数据已经过期了,同时在则是过程中使用了hasKey来判断key是否存在。
3、删除数据
在有些时候需要对过期的数据进行清理,下面就来看看如何清理这些数据。
@Test
public void testDelete(){
redisTemplate.opsForValue().set("deleteKey","hello,nihui");
redisTemplate.delete("deleteKey");
boolean exists = redisTemplate.hasKey("deleteKey");
if (exists){
System.out.println("exists is true");
}else {
System.out.println("exists is false");
}
}
4、Hash
在正常情况下存储一个键自然会对应有一个值,实际上在很多场景中这种操作并不是最好的,Redis存储一个键只会占用很小的内存,但是不管你这个键值字节数有多少都会是一样的,这个时候就是用Hash来解决这个资源浪费的问题。
Hash 就表示使用Hash这种数据结构进行key和value的存储。哈希表本身就是一个键值对的集合。这个集合可以根据kv的大小来动态开辟空间。对于一些有规律性的数据可以使用这种方式进行存储。
从上图可以看到IDEA给出的操作提示,第一个参数表示hash表的Key,第二个参数表示Hash表中的key,第三个参数表示Hash表中的Value。
@Test
public void testHash(){
HashOperations<String,Object,Object> hashOperations = redisTemplate.opsForHash();
hashOperations.put("nihui","hello","hello");
String value = (String) hashOperations.get("nihui","hello");
System.out.println("hash value "+value);
}
5、List
在Redis中的List使用的场景也是比较多的,也作为Redis中最为重要的一个数据结构存在,使用List可以实现一个数据队列的存储,也就是说支持一类数据的存储集合,使用List最常用的一个场景就是消息队列。可以利用List进行Push操作,将任务存储到List中,然后工作线程从中执行先进先出的操作,从List中操作对应的任务。
@Test
public void testList(){
ListOperations<String,String> listOperations = redisTemplate.opsForList();
listOperations.leftPush("list","nihui");
listOperations.leftPush("list","hello");
listOperations.leftPush("list","know");
String value = listOperations.leftPop("list");
System.out.println("list value : "+value.toString());
}
从上面的操作中可以看到从左面进从左面出,表示这个是一个栈,后进先出,所有最后取出的值是know,当然也可以从左边进从右边出构成一个队列。这可以后续自己设置符合自己程序的数据结构进行操作。
注意
Redis List 是实现了一个双向链表,也就是支持反向的查找和遍历操作,但是使用双向链表所带来的结果就是一部分的内存开销,在Redis内部有很多的实现方式。具体的双向链表可以关注后续的数据结构系列。
6、Set
Redis Set对外提供的是一个类似于List的功能,特殊的地方是在于Set是可以自动重排的,需要存储一个列表数据,又不希望出现重复数据的时候可以使用这种数据结构,当然这个也是集合这种数据结构的特点,集合中的元素是不重复的,当然也可以使用Set来判断是否存在某个元素。这个是List不能实现的功能。
@Test
public void testSet(){
String key = "set";
SetOperations<String,String> setOperations = redisTemplate.opsForSet();
setOperations.add(key,"nihui");
setOperations.add(key,"nihui");
setOperations.add(key,"hello");
setOperations.add(key,"world");
Set<String> values = setOperations.members(key);
for (String v:values) {
System.out.println("set value : "+v);
}
}
当然这里往这个Set中存储了两个nihui,但是读取的时候只有一条数据也就是说Set对数据进行了重排。
7、对于Set的其他操作
&emps; 从数学的角度上讲,对于一个集合来说有交集、并集、差集等操作,Redis中对这些操作也提供了一些支持
7.1、difference
@Test
public void testSetDifference(){
SetOperations<String,String> setOperations = redisTemplate.opsForSet();
String key1 = "key1";
String key2 = "key2";
setOperations.add(key1,"nihui");
setOperations.add(key1,"hello");
setOperations.add(key1,"hello");
setOperations.add(key1,"world");
setOperations.add(key2,"xxx");
setOperations.add(key2,"nihui");
Set<String> diffs = setOperations.difference(key1,key2);
for (String v :diffs){
System.out.println("diffs set value :"+v);
}
}
上面这个例子是吧key1中不同于key2 的数据对比出来。类似于在集合A中但是不在集合B中的操作。在数学上被称为是补集。
7.2、unions
@Test
public void testSetUnions(){
SetOperations<String,String> setOperations = redisTemplate.opsForSet();
String key1 = "key1";
String key2 = "key2";
setOperations.add(key1,"nihui");
setOperations.add(key1,"hello");
setOperations.add(key1,"xxx");
setOperations.add(key2,"aa");
setOperations.add(key2,"bb");
setOperations.add(key2,"world");
Set<String> unions = setOperations.union(key1,key2);
for (String v :unions){
System.out.println("unions set value :"+v);
}
}
根据测试结果可以发现,unions会联合两个集合,类似于并集。
注意
其实Set的内部实现是一个Value永远为null的HashMap,也就是说实际是通过计算Hash的方式来快速重排,这个也是为什么Set可以快速判断是否有数据在集合中的原因,如果有重复元素就会产生Hash冲突,如果没有Hash冲突则不会有重复集合。
8、Zet
Redis Sorted Set 的使用场景与Set是类似的,区别是Set不会自动有序,而Sorted Set可以通过用户额外提供的一个Score来实现排序操作并且插入有序,也就是自动排序,
注意这里的排序和重排是两个不一样的概念
在使用Zset的时候需要额外输入一个Score的参数,Zset会根据Score的值来对集合进行排序,可以利用这个特点来实现根据权重进行排序,例如最重要消息为Score为1,重要消息Score为2 然后通过权重来获取消息。
@Test
public void testZset(){
String key = "zset";
redisTemplate.delete(key);
ZSetOperations<String,String> zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add(key,"nihui",1);
zSetOperations.add(key,"hello",6);
zSetOperations.add(key,"world",4);
zSetOperations.add(key,"test",3);
Set<String> zsets = zSetOperations.range(key,0,3);
for (String value:zsets) {
System.out.println("zset value : "+value);
}
Set<String> zsett = zSetOperations.rangeByScore(key,0,3);
for (String value:zsett) {
System.out.println("zsett value : "+value);
}
}
通过上面的例子会发现插入的Zset中的数据会更具Score进行重新排序,根据这特性可以做优先级队列等操作,另外Zset还提供了rangeByScore的方法,获取Score范围内的数据排序数据。
注意
Redis Sorted Set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的成员到Score的映射,而跳跃表中存放的是所有的数据成员,排序依据的是HashMap里的Score,使用跳跃表结构可以获得比较高的查找效率,并且在实现上也相对比较简单。
到这里关于Redis所支持的数据类型的基本操作都已经说完了,其他的高级操作可以继续关注博客提供更优秀的解决方案。
封装缓存
在实际工作中并不是使用RedisTemplate来进行操作的,不是为每个类都注入一个RedisTemplate,而是对不同的功能模块进行封装操作,然后暴露其中的一个服务接口来进行缓存操作。这也符合面向对象的三大特性。
@Service
public class RedisCacheService {
@Autowired
private RedisTemplate redisTemplate;
//增加缓存
public boolean set(final String key,Object value){
boolean result = false;
try{
ValueOperations<Serializable,Object> operations = redisTemplate.opsForValue();
operations.set(key,value);
result = true;
}catch (Exception e){
e.printStackTrace();
}
return result;
}
//删除某一类键
public void removePattern(final String pattern){
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size()>0){
redisTemplate.delete(keys);
}
}
}
在其他地方使用的时候只需要注入这个服务即可
总结
相比较于Memcache来说,Redis是比较优秀的一款高可用性的缓存中间件,在有些企业中更是使用Redis来作为数据库来使用,Spring官方也为Redis提供良好的支持,Redis支持丰富的数据类型以及对数据类型的操作,方便了各种场景下的使用。提供了很多内置的支持。当然这个只是Redis在SpringBoot中的使用方式,后续的博客中将对Redis做一个专题的学习。敬请期待