1、redis 是什么?可以用来做什么?
Redis 是一款使用 C 语言开发的内存型数据库,因此读写速度很快,Redis 常被用作缓存,除了缓存以外还可以作为分布式锁、消息队列等
2、为什么要使用 Redis 作为缓存?
因为 Redis 能更好的做到高性能和高并发
首先是高性能,对于需要从数据库中查询的数据由于其本质是从磁盘里读取数据到内存里,因此和直接从内存里读取数据相比是很慢的。如果访问的数据属于高频数据且不经常改变的话,就可以将其放在 Redis 缓存中,这样就可以大幅加快访问速度提高网站性能,最常见的地方是网站首页、
其次是高并发,Redis 缓存能够支撑的 QPS 很高,单机情况下很容易达到 10w 以上的 QPS(官方),而 MySQL 这一类的数据库的 QPS 在单机情况下大概只有 1w+ 的 QPS ( 4 核 8g 下)
3、redis为什么快?
一、redis完全基于内存设计,相比于其他数据库,他节省了磁盘io的开销;
二、redis使用单线程,即一个线程处理网络请求。避免了上下文切换耗时、多线程存在死锁等问题。在4.0版本引入多线程,但也只是主要针对大键值对的删除和空闲连接的释放,这些工作使用了主线程以外的其他线程进行异步处理,在6.0版本引入了多线程,主要目的是为了提高网络 IO 的读写性能,网络 IO 占用了 Redis 执行期间的大部分 CPU 时间,使用多线程任务可以分摊网络 IO 所需要的总时间,加快速度。而对于命令的执行依然是单线程的;
三、使用了IO多路复用机制的线程模型(select、epoll、poll、Reactor) Redis 基于 Reactor 模式来设计开发了一套高效的事件处理模型;
4、Redis 中给缓存数据设置过期时间有什么用?
- 对于一些特殊的数据,例如验证码、OAuth2 中防止 csrf 的 state、token 等数据,都存在一定的过期时间,如果使用传统数据库,那么还需要手动判断过期,很是麻烦
- 缓解内存消耗
- 保持最终一致性
5、如何保证缓存和数据库数据的一致性
通常缓存和数据库数据的一致性应该保证的是最终一致性,而不是强一致性。
常规思考:
一、先update OLTP数据库,再将redis的缓存给清理掉,用户访问redis没有key访问数据库时加载到redis。
问题,更新数据库时,删除redis出现错误,此时得到的数据仍然是老数据
二、先进性缓存清理,在执行update操作
问题:当有删除redis key 时,刚好有用户进来查询key,此时mysql事务还未提交,导致数据仍然是旧数据
因此我们可以采用延迟双删的模式;一般会采用两种方案
一、
- 先删除缓存
- 再写数据库
- 过一段时间后再次删除缓存
时间长短可由业务确定,通常可以采用统计业务逻辑执行读数据和写缓存的操作时间;
二、
- 先删除缓存
- 再写数据库
- 触发异步写入Kafka队列
- kafka订阅者再次删除缓存
db 更新分为两个阶段,更新前及更新后,更新前的删除很容易理解,在 db 更新的过程中由于读取的操作存在并发可能,会出现缓存重新写入数据,这时就需要更新后的删除
6、Redis 有哪些数据类型
基本数据类型:String、List、Set、Hash 和 Zset
高级数据类型:Geospatial、HyperLogLog 和 Bitmap
往期有大佬出过专刊
7、内存淘汰策略
Redis 可以设置内存最大使用量,当使用量超出时,会实施淘汰策略
Redis 的淘汰策略有
策略 | 描述 |
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止淘汰数据 |
Redis 的淘汰算法并非针对所有 key,而是通过抽样一小部分并从中选出被淘汰的 key
8、持久化方案
持久化主要用于于数据备份和故障恢复,其中memcached也是基于内存的高速缓存系统,但它就不具有持久化功能。。如果没有持久化的话,redis遇到灾难性故障的时候,就会丢失所有的数据。如果通过持久化将数据持久化一份儿在磁盘上去,然后定期比如说同步和备份到一些云存储服务上去,那么就可以保证数据不丢失全部,还是可以恢复一部分数据回来的。除此,持久化还可以用于主从同步等;
一、RDB
rdb指在指定的某个时间段内操作n次,进行快照存储,类似于mysql的dump备份文件。可在redis.config中进行配置参数。
save 900 1
save 300 10
save 60 10000
rdb 可以手动执行save命令,此时将会有主线程进行持久化过程,此时会阻塞redis服务器。
besave流程:
fork线程时,会用到写时拷贝(copy-on-write)
frok线程会和主线程
fork系统调用会产生一个子进程,它与父进程可以共享内存地址,当产fork调动那一时刻,父子进程就有着完全相同的内容。
如果此时主线程client端有写入操作,将会copy一份A数据,不影响fork线程,从而fork线程不会产生全量的内存数据拷贝消耗;
RDB的优缺点:
优点:
1、besave方式对主线程影响小
2、定期存储,适合对数据备份场景
3、存储的rdb文件为二进制文件,恢复速度快
缺点:
1、besave方式,copy-on-write时,如果有新大量数据涌入会导致内存消耗迅速增大
2、由于是定期存储有可能出现数据丢失
二、AOF
由于 RDB 的实时性问题,AOF 成了目前 Redis 持久化的主流方式
AOF 以日志的方式保存每一条写命令和删除命令,数据实时性更高,但是由于每次都会保存写命令,只对原来的 aof 文件进行追加而不修改,因此文件可能会很大
Redis 为了优化 aof 文件的大小提出了一种重写机制
配置
# AOF开启开关 appendonly yes # AOF文件名 appendfilename "appendonly.aof" # AOF策略 appendfsync always # 每个命令都记录 # appendfsync everysec # 每秒记录一次 # appendfsync no # 看操作系统自身调度记录 # 是否阻止对主进程的fsync函数调用进行拦截。如果为no,则不拦截,主进程和子进程均可以操作磁盘的写入。但是可能会出现aof同步阻塞的问题。如果为yes,则不会阻塞,但是这也意味着,可能会损失最多30秒的日志(默认的linux设置情况下) no-appendfsync-on-rewrite no # AOF文件大小比起上次重写时的大小,增长100%触发重写。比如上次重写后大小为30mb,那么aof文件会达到60mb的时候触发重写 auto-aof-rewrite-percentage 100 # 如果文件超过64mb的时候,也会触发重写 auto-aof-rewrite-min-size 64mb # 开启rdb和aof的混合模式 aof-use-rdb-preamble yes
AOF写入策略:
1、Aways 每次写入都会写入disk一份,此方法会比较消耗io性能,比较可靠
2、EverySecond 每秒写入disk一份,性能适中,会丢失部分数据
3、No 由操作系统写disk频率决定,性能最快,容易丢失数据
AOF
AOF日志文件的重写:
随着写入命令的增多AOF文件也会越来越大,我们可以进行配置文件超过多大存储空间,且比上次重写后的体量增加了100%时自动触发重写时,进行AOF重写
此时fork线程和主线程也会用到会用到写时拷贝;
AOF优缺点:
优点:
1、数据丢失可能性较低;
2、有重写机制,可以优化文件大小
缺点:
1、恢复速度比RDB慢,因为RDB是二进制文件
三、混合持久化
混合持久化将RDB和AOF进行了综合,前半大部分数据采用RDB方式,后小半部分采用AOF方式持久化
9、集群模式
单点部署,无论怎么优化,都会可能有服务器宕机的可能,如果此时单节点服务器宕掉以后,此时redis就无法正常服务,那么如何避免这个问题。通常会常用集群部署,在这个时候就会有CAP之间的取舍。
CAP原则:
一致性(Consistency)
所有节点在同一时刻的数据都是一致的。
可用性(Availability)
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容错(Partition-tolerance)
保证系统中任意信息的丢失都不会影响系统的运行。
在redis集群部署中,采用CP,其中可用性:由于redis集群部署,可采用哨兵模式检测master是出现异常情况,当master出现异常,会采用主观下线和客观下线,如果发生客观下线情况,会采用主从切换,选举出新的master,在选举过程中,处理写请求时,并不会响应客户端请求;
redis集群模式
一、主从同步
主从同步即:集群中,拥有一个master多个slave,实现读写分离,其中处理写请求由master节点进行响应请求,写入完成以后,master分发到每个slave里面,进行数据同步,其中分为全量同步和增量同步;
其中主从同步的优点是:负载均衡,读写分离:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
二、全量同步
slave节点初始化时,会进行全量同步操作,全量同步的第一步会slave节点发送sync指令,master节点开始besave操作,存储出rdb文件,将rdb文件传输到slave节点。期间操作的redis数据将采用replication buffer方式进行同步;
其中runid可以认为是redis这次同步任务的线程id,offset是同步数据的偏移量,通常第一次全量同步数据时,默认都是-1;
三、增量同步
从全量同步中,我们可以得知,master和slave都记录了offet偏移量,那么聪offet我们就可以知道此时的slave节点和master节点数据是否一致。replication buffer 其中在发送给slave从节点外也会自己保存一份到复制积压缓冲区,相当于备份写数据;
复制积压缓冲区采用FIFO,由master维护,默认为1MB,主要的作用是备份master数据;由于缓冲区数据量有限制,当offset超过缓冲区大小时,采用全量同步,即:
- offset偏移量之后的数据仍都在复制积压缓冲区里,则执行部分复制;
- offset偏移量之后的数据已不在复制积压缓冲区中,则执行全量复制。
四、哨兵模式
上面描述了集群模式,通常是一个master节点多个slave节点,当master宕掉以后,需要人工去指定master。那么有没有一种检测和选举机制呢,如同ZooKeeper一样,通过心跳机制和选举确认master节点。那就是接下来提到的哨兵模式了;
哨兵节点会提供检测机制,当哨兵 ping master 无反应会出现主观下线;如果是单哨兵时,则会认为是客观下线;此时就会选举出新的master节点;但通常哨兵节点也会出现宕机现象,所以我们通常部署也会采用多哨兵的情况,这样的话,即使单哨兵出现下线,并不会影响检测过程,多哨兵节点,此时的客观下线就有所不同,如果某一个哨兵ping master 无反应,此时出现主观下线,此时如果多哨兵节点都出现主观下线,并超过设置的阈值,则会出现客观下线,出现客观下线后,就会进行选举算法。
五、Redis Cluster
虽然哨兵节点的出现解决了自动master选举的过程,实现了高可用、读写分离,但此时的哨兵模式仍然不是最优的解,例如在哨兵模式中,每个slave存的数据都是master全量的,导致每个slave节点浪费内存空间,例如HBASE在存储时,多节点保存其他节点的部分数据。当使用哨兵模式时,会存在当master客观下线后,选举过程中,写请求无法响应。哨兵节点模式,主要使用场景是读多写少的情况,如果此时,写请求体量过大,此时还是没有解决写请求的瓶颈,于是引入了Redis Cluster;
Redis Cluster:采用了服务器分片技术,采用了多主多从,每个分区都是由一个redis master和多个slave组成;此时如果其中的一个master宕机后会选举slave作为master;其中某个节点出现宕机现象,其他节点会gossip的通信方式,其他节点ping 宕机节点,当宕机节点未反应Pong,认为节点主观下线,当宕机节点pong 节点失效数量超过阈值,则认为客观下线,此时开始选举;
10、redis分布式锁
分布式服务都会面临一个问题,如何确保同一个时间只有一个服务可以访问(操作)某个共享资源,也就是我们需要协调多个服务对资源的互斥访问。为了实现互斥效果最通用的技术就是互斥锁;互斥锁的主要问题就是,谁拿到了锁,我们需要将锁的竞争结果告知给所有服务,为此我们可以将锁信息存储到所有服务都可以访问到的地方;如:redis、数据库、ZooKeeper;
通常使用redis 命令
set lock_name lock_value NX PX 30000 其中NX只有不存在时才能设置成功
那么其中的过程就是,设置锁的key,然后执行业务代码,释放锁给其他服务注册
其中里面比较需要注意的锁lock_value应该被set服务唯一知晓,其他服务未知;
为啥需要这样使用呢,我们不防想一下这种情况:
1、A注册了一个锁,key = lock , value = 1,并设置了过期时间30秒
2、此时A的锁过期时间到了,但是A服务还没执行完成
3、B注册了一个锁,key = lock,value = 1,也设置了过期时间30秒
4、A服务执行完成,释放锁,由于value相同,直接释放了B的锁
5、B执行完成,释放锁时,家被偷了
所以为了避免以上情况,我们value可以设置成随机数,如果还担心出问题,就采用雪花算法或者uuid等方式生成唯一数,在释放锁时,校验value是否一致,一致才删除;
那么我们再思考一个问题,出入过出现单点故障问题,如果此时master宕机后怎么处理,那么通常情况我们会思考故障转移使用slave节点,有点小瑕疵就是,在进行主从同步时,也就是master节点锁信息还没同步到slave节点就发生了故障,重新选举出来的master没有锁信息,那么又会出现多服务同时获得一把锁的情况。那么这情况可以采用,Redis Cluster,那么在这种情况下,我们实现redis锁的情况得调整下;redis官方推荐了一种算法叫redLock,具体实现:
假设我们有N个master节点,节点相互独立;
服务A向N个master循环发送锁请求信息,并设置当前时间T1,其中发送的命令仍然使我们的单实例信息
set lock_name lock_value NX PX 30000 其中NX只有不存在时才能设置成功
此时我们计算成功获取master获取锁的数量,并计算加锁请求时间(sum(当前时间-T1))如果<我们设置的过期时间,我们就认为成功获取了锁,
如果期间没有达到其中要求,我们向所有master去请求释放锁,并随机延迟一个时间,然后再去请求锁
当然redLock算法也会有很多问题,比如:
1、N次加锁带来的性能问题,我们可以采用多路复用技术优化;
2、所有master节点集体宕机,我们可以采用aof方式持久化redis信息,具体就看相关业务场景来取舍;
好了,说了那么多废话,其实在项目中不一定会采取相关的方案,有些时候项目可以使用简单的方案解决对应问题,可以不用设计那么复杂,更主要是了解redis的相关思路,遇到相关bug可以排查,或者写类似方案代码时可以参考这种设计模式。当然本文中会有很多错误的观点和方案,大家可以批评指出优化。