Redis问题收集

命令

set命令

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value
  • XX :只在键已经存在时,才对键进行设置操作。

因为 SET 命令可以通过参数来实现和 SETNXSETEXPSETEX 三个命令的效果,所以将来的 Redis 版本可能会废弃并最终移除 SETNXSETEXPSETEX 这三个命令。

redis命令方式设置锁,可以使用setnx,incr命令,但是这两个命令还要再设置expire过期时间,防止意外退出,锁未删除,所以上述两种方式都不是原子性的,但是可以使用set nx ex来设置,这样可以一次性实现设置锁并设置过期时间

setex案例

18.16.200.68 dev:3>setex address 30 beijing
"OK"
18.16.200.68 dev:3>get address
"beijing"
18.16.200.68 dev:3>ttl address
"23"
18.16.200.68 dev:3>pttl address
"15595"

set ex 案例

18.16.200.68 dev:3>set aa bb ex 30
"OK"
18.16.200.68 dev:3>get aa
"bb"
18.16.200.68 dev:3>ttl aa
"24"
18.16.200.68 dev:3>pttl aa
"21944"

setnx案例

18.16.200.68 dev:3>get aa
null
18.16.200.68 dev:3>setnx aa bb
"1"
18.16.200.68 dev:3>get aa
"bb"
18.16.200.68 dev:3>setnx aa bbb
"0"
18.16.200.68 dev:3>get aa
"bb"

set nx案例

18.16.200.68 dev:3>get aaa
null
18.16.200.68 dev:3>set aaa bbb nx
"OK"
18.16.200.68 dev:3>get aaa
"bbb"
18.16.200.68 dev:3>set aaa ddd nx
null
18.16.200.68 dev:3>get aaa
"bbb"

setnxset nx这两个可以用于判断锁,如果 key 不存在,将 key 设置为 value
如果 key 已存在,则 SETNX 不做任何动作

set nx ex一次性设置锁和过期时间

18.16.200.68 dev:3>get lock
null
18.16.200.68 dev:3>set lock true nx ex 30
"OK"
18.16.200.68 dev:3>ttl lock
"25"
18.16.200.68 dev:3>get lock
"true"
18.16.200.68 dev:3>set lock false nx
null
18.16.200.68 dev:3>get lock
"true"
18.16.200.68 dev:3>ttl lock
"5"

incr命令

18.16.200.68 dev:3>get n1
null
18.16.200.68 dev:3>incr n1
"1"
18.16.200.68 dev:3>get n1
"1"
18.16.200.68 dev:3>incr n1
"2"
18.16.200.68 dev:3>get n1
"2"

key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。

该命令可用于锁,其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。

watch,multi命令

watch命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

Multi 命令用于标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。

18.16.200.68 dev:3>set key 1
"OK"
18.16.200.68 dev:3>get key
"1"
18.16.200.68 dev:3>watch key
"OK"
18.16.200.68 dev:3>set key 2
"OK"
18.16.200.68 dev:3>multi 
"OK"
18.16.200.68 dev:3>set key 3
"QUEUED"
18.16.200.68 dev:3>get key
"QUEUED"
18.16.200.68 dev:3>exec

18.16.200.68 dev:3>get key
"2"

必须是事务执行之前,被监控key被修改,后续事务就无效

sadd命令

18.16.200.68 dev:3>sadd name hongda
"1"
18.16.200.68 dev:3>get name
"WRONGTYPE Operation against a key holding the wrong kind of value"
18.16.200.68 dev:3>sadd name da
"1"
18.16.200.68 dev:3>sadd name da2
"1"
18.16.200.68 dev:3>smembers name
1) "da2"
2) "da"
3) "hongda"

Redis Sadd 命令将一个或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略。

假如集合 key 不存在,则创建一个只包含添加的元素作成员的集合。

当集合 key 不是集合类型时,返回一个错误。

Redis过期字典

db.expires

熟悉 redis 的朋友都知道,每个数据库维护了两个字典:

  • db.dict:数据库中所有键值对,也被称作数据库的 keyspace
  • db.expires:带有生命周期的 key 及其对应的 TTL(存留时间),因此也被称作 expire set

maxmemory-samples

 为了保证性能,redis 中使用的 LRU 与 LFU 算法是一类近似实现。
 简单来说就是:算法选择被淘汰记录时,不会遍历所有记录,而是以 随机采样 的方式选取部分记录进行淘汰。
maxmemory-samples 选项控制该过程的采样数量,增大该值会增加 CPU 开销,但算法效果能更逼近实际的 LRU 与 LFU 。

lazyfree-lazy-eviction

 清理缓存就是为了释放内存,但这一过程会阻塞主线程,影响其他命令的执行。
 当删除某个巨型记录(比如:包含数百条记录的 list)时,会引起性能问题,甚至导致系统假死。
 延迟释放 机制会将巨型记录的内存释放,交由其他线程异步处理,从而提高系统的性能。
 开启该选项后,可能出现使用内存超过 maxmemory 上限的情况。

Redis中ttl命令

18.16.200.68 dev:3>set name hongda
"OK"
18.16.200.68 dev:3>get name
"hongda"
18.16.200.68 dev:3>ttl name
"-1"
18.16.200.68 dev:3>expire name 10
"1"
18.16.200.68 dev:3>ttl name
"8"
18.16.200.68 dev:3>get name
"hongda"
18.16.200.68 dev:3>ttl name
"1"
18.16.200.68 dev:3>ttl name
"-2"
18.16.200.68 dev:3>get name
null

当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回 key 的剩余生存时间。

Redis如果没有设置expire,是否默认永不过期

Redis无论有没有设置expire,他都会遵循redis的配置好的删除机制,在配置文件里设置:
redis最大内存不足"时,数据清除策略,默认为"volatile-lru"。

如果设置的清除策略是volatile-lru,即从设置了过期时间的key中使用LRU算法进行淘汰,

在这种清除策略下,如果没有设置有效期,即使内存用完,redis 自动回收机制也是看设置了有效期的,不会动没有设定有效期的,如果清理后内存还是满的,就不再接受写操作。

Redis的持久化

RDB快照

RDB(快照)持久化:保存某个时间点的全量数据快照,生成RDB文件在磁盘中。RDB文件是一个压缩过的二进制文件,可以还原为Redis的数据。

触发和载入方式

  • 手动触发方式

    • SAVE命令:阻塞Redis的服务器进程,直到RDB文件被创建完毕,阻塞期间服务器不能处理任何命令请求。
    • BGSAVE命令:Fork出一个子进程来创建RDB文件,不阻塞服务器进程。lastsave 指令可以查看最近的备份时间。
  • 载入方式

    • Redis没有主动载入RDB文件的命令,RDB文件是在服务器启动时自动载入,只要Redis服务器检测到RDB文件的存在,即会载入。且载入过程,服务器也会是阻塞状态
  • 自动触发方式

    • 根据redis.conf配置里的save m n定时触发(用的是BGSAVE),m表示多少时间内,n表示修改次数。save可以设置多个条件,任意条件达到即会执行BGSAVE命令

      	save 900 1  //设置条件1,即服务器在900秒内,对数据库进行了至少1次修改,即会触发BGSAVE
      	save 300 10 //设置条件2,即服务器在300秒内,对数据库进行了至少10次修改,即会触发BGSAVE
      	save 60 1000  //设置条件3,即服务器在60秒内,对数据库进行了至少1000次修改,即会触发BGSAVE
      
    • redis如何保存自动触发方式的save配置呢

      • redisServer结构中维护了一个saveParam的数组,数组每个saveParam都存储着一个save条件,如下图:
        • img
        • img
      • 前文所述三个save,其saveParam的数组将会是下图的样子
        • img
    • 自动触发方式如何实现的呢

      • redisServer结构维护了一个dirty计数器和lastsave属性。
      • dirty计数器记录了上次SAVE或者BGSAVE之后,数据库执行了多少次的增删改,当服务器成功执行一个修改命令后,程序就会对该值+1,(对集合操作n个元素,dirty+n)。SAVE或者BGSAVE命令执行后,dirty计数器清零。
      • lastsave属性是一个unix时间戳,记录了服务器上次成功执行SAVE或者BGSAVE命令的时间。
      • Redis服务器有个周期性操作函数serverCron,默认每100毫秒执行一次,它其中一项工作就是检查saveParam保存的条件,并根据dirty和lastsave字段判断是否有哪一条条件已经被满足。

快照期间,是否可以对数据进行改动

为了快照而暂停写操作,肯定是不能接受的。所以这个时候,Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。

此时,如果主线程对这些数据也都是读操作(键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

AOF日志

AOF持久化的实现

AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。

  • 命令追加
    • 当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf缓存区的末尾。
  • AOF文件的写入和同步
    • Redis的服务器进程就是一个事件循环。
    • 每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否将缓冲区的内容写入和保存到AOF文件里面。
    • flushAppendOnlyFile函数根据配置项appendsync的不同选值有不同的同步策略。
    • img

AOF文件的载入

Redis读取AOF文件并还原数据库状态的详细步骤如下:

  • 服务器创建一个不带网络连接的伪客户端(fake client)(因为Redis的命令只能在客户端上下文中执行);
  • 从AOF文件中分析并读取出一条写命令。
  • 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。

AOF重写

体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。

通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

我们称新的AOF文件为AOF重写文件AOF重写文件不是像AOF一样记录每一条的写命令,也不是对AOF文件的简单复制和压缩。AOF重写是通过读取当前Redis数据库状态来实现的

AOF中,我们要保存四条写命令,而在AOF重写文件中,我们使用一条SADD animals "Dog" "Panda" "Tiger" "Lion" "Cat"来替代四条命令。

从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。(比如连续6条RPUSH命令会被整合成1条)

在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量(默认为64)的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。例如如果SADD后面加入的元素为90条,那么会分成两条SADD,第一条SADD 64个元素,第二条SADD 36个元素。

总结

RDB 快照

优点:文件结构紧凑,节省空间,易于传输,能够快速恢复

缺点:生成快照的开销只与数据库大小相关,当数据库较大时,生成快照耗时,无法频繁进行该操作

AOF 日志

优点:细粒度记录对磁盘I/O压力小,允许频繁落盘,数据丢失的概率极低

缺点:恢复速度慢;记录日志开销与更新频率有关,频繁更新会导致磁盘 I/O 压力上升

RDB 和 AOF 到底该如何选择

  • 不要仅仅使用 RDB,因为那样会导致你丢失很多数据;
  • 也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug
  • redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOFRDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。

RDB-AOF 混合持久化

Redis 用户通常会因为 RDB 持久化和 AOF 持久化之间不同的优缺点而陷入两难的选择当中:

  • RDB 持久化能够快速地储存和恢复数据, 但是在服务器停机时却会丢失大量数据;
  • AOF 持久化能够有效地提高数据的安全性, 但是在储存和恢复数据方面却要耗费大量的时间。

为了让用户能够同时拥有上述两种持久化的优点, Redis 4.0 推出了一个能够“鱼和熊掌兼得”的持久化方案 —— RDB-AOF 混合持久化: 这种持久化能够通过 AOF 重写操作创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF 文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态: 至于那些在重写操作执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 数据之后。

RDB-AOF 混合持久化功能默认是处于关闭状态的, 为了启用该功能, 用户不仅需要开启 AOF 持久化功能, 还需要将 aof-use-rdb-preamble 选项的值设置为真

Redis的淘汰策略

淘汰策略

当达到内存使用上限maxmemory时,可指定的清理缓存所使用的策略有:

  • noeviction 当达到最大内存时直接返回错误,不覆盖或逐出任何数据(默认的)
  • allkeys-lfu 淘汰整个 keyspace 中最不常用的 (LFU) 键 (4.0 或更高版本)
  • allkeys-lru 淘汰整个 keyspace 最近最少使用的 (LRU) 键
  • allkeys-random 淘汰整个 keyspace 中的随机键
  • volatile-ttl 淘汰 expire set 中 TTL 最短的键
  • volatile-lfu 淘汰 expire set 中最不常用的键 (4.0 或更高版本)
  • volatile-lru 淘汰 expire set 中最近最少使用的 (LRU) 键
  • volatile-random 淘汰 expire set 中的随机键

 当 expire set 为空时,volatile-*noeviction 行为一致。

查看Redis设置的内存大小

通过配置文件查看

通过在Redis安装目录下面的redis.conf配置文件中添加以下配置设置内存大小

通过命令查看并设置内存大小

λ redis-cli -h 18.16.200.82 -p 6379 -a shitou123 --raw
18.16.200.82:6379> config get maxmemory
maxmemory
0
18.16.200.82:6379> config set maxmemory 800mb
OK
18.16.200.82:6379> config get maxmemory
maxmemory
838860800

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存

查看Redis设置的淘汰策略

设置及查看redis淘汰策略

18.16.200.82:6379> config get maxmemory-policy
maxmemory-policy
noeviction
18.16.200.82:6379> config set maxmemory-policy volatile-lru
OK
18.16.200.82:6379> config get maxmemory-policy
maxmemory-policy
volatile-lru

LRU算法 (最近最少使用)

LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。 其核心思想是:可以记录每个缓存记录的最近访问时间,最近未被访问时间最长的数据会被首先淘汰。
其原理是维护一个双向链表,key -> node,其中node保存链表前后节点关系及数据data。新插入的key时,放在头部,并检查是否超出总容量,如果超出则删除最后的key;访问key时,无论是查找还是更新,将该Key被调整到头部。

Redis中实际使用的LRU算法

Redis 中的 LRU 不是严格意义上的LRU算法实现,是一种近似的 LRU 实现,主要是为了节约内存占用以及提升性能。Redis 有这样一个配置 —— maxmemory-samplesRedisLRU 是取出配置的数目的key,然后从中选择一个最近最不经常使用的 key 进行置换,默认的 5,如下:

maxmemory-samples 5

可以通过调整样本数量来取得 LRU 置换算法的速度或是精确性方面的优势。

Redis 不采用真正的 LRU 实现的原因是为了节约内存使用。

Redis 如何管理热度数据

前面我们讲述字符串对象时,提到了 redisObject 对象中存在一个 lru属性:

typedef struct redisObject {
        unsigned type:4;//对象类型(4位=0.5字节)
        unsigned encoding:4;//编码(4位=0.5字节)
        unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
        int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节)
        void *ptr;//指向底层实际的数据存储结构,如:SDS等(8字节)
        } robj;

lru 属性是创建对象的时候写入,对象被访问到时也会进行更新。正常人的思路就是最后决定要不要删除某一个键肯定是用当前时间戳减去 lru,差值最大的就优先被删除。但是 Redis 里面并不是这么做的,Redis 中维护了一个全局属性 lru_clock,这个属性是通过一个全局函数 serverCron 每隔 100 毫秒执行一次来更新的,记录的是当前 unix时间戳。

最后决定删除的数据是通过 lru_clock 减去对象的 lru 属性而得出的。那么为什么Redis 要这么做呢?直接取全局时间不是更准确吗?

这是因为这么做可以避免每次更新对象的 lru 属性的时候可以直接取全局属性,而不需要去调用系统函数来获取系统时间,从而提升效率(Redis当中有很多这种细节考虑来提升性能,可以说是对性能尽可能的优化到极致)。

不过这里还有一个问题,我们看到,redisObject 对象中的 lru 属性只有 24 位,24 位只能存储 194 天的时间戳大小,一旦超过 194 天之后就会重新从 0 开始计算,所以这时候就可能会出现 redisObject 对象中的 lru 属性大于全局的 lru_clock 属性的情况。

正因为如此,所以计算的时候也需要分为 2 种情况:

  • 当全局 lruclock > lru,则使用 lruclock - lru 得到空闲时间。
  • 当全局 lruclock < lru,则使用 lruclock_max(即 194 天) -lru + lruclock 得到空闲时间。

需要注意的是,这种计算方式并不能保证抽样的数据中一定能删除空闲时间最长的。这是因为首先超过 194 天还不被使用的情况很少,再次只有 lruclock2 轮继续超过lru 属性时,计算才会出问题。

比如对象 A 记录的 lru1 天,而 lruclock 第二轮都到 10 天了,这时候就会导致计算结果只有 10-1=9 天,实际上应该是 194+10-1=203天。但是这种情况可以说又是更少发生,所以说这种处理方式是可能存在删除不准确的情况,但是本身这种算法就是一种近似的算法,所以并不会有太大影响。

LFU算法 (最不经常使用)

LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。

LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。LFU原理使用计数器来对key进行排序,每次key被访问的时候,计数器增大。计数器越大,可以约等于访问越频繁。具有相同引用计数的数据块则按照时间排序。

LFU 全称为:Least Frequently Used。即:最不经常使用,这个主要针对的是使用频率。这个属性也是记录在redisObject 中的 lru 属性内。

当我们采用 LFU 回收策略时,lru 属性的高 16 位用来记录访问时间(last decrement time:ldt,单位为分钟),低 8 位用来记录访问频率(logistic counter:logc),简称 counter

访问频次递增

LFU 计数器每个键只有 8 位,它能表示的最大值是 255,所以 Redis使用的是一种基于概率的对数器来实现 counter 的递增。r

给定一个旧的访问频次,当一个键被访问时,counter 按以下方式递增:

  1. 提取 01 之间的随机数 R
  2. counter - 初始值(默认为 5),得到一个基础差值,如果这个差值小于 0,则直接取 0,为了方便计算,把这个差值记为 baseval
  3. 概率 P 计算公式为:1/(baseval * lfu_log_factor + 1)
  4. 如果 R < P 时,频次进行递增(counter++)。

公式中的 lfu_log_factor 称之为对数因子,默认是 10 ,可以通过参数来进行控制:

lfu_log_factor 10

下图就是对数因子 lfu_log_factor 和频次 counter 增长的关系图:

图片图片

可以看到,当对数因子 lfu_log_factor100 时,大概是 10M(1000万) 次访问才会将访问 counter 增长到 255,而默认的 10 也能支持到 1M(100万) 次访问 counter才能达到 255 上限,这在大部分场景都是足够满足需求的。

访问频次递减

如果访问频次 counter 只是一直在递增,那么迟早会全部都到 255,也就是说 counter一直递增不能完全反应一个 key 的热度的,所以当某一个 key 一段时间不被访问之后,counter 也需要对应减少。

counter 的减少速度由参数 lfu-decay-time 进行控制,默认是 1,单位是分钟。默认值 1 表示:N 分钟内没有访问,counter 就要减 N

lfu-decay-time 1

具体算法如下:

  1. 获取当前时间戳,转化为分钟 后取低 16 位(为了方便后续计算,这个值记为 now)。
  2. 取出对象内的 lru 属性中的高 16 位(为了方便后续计算,这个值记为 ldt)。
  3. lru > now 时,默认为过了一个周期(16 位,最大 65535),则取差值 65535-ldt+now:当 lru <= now 时,取差值 now-ldt(为了方便后续计算,这个差值记为idle_time)。
  4. 取出配置文件中的 lfu_decay_time 值,然后计算:idle_time / lfu_decay_time(为了方便后续计算,这个值记为num_periods)。
  5. 最后将counter减少:counter - num_periods

看起来这么复杂,其实计算公式就是一句话:取出当前的时间戳和对象中的 lru 属性进行对比,计算出当前多久没有被访问到,比如计算得到的结果是 100 分钟没有被访问,然后再去除配置参数 lfu_decay_time,如果这个配置默认为 1也即是 100/1=100,代表100 分钟没访问,所以 counter 就减少 100

Redis分布式锁问题

Redis锁必须设置过期时间

如果不设置过期时间,客户端故障,锁就永远一直存在,资源永远不能被再次获取

Redis锁中value设置随机值

场景:客户A获取锁,设置过期时间5s,但是因为某些原因超时,超时期间客户B也获取了同样key的锁,

客户A执行完,删除键值为key的锁,但是其实该锁为客户B的

解决方法:保证每个客户端可以区分自己的锁,比如即使key值相等,也可以通过设置value来区分,删除锁之前,可以先比对key,value,再进行删除。

Redis锁中的删除操作使用lua脚本

上述的删除操作,必须先获取锁,比对key,value,再进行删除,那么就必须调用到Redis的get,del命令,这样明显就不是原子性操作,不安全。

Redis锁过期问题解决办法 (看门狗)

设置过期时间,如果到过期时间,但是任务还未执行完毕,其他任务就会获取锁,这时就会有多个任务同时获取到资源

现有的解决办法是:

redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。这个机制也被叫做:“看门狗”。

看门狗功能是Redisson的,不是redis的,获取锁没指定过期时间的,看门狗就会生效

默认情况下,lockWatchdogTimeout(可配)为30s,会有task每10s循环判断,如果该线程还持有锁执行任务,就重置延时30s,直到锁丢失,获取线程不持有该锁

舒服了,踩到一个关于分布式锁的非比寻常的BUG

Redisson分布式锁存储值格式

hset命令赋值,key为锁的名称,field为“客户端唯一ID:线程ID”,value为1

客户端唯一id,就是uuid,value为可重入锁次数

建议

建议使用Redisson,源码中加锁/释放锁操作都是用lua脚本完成的,封装的非常完善,开箱即用。

机制,failover策略不可靠

主从同步通常是异步的,并不能真正的容错。

造成锁不独享的场景如下图所示:

20190817170800716.png

  1. 客户端A申请从master实例获取锁key=test001,由于之前key=test001在master实例上不存在,所以客户端A获取锁成功。
  2. master在通过异步主从同步将key=test001同步至slave之前挂掉了,此时slave经过failover升级为master,但是此时slave上并无key=test001。
  3. 此时,客户端B申请从redis获取锁key=test001,由于此时slave上不存在key=test001,同样的,客户端B获取锁成功。
  4. 最终的结果是,由于关键时刻的master宕机,造成两个客户端同时加锁成功,这与分布式锁的独享特性相互违背。

为什么Redis单线程效率高

Redis官方提供的数据是可以达到10w+QPS(每秒内查询次数)

Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。

  • 基于内存操作
  • 单线程,避免了线程上下文频繁切换,也避免了各种加锁,释放锁问题
  • 采用网络IO多路复用技术来提升Redis的网络IO利用率

采用非阻塞IO,使用epoll作为IO多路复用技术的实现,让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

如果宿主机的cpu性能太高或太低,可以多起几个Redis进程,因为多起几个进程可以利用cpu多核优势。

缺点:因为是单线程的,所以如果某个命令执行事件过长,会导致其他命令被阻塞。

Redis 的瓶颈并不在 CPU,而在内存和网络。

内存不够的话,可以加内存或者做数据结构优化和其他优化等,但网络的性能优化才是大头,网络 IO 的读写在 Redis 整个执行期间占用了大部分的 CPU 时间,如果把网络处理这部分做成多线程处理方式,那对整个 Redis 的性能会有很大的提升。

Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了?

Jedis,Redisson,Lettuce三个Redis客户端框架区别

Redis底层数据结构

  • Redis 字符串,是自己构建了一种名为 简单动态字符串(simple dynamic string , SDS)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。
  • Redis List ,底层是 ZipList ,不满足 ZipList 就使用双向链表。ZipList 是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。

选择合适Redis数据结构,减少80%的内存占用

Redis 5种数据结构 及使用场景分析

Redis 常用数据结构及其底层存储实现总结

hash与String对比

hash类型数据比较少时,使用的时ziplist,比较省空间(相对于hash中设置key,value方式),但是相比String序列化对象不一定省空间,数据量大了就变成dict方式

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

在如下两个条件之一满足的时候,ziplist会转成dict:

  • 当hash中的数据项(即field-value对)的数目超过512的时候,也就是ziplist数据项超过1024的时候(请参考t_hash.c中的hashTypeSet函数)。
  • 当hash中插入的任意一个value的长度超过了64的时候(请参考t_hash.c中的hashTypeTryConversion函数)。

Redis的hash之所以这样设计,是因为当ziplist变得很大的时候,它有如下几个缺点:

  • 每次插入或修改引发的realloc操作会有更大的概率造成内存拷贝,从而降低性能。
  • 一旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更大的一块数据。
  • 当ziplist数据项过多的时候,在它上面查找指定的数据项就会性能变得很低,因为ziplist上的查找需要进行遍历。

总之,ziplist本来就设计为各个数据项挨在一起组成连续的内存空间,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。

hash比较好的就是可以hget直接获取value值(hset直接设置value值)

Redis过期策略有哪些

惰性删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key ,并按照 key 不存在去处理。惰性删除,对内存不太好,已经过期的 key 会占用太多的内存。

定期删除:每隔一段时间,就会对 Redis 进行检查,主动删除一批已过期的 key。

Redis集群作用

  • 自动将数据进行分片,每个master上放置一部分数据
  • 提供内置的高可用支持,部分master不可用时,还是可以继续使用的。

redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

Redis 是怎么进行水平扩容的? Redis 集群数据分片的原理是什么?

Redis 数据分片原理是哈希槽(hash slot)。

Redis 集群有 16384 个哈希槽。每一个 Redis 集群中的节点都承担一个哈希槽的子集。

哈希槽让在集群中添加和移除节点非常容易。例如,如果我想添加一个新节点 D ,我需要从节点 A 、B、C 移动一些哈希槽到节点 D。同样地,如果我想从集群中移除节点 A ,我只需要移动 A 的哈希槽到 B 和 C。当节点 A 变成空的以后,我就可以从集群中彻底删除它。因为从一个节点向另一个节点移动哈希槽并不需要停止操作,所以添加和移除节点,或者改变节点持有的哈希槽百分比,都不需要任何停机时间(downtime)。

一致性hash算法

一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环, 我们对 key 进行哈希计算,使用哈希后的结果对 2 ^ 32 取模,hash 环上必定有一个点与这个整数对应。依此确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。 一致性 Hash 算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。 比如,集群有四个节点 Node A 、B 、C 、D ,增加一台节点 Node X。Node X 的位置在 Node B 到 Node C 直接,那么受到影响的仅仅是 Node B 到 Node X 间的数据,它们要重新落到 Node X 上。 所以一致性哈希算法对于容错性和扩展性有非常好的支持。

Redis变慢原因分析

你需要去查看一下 Redis 的慢日志(slowlog)。

Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。

查看 Redis 慢日志之前,你需要设置慢日志的阈值。例如,设置慢日志的阈值为 5 毫秒,并且保留最近 500 条慢日志记录:

# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500

查看慢查询日志:

127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693       # 慢日志ID
   2) (integer) 1593763337  # 执行时间戳
   3) (integer) 5299        # 执行耗时(微秒)
   4) 1) "LRANGE"           # 具体执行的命令和参数
      2) "user_list:2000"
      3) "0"
      4) "-1"
2) 1) (integer) 32692
   2) (integer) 1593763337
   3) (integer) 5044
   4) 1) "GET"
      2) "user_info:1000"
...

变慢的原因:

  • redis操作内存数据,时间复杂度较高,需要花费更多的cpu资源
  • redis一次需要返回的数据过多,更多时间花费在数据协议组装和网络传输。
  • bigkey,一个key写入的value太大,分配内存,释放内存也比较耗时
  • 有规律的变慢,大概是redis设置为主动过期,大量key集中到期,主线程删除过期key
  • 内存到达上限,先要根据淘汰策略剔除一部分数据,再把新数据写入
  • rdbaop rewrite期间延迟,主线程需要创建一个子线程进行数据持久化,创建子线程会调用操作系统的fork函数,fork会消耗大量cpu资源,在fork之前,整个redis会被阻塞,无法处理客户端请求。
  • 操作系统是否开启内存大页机制,redis申请内存变大,申请内存耗时变长,导致每个写请求延迟增加。
  • AOF刷盘机制设置为always,即每次执行写操作立刻刷盘
  • 设置了绑定cpu
  • 查看redis是否使用了swap,swap是使用磁盘,性能差。
  • redis设置为开启内存碎片整理,也会导致redis性能下降。
  • 网络IO过载

Redis 为什么变慢了?一文讲透如何排查 Redis 性能问题 | 万字长文

Redis选举

slave(从节点)发现自己的master(主节点)不可用时,变尝试进行Failover,以便称为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:

  1. slave发现自己的master不可用;
  2. slave将记录集群的currentEpoch(选举周期)加1,并广播FAILOVER_AUTH_REQUEST 信息进行选举;
  3. 其他节点收到FAILOVER_AUTH_REQUEST信息后,只有其他的master可以进行响应,master收到消息后返回FAILOVER_AUTH_ACK信息,对于同一个Epoch,只能响应一次ack;
  4. slave收集maste返回的ack消息
  5. slave判断收到的ack消息个数是否大于半数的master个数,若是,则变成新的master
  6. 广播Pong消息通知其他集群节点,自己已经成为新的master

注意:从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票。

  • 延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
  • SLAVE_RANK表示此slave已经从master复制数据的总量的rankRank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。

Redis集群为什么至少需要三个master节点?

因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

Redis集群为什么至少推荐节点数为奇数?

奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

网络不稳定是否会是否引起选举?

真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。

为解决这种问题,Redis Cluster 提供了一种选项cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。

Redis主从数据同步

全量复制

  1. slave第一次启动时,连接Master,发送PSYNC命令,格式为psync {runId} {offset}
  • {runId} 为master的运行id;{offset}为slave自己的复制偏移量
  • 由于此时是slave第一次连接master,slave不知道master的runId,也不知道自己偏移量,这时候会传一个问号和-1,告诉master节点是第一次同步。格式为psync ? -1
  1. 当master接收到psync ? -1时,就知道slave是要全量复制,就会将自己的runIdoffset告知slave,回复命令+fullresync {runId} {offset}。同时,master会执行bgsave命令来生成RDB文件,并使用缓冲区记录此后的所有写命令
  • slave接受到master的回复命令后,会保存master的runIdoffset
  • slave此时处于同步状态,如果此时收到请求,当配置参数slave-server-stale-data yes时,会响应当前请求,no则返回错误。
  1. master bgsave执行完毕,向Slave发送RDB文件,同时继续缓冲此期间的写命令。RDB文件发送完毕后,开始向Slave发送存储在缓冲区的写命令
  2. slave收到RDB文件,丢弃所有旧数据,开始载入RDB文件;并执行Master发来的所有的存储在缓冲区里的写命令。
  3. 此后 master 每执行一个写命令,就向Slave发送相同的写命令。

增量复制

  1. 如果出现网络闪断或者命令丢失等异常情况时,当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作,格式为psync {runId} {offset}
  2. 主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制;否则进行全量复制。
  3. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

先更新数据库,再删除缓存,会有什么问题?

这5个常问的Redis面试题你答得出来吗?(详细剖析)

先更新数据库,再删除缓存。可能出现以下情况:

  • 如果更新完数据库, Java 服务提交了事务,然后挂掉了,那 Redis 还是会执行,这样也会不一致。
  • 如果更新数据库成功,删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。

先删除缓存,再更新数据库。

  • 如果删除缓存失败,那就不更新数据库,缓存和数据库的数据都是旧数据,数据是一致的。
  • 如果删除缓存成功,而数据库更新失败了,那么数据库中是旧数据,缓存中是空的,数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。

先删除缓存,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致,怎么处理?

分布式锁

Redission框架中的看门狗原理

参考:

Redis: 分布式锁的官方算法RedLock以及Java版本实现库Redisson

Redlock:Redis分布式锁最牛逼的实现

冷饭新炒:理解Redisson中分布式锁的实现

redis缓存淘汰策略LRU和LFU对比与分析

LRU和LFU算法以及其在Redis中的实现

redis的过期时间和过期删除机制

Redis数据库结构/键空间/过期字典/事务/锁/持久化

Redis 缓存过期(maxmemory) 配置/算法 详解

LRU和LFU算法以及其在Redis中的实现

Redis数据库结构/键空间/过期字典/事务/锁/持久化

细说Redis分布式锁:setnx/redisson/redlock?了解一波?

redis 分布式锁的 5个坑,真是又大又深

分布式系统架构,回顾2020年常见面试知识点梳理(每次面试都会问到其中某一块知识点)

Redis笔记-持久化策略

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章