LOADING

加载过慢请开启缓存 浏览器默认开启

Redis基础整理

Redis作为MySQL缓存的原因

  1. 高性能:从内存中读取数据效率远高于mysql从硬盘上读取数据
  2. 高并发

Redis高并发的原因

  1. 单线程模型
  2. 非阻塞IO多路复用机制
  3. 内存存储和高速缓存
  4. 丰富的数据结构和高效的操作命令
  5. 主从复制和集群架构

数据结构有哪些,使用场景,底层实现数据结构

  1. String字符串:缓存对象,常规计数,分布式锁等,使用简单动态字符串SDS实现
  2. List:消息队列,旧版本使用双向链表或者压缩列表实现,显著使用quickList实现
  3. Hash: 缓存对象,购物车等,原来的使用压缩列表胡总哈希表实现,现在统一改为使用listpack实现。
  4. Set:需要进行聚合计算的:如点赞,共同好友,抽奖活动等,使用哈希表或者整数集合实现,具有根据元素个数是否大于512个来决定。
  5. ZSet:排序的场景中:排行榜,电话或者身份证等,使用listpack或者跳表实现
  6. BitMap:二值状态统计的场景:签到,判断用户的登陆状态,统计连续签到用户的数量等
  7. HyperLogLog:海量数据统计的场景:比如百万级的用户活跃度统计
  8. GEO:存储地理位置的场景
  9. Stream:消息队列,相比于List来说多了两个特性:自动生成全局唯一消息ID,支持以消费组形式消费数据

Redis是否是单线程的

不是,redis的单线程是指「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,但是其本身不是单线程的,redis启动时会有三个后台线程:

  1. 处理关闭文件:当队列中有任务时,调用close将文件关闭
  2. AOF刷盘:调用fsync进行AOF文件刷盘
  3. 异步释放Redis内存,也就是lazyfree线程。

在redis6.0之后,还会额外创建6个线程,其中多出来的三个线程是三个I/O线程,用来分担redis网络I/O压力的。

Redis的线程模型

原来设计为单线程是因为限制redis性能的瓶颈是内存大小和网络I/O,而且多线程会增加系统复杂度,增加由于线程切换,加锁解锁死锁等造成的性能损耗,所以单线程是最好的选择。现在引入多线程是在单线程的基础上引入了IO线程池,将IO操作交给IO线程池来处理,主线程只负责接收客户端请求,解析请求,将IO操作交给IO线程池,然后将结果返回给客户端,这样就可以提高redis的性能。

Redis的持久化机制

  1. RDB:快照形式,将内存中的数据以快照的形式写入到磁盘中,恢复时直接将快照文件读入内存即可,缺点是如果redis意外宕机,会丢失最后一次快照后的所有数据。
  2. AOF:日志形式,将每次写操作记录到日志文件中,恢复时将日志文件中的命令重新执行一遍即可,缺点是日志文件会越来越大,需要定期进行重写,而且重写过程中会阻塞主线程,影响redis的性能。

AOF日志

每次执行完一条操作命令后,使用追加的方式写入到一个文件中,然后再redis重启时,会读取改文件命令,重新执行一遍,从而达到恢复数据的目的。AOF日志有三种同步策略:

  1. Always:每次操作都同步AOF日志数据写入硬盘
  2. Everysec:每秒同步一次AOF日志数据写入硬盘
  3. No:由操作系统决定何时同步AOF日志数据写入硬盘

当AOF日志过大时,会触发AOF重写机制,在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新的AOF文件中,等到全部记录后,就将新的AOF文件替换现有的AOF文件。本质上就是一种数据覆盖,使用最新的数据覆盖之前有的重复的且失效的命令。重写是由后台子进程bgrewirteaof实现。这样有两点好处

重写不影响主进程,不会阻塞主进程,不会影响redis的性能
子进程带有主进程的数据副本,这样的话父子进程是共享内存数据的,不用加锁来保证数据的安全,父子进程任意一方修改了共享内存,就会发生写时复制,于是父子进程就有了独立的数据副本,这样就保证了数据的安全性。子进程对共享的内存数据是只读权限,重写AOF子进程会读取数据库中的所有数据,然后将数据写入到新的AOF文件中,这样就保证了数据的一致性。

如果在AOF重写过程中,主进程修改了已经存在的key-value,那么会怎么样?

为防止这种情况所导致的数据不一致情况,Redis设置了一个AOF重写缓存区,这个缓存区在创建bgrwirteaof后开始使用,在重写阶段,当redis执行完一个写命令之后,会同时将这个命令写入到AOF缓存区和AOF重写缓存区,当子进程完成AOF重写(扫描数据库中的所哟数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后会向主进程发送一条信号,信号是进程间的一种异步通信方式,主进程收到信号后,会调用一个信号处理函数,将AOF重写缓存区中的所有内存追加到新的AOF的文件中,这样保证数据的一致性。然后新的AOF文件覆盖之前的AOF文件,完成AOF重写。

如果在AOF重写的过程中机器挂了,AOF重写缓冲区里面的内容会丢失吗?

会丢失

Redis实现服务的高可用

  1. 主从复制:主从复制是指将一台redis服务器的数据复制到其他的redis服务器,这样的话,所有的服务器都有相同的数据,当主服务器出现故障时,可以使用从服务器提供服务,从而实现高可用。
    其中主服务器负责读写,从服务器负责读,当主服务器出现故障时,可以将从服务器提升为主服务器,从而实现高可用。主从服务器之间的命令复制是异步进行的。无法实现数据的强一致性,只能做到最终一致性
  2. 哨兵模式:哨兵模式是指在主从复制的基础上,引入哨兵节点,哨兵节点是一个独立的进程,主要用于监控redis主从服务器的状态,当主服务器出现故障时,哨兵节点会自动将从服务器提升为主服务器,当主服务器恢复后,又会将主服务器降级为从服务器,从而实现高可用。
  3. 切片集群:切片集群是指将数据分片存储在多台redis服务器上,每台redis服务器存储一部分数据,当需要访问数据时,客户端会先访问路由节点,路由节点会根据数据的key值,将数据路由到对应的redis服务器上,然后再由该redis服务器处理请求,最后将结果返回给客户端,这样就实现了数据的分片存储,从而实现了高可用。
    切片方式:CRC算法,取模算法,一致性哈希算法
    将得到的哈希槽分配到具体的redis节点上可以使用平均分配也可以手动分配。

集群脑裂导致数据丢失怎么办?

首先脑裂是指:主从服务器之间的通信丢失,但是主服务仍然保持和客户端的通信,由于哨兵机制的存在会认为此时的与客户端正常通信的主服务器挂掉了,从而在从服务器中选取出新的主服务器,从而导致一个集群出现了两个主节点。然后突然网络恢复了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
解决方案:当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。在 Redis 的配置文件中有两个参数我们可以设置:
min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。
这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。
等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。

Redis的过期策略

一种是定时删除策略,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。另一种是惰性删除策略,即在访问某个key时,先判断是否过期,如果过期则删除。这样可以减少内存的占用。
惰性过期的优缺点:
优点:只有每次访问相关数据才会检测key是否过期,占用系统资源少,对CPU时间友好
缺点:如果某个key一直没有被访问,那么它就一直不会过期,占用内存资源,对内存不友好。
定时过期的优缺点和惰性过期策略正好相反

Redis持久化时,对过期键会怎么处理?

在RDB持久化时,RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
RDB文件生成阶段:从内存状态持久化成RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
RDB加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。

在AOF持久化时,当AOF文件写入阶段,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。在重写阶段,会先对Redis重点键值对进行检查,过期键不会被写入到重写后的 AOF 文件中。

Redis 主从模式中,对过期键会如何处理?

从服务器会保存过期的key,直到主服务器删除了过期的key,然后从服务器才会删除过期的key,而主服务器key到期是会在AOF文件中追加一条del指令,从服务器会同步这条指令,然后删除过期的key。

Redis 内存满了,会发生什么?

触发内存淘汰机制

Redis的内存淘汰策略

  1. 不进行数据淘汰,直接报错
  2. 进行数据淘汰:
    1. 随机删除:从所有的key中随机选择一个key进行删除
    2. 优先过期更早的键值
    3. 淘汰所有设立过期时间的键值对中,最久没有使用的键值对
    4. 淘汰所有设立过期时间的键值对中,最少使用的键值对

在所有数据范围内进行淘汰:

  1. allkeys-lru:从所有的key中,选择最近最少使用的key进行删除
  2. allkeys-random:从所有的key中,随机选择一个key进行删除
  3. allkeys-lfu:从所有的key中,选择最少使用的key进行删除

Redis如何实现延迟队列

  1. 使用zset,将任务的执行时间作为score,任务的id作为value,然后使用zrangebyscore命令获取当前时间到指定时间的任务,然后将任务发送到消息队列中,等到指定时间到了之后,再从消息队列中取出任务进行执行。

Redis的大key问题

所谓的大key问题是指key所对应的value很大,比如String类型的值大于10kb,hash,list,set,zset类型的元素个数超过5000个。
造成的问题:

  1. 客户端超时阻塞:由于redis是单线程的,然后操作大key时会比较耗时,从而阻塞了其他客户端的请求。
  2. 引发网络阻塞:由于大key的value很大,从而占用了大量的网络带宽,从而引发网络阻塞。
  3. 阻塞工作线程:使用del命令删除大key时,会阻塞工作线程,从而影响redis的性能。
  4. 内存分布不均:集群模式在slot分片均匀的情况下,会出现数据和查询倾斜情况,部分有大key的redis节点占用内存多,QPS也会大

查找大key

  1. redis-cli –bigkeys
redis-cli - h 127.0.0.1 -p 6379 -a password --bigkeys

主节点执行会造成阻塞,从节点执行不会造成阻塞,这种方法只能查出最大的key,并不能查出前N位的大key,在集合类型中不适用,因为这种方法统计的是集合的元素个数,而不是集合的大小。

  1. 使用redis-rdb-tools工具
rdb dump -c memory --bytes 10240 -f redis.csv
  1. 适用Scan命令
scan 0 count 1000

删除大key

本质上是要释放键值对占用的内存空间
释放过程:操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配,具体的删除策略

  1. 分批删除
  2. 使用redis的异步删除机制,使用unlink命令删除大key,然后使用redis的异步删除机制来释放内存空间,这样就不会阻塞工作线程,从而提高redis的性能。

Redis管道的作用

Redis 管道是一种提高 Redis 性能的方式,它可以在客户端一次性发送多个命令,然后一次性读取多个命令的返回结果。这样可以减少客户端与服务端之间的网络通信次数,从而提高 Redis 的性能。
Redis 管道的实现原理是:客户端一次性发送多个命令到服务端,服务端将命令放入队列中,然后依次执行命令,最后将命令的结果返回给客户端。这样就减少了客户端与服务端之间的网络通信次数,从而提高了 Redis 的性能。

Redis事务支持回滚吗?

不支持,redis的事务是乐观锁,如果在事务执行过程中,某个key被其他客户端修改了,那么事务会执行失败,但是不会回滚,而是继续执行后面的命令。但是Redis提供了Discard命令,用来主动放弃事务执行,把暂存的命令队列清空,但是起不到回滚的效果

Redis实现分布式锁

  1. setnx+expire
setnx lock:resourceId 1
expire lock:resourceId 5
  1. setnx+getset
setnx lock:resourceId 1
getset lock:resourceId 1
  1. redlock
set lock:resourceId randomValue NX PX 30000
  1. redission
RLock lock = redisson.getLock("lock:resourceId");
lock.lock();
lock.unlock();

基于redis节点实现分布式锁时,对于加锁操作,要满足三个条件

  1. 加锁包括了读取锁变量,检测锁变量值和设置变量值三个操作,这三个操作必须是原子性的,否则会出现死锁,我们使用setnx命令来实现原子性操作。
  2. 锁变量需要设置过期时间,防止死锁,我们使用expire命令来设置过期时间。
  3. 锁变量的值能够区分不同的客户端,防止误删锁,我们使用uuid来生成唯一的锁变量值。

例如:redis加锁

SET lock_key unique_value NX PX 30000

lua脚本解锁

if redis.call("get",KEYS[1])== ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Redis实现分布式锁的优点和缺点

  1. 性能高效,实现方便,可以使用redis的setnx命令来实现分布式锁,而且redis是单线程的,所以性能很高。
  2. 避免单点故障,redis支持主从复制,主节点挂了,可以使用从节点提供服务,从而避免单点故障。
    缺点:
  3. 超时时间不好控制,如果业务执行时间超过了锁的超时时间,那么锁就会失效,其他客户端就可以获取锁,从而造成数据不一致。(在Redison中有看门狗机制)
  4. redis主从复制是异步复制,主节点挂了,从节点可能还没有同步到最新的数据,从而造成数据丢失。导致分布式锁的不可靠性。(Redlock算法解决)

Redis如何判断数据是否过期

Redis中有个过期字典(可以看作是一个hash表)来保存数据的过期时间,过期字典的的键指向Redis数据库中的某个key键,过期字典的值是一个long long 类型的整数,这个整数保存了key所指向的数据库键的过期时间

本文作者:GWB
当前时间:2023-11-09 11:11:09
版权声明:本文由gwb原创,本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 国际许可协议。
转载请注明出处!