迎着风跑  

Redis进阶

Redis持久化【掌握】

Redis事务【了解】

Redis消息发布定阅【掌握】

Redis集群配置【掌握】

SpringBoot整合Redis<font color=red>【重点】</font>

 

一、Redis持久化

redis是一个内存数据库,当redis服务器重启,获取电脑重启,数据会丢失,我们可以将redis内存中的数据持久化保存到硬盘的文件中。

redis提供两种持久化方式:

​ RDB:快照,通过从服务器保存和持久化

​ AOF:日志,操作生成相关日志,并通过日志来恢复数据。couchDB对于数据内容,不修改,只追加,则文件本身就是日志,不会丢失数据.

<font color=red>注意:redis默认开启了RDB持久化</font>

 

1、RDB持久化

​ 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里,Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

<font color=red>注:fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程,在每次redis服务器启动的时候,会自动把dump.rdb这个文件的键值对 全部读取到内存</font>

<font color=blue> 步骤一:编辑redis.conf配置文件</font>

RDB快照相关参数:
save 900 1   #刷新快照到硬盘中,必须满足两者要求才会触发,即900秒之后至少1个关键字发生变化。
save 300 10 #必须是300秒之后至少10个关键字发生变化。
save 60 10000 #必须是60秒之后至少10000个关键字发生变化。
上面三个参数屏闭后,rdb方式就关闭了

stop-writes-on-bgsave-error yes   #后台存储错误停止写。
rdbcompression yes   #使用LZF压缩rdb文件。
rdbchecksum yes   #存储和加载rdb文件时校验。
dbfilename dump.rdb   #设置rdb文件名。
dir ./   #设置工作目录,rdb文件会写入该目录。

 

 

<font color=blue> 步骤二:重启redis查看数据是否存在</font>

  • 先删除dump.rdb

  • 使用客户端连接并添加数据

  • 杀掉redis进程


    pkill -9 redis
  • 启动redis查看数据是否存在

<font color=red>RDB的缺陷:在2个保存点之间断电,将会丢失1-N分钟的数据出于对持久化的更精细要求,redis增添了aof方式 append only file</font>

2、AOF日志持久化

AOF日志原理

 

 

思想:内存每写一条,就备份一条,时间间隔是1秒钟,缺点:文件大,写操作频繁。

  • 以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),

  • 只许追加文件但不可以改写文件,redis启动之初会读取该文件(aof文件)重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

  • aof保存的是appendonly.aof文件


AOF日志相关参数:
appendonly no # 是否打开aof日志功能 no:不开启   yes:开启日志
appendfsync always   # 每1个命令,都立即同步到aof. 安全,速度慢
everysec # 折衷方案,每秒写1次
no     # 写入工作交给操作系统,由操作系统判断缓冲区大小,统一写入到aof. 同步频率低,速度快
no-appendfsync-on-rewrite yes: # 正在导出rdb快照的过程中,要不要停止同步aof

配置开启AOF日志

 

 

配置存储方案

 

 

保存退出并杀掉redis进程


pkill -9 redis

打印日志文件内容


more /usr/local/bin/appendonly.aof

 

 

<font color=red>因为没有操作所以没有日志信息</font>

启动redis服务端并启动客户端连接并创建key

 

 

查看日志

 

 

3、AOF重写

思考:如果对同一个key进行多次操作,在aof日志中怎样表现操作记录,一条还是n条?

<font color=blue size=4>案例 :创建age并改变五次值</font>

 

 

<font color=red>日志会将每一步操作都记录,如果要对一个key操作多次,在数据上的表现只有一个但在日志中会有n条记录。当数据丢失需要找回数据的时候怎样找到正确的值?</font>

aof重写是将内存中的key和value逆化为redis命令重新保存到日志中,就好像是将所执行的操作做的总结。


aof重写相关参数:
auto-aof-rewrite-percentage 100 #aof文件大小比起上次重写时的大小,增长率100%时,重写
auto-aof-rewrite-min-size 64mb #aof文件,至少超过64M时,重写

 

 

<font color=red>问: 在dump rdb过程中,aof如果停止同步,会不会丢失?</font>

<font color=blue >答: 不会,所有的操作缓存在内存的队列里, dump完成后,统一操作.</font>

<font color=red> 问: aof重写是指什么?</font>

<font color=blue >答: aof重写是指把内存中的数据,逆化成命令,写入到.aof日志里.以解决 aof日志过大的问题.</font>

<font color=red> 问: 如果rdb文件,和aof文件都存在,优先用谁来恢复数据?</font>

<font color=blue >答: aof</font>

<font color=red>问: 2种是否可以同时用?</font>

<font color=blue >答: 可以,而且推荐这么做</font>

<font color=red>问: 恢复时rdb和aof哪个恢复的快</font>

<font color=blue >答: rdb快,因为其是数据的内存映射,直接载入到内存,而aof是命令,需要逐条执行</font>


问题思考:在使用rdb做持久化时,我们关掉了redis服务,然后重新打开,保存的数据还在。但在做aof的时候我们将redis服务关闭后再打开数据就没有了。在上面不是配置过rdb持久化吗,为什么没起作用?
答:当rdb中有数据,并开启了AOF选项,重启redis服务后会产生一个空的aof文件,当rdb和aof文件都存在,会以aof文件来恢复数据。

4、持久化总结

官网建议

1)、RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储

2)、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大

3)、只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.

4)、同时开启两种持久化方式

在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

5)、性能建议

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。AOF好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite是将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。如果不Enable AOF ,仅靠Master-Slave Replication实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构

 

二、Redis事务

Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:

  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务。

  • 命令入队。

  • 执行事务。

redis事务 将数据存放到一个队列中,一次性、顺序性、排他性的执行一系列命令

redis事务让我们可以在redis一次执行多条命令。类似于批处理

Redis与 mysql事务的对比

 mysqlredis
开启 start transaction multi
语句 普通sql 普通命令
失败 rollback 回滚 discard 取消
成功 commit exec
序号命令及描述
1 multi 标记一个事务块的开始
2 exec 执行事务块内的所有命令
3 discard 取消事务,放弃执行事务块内的所有命令
4 watch key [key ...] 监视一个或多个key,如果在事务执行前这个(或这些)key被其他命令所改动,那么事务将会被打断
5 unwatch 取消对所有key的监视

1、正常执行redis事务

<font color=blue size=4>案例:使用redis事务添加key</font>

 

 

2、放弃事务

放弃事务后key将不会添加到redis中

<font color=blue size=4>案例:使用redis事务添加key并放弃执行</font>

 

 

3、全体连坐

<font color=blue size=4>案例:一条出错全体不执行</font>

 

 

4、冤有头债有主

<font color=blue size=4>案例:谁出错就干掉谁,其他保存</font>

 

 

<font color=red>提示:和传统的mysql事务不同的事,即使我们的事务块中的某一个操作失败,我们也无法在这一组命令中让整个状态回滚到操作之前。redis中没有回滚操作</font>

 

三、Redis消息发布定阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

Redis 客户端可以订阅任意数量的频道。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

 

 

<font color=blue size=4>案例:创建消息发布端和定阅端</font>


定阅端语法:SUBSCRIBE redisChat
eg:SUBSCRIBE 频道名称

 

 

再创建一个客户端在同一频道发布消息


语法:PUBLISH redisChat "Redis is a great caching technique"
eg:PUBLISH 频道名称 "发布内容"

 

 

订阅端实时接收到消息

 

 

 

四、Redis集群配置


Redis集群好处:
主从备份 防止主机宕机
读写分离,分担master的任务
任务分离,如从服分别分担备份工作与计算工作

1、redis主从复制(Master/Slave)

​ 主从复制:主机数据更新后根据配置和策略,自动同步到备机,Master以写为主,Slave以读为主

<font color=red>注:redis的主从复制解决的问题就是:读写分离和容灾恢复</font>

redis集群模式

 

 

<font color=blue>案例:一主二从,一台主机两台从机</font>

复制多个redis.conf文件,并按下列要求进行修改

拷贝多个redis.conf文件
[root@localhost bin]# cp redis.conf redis6379.conf
[root@localhost bin]# cp redis.conf redis6380.conf
[root@localhost bin]# cp redis.conf redis6381.conf
修改每个配置文件的信息  
1、redis6379.conf
daemonize yes
port 6379
redis6379.pid   保存进程文件的文件名
logfile 6379.log

另外两个配置修改同上。
[root@localhost bin]# ./redis-server redis6379.conf
[root@localhost bin]# ./redis-server redis6380.conf
[root@localhost bin]# ./redis-server redis6381.conf

分别启动它们三个redis

 

 

创建三个会话窗口,使用客户端连上后并输入:info replication查看信息

 

 

将6380和6381设为从机


从库连接到主库的语法:slaveof   主库IP   主库端口

 

 

将6380和6381执行安然无恙slaveof命令后,再执行 info replication。6380和6381已经变成从机slave了

主机6379 写一条命令,2台从机负责取

 

 

思考1:主机先set k1到k4,从机再连接到主机,主机然后set k5,从机能取到k5,那么从机是否能取到k1 到k4的数据呢? 因为是先主机存k1 到k4,然后从机在连接主机的,k5当然可以取到,那么k1到k4其实也可以取到,因为从机连接主机的时候,它是一条条指令的读取的

思考2:主从机都去set一个k6是否可以操作

思考3:主机宕机情况,那么从机是否会变成主机(把主机shutdown,然后看从机状态)

思考4:如果主机恢复后,主机是否还是主机,如果是那么这个时候主机写一条命令,2个从机是否能收到

思考5:如果从机宕机了,主机这个set k9,从机是否能收到,如果从机连接了,是否能收到

 

  • 反客为主

SLAVEOF no one:使当前数据库停止与其他数据库的同步,转成主数据库

实验1:6379是主机,6380和6381是从机。主机6379宕机,6380和6381还是从机,在6380上执行SLAVEOF no one,此时6380已经变成主机了,让6381去连接6380这个主机。这个时候是6380带着6381混了

思考1:如果这个时候6379复活,是否是主机,如果不是,能否收到6381主机的写指令的值。如果收不到怎么办?

总结:复制原理

slave启动成功连接到master后会发送一个sync命令,Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步

2)、全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

3)、增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步

4)、但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

 

2、哨兵模式(sentinel)

概念:反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

<font color=blue>案例:6379带着6380和6381,配置一主二从</font>

新建sentinel.conf文件,名字绝不能错,然后在该文件填写如下内容


语法:sentinel monitor 被监控数据库名字(自己起名字) 127.0.0.1 6379 1
  示例:sentinel monitor host6379 127.0.0.1 6379 1
说明:数字 1表示选举,某个slaver得到超过1票则成成为Master节点
启动哨兵:redis-sentinel sentinel.conf

 

 

启动哨兵

​ 

 

 

此时把6379执行shutdown,然后看看投票结果

 

 

 

五、SpringBoot操作Redis

1、 添加redis启动器

pring Boot 提供了对 Redis 集成的组件包:spring-boot-starter-data-redis,它依赖于 spring-data-redislettuce

另外,这里还有两个小细节:

  • Spring Boot 1.x 时代,spring-data-redis 底层使用的是 Jedis;2.x 时代换成了 Lettuce

  • Lettuce依赖于 commons-pool2


<!-- springboot整合redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 使用 lettuce 时要加这个包;使用 jedis 时则不需要。-->
<dependency>
 <groupId>org.apache.commons</groupId>
 <artifactId>commons-pool2</artifactId>
</dependency>
  • Lettuce 的 timed out 问题

在 Spring Boot 2.x 默认使用 Lettuce 之后,会偶发性出现 Redis command timed out 问题,从而导致客户端(Java 代码)无法连接到 Redis Server 的问题。

而且该问题无法正面解决。网上通用的解决方案是:放弃使用 Lettuce 驱动,转而使用 Jedis 驱动。

这种情况下,你需要手动排除 spring-data-redis 对 Lettuce 的依赖,并引入 Jedis 依赖。


<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
 <exclusions> <!-- 从依赖关系中排除 -->
   <exclusion>
     <groupId>io.lettuce</groupId>
     <artifactId>lettuce-core</artifactId>
   </exclusion>
 </exclusions>
</dependency>
<dependency>
 <groupId>redis.clients</groupId>
 <artifactId>jedis</artifactId>
</dependency>
<!-- 此时,也就不想再需要使用 apache 的 commons-pool 包了。-->

如果springboot的版本是2.3.7 中这个问题不存在了

2、修改配置文件application.properties


## Redis 服务器地址
spring.redis.host=localhost
## Redis 服务器连接端口
spring.redis.port=6379
## Redis 数据库索引(默认为 0)
spring.redis.database=0

## 以下非必须,有默认值
## Redis 服务器连接密码(默认为空)
spring.redis.password=
## 连接池最大连接数(使用负值表示没有限制)默认 8
spring.redis.lettuce.pool.max-active=8
## 连接池最大阻塞等待时间(使用负值表示没有限制)默认 -1
spring.redis.lettuce.pool.max-wait=-1
## 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
## 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0

3、创建测试类

在单元测试中,注入 RedisTemplate<Object, Object>StringRedisTemplate


这两个 bean 被声明在了 ...RedisAutoConfiguration 中。在你没有自己配置 RedisTemplate 的 Bean 的情况下,spring-data-redis 使用的就是它们俩(中的一个)。
另外,StringRedisTemplate 是 RedisTemplate 的子类,它等同于 RedisTemplate<String, String> 。
StringRedisTemplate 比 RedisTemplate<Object, Object> 更简单、常见。RedisTemplate<Object, Object> 会涉及一个转换器(Serializer)的概念。优先考虑使用 StringRedisTemplate

/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {

   @Autowired
   private StringRedisTemplate redisTemplate;

   /**
    * 保存字符串到redis中
    */
   @Test  //导入org.junit.jupiter.api.Test;包
   public void testString(){
       redisTemplate.opsForValue().set("hello","Hello Redis");
  }
}

在这个单元测试中,我们使用 redisTemplate 存储了一个字符串 "Hello Redis"

Spring Data Redis 针对 api 进行了重新归类与封装,将同一类型的操作封装为 Operation 接口:

专有操作说明
ValueOperations string 类型的数据操作
ListOperations list 类型的数据操作
SetOperations set 类型数据操作
ZSetOperations zset 类型数据操作
HashOperations map 类型的数据操作

@Autowired
private StringRedisTemplate redisTemplate;

@Test
public void contextLoad() {
   assertNotNull(redisTemplate);
   ValueOperations<String, String> stringOperations = redisTemplate.opsForValue();
   HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
   ListOperations<String, String> listOperations  = redisTemplate.opsForList();
   SetOperations<String, String> setOperations = redisTemplate.opsForSet();
   ZSetOperations<String, String> zsetOperations = redisTemplate.opsForZSet();
}

4、RedisTemplate 和 Serializer(了解)

RedisTemplate<Object, Object> 看起来比 StringRedisTemplate 更『牛逼』一些,因为它不强求键和值的类型必须是 String 。

但是很显然,这和 Redis 的实际情况是相违背的:在最小的存储单元层面,Redis 本质上只能存字符串,不可能存其它的类型。这么看来,StringRedisTemplate 更贴合 Redis 的存储本质。那么 RedisTemplate 是如何实现以任何类型(只要是实现了 Serializable 接口)作为键值对的?通过 Serializer

RedisTemplate 会将你交给它的作为键或值的任意类型对象(唯一要求是实现了 Serializable 接口)使用 Serializer 进行转换,转换成字符串,然后再存入 Redis 中。这样就没有违背『Redis 的最小存储单元中只能存字符串』的准则。

RedisTemplate 默认使用的是 JdkSerializationRedisSerializer 进行 Object 到 String 的双向转换工作。它将对象转换为字节数组的字符串形式。

考虑到『对象的字节数组的字符串形式』不便于阅读,因此,你可以考虑将默认的 JdkSerializationRedisSerializer 替换掉。这种情况下,你就需要自己去声明 RedisTemplate(@Bean)


对象的字节数组的字符串形式如下。看起来怪怪的感觉。

127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x0cdepartment:1" "\xac\xed\x00\x05sr\x00"com.woniu.example1.bean.Department\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x03L\x00\x02idt\x00\x13Ljava/lang/Integer;L\x00\blocationt\x00\x12Ljava/lang/String;L\x00\x04nameq\x00~\x00\x02xpsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01t\x00\x05Wuhant\x00\aTesting"

记得要加 jackson 的依赖:


<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-databind</artifactId>
</dependency>

@Configuration
public class RedisConfig {
   @Bean(name = "redisTemplate")
   public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
           throws UnknownHostException {
       RedisTemplate<String, Object> template = new RedisTemplate<>();
       template.setConnectionFactory(redisConnectionFactory);
       template.setKeySerializer(RedisSerializer.string());
       template.setValueSerializer(RedisSerializer.json());
       return template;
  }
}

5、SpringBoot操作Stringng字符串

a、 添加字符串到redis


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {
   @Autowired
   private StringRedisTemplate redisTemplate;

   /**
    * 操作字符串
    */
   @Testjava
   public void testObj() {
       ValueOperations<String, String> operations = redisTemplate.opsForValue();
       operations.set("id", "9527");
       operations.set("name", "tom");
       operations.set("age", "21");
       String name = operations.get("name");
       System.out.println(name);
  }
}

b、键过期

key的自动过期问题,Redis 在存入每一个数据的时候都可以设置一个超时间,过了这个时间就会自动删除数据。

新建一个 Student 对象,存入 Redis 的同时设置 100 毫秒后失效,设置一个线程暂停 1000 毫秒之后,判断数据是否存在并打印结果。


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {
   @Resource
   private RedisTemplate<String,Object> redisTemplate;

   /**
    * 操作字符串
    */
   @Test
   public void testObj() throws InterruptedException {
       final  String key="st";

       Student student = new Student(1,"tom");

       ValueOperations<String, Object> operations =redisTemplate.opsForValue();
      operations.set(key,student,100, TimeUnit.MILLISECONDS);

       Thread.sleep(1000);
       boolean exists = redisTemplate.hasKey(key);
       System.out.println( exists ? "key is true" : "key is false" );
  }
}

从结果可以看出,Reids 中已经不存在 Student 对象了,此数据已经过期,同时我们在这个测试的方法中使用了 hasKey("expire") 方法,可以判断 key 是否存在

c、删除数据

有些时候,我们需要对过期的缓存进行删除,下面来测试此场景的使用。首先 set 一个字符串 hello world,紧接着删除此 key 的值,再进行判断。


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {
   @Resource
   private RedisTemplate<String,Object> redisTemplate;

   /**
    * 删除键
    */
   @Test
   public void testDelString(){
       String key="uname";
       ValueOperations<String, Object> operations=redisTemplate.opsForValue();
       operations.set(key,"tom");

       //删除键
       redisTemplate.delete(key);

       //判断键是否存在
       boolean exists = redisTemplate.hasKey(key);
       System.out.println( exists ? "key is true" : "key is false" );
  }
}

6、SpringBoot操作Hash(哈希)

一般我们存储一个键,很自然的就会使用 get/set 去存储,实际上这并不是很好的做法。Redis 存储一个 key 会有一个最小内存,不管你存的这个键多小,都不会低于这个内存,因此合理的使用 Hash 可以帮我们节省很多内存。

Hash Set 就在哈希表 Key 中的域(Field)的值设为 value。如果 Key 不存在,一个新的哈希表被创建并进行 HSET 操作;如果域(field)已经存在于哈希表中,旧值将被覆盖。

先来看 Redis 对 Pojo 的支持,新建一个 Student 对象(需要实现 Serializable 接口),放到缓存中,再取出来。


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {

   @Resource
   private RedisTemplate<String, Object> redisTemplate;

   /**
    * 测试hash
    */
   @Test
   public void testHash() {
       String key = "tom";
       HashOperations<String, Object, Object> operations = redisTemplate.opsForHash();
       operations.put(key, "name", "tom");
       operations.put(key, "age", "20");

       String value= (String) operations.get(key,"name");
       System.out.println(value);
  }
}

输出结果:


tom

根据上面测试用例发现,Hash set 的时候需要传入三个参数,第一个为 key,第二个为 field,第三个为存储的值。一般情况下 Key 代表一组数据,field 为 key 相关的属性,而 value 就是属性对应的值。

7、SpringBoot操作List集合类型

Redis List 的应用场景非常多,也是 Redis 最重要的数据结构之一。 使用 List 可以轻松的实现一个队列, List 典型的应用场景就是消息队列,可以利用 List 的 Push 操作,将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {
   @Resource
   private RedisTemplate<String, Object> redisTemplate;

   /**
    * 测试List
    */
   @Test
   public void testList() {
       final String key = "lst";
       ListOperations<String,Object> list = redisTemplate.opsForList();
       list.leftPush(key, "hello");
       list.leftPush(key, "world");
       list.leftPush(key, "goodbye");
       String value = (String) list.leftPop(key);

       System.out.println(value.toString());
  }
}

输出结果


goodbye

上面的例子我们从左侧插入一个 key 为 "list" 的队列,然后取出左侧最近的一条数据。其实 List 有很多 API 可以操作,比如从右侧进行插入队列,从右侧进行读取,或者通过方法 range 读取队列的一部分。接着上面的例子我们使用 range 来读取。


/**
    * 测试List
    */
   @Test
   public void testList() {
       final String key = "lst";
       ListOperations<String,Object> list = redisTemplate.opsForList();
       list.leftPush(key, "hello");
       list.leftPush(key, "world");
       list.leftPush(key, "goodbye");

       List<Object> values = list.range(key, 0, 2);
       for (Object v : values) {
           System.out.println("list range :" + v);
      }
  }

输出结果:


list range :goodbye
list range :world
list range :hello

range 后面的两个参数就是插入数据的位置,输入不同的参数就可以取出队列中对应的数据。

Redis List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

8、SpringBoot操作Set集合类型

Redis Set 对外提供的功能与 List 类似,是一个列表的功能,特殊之处在于 Set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个成员是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {
   @Autowired
   private RedisTemplate<String, Object> redisTemplate;

   /**
    * 测试Set
    */
   @Test
   public void testSet() {
       final String key = "lset";
       SetOperations<String,Object> set = redisTemplate.opsForSet();
       set.add(key, "hello");
       set.add(key, "world");
       set.add(key, "world");
       set.add(key, "goodbye");
       Set<Object> values = set.members(key);
       for (Object v : values) {
           System.out.println("set value :" + v);
      }
  }
}

输出结果:


set value :hello
set value :world
set value :goodbye

通过上面的例子我们发现,输入了两个相同的值 world,全部读取的时候只剩下了一条,说明 Set 对队列进行了自动的排重操作。另外,Redis 为集合提供了求交集、并集、差集等操作,可以非常方便的使用,这里就不一一举例了。

9、SpringBoot操作ZSet集合类型

Redis Sorted Set 的使用场景与 Set 类似,区别是 Set 不是自动有序的,而 Sorted Set 可以通过用户额外提供一个优先级(Score)的参数来为成员排序,并且是插入有序,即自动排序。

在使用 Zset 的时候需要额外的输入一个参数 Score,Zset 会自动根据 Score 的值对集合进行排序,我们可以利用这个特性来做具有权重的队列,比如普通消息的 Score 为 1,重要消息的 Score 为 2,然后工作线程可以选择按 Score 的倒序来获取工作任务。


/**
* redis测试类
*/
@SpringBootTest
public class RedisTest {
   @Resource
   private RedisTemplate<String, Object> redisTemplate;

/**
    * 测试ZSet
    */
   @Test
   public void testZset() {
       final String key = "lz";
       ZSetOperations<String,Object> zset = redisTemplate.opsForZSet();
       zset.add(key, "hello", 1);
       zset.add(key, "world", 6);
       zset.add(key, "good", 4);
       zset.add(key, "bye", 3);

       Set<Object> zsets = zset.range(key, 0, 3);
       for (Object v : zsets) {
           System.out.println("zset-A value :"+v);
      }
       
       System.out.println("=======");
       Set<Object> zsetB = zset.rangeByScore(key, 0, 3);
       for (Object v : zsetB) {
           System.out.println("zset-B value :"+v);
      }
  }
}

输出结果:


zset-A value : hello
zset-A value : bye
zset-A value : good
zset-A value : world
zset-B value : hello
zset-B value : bye

 

六、Springboot整合redis实战

1、RedisTemplate方式

修改service


@Service
@Transactional
public class UserService implements IUserService {

   @Autowired
   private UsersMapper usersMapper;

   @Resource
   private RedisTemplate<String,String> redisTemplate;
   //@Autowired
   //private StringRedisTemplate redisTemplate;

   /**
    * 查询所有
    * @return
    */
   @Override
   public List<Users> findUsers() throws JsonProcessingException {
       //userlist为存储在redis中的键
       BoundValueOperations<String,String> stringTemplate=redisTemplate.boundValueOps("userlist");
       //从redis中取出数据
       Object str = stringTemplate.get();
       List<Users> users =null;

       //jackson对象
       ObjectMapper mapper=new ObjectMapper();
       if(str==null){
           //redis中没有数据,执行查询操作
           users=usersMapper.selectByExample(new UsersExample());
           //将集合序列化为json字符串
           str=mapper.writeValueAsString(users);
           //将查询到的结果保存到redis中
           redisTemplate.boundValueOps("userlist").set(str.toString());

           System.out.println("从数据库查询了数据:");
           System.out.println(users.toString());
      }else{
           //redis中有数据则直接取出
           System.out.println("从redis中取出了数据:");
            users=mapper.readValue(str.toString(),new TypeReference<List<Users>>(){});
           System.out.println(users.toString());
      }
       return users;
  }
}

<font color=red>注意:当执行增删改操作,为了保证和数据库数据一致性, redis的缓存要删除</font>

2、Redis Repositories方式

Spring Data Redis 从 1.7 开始提供 Redis Repositories ,可以无缝的转换并存储 domain objects,使用的数据类型为哈希(hash)。

Spring Data Redis 的 Repository 的基本实现为:CrudRepository

基础用法(Usage)分为以下三步

a、启用 Repository 功能

编写一个配置类(或直接利用 Spring Boot 的入口类),在其上标注 @EnableRedisRepositories(basePackages = "..."),表示启用 Repository 功能。

属性 basePackages 如果不赋值,那么默认是扫描入口类平级及之下的所有类,看它们谁的头上有 @Repository 注解。如果是同时使用 spring-data-jpa 和 spring-data-redis 时,由于它们的 Repository 的祖先中都有 CrudRepository 因此会造成冲突。虽有,最好还是加上 basePackages 属性并为它赋值,指定各自扫描的路径,以避免冲突


@SpringBootApplication
@MapperScan(basePackages = {"cn.woniu.dao"})
@EnableTransactionManagement(proxyTargetClass = true)
@EnableRedisRepositories(basePackages = {"cn.woniu.redis"}) //开启redis的Repository功能
public class ApplicationApp {
    public static void main(String[] args) {
        SpringApplication.run(ApplicationApp.class,args);
  }
}

b、注解需要缓存的实体

添加关键的两个注解 @RedisHash@Id ;


@RedisHash("user")
public class User implements Serializable {
   private static final long serialVersionUID = 1L;

   @Id
   private Long id;
   private String userName;
   private String password;
   private String email;
}
注解说明
@RedisHash 表示将 User 类的对象都对于 Redis 中的名为 user 的 Set 中。
@Id 标注于对象的唯一性标识上。

如果将多个 User 对象通过 Repository 存储于 Redis 中,那么,它们每个的 key 分别是:*user:<Id> 。例如:user:1user:2user:3、...

获取它们每个对象的属性的命令为:


hget user:1 userName

c、创建一个 Repository 接口

自定的 Repository 接口必须继承 CrudRepository,才能“天生”具有存取数据的能力


/**
* 操作用户相关redis数居
*/
@Repository
public interface RedisUsersRepository extends CrudRepository<Users, Long> {

}

d、修改service


@Service
@Transactional
public class UsersService implements IUsersService {

  // @Resource //默认按名字安装,去容器池找stringRedisTemplate这个对象名
   //private StringRedisTemplate stringRedisTemplate;
   //@Resource
   //private StringRedisTemplate redisTemplate;//报错
   //@Autowired
  // private StringRedisTemplate redisTemplate;//可以

   //获得service
   @Autowired
   private UsersMapper usersMapper;

   //获得users对应的redis操作类
   @Autowired
   private RedisUsersRepository redisUsersRepository;

   @Override
   public List<Users> findUsers() {
       List<Users> usersList=null;
       
       //查询redis中是否有数据
       Iterable<Users> usersIterable=redisUsersRepository.findAll();
       //判断redis中是否有数据,没有数据则从数据库查询
       if(usersIterable.iterator().hasNext()==false){
          //"没有数据从数据库查询
           usersList=usersMapper.selectByExample(new UsersExample());
           //将查询到的结果保存到redis中
           redisUsersRepository.saveAll(usersList);
      }else{
          //redis中有数据,从redis中取出
           usersList= (List<Users>) usersIterable;
      }
       return usersList;
  }
}

<font color=red>注意:当执行增删改操作,为了保证和数据库数据一致性, redis的缓存要删除</font>

 

 

posted on 2021-11-03 18:02  迎着风跑  阅读(143)  评论(0编辑  收藏  举报