赞
踩
(1)高性能:Redis是基于内存的,读写速度非常快,可以支持10w的QPS。
(2)用途多样:缓存、消息队列、分布式锁等
(3)支持多样的格式:String、Hash、Set、ZSet、List、BitMaps、HyperLogLog、GEO
(4)高级特性:事务、持久化、Lua脚本、原生集群、发布/订阅模式、内存淘汰、过期删除等
相同点:
都是基于内存的数据库
都有过期策略
性能都很高
不同点:
Redis支持更丰富的数据类型,Memcache只支持最简单的key-value数据类型。
Redis有持久化机制,Memcached没有,断电即失;
Redis原生支持集群,Memcached没有原生集群,需要依靠客户端实现集群分片写入数据;
Redis支持发布/订阅模式、Lua脚本、事务等功能,Memcahed不支持上述高级特性;
(1)高性能
用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。
存在的问题:MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题(后面会详细提到怎么解决这个问题)
(2)高并发
单台设备的 Redis 的 QPS 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
(1)String
存储的值类型:字符串、整形、浮点型
相关操作:对整个/部分字符串进行操作、浮点数/整数进行自增或者自减操作
应用场景:缓存对象、常规计数、分布式锁、共享Session信息等
(2)List
存储的值类型:字符串
操作:链表两端进行push/pop、读取单个/多个元素、根据值查找/删除元素
应用场景:消息队列(生产者需要自行实现全局唯一ID、不可能以消费组形式消费数据)
(3)Hash
存储值类型:包含键值对的无序散列表
操作:添加、获取、删除
应用场景:缓存对象、购物车等
(4)Set
存储值类型:字符串的无序集合
操作:添加、获取、删除、聚合操作(并交差集)
应用场景:点赞、共同关注、抽奖活动等
(5)ZSet
存储值类型:存储键值对
操作:元素排序按照分数大小决定、添加、获取、删除、根据分值范围/成员来获取元素
应用场景:排行榜、延迟队列等
(6)BitMap
应用场景:签到、判断用户登录状态、连续签到用户总数等
(7)HyperLogLog
应用场景:海量数据基数统计
(8)GEO
应用场景:存储地理位置信息(滴滴叫车)
(9)Stream
应用场景:消息队列(支持自动生成全局唯一ID、支持以消费者组形式消费数据)
(1)String
底层数据结构主要是SDS(简单动态字符串)
相比原生C字符串来说,有以下的优势
01.SDS保存二进制数据,可以保存各种格式的数据(文本、图片、视频、音频等)
02.而 SDS 结构里用 len 属性记录了字符串长度,所以获取字符串长度时间复杂度为O(1)
03.Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。
(2)List
如果列表的元素个数小于 512 个, 列表每个元素的值都小于 64 字节,
Redis 会使用【压缩列表】作为 List 类型的底层数据结构;
如果列表的元素不满足上面的条件,
Redis 会使用【双向链表】作为 List 类型的底层数据结构;
在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 【quicklist】 实现了,
替代了双向链表和压缩列表。
(3)Hash
如果哈希类型元素个数小于 512 个, 所有值小于 64 字节,
Redis 会使用【压缩列表】作为 Hash 类型的底层数据结构;
如果哈希类型元素不满足上面条件,
Redis 会使用【哈希表】作为 Hash 类型的底层数据结构。
在 Redis 7.0 中,【压缩列表数据结构已经废弃了】,
交由 【listpack】 数据结构来实现了。
(4)Set
如果集合中的元素都是整数且元素个数小于 512个,
Redis 会使用【整数集合】作为 Set 类型的底层数据结构;
如果集合中的元素不满足上面条件,
则 Redis 使用【哈希表】作为 Set 类型的底层数据结构。
(5)ZSet
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,
Redis 会使用【压缩列表】作为 Zset 类型的底层数据结构;
如果有序集合的元素不满足上面的条件,
Redis 会使用【跳表】作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,【压缩列表数据结构已经废弃了】,
交由 【listpack】 数据结构来实现了。
各种底层数据类型是怎么实现的,有什么优势和劣势,也是一个比较重要的内容,但是由于文章篇幅的限制就不展开了,有兴趣的朋友可以自己去了解下。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
不是的,除了处理客户端请求的主线程外,Redis 在启动的时候,是会启动后台线程。
后台线程的类型:
01.关闭文件
02.AOF刷盘
03.异步释放内存
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
初始化:
(1)首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 一个服务端 socket
(2)然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
(3)然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。
事件处理:
(1)先调用【处理发送队列函数】,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册【写事件处理函数】,等待 epoll_wait 发现可写后再处理 。
(2)调用 epoll_wait 函数等待事件的到来
连接事件到来,则会调用【连接事件处理函数】
01.调用 accpet 获取已连接的 socket
02.调用 epoll_ctl 将已连接的 socket 加入到 epoll
03.注册【读事件处理函数】
读事件到来,则会调用【读事件处理函数】
01.调用 read 获取客户端发送的数据
02.解析命令
03.处理命令
04.将客户端对象添加到发送队列
05.将执行结果写到发送缓存区等待发送
写事件到来,则会调用【写事件处理函数】
通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理。
(1)Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的【内存】或者【网络带宽】,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
(2)Redis 采用【单线程模型】可以【避免了多线程之间的竞争】,省去了【多线程切换】带来的时间和性能上的开销,而且也不会导致【死锁】问题。
(3)Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求。
Redis不采用多线程模型是因为多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
Redis 6.0 对于【网络 I/O 】采用多线程来处理。但是对于【命令的执行】,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令
默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。
要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。
Redis 6.0 版本之后,启动的线程如下:
(1)Redis-server:Redis的主线程,主要负责执行命令
(2)三个后台线程(分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;)
(3)io_thd_1、io_thd_2、io_thd_3
三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。
(1)AOF
实现方式:Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
AOF回写流程:
1.Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
2.然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘
3.具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
AOF缓存内容3种写回硬盘的策略:
1.Always:每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
2.Everysec:每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
3.No:由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
硬盘写入的频率约高,可靠性越高,数据越不容易丢失,同时性能开销会变大。
(2)RDB
RDB 快照就是记录某一个瞬间的内存数据
生成RDB文件命令:
1.save:在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
2.bgsave: 创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
bgsave的写时复制技术:
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。
如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
RDB悖论:
Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作,【如果频率太频繁,可能会对 Redis 性能产生影响。】【如果频率太低,服务器故障时,丢失的数据会更多。】
(3)混合持久化方式
RDB和AOF各有优劣:
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
实现方式:
混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 【AOF 重写日志】时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
优点:混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
缺点:
1.AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
2.兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。
(1)避免额外的检查开销
因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。
(2)不会阻塞当前写操作命令的执行
因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
缺点:
1.数据可能会丢失:执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
2.可能阻塞其他操作:由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 【AOF 日志也是在主线程中执行】,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
(1)为什么需要AOF重写机制?
Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
(2)AOF重写机制的操作
AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
(3)为什么采用子进程来进行AOF重写?
子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
如果是使用线程,【多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。】而使用子进程,创建子进程时,父子进程是共享内存数据的,不过【这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。】
(4)怎么解决重写过程中,导致的子进程与主进程数据不一致问题?
Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
当子进程完成 AOF 重写工作后,会向主进程发送一条信号,主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
1.将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
2.新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
实现:
1.将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。
2.主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
存在的问题:
在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。
Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。
为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且【提供主从节点故障转移的功能】。
(1)为什么需要切片集群模式?
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
(2)实现
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
数据分槽过程:
1.根据键值对的 key,按照 CRC16 算法 (opens new window)计算一个 16 bit
2.再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
哈希槽映射Redis节点方式:
1.平均分配:在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
2.手动分配:可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。
(3)存在的问题
集群脑裂问题:
由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
解决方案:
当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
相关配置:
1.min-slaves-to-write x:主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
2.min-slaves-max-lag x:主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
问题解决分析:
1.即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。
2.等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中,如果不在,则正常读取键值;如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。过期则删除。
(1)惰性删除
实现:惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
优点:每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
缺点:如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
(2)定期删除
实现:定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
流程:
1.从过期字典中随机抽取 20 个 key;
2.检查这 20 个 key 是否过期,并删除已过期的 key;
3.如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
优点:通过限制删除操作执行的【时长】和【频率】,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
(3)混合删除
惰性删除策略和定期删除策略都有各自的优点,
所以 Redis 选择「惰性删除+定期删除」这两种策略配和使用,
以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。
从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
(1)RDB
RBD文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中
RDB 加载阶段
1.主服务器:在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。
2.从服务器:在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
(2)AOF
AOF 文件写入阶段:如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
AOF 重写阶段:会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。
Redis是基于内存的数据库,如果内存被占满了不进行内存淘汰的话,那么会导致Redis不可用。
(1)不进行数据淘汰的策略
它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
(2)进行数据淘汰的策略
(2-1)在设置了过期时间的数据中进行淘汰
1.volatile-random:随机淘汰设置了过期时间的任意键值;
2.volatile-ttl:优先淘汰更早过期的键值。
3.volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
4.volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
(2-2)在所有数据范围内进行淘汰
1.allkeys-random:随机淘汰任意键值;
2.allkeys-lru:淘汰整个键值中最久未使用的键值;
3.allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
(1)实现
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
(2)缺点
需要用链表管理所有的缓存数据,这会带来额外的空间开销;
当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
(1)实现
在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
(2)优点
1.不用为所有的数据维护一个大链表,节省了空间占用;
2.不用在每次数据访问时都移动链表项,提升了缓存的性能;
(3)存在问题
无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,
那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
(1)为什么需要LFU?
解决Redis实现的LRU算法存在的缓存污染问题
(2)实现方式
在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,
高 16bit 存储 ldt(Last Decrement Time),用来记录 key 的访问时间戳;
低 8bit 存储 logc(Logistic Counter),用来记录 key 的访问频次。
当一个数据被再次访问时,就会增加该数据的访问次数。
这样就解决了偶尔被访问一次之后,
数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
(1)缓存雪崩
问题背景:
通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。
问题描述:
大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
解决方案:
1.将缓存失效时间随机打散
2.设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。
(2)缓存击穿
问题背景:
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
问题描述:
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
发生情况:
1.业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
2.黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
解决方案:
1.非法请求的限制:在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
2.设置空值或者默认值:针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
3.布隆过滤器:写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
4.互斥锁方案:保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
5.不给热点数据设置过期时间:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
(1)Cache Aside(旁路缓存)策略
概述:
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
读策略:如果读取的数据命中了缓存,则直接返回数据;如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
写策略:先更新数据库中的数据,再删除缓存中的数据。
使用场景:Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。
(2)Read/Write Through(读穿 / 写穿)策略
概述:
Read/Write Through(读穿 / 写穿)策略原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
Read Through 策略:
先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。
Write Through 策略:
当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在;
如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。
如果缓存中数据不存在,直接更新数据库,然后返回;
使用少的原因:我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库和自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略。
(3)Write Back(写回)策略
概述:
Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
实际上,Write Back(写回)策略也不能应用到我们常用的数据库和缓存的场景中,因为 Redis 并没有异步更新数据库的功能。
Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back(写回)策略。
使用场景:
Write Back 策略特别适合写多的场景,因为发生写操作的时候, 只需要更新缓存,就立马返回了。比如,写文件的时候,实际上是写入到文件系统的缓存就返回了,并不会写磁盘。
存在问题:
数据不是强一致性的,而且会有数据丢失的风险,因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。
MySQL 在执行事务时,会提供回滚机制,当事务执行发生错误时,事务中的所有操作都会撤销,已经修改的数据也会被恢复到事务执行前的状态。
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。
不保证,事务执行过程中,如果命令入队时没报错,而事务提交后,实际执行时报错了,正确的命令依然可以正常执行。
(1)Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
(2)不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。
(1)Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁.
(2) Redis 的读写性能高,可以应对高并发的锁操作场景。
(3)Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁
(1)Not Exist:加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
(2)Expired:锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
(3)加锁、解锁在同一客户端进行:锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;
SET lock_key unique_value NX PX 10000
(1)保证执行操作的客户端就是加锁的客户端
(2)将 lock_key 键删除
解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
优点:
(1)性能高效
(2)实现方便
(3)避免单点故障
缺点:
(1)超时时间不好设置
如果锁的超时时间设置过长,会影响性能
如果设置的超时时间过短会保护不到共享资源
如何合理设置超时时间?
我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。
实现:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可
(2)Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。
如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,
Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,
所以多个应用服务就可以同时获取到锁。
为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。
它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。
让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
这样一来,即使有某个 Redis 节点发生故障,因为锁的数据在其他节点上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。
Redlock加锁过程:
(1) 客户端获取当前时间(t1)
(2) 客户端按顺序依次向 N 个 Redis 节点执行加锁操作
如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给「加锁操作」设置一个超时时间,加锁操作的超时时间需要远远地小于锁的过期时间,一般也就是设置为几十毫秒。
加锁成功的条件:
1.客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁;
2.客户端从大多数节点获取锁的总耗时(t2-t1)小于锁设置的过期时间。
加锁成功的后置操作:
加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
加锁失败的后置流程:
客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。