目录
什么是redis的缓存穿透;什么是缓存雪崩;怎么解决这些问题;缓存穿透不通过ip过滤,最简单的方式怎么解决;
redis底层的实现原理有去研究过吗;为什么redis的性能能达到这么快呢;
redis里面有个string,一个字符串类型的值能存储最大容量是多少;你知道他的底层是怎么实现的;
redis的keys为什么影响性能,redis时间复杂度是O(n)的命令;
redis有做集群吗;怎么做集群的;你在项目中还遇到什么问题吗;
redis持久化问题;会有数据损失吗,开启aof的持久化吗;
你对sentinel hystrix有用过吗;你们微服务有用sentinel 吗
springboot整合redis,注解方式使用 Redis 缓存
在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?
项目里要用缓存,主要是俩用途,高性能和高并发
- 高性能
一个请求过来,半天查出来一个结果,耗时600ms。但是这个结果可能接下来几个小时都不会变了,加缓存,下次再有人查,别走mysql折腾600ms了。直接从缓存里,通过一个key查出来一个value,2ms搞定。性能提升300倍。
- 高并发
mysql这么重的数据库,单机支撑到2000qps也开始容易报警了。
所以要是你有个系统,高峰期一秒钟过来的请求有1万,那一个mysql单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放mysql。缓存功能简单,说白了就是key-value式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发so easy。单机承载并发量是mysql单机的几十倍。
不良的后果
1)缓存与数据库双写不一致
2)缓存雪崩
3)缓存穿透
4)缓存并发竞争
什么是redis的缓存穿透;什么是缓存雪崩;怎么解决这些问题;缓存穿透不通过ip过滤,最简单的方式怎么解决;
- 缓存处理流程
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。
- 缓存穿透
描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 缓存击穿
描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
设置热点数据永远不过期。
加互斥锁,互斥锁参考代码如下:
说明:
1)缓存中有数据,直接走上述代码13行后就返回结果了
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。
- 缓存雪崩
描述:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。
redis 和 memcached 的区别:
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!
redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
Redis 的线程模型是什么?为什么单线程的
一、概述
【1】Redis 是基于 Reactor 模式开发的网络事件处理器:这个处理器被称为文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型:
■ 文件事件处理器使用 I/O 多路复用(multiplexing)机制监听多个套接字 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
■ 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时。与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
【2】虽然文件事件处理器以单线程的方式运行,但其使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
二、文件事件处理器的结构
【1】文件事件处理器的结构包含 4 个部分:
● 多个 socket
● IO 多路复用程序
● 文件事件分派器
● 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
【2】多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字, 如图:
文件事件分派器接收 I/O 多路复用程序传来的套接字, 并根据套接字产生的事件的类型, 调用相应的事件处理器。服务器会为执行不同任务的套接字关联不同的事件处理器, 这些处理器是一个个函数, 它们定义了某个事件发生时, 服务器应该执行的动作。
【3】I/O 多路复用程序的实现:Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select、epoll、evport 和 kqueue 这些 I/O 多路复用函数库来实现的, 每个 I/O 多路复用函数库在 Redis 源码中都对应一个单独的文件, 比如 ae_select.c、ae_epoll.c、ae_kqueue.c , 诸如此类。因为 Redis 为每个 I/O 多路复用函数库都实现了相同的 API , 所以 I/O 多路复用程序的底层实现是可以互换的, 如下图所示:
Redis 在 I/O 多路复用程序的实现源码中用 #include 宏定义了相应的规则, 程序会在编译时自动选择系统中性能最高的 I/O 多路复用函数库来作为 Redis 的 I/O 多路复用程序的底层实现:
/* 包括此系统支持的最佳复用层。
* 以下应按性能降序排列。 */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
redis是什么语言开发的;
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写。
redis底层的实现原理有去研究过吗;为什么redis的性能能达到这么快呢;
官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
多路 I/O 复用模型:
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
原因是因为他底层的话是用的是这种io多路复用的这种机制像我们linux底下的这种epoll 就是根据事件来触发的 当你有这种事件的时候 就会通知我的 那我就会进行一个事件的触发 而且还防止了我们多线程之间的一个上下文切换或者锁的一些竞争的一些情况 从而他的性能会比较高一点 但是我们对其禁止使用一些严重影响他性能的一些命令 例如说是keys啊 这种 我们会给他重命名掉 不让他使用
Redis五种数据结构及操作
对redis来说,所有的key(键)都是字符串。
1.String 字符串类型
是redis中最基本的数据类型,一个key对应一个value。
常用命令:
基础命令
- set :设置存储在给定键中的值
- get:获取存储在给定键中的值
- del:删除存储在给定键中的值
字符串
- INCR:返回增加后键的值
- DECR:返回删除后键的值
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> del hello
(integer) 1
127.0.0.1:6379> get hello
(nil)
127.0.0.1:6379> get counter
"2"
127.0.0.1:6379> incr counter
(integer) 3
127.0.0.1:6379> get counter
"3"
127.0.0.1:6379> incrby counter 100
(integer) 103
127.0.0.1:6379> get counter
"103"
127.0.0.1:6379> decr counter
(integer) 102
127.0.0.1:6379> get counter
"102"
实战场景:
1.缓存: 做简单的 KV 缓存。经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
2.计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
3.session:常见方案spring session + redis实现session共享,
2.Hash (哈希)
是一个Mapmap,指值本身又是一种键值对结构,如 value={{field1,value1},......fieldN,valueN}}
使用:所有hash的命令都是 h 开头的
- hget:获取存储在哈希表中指定字段的值
- hset:获取存储在哈希表中指定字段的值
- hdel:删除一个或多个哈希表字段
- hgetall:获取在哈希表中指定 key 的所有字段和值
127.0.0.1:6379> hset user name1 hao
(integer) 1
127.0.0.1:6379> hset user email1 hao@163.com
(integer) 1
127.0.0.1:6379> hgetall user
1) "name1"
2) "hao"
3) "email1"
4) "hao@163.com"
127.0.0.1:6379> hget user user
(nil)
127.0.0.1:6379> hget user name1
"hao"
127.0.0.1:6379> hset user name2 xiaohao
(integer) 1
127.0.0.1:6379> hset user email2 xiaohao@163.com
(integer) 1
127.0.0.1:6379> hgetall user
1) "name1"
2) "hao"
3) "email1"
4) "hao@163.com"
5) "name2"
6) "xiaohao"
7) "email2"
8) "xiaohao@163.com"
实战场景:
这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 redis 里,然后每次读写缓存的时候,可以就操作 hash 里的某个字段。
1.缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
3.链表
List 说白了就是链表(redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。
使用列表的技巧
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
操作命令
Lpush——先进后出,在列表头部插入元素
Rpush——先进先出,在列表的尾部插入元素
Lrange——出栈,根据索引,获取列表元素
Lpop——左边出栈,获取列表的第一个元素
Rpop——右边出栈,获取列表的最后一个元素
Lindex——根据索引,取出元素
Llen——链表长度,元素个数
Lrem——根据key,删除n个value
Ltrim——根据索引,删除指定元素
Rpoplpush——出栈,入栈
Lset——根据index,设置value
Linsert before——根据value,在之前插入值
Linsert after——根据value,在之后插入值
注意
出栈,该元素在链表中,就不存在了
左边,默认为列表的头部,索引小的一方
右边,默认为列表的尾部,索引大的一方
使用:
127.0.0.1:6379> lpush mylist 1 2 ll ls mem
(integer) 5
127.0.0.1:6379> lrange mylist 0 -1
1) "mem"
2) "ls"
3) "ll"
4) "2"
5) "1"
127.0.0.1:6379>
实战场景:
比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
1.timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。
4.Set 集合
集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。
使用:命令都是以s开头的 sset 、srem、scard、smembers、sismember
127.0.0.1:6379> sadd myset hao hao1 xiaohao hao
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "xiaohao"
2) "hao1"
3) "hao"
127.0.0.1:6379> SISMEMBER myset hao
(integer) 1
实战场景;
1.标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。把两个大 V 的粉丝都放在两个 set 中,对两个 set 做交集。
2.点赞,或点踩,收藏等,可以放到set中实现
5.zset 有序集合
有序集合和集合有着必然的联系,保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,它给每个元素设置一个分数,作为排序的依据。
(有序集合中的元素不可以重复,但是score 分数 可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同)。
使用: 有序集合的命令都是 以 z 开头 zadd 、 zrange、 zscore
127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao
(integer) 2
127.0.0.1:6379> ZRANGE myscoreset 0 -1
1) "xiaohao"
2) "hao"
127.0.0.1:6379> ZSCORE myscoreset hao
"100"
实战场景:
1.排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
redis的过期策略都有哪些?手写一下LRU代码实现?
定期删除+惰性删除。
定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!
如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? redis 内存淘汰机制。
redis 提供 6种数据淘汰策略:
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
手写一下LRU代码实现?
现场手写最原始的LRU算法,代码量太大了,不太现实。但可以写一个利用已有的jdk数据结构实现一个LRU。
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
// 这里就是传递进来最多能缓存多少数据
public LRUCache(int cacheSize) {
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); // 这块就是设置一个hashmap的初始大小,同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > CACHE_SIZE; // 这个意思就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
}
}
redis里面有个string,一个字符串类型的值能存储最大容量是多少;你知道他的底层是怎么实现的;
最大容量是512M。
对于字符串类型,其做出了改进,是一种基于动态字符串sds实现,redis作为数据库,查询必然多,修改也会有一定多,sds解决了C语言字符串动态扩展的不方便,以及查询长度操作从O(n)变为了O(1)。
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS 遵循 C 字符串以空字符结尾的惯例, 保存空字符的 1 字节空间不计算在 SDS 的 len 属性里面, 并且为空字符分配额外的 1 字节空间, 以及添加空字符到字符串末尾等操作都是由 SDS 函数自动完成的, 所以这个空字符对于 SDS 的使用者来说是完全透明的。
遵循空字符结尾这一惯例的好处是, SDS 可以直接重用一部分 C 字符串函数库里面的函数。
其free属性是代表buf数组没有被利用的空间数,便于sds的空间分配策略。
这样的设计也打破了C语言字符串会自动认为’\0’为分隔符号,但是sds不会,所以可以保存的字符串中间存在空字符
通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略
redis的keys为什么影响性能,redis时间复杂度是O(n)的命令;
keys时间复杂度是O(n),flushdb、flushall这类命令我们可以配置redis.conf
禁用这些命令
使用scan替代keys命令
语法:
scan cursor [MATCH pattern] [COUNT count]
案例:
scan 0 match report:* count 10
1) "3932160"
2) 1) "report:12360412"
2) "report:12749274"
scan 第一个参数是游标,表示从游标开始
返回的第一行是游标,第二行是匹配到的数据,
如果第一行返回0,表示没有更多数据,否则下次使用scan时,就要用第一行返回的值作为scan的游标
一般用redis都做什么;
- 性能:
我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存,这样,后面的请求就去缓存中读取,请求使得能够迅速响应。
- 并发:
在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用的的Redis的做一个缓冲操作,让请求先访问到的Redis的的,而不是直接访问数据库。
- 分布式锁等其他功能
Redis的的的还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件代替,
Redis 哈希槽的概念
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384个哈希槽,每个 key通过 CRC16校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash槽。
redis有做集群吗;怎么做集群的;你在项目中还遇到什么问题吗;
参考:https://blog.csdn.net/m0_37235955/article/details/105349705
如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
参考:
https://www.jianshu.com/p/8bddd381de06
cps是多少;一秒钟处理多少;
分布式锁的实现方式;有哪些常见的数据分布式算法,比如说现在部署了3个redis,里面的内存是不一样的,怎么保证有一个内存回落到固定的一个redis实例上面的分布式算法;一致性hash算法相较于普通hash算法有什么优势;
你对sentinel hystrix有用过吗;你们微服务有用sentinel 吗
因为他需要监控redis里面 因为redis属于非幂等性的集群 那我搭建完集群之后我还需要搭建主从复制 如果说我们不用sentinel 的话肯定会对我们数据造成一定的影响的
sentinel 是你们自己搭的吗直接从ali看了 就搭了吗?hystrix你们也用过吗
说一下你们的熔断是怎么做的
当我们另一个服务不能用的时候他会调用failback给他返回一个页面
你认为什么时候是不可用的
当高并发或者是服务降级的时候 你这个系统可能不是很重要这个时候就会出发这种failback
sentinel hystrix的区别
你们用redis主要是用的缓存这一块吗说一下你怎么用redis实现分布式锁的
它提供了这种命令像nx ex 也就是说是给他指定的一个变量然后当他为1的时候肯定是一个加锁的状态那么如果说是我操作完之后的话 我是要对他进行一个释放的 然后别的会通过这个命令来判断如果当前存在而且为1的话他肯定是拿不到锁的
springboot整合redis,注解方式使用 Redis 缓存
使用缓存有两个前置步骤
-
在
pom.xml
引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在启动类上加注解
@EnableCaching
@SpringBootApplication @EnableCaching public class SellApplication { public static void main(String[] args) { SpringApplication.run(SellApplication.class, args); } }
常用的注解有以下几个
@Cacheable
查询和添加缓存 eg:@Cacheable(cacheNames = "product", key = "123")
,属性如下图
用于查询和添加缓存,第一次查询的时候返回该方法返回值,并向 Redis 服务器保存数据。
以后调用该方法先从 Redis 中查是否有数据,如果有直接返回 Redis 缓存的数据,而不执行方法里的代码。如果没有则正常执行方法体中的代码。
value 或 cacheNames 属性做键,key 属性则可以看作为 value 的子键, 一个 value 可以有多个 key 组成不同值存在 Redis 服务器。
验证了下,value 和 cacheNames 的作用是一样的,都是标识主键。两个属性不能同时定义,只能定义一个,否则会报错。
condition 和 unless 是条件,后面会讲用法。其他的几个属性不常用,其实我也不知道怎么用…
@CachePut
更新 Redis 中对应键的值。属性和@Cacheable
相同 eg:@CachePut(cacheNames = "prodcut", key = "123")
@CacheEvict
删除 Redis 中对应键的值。eg:@CacheEvict(cacheNames = "prodcut", key = "123")
3.1 添加缓存
在需要加缓存的方法上添加注解 @Cacheable(cacheNames = "product", key = "123")
,
cacheNames
和 key
都必须填,如果不填 key
,默认的 key
是当前的方法名,更新缓存时会因为方法名不同而更新失败。
如在订单列表上加缓存
@RequestMapping(value = "/list", method = RequestMethod.GET)
@Cacheable(cacheNames = "product", key = "123")
public ResultVO list() {
// 1.查询所有上架商品
List<ProductInfo> productInfoList = productInfoService.findUpAll();
// 2.查询类目(一次性查询)
//用 java8 的特性获取到上架商品的所有类型
List<Integer> categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());
List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes);
List<ProductVO> productVOList = new ArrayList<>();
//数据拼装
for (ProductCategory category : productCategoryList) {
ProductVO productVO = new ProductVO();
//属性拷贝
BeanUtils.copyProperties(category, productVO);
//把类型匹配的商品添加进去
List<ProductInfoVO> productInfoVOList = new ArrayList<>();
for (ProductInfo productInfo : productInfoList) {
if (productInfo.getCategoryType().equals(category.getCategoryType())) {
ProductInfoVO productInfoVO = new ProductInfoVO();
BeanUtils.copyProperties(productInfo, productInfoVO);
productInfoVOList.add(productInfoVO);
}
}
productVO.setProductInfoVOList(productInfoVOList);
productVOList.add(productVO);
}
return ResultVOUtils.success(productVOList);
}
可能会报如下错误
对象未序列化。让对象实现 Serializable
方法即可
@Data
public class ProductVO implements Serializable {
private static final long serialVersionUID = 961235512220891746L;
@JsonProperty("name")
private String categoryName;
@JsonProperty("type")
private Integer categoryType;
@JsonProperty("foods")
private List<ProductInfoVO> productInfoVOList ;
}
生成唯一的 id 在 IDEA 里有一个插件:GenerateSerialVersionUID
比较方便。
重启项目访问订单列表,在 rdm 里查看 Redis 缓存,有 product::123
说明缓存成功。
3.2 更新缓存
在需要更新缓存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")
注意
cacheNames
和key
要跟@Cacheable()
里的一致,才会正确更新。@CachePut()
和@Cacheable()
注解的方法返回值要一致
3.3 删除缓存
在需要删除缓存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123")
,执行完这个方法之后会将 Redis 中对应的记录删除。
3.4 其他常用功能
-
cacheNames
也可以统一写在类上面,@CacheConfig(cacheNames = "product")
,具体的方法上就不用写啦。@CacheConfig(cacheNames = "product") public class BuyerOrderController { @PostMapping("/cancel") @CachePut(key = "456") public ResultVO cancel(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ buyerService.cancelOrder(openid, orderId); return ResultVOUtils.success(); } }
-
Key 也可以动态设置为方法的参数
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
如果参数是个对象,也可以设置对象的某个属性为 key。比如其中一个参数是 user 对象,key 可以写成
key="#user.id"
-
缓存还可以设置条件。
设置当 openid 的长度大于3时才缓存
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
还可以指定
unless
即条件不成立时缓存。#result
代表返回值,意思是当返回码不等于 0 时不缓存,也就是等于 0 时才缓存。@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3", unless = "#result.code != 0") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }