简单来说 Redis 就是⼀个使⽤ C 语⾔开发的数据库(非关系型的数据库),不过与传统数据库不同的是 Redis 的数据是存在内存中的,也就是它是内存数据库,所以读写速度⾮常快,因此 Redis 被⼴泛应⽤于缓存⽅向。另外,Redis除了做缓存之外,Redis也经常⽤来做分布式锁,甚⾄是消息队列。Redis提供了多种数据类型来⽀持不同的业务场景。Redis还⽀持事务 、持久化、Lua 脚本、多种集群⽅案。
非关系型数据库:非结构化存储,高并发下读写能力强大,易拓展,
redis的优势和特点:
- 1、redis数据读写速度非常快,性能极高,高并发读写。因为它把数据都读取到内存当中操作,而且redis是用C语言编写的,是最“接近“”操作系统的语言,所以执行速度相对较快。
- 2、redis虽然数据的读取都存在内存当中,但是最终它是支持数据持久化到磁盘当中。
- 3、redis提供了丰富的数据结构来⽀持不同的业务场景。
- 4、redis的所有操作都是原子性,支持事务,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行。
- 5、redis支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
Redis的缺点: 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
分布式缓存常⻅的技术选型⽅案有哪些?分布式缓存主要解决的是单机缓存的容量受服务器限制并且⽆法保存通⽤的信息。因为,本地缓存只在当前服务⾥有效,⽐如如果你部署了两个相同的服务,他们两者之间的缓存数据是⽆法共同的。
分布式缓存系统出现的原因
- 互联网或电商应用系统中,业务需求复杂,必须对整个业务系统进行垂直拆分,以保证各个业务模块清晰并对外提供服务。
- 用户群体广泛就必然存在高并发的问题,如果将引用系统部署在单节点服务器上,势必会对单服务器造成巨大的访问压力,因此需要将系统部署到不同的节点上,同时也要将不同的数据分散到不同的节点上。
- 互联网时代大数据为王,人类正从IT时代走向DT时代,因为数据量大所以要对数据进行分布式处理。
分布式缓存的话,使⽤的⽐较多的主要是 Memcached 和 Redis。不过,随着 Redis 的发展,⼤家慢慢都转⽽使⽤更加强⼤的 Redis 了。
Redis和Memcached的区别共同点 :
-
- 都是基于内存的数据库,⼀般都⽤来当做缓存使⽤。
-
- 都有过期策略。
-
- 两者的性能都⾮常⾼。
区别 :
-
Redis ⽀持更丰富的数据类型(⽀持更复杂的应⽤场景)。Redis 不仅仅⽀持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只⽀持最简单的 k/v 数据类型。
-
Redis ⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进⾏使⽤,⽽ Memecache把数据全部存在内存之中。
-
Redis有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
-
Redis 在服务器内存使⽤完之后,可以将不⽤的数据放到磁盘上。但是,Memcached 在服务器内存使⽤完之后,就会直接报异常。
-
Memcached没有原⽣的集群模式,需要依靠客户端来实现往集群中分⽚写⼊数据;但是Redis⽬前是原⽣⽀持cluster 模式的.
-
Redis **⽀持发布订阅模型、Lua 脚本、事务等功能,⽽ Memcached 不⽀持。并且,****Redis⽀持更多的编程语⾔。
-
Memcached过期数据的删除策略只⽤了惰性删除,⽽ Redis 同时使⽤了惰性删除与定期删除。
-
- 如果⽤户请求的数据在缓存中就直接返回。
-
- 缓存中不存在的话就看数据库中是否存在。
-
- 数据库中存在的话就更新缓存中的数据,然后返回数据。
-
- 数据库中不存在的话就返回空数据。
简单来说使⽤缓存主要是为了提升⽤户体验以及应对更多的⽤户。
⾯我们可以从“⾼性能”和“⾼并发”这两点来看待这个问题:
⾼性能 :
我们设想这样的场景:假如⽤户第⼀次访问数据库中的某些数据的话,这个过程是⽐较慢,毕竟是从硬盘中读取的。但是,如果说,⽤户访问的数据属于⾼频数据并且不会经常改变的话,那么我们就可以很放⼼地将该⽤户访问的数据存在缓存中。那么⽤户下⼀次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。不过,要保持数据库和缓存中的数据的⼀致性。 如果数据库中的对应数据改变的之后,要同步改变缓存中相应的数据
⾼并发:
⼀般像 MySQL 这类的数据库的QPS(服务器每秒可以执⾏的查询次数) ⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存之后很容易达到 10w+,甚⾄最⾼能达到 30w+(就单机 redis 的情况,redis 集群的话会更⾼)。所以,直接操作缓存能够承受的数据库请求数量是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样⽤户的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。进⽽,我们也就提⾼了系统整体的并发。
Redis常见数据结构及其使用场景分析在 redis 中一共有5种数据结构,那每种数据结构的使用场景都是什么呢?
-
String——字符串
-
Hash——字典
-
List——列表
-
Set——集合
-
Sorted Set——有序集合
1.String 字符串类型
是redis中最基本的数据类型,一个key对应一个value。String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
实战场景:(⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等)
- 1.缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
- 2.计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
- 3.session:常见方案spring session + redis实现session共享,
2.Hash字典
内部是:数组+链表;特别适合⽤于存储对象
实战场景:(系统中对象数据的存储)
- 1.缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
3.list链表
List 说白了就是链表(redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。
实战场景:(发布与订阅或者说消息队列、慢查询)
- 1.timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。
4.Set 集合
集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。
实战场景;(需要存放的数据不能重复以及需要获取多个数据源交集和并集)
- 1.标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
- 2.点赞,或点踩,收藏等,可以放到set中实现
5.zset 有序集合
有序集合和集合有着必然的联系,保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,它给每个元素设置一个分数,作为排序的依据。(有序集合中的元素不可以重复,但是score 分数 可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同)。
实战场景:(需要对数据根据某个权重进⾏排序的场景)
- 1.排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
redis 其他功能使用场景:
- 订阅-发布系统
在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
Redis单线程模型?Redis为什么不使用多线程?Redis单线程模型:
虽然说 Redis 是单线程模型,但实际上,Redis在 4.0 之后的版本中就已经加⼊了对多线程的⽀持。不过,Redis 4.0 增加的多线程主要是针对⼀些⼤键值对的删除操作的命令,使⽤这些命令就会使⽤主处理之外的其他线程来“异步处理”。⼤体上来说,Redis 6.0 之前主要还是单线程处理。
**那,**Redis6.0 之前 为什么不使⽤多线程?
我觉得主要原因有下⾯ 3 个:
-
- 单线程编程容易并且更容易维护;
-
- Redis 的性能瓶颈不在 CPU ,主要在内存和⽹络;
-
- 多线程就会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能
Redis6.0之后为何引⼊了多线程?
Redis6.0 引⼊多线程主要是为了提⾼⽹络 IO 读写性能,因为这个算是 Redis 中的⼀个性能瓶颈(Redis 的瓶颈主要受限于内存和⽹络)。
虽然,Redis6.0 引⼊了多线程,但是 Redis 的多线程只是在⽹络数据的读写这类耗时操作上使⽤了, 执⾏命令仍然是单线程顺序执⾏。因此,你也不需要担⼼线程安全问题。Redis6.0 的多线程默认是禁⽤的,只使⽤主线程。
如需开启需要修改 redis 配置⽂件 redis.conf,(io-threads-do-reads yes)开启多线程后,还需要设置线程数,否则是不⽣效的。同样需要修改 redis 配置⽂件 redis.conf,(io-threads 4)
Redis给缓存数据设置过期时间有啥用?如何判断数据是否过期?⼀般情况下,我们设置保存的缓存数据的时候都会设置⼀个过期时间。为什么呢?因为内存是有限的,如果缓存中的所有数据都是⼀直保存的话,分分钟直接Out of memory。过期时间除了有助于缓解内存的消耗,还有实际的场景需要:
很多时候,我们的业务场景就是需要某个数据只在某⼀时间段内存在,⽐如我们的短信验证码可能只在1分钟内有效,**⽤户登录的 token **可能只在 1 天内有效。如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多
Redis 通过⼀个过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是⼀个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。如果过期字典的某个键的值变为为null,就说明该数据过期了
过期的数据删除策略?如果假设你设置了⼀批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进⾏删除的呢?
常⽤的过期数据的删除策略就两个(重要!⾃⼰造缓存轮⼦的时候需要格外考虑的东⻄):
-
惰性删除 :只会在取出key的时候才对数据进⾏过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
-
定期删除 : 每隔⼀段时间抽取⼀批 key 执⾏删除过期key操作。并且,Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对CPU时间的影响。定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,**所以Redis 采⽤的是 定期删除+惰性删除 **。
但是,仅仅通过给 key 设置过期时间还是有问题的。思考一下,如果定期删除漏掉了很多过期的key,而我们也没有再去访问它,如果不加处理,很可能导致⼤量过期 key 堆积在内存⾥,然后就Out of memory导致内存耗尽。怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。
Redis 内存淘汰机制[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRdlurya-1632496924559)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210907202649690.png)]
volatile 为前缀的策略都是从已过期的数据集中进行淘汰。 allkeys 为前缀的策略都是面向所有key进行淘汰。 LRU(least recently used)最近最少用到的。 LFU(Least Frequently Used)最不常用的。 它们的触发条件都是Redis使用的内存达到阈值时。
Redis持久化机制(怎么保证Redis挂掉之后再重启数据可以进⾏恢复?)很多时候我们需要持久化数据,也就是将内存中的数据写⼊到硬盘⾥⾯,⼤部分原因是为了之后重⽤数据(⽐如重启机器、机器故障之后恢复数据),或者是为了防⽌系统故障⽽将数据备份到⼀个远程位置。
Redis支持两种持久化方式:
快照持久化(RDB)默认:
Redis 可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本(Redis 主从结构,主要⽤来提⾼ Redis 性能),还可以将快照留在原地以便重启服务器的时候使⽤。
只追加文件持久化(AOF):
与快照持久化相⽐,AOF 持久化 的实时性更好,因此已成为主流的持久化⽅案。默认情况下Redis 没有开启 AOF⽅式的持久化,可以通过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执⾏⼀条会更改 Redis 中的数据的命令,Redis 就会将该命令写⼊硬盘中的 AOF ⽂件。AOF ⽂件的保存位置和 RDB ⽂件的位置相同,都是通过 dir 参数设置的,默认的⽂件名是 appendonly.aof。 在 Redis 的配置⽂件中存在三种不同的 AOF 持久化⽅式,它们分别是:
appendfsync always #每次有数据修改发⽣时都会写⼊AOF⽂件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步⼀次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进⾏同步
为了兼顾数据和写⼊性能,⽤户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步⼀次AOF ⽂件,Redis 性能⼏乎没受到任何影响。⽽且这样即使出现系统崩溃,⽤户最多只会丢失⼀秒之内产⽣的数据。当硬盘忙于执⾏写⼊操作的时候,Redis 还会优雅的放慢⾃⼰的速度以便适应硬盘的最⼤写⼊速度。
Redis 4.0 开始⽀持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF ⽂件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的,AOF ⾥⾯的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
Redis事务?Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四⼤特性:1. 原⼦性,2. 隔离性,3. 持久性,4. ⼀致性。
Redis 是不⽀持 roll back **的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)**Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
redis 事务的相关命令:
序号命令及描述1DISCARD 取消事务,放弃执行事务块内的所有命令。2EXEC 执行所有事务块内的命令。3MULTI 标记一个事务块的开始。4UNWATCH 取消 WATCH 命令对所有 key 的监视。5[WATCH key key …] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。你可以将Redis中的事务就理解为 :Redis事务提供了⼀种将多个命令请求打包的功能。然后,再按顺序执⾏打包的所有命令,并且不会被中途打断
缓存穿透前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。
什么是缓存穿透?
缓存穿透说简单点就是⼤量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这⼀层。举个例⼦:某个⿊客故意制造我们缓存中不存在的 key 发起⼤量请求,导致⼤量请求落到数据库。(或者程序员在写代码时出现代码逻辑错误)
解决办法:
最基本的就是⾸先做好参数校验,⼀些不合法的参数请求直接抛出异常信息返回给客户端。⽐如查询的数据库 id 不能⼩于 0、传⼊的邮箱格式不对的时候直接返回错误消息给客户端等。
缓存⽆效key:如果缓存和数据库都查不到某个 key 的数据,就写⼀个到 Redis 中去并设置过期时间,具体命令: SET key value EX 10086 。这种⽅式可以解决请求的 key 变化不频繁的情况,如果⿊客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存⼤量⽆效的 key 。很明显,这种⽅案并不能从根本上解决此问题。如果⾮要⽤这种⽅式来解决穿透问题的话,尽量将⽆效的 key 的过期时间设置短⼀点⽐如 1 分钟。另外,这⾥多说⼀嘴,⼀般情况下我们是这样设计 key 的:
表名:列名:主键名:主键值
布隆过滤器:布隆过滤器是⼀个⾮常神奇的数据结构,通过它我们可以⾮常⽅便地判断⼀个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“⼈”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当⽤户请求过来,先判断⽤户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会⾛下⾯的流程。加⼊布隆过滤器之后的缓存处理流程图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oMZHLObR-1632496924599)(C:\Users\LENOVO-LX\AppData\Roaming\Typora\typora-user-images\image-20210907210720335.png)]
当然可能会存在误判的情况,布隆过滤器说某个元素存在,⼩概率会误判。布隆过滤器说某个元素不在,那么这个元素⼀定不在
如何解决 Redis 的并发竞争 Key 问题?
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)基于zookeeper临时有序节点可以实现的分布式锁。
大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁
缓存雪崩什么是缓存雪崩?(大面积的key同时过期)
缓存雪崩描述的就是这样⼀个简单的场景:缓存在同⼀时间⼤⾯积的失效,后⾯的请求都直接落到了数据库上,造成数据库短时间内承受⼤量请求。 这就好⽐雪崩⼀样,摧枯拉朽之势,数据库的压⼒可想⽽知,可能直接就被这么多请求弄宕机了。举个例⼦:系统的缓存模块出了问题⽐如宕机导致不可⽤。造成系统的所有访问,都要⾛数据库。
还有⼀种缓存雪崩的场景是:有⼀些被⼤量访问的数据(热点缓存)在某⼀时刻⼤⾯积失效,导致对应的请求直接落到了数据库上。 这样的情况,有下⾯⼏种解决办法:举个例⼦ :秒杀开始 12 个⼩时之前,我们统⼀存放了⼀批商品到 Redis 中,设置的缓存过期时间也是 12 个⼩时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩⼀样可怕。
什么是热点数据集中失效:我们一般都会给缓存设定一个失效时间,过了失效时间后,该数据库会被缓存直接删除,从而一定程度上保证数据的实时性。
解决:使用互斥锁
我们可以使用缓存自带的锁机制,当第一个数据库查询请求发起后,就将缓存中该数据上锁;此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新值缓存后,释放锁;此时其他被阻塞的查询请求将可以直接从缓存中查到该数据。
**当某一个热点数据失效后,只有第一个数据库查询请求发往数据库,其余所有的查询请求均被阻塞,从而保护了数据库。**但是,由于采用了互斥锁,其他请求将会阻塞等待,此时系统的吞吐量将会下降。这需要结合实际的业务考虑是否允许这么做。
互斥锁可以避免某一个热点数据失效导致数据库崩溃的问题,而在实际业务中,往往会存在一批热点数据同时失效的场景。那么,对于这种场景该如何防止数据库过载呢?
解决办法:
针对 Redis服务不可⽤的情况:
-
- 采⽤Redis 集群,避免单机出现问题整个缓存服务都没办法使⽤。
-
- 限流,避免同时处理⼤量的请求。(通过加锁或者队列来控制读数据库写缓存的线程数量)
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
- 事后:利用 redis 持久化机制保存的数据尽快恢复缓存
针对热点缓存失效的情况:
-
- 设置不同的失效时间⽐如随机设置缓存的失效时间,避免同时失效。
-
- 缓存永不失效。
- 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适⽤。
- 增加cache更新重试机制(常⽤): 如果 cache 服务当前不可⽤导致缓存删除失败的话,我们就隔⼀段时间进⾏重试,重试次数可以⾃⼰定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存⼊队列中,等缓存服务可⽤之后,再将 缓存中对应的 key 删除即可。
(一个key过期)雪崩可以拆分成一个一个缓存击穿的集合。
缓存穿透,与击穿的区别就是: 击穿:数据库里“有”数据; 穿透:数据库里“没”数据。
就是某一个热点数据,缓存中某一时刻失效了,因而大量并发请求打到数据库上,就像被击穿了一样。说白了,就是某个数据,数据库有,但是缓存中没有。那么,缓存击穿,就会使得因为这一个热点数据,将大量并发请求打击到数据库上,从而导致数据库被打垮。