redis相关问题剖析

[TOC]

Redis 学习笔记

初识 Redis

Redis 是一种基于键值对的 NoSQL 数据库,Redis 中的值可以是由 string、hash、list、set、zset 等多种数据结构和算法组成,因此 Redis 可以满足很多应用场景。Redis 将所有数据都存放在内存中,所以它的读写能力也非常高。Redis 还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会丢失。除了这些功能,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 等附加功能。

Redis 的特性

速度快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

基于键值对的数据结构服务器

与很多键值对数据库不同的是,Redis 中的值不仅可以是字符串,还可以是具体的数据结构,这样不仅能应用于多种场景开发,也可以提高开发效率。Redis 的全称是 REmote Dictionary Server,它主要提供五种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础上演变出了位图和 HyperLogLog 两种数据结构,随着 LBS 基于位置服务的发展,Redis 3.2 加入了有关 GEO 地理信息定位的功能。

丰富的功能

① 提供了键过期功能,可以实现缓存。② 提供了发布订阅功能,可以实现消息系统。③ 支持 Lua 脚本,可以创造新的 Redis 命令。④ 提供了简单的事务功能,能在一定程度商保证事务特性。⑤ 提供了流水线功能,这样客户端能将一批命令一次性传到 Redis,减少了网络开销。⑤IO多路复用

持久化

通常来说数据放在内存中是不安全的,一旦发生断电或故障数据就可能丢失,因此 Redis 提供了两种持久化方式 RDB 和 AOF 将内存的数据保存到硬盘中。

对单线程的理解?

img

img

redis的IO复用是基于单线程的,但它的一些模块,比如删除过期缓存是多线程的

Redis 的使用场景

缓存

缓存机制几乎在所有大型网站都有使用,合理使用缓存不仅可以加快数据的访问速度,而且能够有效降低后端数据源的压力。Redis 提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。

排行榜系统

排行榜系统几乎存在于所有网站,Redis 提供了列表和有序集合数据结构,合理使用这些数据结构可以方便构建各各种排行榜系统。

计数器应用

计数器在网站中的作用很重要,例如视频网站有播放数、电商网站有浏览数,为了保证数据实时性,每一次播放和浏览都要做加 1 的操作,如果并发量很大对于传统关系型数据库的性能是很大的挑战。Redis 天然支持计数功能而且性能也非常好。

社交网络

粉丝、共同好友/喜好、推送、下拉刷新等是社交网络的必备功能,由于社交网站的访问量通常很大,而且关系型数据不太适合保存这种类型的数据,Redis 提供的数据结构可以相对容易地实现这些功能。

消息队列系统

消息队列系统是一个大型网站的必备基础组件,因为其具有业务解耦、非实时业务削峰等特性。Redis 提供了发布订阅和阻塞队列的功能,对于一般的消息队列功能基本可以满足。

Redis 不适合非常大的数据量,成本非常高,也不适合冷数据,会浪费内存。


API 的理解和使用

字符串

字符串类型是 Redis 最基础的数据结构,键都是字符串类型,而且其他几种数据结构都是在字符串类型的基础上构建的。字符串类型的值可以实际可以是字符串(简单的字符串、复杂的字符串如 JSON、XML)、数字(整形、浮点数)、甚至二进制(图片、音频、视频),但是值最大不能超过 512 MB。

常用命令

设置值

set key value [ex seconds] [px millseconds] [nx|xx]

  • ex seconds:为键设置秒级过期时间,跟 setex 效果一样
  • px millseconds:为键设置毫秒级过期时间
  • nx:键必须不存在才可以设置成功,用于添加,跟 setnx 效果一样。由于 Redis 的单线程命令处理机制,如果多个客户端同时执行,则只有一个客户端能设置成功,可以用作分布式锁的一种实现。
  • xx:键必须存在才可以设置成功,用于更新

获取值

get key,如果不存在返回 nil

批量设置值

mset key value [key value...]

批量获取值

mget key [key...]

批量操作命令可以有效提高开发效率,假如没有 mget,执行 n 次 get 命令需要 n 次网络时间 + n 次命令时间,使用 mget 只需要 1 次网络时间 + n 次命令时间。

Redis 可以支持每秒数万的读写操作,但这指的是 Redis 服务端的处理能力,对于客户端来说一次命令处理命令时间还有网络时间。因为 Redis 的处理能力已足够高,对于开发者来说,网络可能会成为性能瓶颈。

计数

incr key

incr 命令用于对值做自增操作,返回结果分为三种:① 值不是整数返回错误。② 值是整数,返回自增后的结果。③ 值不存在,按照值为 0 自增,返回结果 1。除了 incr 命令,还有自减 decr、自增指定数字 incrby、自减指定数组 decrby、自增浮点数 incrbyfloat。


不常用命令

追加值

append key value,可以向字符串尾部追加值

字符串长度

strlen key

设置并返回原值

getset key value

设置指定位置的字符

setrange key offset value

获取部分字符串

getrange key start end,start 和 end分别是开始和结束的偏移量,偏移量从 0 开始计算。


内部编码

字符串类型的内部编码有三种:

  • int:8 个字节的长整形
  • embstr:小于等于 39 个字节的字符串
  • raw:大于 39 个字节的字符串

典型使用场景

缓存功能

Redis 作为缓存层,MySQL 作为存储层,首先从 Redis 获取数据,如果没有获取到就从 MySQL 获取,并将结果写回到 Redis,添加过期时间。

计数

Redis 可以实现快速计数功能,例如视频每播放一次就用 incy 把播放数加 1。

共享 Session

一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,但会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问负载到不同服务器上,用户刷新一次可能会发现需要重新登陆。为解决该问题,可以使用 Redis 将用户的 Session 进行集中管理,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或查询登录信息都直接从 Redis 集中获取。

限速

例如为了短信接口不被频繁访问会限制用户每分钟获取验证码的次数或者网站限制一个 IP 地址不能在一秒内访问超过 n 次。可以使用键过期策略和自增计数实现。


哈希

哈希类型是指键值本身又是一个键值对结构,哈希类型中的映射关系叫做 field-value,这里的 value 是指 field 对于的值而不是键对于的值。

命令

设置值

hset key field value,如果设置成功会返回 1,反之会返回 0,此外还提供了 hsetnx 命令,作用和 setnx 类似,只是作用于由键变为 field。

获取值

hget key field,如果不存在会返回 nil。

删除 field

hdel key field [field...],会删除一个或多个 field,返回结果为删除成功 field 的个数。

计算 field 个数

hlen key

批量设置或获取 field-value

1
2
hmget key field [field...]
hmset key field value [field value...]

hmset 需要的参数是 key 和多对 field-value,hmget 需要的参数是 key 和多个 field。

判断 field 是否存在

hexists key field,存在返回 1,否则返回 0。

获取所有的 field

hkeys key,返回指定哈希键的所有 field。

获取所有 value

hvals key,获取指定键的所有 value。

获取所有的 field-value

hgetall key,获取指定键的所有 field-value。

计数

hincrby key fieldhincrbyfloat key field,作用和 incrby 和 incrbyfloat 一样,作用域是 field。

计算 value 的字符串长度

hstrlen key field


内部编码

哈希类型的内部编码有两种:

  • ziplist 压缩列表:当哈希类型元素个数和值小于配置值(默认 512 个和 64 字节)时会使用 ziplist 作为内部实现,使用更紧凑的结构实现多个元素的连续存储,在节省内存方面比 hashtable 更优秀。
  • hashtable 哈希表:当哈希类型无法满足 ziplist 的条件时会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度都为 O(1)。

使用场景

缓存用户信息,有三种实现:

  • 原生字符串类型:每个属性一个键。

    1
    2
    3
    set user:1:name tom
    set user:1:age 23
    set user:1:city xi'an

    优点:简单直观,每个属性都支持更新操作。

    缺点:占用过多的键,内存占用量较大,用户信息内聚性差,一般不会在生产环境使用。

  • 序列化字符串类型:将用户信息序列化后用一个键保存。

    1
    set user:1 serialize(userInfo)

    优点:编程简单,如果合理使用序列化可以提高内存使用率。

    缺点:序列化和反序列化有一定开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到 Redis。

  • 哈希类型:每个用户属性使用一对 field-value,但只用一个键保存。

    1
    hmset user:1 name tom age 23 city xi'an

    优点:简单直观,如果合理使用可以减少内存空间使用。

    缺点:要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,hashtable 会消耗更多内存。


列表

列表类型是用来存储多个有序的字符串,列表中的每个字符串称为元素,一个列表最多可以存储 2^32^-1 个元素。可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发中有很多应用场景。

列表类型有两个特点:① 列表中的元素是有序的,可以通过索引下标获取某个元素或者某个范围内的元素列表。② 列表中的元素可以重复。

命令

添加操作

从右边插入元素:rpush key value [value...]

从左到右获取列表的所有元素:lrange 0 -1

从左边插入元素:lpush key value [value...]

向某个元素前或者后插入元素:linsert key before|after pivot value,会在列表中找到等于 pivot 的元素,在其前或后插入一个新的元素 value。

查找

获取指定范围内的元素列表:lrange key start end,索引从左到右的范围是 0N-1,从右到左是 -1-N,lrange 中的 end 包含了自身。

获取列表指定索引下标的元素:lindex key index,获取最后一个元素可以使用 lindex key -1

获取列表长度:llen key

删除

从列表左侧弹出元素:lpop key

从列表右侧弹出元素:rpop key

删除指定元素:lrem key count value,如果 count 大于 0,从左到右删除最多 count 个元素,如果 count 小于 0,从右到左删除最多个 count 绝对值个元素,如果 count 等于 0,删除所有。

按照索引范围修剪列表:ltrim key start end,只会保留 start ~ end 范围的元素。

修改

修改指定索引下标的元素:lset key index newValue

阻塞操作

阻塞式弹出:blpop/brpop key [key...] timeout,timeout 表示阻塞时间。

当列表为空时,如果 timeout = 0,客户端会一直阻塞,如果在此期间添加了元素,客户端会立即返回。

如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。

如果多个客户端对同一个键执行 brpop,那么最先执行该命令的客户端可以获取弹出的值。


内部编码

列表的内部编码有两种:

  • ziplist 压缩列表:跟哈希的 zipilist 相同,元素个数和大小小于配置值(默认 512 个和 64 字节)时使用。
  • linkedlist 链表:当列表类型无法满足 ziplist 的条件时会使用linkedlist。

Redis 3.2 提供了 quicklist 内部编码,它是以一个 ziplist 为节点的 linkedlist,它结合了两者的优势,为列表类提供了一种更为优秀的内部编码实现。


使用场景

消息队列

Redis 的 lpush + brpop 即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

文章列表

每个用户有属于自己的文章列表,现在需要分页展示文章列表,就可以考虑使用列表。因为列表不但有序,同时支持按照索引范围获取元素。每篇文章使用哈希结构存储,例如每篇文章有三个属性,title、timestamp 和 content:

hmset article:k title t timestamp 147651524 content c

向用户文章列表添加文章,user:{id}:articles 作为用户文章列表的键:

lpush user:k:articles article:k

分页获取用户文章列表,例如以下伪代码获取用户 id = 1 的前 10 篇文章。

1
2
3
articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}

使用列表类型保存和获取文章列表存在两个问题:① 如果每次分页获取的文章个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 Pipeline 批量获取,或者考虑将文章数据序列化为字符串类型,使用 mget 批量获取。② 分页获取文章列表时,lrange 命令在列表两端性能较好,但如果列表大,获取中间范围的元素性能会变差,可以考虑将列表做二级拆分,或使用 Redis3.2 的 quicklist。


lpush + lpop = 栈

lpush + rpop = 队列

lpush + ltrim = 优先集合

lpush + brpop = 消息队列


集合

集合类型也是用来保存多个字符串元素,和列表不同的是集合不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储 2^32^-1 个元素。Redis 除了支持集合内的增删改查,还支持多个集合取交集、并集、差集。

命令

集合内操作

添加元素

sadd key element [element...],返回结果为添加成功的元素个数。

删除元素

srem key element [element...],返回结果为成功删除的元素个数。

计算元素个数

scard key,时间复杂度为 O(1),会直接使用 Redis 内部的遍历。

判断元素是否在集合中

sismember key element,如果存在返回 1,否则返回 0。

随机从集合返回指定个数个元素

srandmember key [count],如果不指定 count 默认为 1。

从集合随机弹出元素

spop key,可以从集合中随机弹出一个元素。

获取所有元素

smembers key


集合间操作

求多个集合的交集

sinter key [key...]

求多个集合的并集

sunion key [key...]

求多个集合的差集

sdiff key [key...]

保存交集、并集、差集的结果

1
2
3
sinterstore destination key [key...]
sunionstore destination destination key [key...]
sdiffstore destination key [key...]

集合间的运算在元素较多的情况下会比较耗时,所以 Redis 提供了这三个指令将集合间交集、并集、差集的结果保存在 destination key 中。


内部编码

集合类型的内部编码有两种:

  • intset 整数集合:当集合中的元素个数小于配置值(默认 512 个时),,且都是整数时,使用 intset。
  • hashtable 哈希表:当集合类型无法满足 intset 条件时使用 hashtable。当某个元素不为整数时,也会使用 hashtable。

使用场景

集合类型比较典型的使用场景是标签,例如一个用户可能与娱乐、体育比较感兴趣,另一个用户可能对例时、新闻比较感兴趣,这些兴趣点就是标签。这些数据对于用户体验以及增强用户黏度比较重要。

给用户添加标签

1
2
3
4
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag3 tag4 tag5
...
sadd user:k:tags tagx tagy tagz

给标签添加用户

1
2
3
4
sadd tag:1:users user:1 user:3
sadd tag:2:users user:1 user:4 user:5
...
sadd tag:k:users user:x user:y ...

用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致。

删除用户标签

1
srem user:1:tags tag1 tag5

删除标签下的用户

1
srem tag:1:users user:1

删除也同样应该放在一个事务中。

求两个用户共同感兴趣的标签

1
sinter user:1:tags user:2:tags

sadd = 标签

spop/srandmember = 生成随机数,比如抽奖

sadd + sinter = 社交需求


有序集合

有序集合保留了集合不能有重复成员的特性,不同的是可以排序。但是它和列表使用索引下标作为排序依据不同的是,他给每个元素设置一个分数(score)作为排序的依据。有序集合提供了获取指定分数和元素查询范围、计算成员排名等功能。

数据结构 是否允许元素重复 是否有序 有序实现方式 应用场景
列表 下标 时间轴,消息队列
集合 / 标签,社交
有序集合 分值 排行榜,社交

命令

集合内

添加成员

zadd key score member [score member...],返回结果是成功添加成员的个数

Redis 3.2 为 zadd 命令添加了 nx、xx、ch、incr 四个选项:

  • nx:member 必须不存在才可以设置成功,用于添加
  • xx:member 必须存在才能设置成功,用于更新
  • ch:返回此次操作后,有序集合元素和分数变化的个数
  • incr:对 score 做增加,相当于 zincrby

zadd 的时间复杂度为 O(logn),sadd 的时间复杂度为 O(1)。

计算成员个数

zcard key,时间复杂度为 O(1)。

计算某个成员的分数

zscore key member ,如果不存在则返回 nil。

计算成员排名

zrank key member,从低到高返回排名

zrevrank key member,从高到低返回排名

删除成员

zrem key member [member...],返回结果是成功删除的个数。

增加成员的分数

zincrby key increment member

返回指定排名范围的成员

zrange key start end [withscores]

zrevrange key start end [withscores]

zrange 从低到高返回,zrevrange 从高到底返回,如果加上 withscores 选项同时会返回成员的分数。

返回指定分数范围的成员

zrangebyscore key min max [withscores] [limit offset count]

zrevrangebyscore key min max [withscores] [limit offset count]

zrangebyscore 从低到高返回,zrevrangebyscore 从高到底返回,如果加上 withscores 选项同时会返回成员的分数。[limit offset count] 可以限制输出的起始位置和个数。

返回指定分数范围成员个数

zcount key min max

删除指定排名内的升序元素

zremrangebyrank key start end

删除指定分数范围内的成员

zremrangebyscore key min max


集合间的操作

交集

zinterstore destination numkeys key [key...] [weights weight [weight...]] [aggregate sum|min|max]

  • destination:交集结果保存到这个键

  • numkeys:要做交集计算键的个数

  • key [key…]:需要做交集计算的键

  • weights weight [weight…]:每个键的权重,默认 1

  • aggregate sum|min|max:计算交集后,分值可以按和、最小值、最大值汇总,默认 sum

并集

zunionstore destination numkeys key [key...] [weights weight [weight...]] [aggregate sum|min|max]


内部编码

有序集合的内部编码有两种:

  • ziplist 压缩列表:当有序集合元素个数和值小于配置值(默认128 个和 64 字节)时会使用 ziplist 作为内部实现。
  • skiplist 跳跃表:当 ziplist 不满足条件时使用,因为此时 ziplist 的读写效率会下降。

使用场景

有序集合的典型使用场景就是排行榜系统。

例如用户 mike 上传了一个视频并添加了 3 个赞,可以使用有序集合的 zadd 和 zincrby:

1
zadd user:ranking:2020_06_19 3 mike

如果之后再获得一个赞,可以使用 zincrby:

1
zincrby user:ranking:2020_06_19 1 mike

例如需要将用户 tom 从榜单删除,可以使用 zrem:

1
zrem user:ranking:2020_06_19 tom

展示获取赞数最多的十个用户:

1
zrevrange user:ranking:2020_06_19 0 9

展示用户信息及用户分数,将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户分数和排名可以使用 zscore 和 zrank:

1
2
3
hgetall user:info:tom
zscore user:ranking:2020_06_19 tom
zrank user:ranking:2020_06_19 tom

键管理

单个键管理

键重命名

rename key newkey

如果 rename 前键已经存在,那么它的值也会被覆盖。

为了防止强行覆盖,Redis 提供了 renamenx 命令,确保只有 newkey 不存在时才被覆盖。由于重命名键期间会执行 del 命令删除旧的键,如果键对应值比较大会存在阻塞的可能。

随机返回一个键

random key

键过期

expire key seconds:键在 seconds 秒后过期

expireat key timestamp:键在秒级时间戳 timestamp 后过期

如果过期时间为负值,键会被立即删除,和 del 命令一样。

persist 命令可以将键的过期时间清除。

对于字符串类型键,执行 set 命令会去掉过期时间,set 命令对应的函数 setKey 最后执行了 removeExpire 函数去掉了过期时间。

Redis 不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素设置过期时间。

setex 命令作为 set + expire 的组合,不单是原子执行并且减少了一次网络通信的时间。

键迁移

  • move

    move key db

    move 命令用于在 Redis 内部进行数据迁移,move key db 就是把指定的键从源数据库移动到目标数据库中。

  • dump + restore

    dump key

    restore key ttl value

    可以实现在不同的 Redis 势力之间进行数据迁移,分为两步:

    ① 在源 Redis 上,dump 命令会将键值序列化,格式采用 RDB 格式。

    ② 在目标 Redis 上,restore 命令将上面序列化的值进行复原,ttl 参数代表过期时间, ttl = 0 则没有过期时间。

    整个迁移并非原子性的,而是通过客户端分步完成,并且需要两个客户端。

  • migrate

    实际上 migrate 命令就是将 dump、restore、del 三个命令进行组合,从而简化了操作流程。migrate 具有原子性,且支持多个键的迁移,有效提高了迁移效率。实现过程和 dump + restore 类似,有三点不同:

    ① 整个过程是原子执行,不需要在多个 Redis 实例开启客户端。

    ② 数据传输直接在源 Redis 和目标 Redis 完成。

    ③ 目标 Redis 完成 restore 后会发送 OK 给源 Redis,源 Redis 接收后会根据 migrate 对应的选项来决定是否在源 Redis 上删除对应的键。

命令 作用域 原子性 支持多个键
move Redis 实例内部
dump + restore Redis 实例之间
migrate Redis 实例之间

遍历键

全量遍历键

keys pattern

*代表匹配任意字符,? 匹配一个字符,[] 匹配部分字符,例如 [1,3] 匹配 1 和 3, [1-3] 匹配 1 到 3 的任意数字,\用来做转义。

keys * 遍历所有的键,一般不在生产环境使用,在以下情况可以使用:

① 在一个不对外提供服务的 Redis 从节点上执行,不会阻塞客户端的请求,但会影响主从复制。

② 如果确定键值总数比较少可以执行。


渐进式遍历

Redis 从 2.8 版本后提供了一个新的命令 scan,能有效解决 keys 存在的问题。和 keys 遍历所有键不同,scan 采用渐进式遍历的方式解决阻塞问题,每次 scan 的时间复杂度为 O(1),但是要真正实现 keys 的功能可能需要执行多次 scan。

1
scan cursor [match pattern] [count number]

cursor 是必须参数,代表一个游标,第一次遍历从 0 开始,每次 scan 完会返回当前游标的值,直到值为 0 表示遍历结束。

match pattern 是可选参数,作用是模式匹配。

count number 是可选参数,作用是表明每次要遍历的键个数,默认值为 10。

除了 scan 外,Redis 提供了面向哈希、集合、有序集合的扫描遍历命令,解决了 hgetall、smembers、zrange 可能产生的阻塞问题,对应命令分别为 hscan、sscan、zscan。

渐进式遍历可以有效解决 keys 命令可能产生的阻塞问题,但是如果在 scan 过程中有键的变化,那么遍历效果可能会遇到问题:新增的键没有被遍历到,遍历了重复的键等情况。


数据库管理

切换数据库

select dbIndex

Redis 中默认配置有 16 个数据库,例如 select 0 将切换到第一个数据库,数据库之间的数据是隔离的。

flushdb/flushall

用于清除数据库,flushdb 只清除当前数据库,flushall 会清除所有数据库。如果当前数据库键值数量比较多,flushdb/flushall 存在阻塞 Redis 的可能性。


总结

Redis 提供 5 种数据结构,每种数据结构都有多种内部编码实现。

纯内存存储、IO 多路复用计数、单线程架构是造就 Redis 高性能的三个因素。

由于 Redis 的单线程结构,所以需要每个命令能被快速执行完,否则会存在阻塞的可能。

批量操作(例如 mget、mset、hmset 等)能够有效提高命令执行的效率,但要注意每次批量操作的个数和字节数。

persist 命令可以删除任意类型键的过期时间,但 set 也会删除字符串类型键的过期时间。

move、dump + restore、migrate 是 Redis 发展过程中三种迁移键的方式,其中 move 命令基本废弃,migrate 命令用原子性的方式实现了 dump + restore,并且支持批量操作,是 Redis Cluster 实现水平扩容的重要工具。

scan 命令可以解决 keys 命令可能带来的阻塞问题,同时 Redis 还提供了 hscan、sscan、zscan 渐进式遍历 hash、set、zset。


高级功能

事务

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
1.redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3.如果在一个事务中出现运行错误,那么正确的命令会被执行。
注:redis的discard只是结束本次事务,正确命令造成的影响仍然存在.

1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。


Bitmaps

Bitmaps 本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。

Bitmaps 单独提供了一套命令,所以在 Redis 使用 Bitmaps 和使用字符串的方法不太相同,可以把 Bitmaps 看作一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标叫做偏移量。

命令

例:将每个独立用户是否访问过网站存放在 Bitmaps 中,将访问过的用户记作 1,没有访问过的记作 0,偏移量作为用户的 id。

设置值

1
setbit key offset value

设置键的第 offset 个位的值,假设有 20 个用户,id 为 0、5、11、15、19 的用户对网站进行了访问,那么初始化如下:

1
2
3
4
5
setbit unique:users:2020-06-20 0 1
setbit unique:users:2020-06-20 5 1
setbit unique:users:2020-06-20 11 1
setbit unique:users:2020-06-20 15 1
setbit unique:users:2020-06-20 19 1

很多应用的用户 id 直接以一个指定数字开头,例如 10000,直接将用户 id 与 Bitmaps 的偏移量对应势必会造成一定浪费,通常做法是每次做 setbit 操作时将用户 id 减去这个指定数字。在第一次初始化 Bitmaps 时,如果偏移量非常大,那么整个初始化过程会执行比较慢,可能造成阻塞。

获取值

1
getbit key offset

获取键的第 offset 个位的值,例如获取 id 为 8 的用户是否在 2020-06-20 这天访问过:

1
getbit unique:users:2020-06-20 8

获取指定范围值为 1 的个数

1
bitcount key [start end]

例如获取 2020-06-20 这天访问过的用户数量

1
bitcount unique:users:2020-06-20

start 和 end 代表起始和结束字节数。

Bitmaps 间的运算

1
bitop op destkey key [key...]

bitop 是一个复合操作,它可以做交集、并集、非、异或并将结果保存到 destkey 中。

例如计算 2020-06-20 和 2020-06-21 都访问过网站的用户数量:

1
2
bitop and unique:users:and:2020-06-20_21 unique:users:2020-06-20 unique:users:2020-06-21
bitcount unique:users:and:2020-06-20_21

例如计算 2020-06-20 和 2020-06-21 任意一天访问过网站的用户数量:

1
2
bitop or unique:users:or:2020-06-20_21 unique:users:2020-06-20 unique:users:2020-06-21
bitcount unique:users:or:2020-06-20_21

计算第一个值为 tartgetBit 的偏移量

1
bitops key targetBit [start] [end]

例如计算 2020-06-20 当前访问网站的最小用户 id:

1
bitops unique:users:2019-06-20 1

假设网站的活跃用户量很大,使用 Bitmaps 相比 set 可以节省很多内存,但如果活跃用户很少就会浪费内存。


HyperLogLog

HyperLogLog 不是一种新的数据结构,实际也是字符串类型,是一种基数算法。提供 HyperLogLog 可以利用极小的内存空间完成独立总数的统计,数据集可以是 IP、Email、ID 等。

添加

pfadd key element [element...],如果添加成功会返回 1

计算独立用户数

pfcount key [key...]

合并

pfmerge destkey sourcekey [sourcekey...]

HyperLogLog 内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条:

  • 只为了计算独立总数,不需要获取单条数据。
  • 可以容忍一定误差率,毕竟 HyperLogLog 在内存占用量上有很大优势。

发布订阅

Redis 提供了基于发布/订阅模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发送消息,订阅该频道的每个客户端都可以收到该消息。

命令

发布消息

publish channel message,返回结果为订阅者的个数。

订阅消息

subscribe channel [channel..],订阅者可以订阅一个或多个频道。

客户端在执行订阅命令后会进入订阅状态,只能接收 subscribe、psubscribe、unsubscribe、punsubscribe 的四个命令。新开启的订阅客户端,无法收到该频道之前的消息,因为 Redis 不会对法捕的消息进行持久化。

和很多专业的消息队列系统如 Kafka、RocketMQ 相比,Redis 的发布订阅略显粗糙,例如无法实现消息堆积和回溯,但胜在足够简单,如果当前场景可以容忍这些缺点,也是一个不错的选择。

取消订阅

unsubscribe [channel [channel...]]

客户端可以通过 unsubscribe 命令取消对指定频道的订阅,取消成功后不会再收到该频道的发布消息。

按照模式订阅和取消订阅

psubscribe pattern [pattern...]

punsubscribe pattern [pattern...]

这两种命令支持 glob 风格,例如订阅所有以 it 开头的频道:psubscribe it*

查询订阅

查看活跃的频道:pubsub channels [pattern],活跃频道是指当前频道至少有一个订阅者。

查看频道订阅数:pubsub numsub [channel ...]

查看模式订阅数:pubsub numpat


使用场景

聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,以服务器解耦为例:视频管理系统负责管理视频信息,用户通过各种客户端获取视频信息。

假如视频管理员在视频管理系统中对视频信息进行了更新,希望及时通知给视频服务端,就可以采用发布订阅模式,发布视频信息变化的消息到指定频道,视频服务订阅这个频道及时更新视频信息,通过这种方式实现解耦。

视频服务订阅 video:changes 频道:

1
subscribe video:changes

视频管理系统发布消息到 video:changes 频道:

1
publish video:changes "video1,video3,video5"

视频服务收到消息,对视频信息进行更新..


总结

慢查询中有两个重要参数 slowlog-log-slower-than 和 slowlog-max-len。

慢查询不包括命令网络传输和排队时间。

有必要将慢查询定期存放。

Pipeline 可以有效减少 RTT 次数,但每次 Pipeline 的命令数量不能无节制。

Redis 可以使用 Lua 脚本创造出原子、高效、自定义命令组合。

Bitmaps 可以用来做独立用户统计,有效节省内存。

Bitmaps 中 setbit 一个大的偏移量,由于申请大量内存会导致阻塞。

HyperLogLog 虽然在统计独立总量时存在一定误差,但是节省的内存量十分惊人。

Redis 的发布订阅相比许多专业消息队列系统功能较弱,不具备息堆积和回溯能力,但胜在足够简单。

Redis 3.2 提供了 GEO 功能,用来实现基于地理位置信息的应用,底层实现是 zset。

分布式

Redis实现分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

img

使用SETNX完成同步锁的流程及事项如下:

使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间

释放锁,使用DEL命令将锁数据删除

缓存异常

缓存穿透

描述:

​ 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

缓存雪崩

  1. 针对 Redis 服务不可用的情况:

    1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
    2. 限流,避免同时处理大量的请求。

    针对热点缓存失效的情况:

    1. 设置不同的失效时间比如随机设置缓存的失效时间。
    2. 缓存永不失效。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

另外,这里多说一嘴,一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值

如果用 Java 代码展示的话,差不多是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object getObjectInclNullById(Integer id) {
// 从缓存中获取数据
Object cacheValue = cache.get(id);
// 缓存为空
if (cacheValue == null) {
// 从数据库中获取
Object storageValue = storage.get(key);
// 缓存空对象
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
// 必须设置过期时间,否则有被攻击的风险
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
}Copy to clipboardErrorCopied

2)布隆过滤器

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器之后的缓存处理流程图如下。

image

但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!

我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)

更多关于布隆过滤器的内容可以看我的这篇原创:《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
**缓存穿透**是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

**解决方案**

1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

**附加**

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐)

就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

is 对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。

优点 缺点
直连 简单方便,适用于少量长期连接的场景。 存在每次连接关闭 TCP 连接的开销,资源无法控制可能出现连接泄露,Jedis 对象线程不安全
连接池 无需每次连接都生成 Jedis 对象降低开销,使用连接池的形式保护和控制资源的使用 相对于直连比较麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题

持久化

快照(snapshotting)持久化(RDB)

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。默认的文件名为dump.rdb。

1、save触发方式

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:

img

执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都是几万或者是几十万,这种方式显然不可取。

2、bgsave触发方式

执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体流程如下:

img

具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。

3、自动触发

自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

1
2
3
4
5
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。Copy to clipboardErrorCopied

img

AOF(append-only file)持久化

与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

1
appendonly yesCopy to clipboardErrorCopied

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

1
2
3
appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步Copy to clipboardErrorCopied

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

RDB 的优势和劣势

①、优势

(1)RDB,全量备份,非常适合用于进行备份和灾难恢复。

(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

②、劣势

RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

AOF 的优势和劣势

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。

(2)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

5、缺点

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

相关 issue783:Redis 的 AOF 方式


其他问题

事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。

MULTI
OK
INCR foo
QUEUED
INCR bar
QUEUED
EXEC

1) (integer) 1
2) (integer) 1
Copy to clipboardErrorCopied
使用 MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令。

Redis官网相关介绍 https://redis.io/topics/transactions 如下:

redis事务

但是,Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: 1. 原子性,2. 隔离性,3. 持久性,4. 一致性。

原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。

Redis官网也解释了自己为啥不支持回滚。简单来说就是Redis开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

redis roll back

你可以将Redis中的事务就理解为 :Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

Redis线程模型

文件事件处理器包括分别是套接字、 I/O 多路复用程序、 文件事件分派器(dispatcher)、 以及事件处理器。使用 I/O 多路复用程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
工作原理:
1)I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
尽管多个文件事件可能会并发地出现, 但 I/O 多路复用程序总是会将所有产生事件的套接字都入队到一个队列里面, 然后通过这个队列, 以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字: 当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字。如果一个套接字又可读又可写的话, 那么服务器将先读套接字, 后写套接字.
在这里插入图片描述

淘汰策略

redis 提供 6种数据淘汰策略:

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中随机移除key
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错,无法写入新数据,一般不采用

Redis与Memcached的区别

两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:

对比参数 Redis Memcached
类型 1. 支持内存 2. 非关系型数据库 1. 支持内存 2. 键值对形式 3. 缓存形式
数据存储类型 1. String 2. List 3. Set 4. Hash 5. Sort Set 【俗称ZSet】 只支持简单的key-value
持久化支持 1. RDB 2. AOF 不支持
集群模式 原生支持 cluster 模式,可以实现主从复制,读写分离 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
内存管理机制 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘 Memcached 的数据则会一直在内存中,Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
  • 存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性。
  • 数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,,而redis支持五种数据类型。
  • 用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
  • value的大小:\redis可以达到1GB,而memcache只有1MB**

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

Redis的IO复用

Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

既然是单线程,那怎么监听大量的客户端连接呢?

Redis 通过IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

这样的好处非常明显: I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。

另外, Redis 服务器是一个事件驱动程序,服务器需要处理两类事件: 1. 文件事件; 2. 时间事件。

时间事件不需要多花时间了解,我们接触最多的还是 文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。

《Redis 设计与实现》有一段话是如是介绍文件事件的,我觉得写得挺不错。

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

可以看出,文件事件处理器(file event handler)主要是包含 4 个部分:

  • 多个 socket(客户端连接)
  • IO 多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将 socket 关联到相应的事件处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

img

gossip?

gossip协议包含多种消息,包括ping,pong,meet,fail,等等

Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:

  • Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
  • Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
  • Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。
  • Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。

img

redis键的删除策略

问题:如果一个键过期了,那么它什么时候会被删除呢?有三个答案

1
定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在过期时间来临时,立即执行对键的删除操作 .。

这种过期策略对内存友好,但对CPU是非常不友好的。因为还要主动设定定时器并主动删除,即通过CPU换内存(CPU主动删除了内存中的数据嘛,CPU忙,内存空闲)。可以想象一下在大数据大并发的时候,这种机制是多么灾难

1
惰性删除(Lazy Expiration):redis内部不会监视记录是否过期,但是每次从键空间获取键时,检查记录是否过期。这种技术被称为lazy(惰性)expiration。

与定时删除相反,此删除机制对内存是不友好的对CPU是友好的,惰性删除不会主动去删除已到期(expire)的键,而是等有需要调用的时候再判断,即内存换CPU(内存中有大量残留数据)

1
定期删除:每隔一段时间,对redis数据库进行一次检查,删除里面的过期键

属于前两种方法的折中.但要注意操作检查执行的时间和频率,如果删除操作做的太频繁,或者检查执行的时间太长,定期策略就会退化成定时策略。

Redis服务器实际使用的是惰性删除和定期删除策略两种策略,配合使用两种策略和在使用CPU和内存找到平衡点。

总结

I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。