Redis专题:基础知识点

1,683 阅读15分钟

微信搜索公众号“码路印记”,点关注不迷路!

从本文开始我将开启Redis专题,逐步整理一些Redis的知识要点并分享出来,目前整理了以下要点。今天先分享第一篇,主要是Redis的一些基本知识点,也是通过这篇文章来挖掘需要完善Redis的知识体系。 image.png

Redis是什么?

Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库,其具备如下特性:基于内存运行,性能高效;支持分布式,理论上可以无限扩展;key-value存储系统;开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

相比于其他数据库类型,Redis具备的特点是:C/S通讯模型、单进程单线程模型、丰富的数据类型、操作具有原子性、持久化、高并发读写、支持lua脚本。

由于Redis出色的性能表现,全世界各大厂都在拥抱与使用,如github、twitter、阿里巴巴、百度,并且一些大厂在开源版本的基础上继续深度开发与定制。当然,Redis使用如此广泛,它也成为了我们开发者的必备技能,日常工作与面试已经无法无视它的存在。

Redis数据类型及操作指令

Redis常用的数据类型有String、Hash、List、Set、SortedSet,一般来讲熟练使用这些数据类型及操作指令可以满足我们绝大多数的工作场景。除此之外,Redis还支持一些高级的类型,比如HyperLogLog、Geo、Pub/Sub等。

本文对常用的五种数据类型及指令进行简单总结,若有未列出的指令,大家可以到redis.io/commands#进行查询。

String

它是一个二进制安全的字符串,意味着它不仅能够存储字符串、还能存储图片、视频等多种类型, 最大长度支持512M。以下列举了String类型常用的指令及使用示例。

指令用途
SET key value设置指定 key 的值
GET key获取指定 key 的值。
GETSET key value将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
MGET key1 [key2..]获取所有(一个或多个)给定 key 的值。
SETEX key seconds value将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。
SETNX key value只有在 key 不存在时设置 key 的值。
MSET key value [key value...]同时设置一个或多个 key-value 对。
INCR key将 key 中储存的数字值增一。
INCRBY key increment将 key 所储存的值加上给定的增量值(increment) 。
DECR key将 key 中储存的数字值减一。
DECRBY key decrementkey 所储存的值减去给定的减量值(decrement) 。
DEL key [key1..]删除给定的key,返回被删除的数量

String类型是简洁的key-value结构,也是我们开发中最常用的一种类型,使用方式类似于Java中的Map类型。而且,Redis提供了对数值类型的自增、自减指令,可以使用它来实现计数器、发号器等功能。

Hash

该类型是由field和关联的value组成的map,其中,field和value都是字符串类型的。Hash类型与关系型数据库的表记录类似,key为主键,field为列名,value为列对应的值,通过Hash我们可以描述一个对象的多个特征。Hash的操作命令如下:

指令用途
HDEL key field1 [field2]删除一个或多个哈希表字段
HEXISTS key field查看哈希表 key 中,指定的字段是否存在。
HGET key field获取存储在哈希表中指定字段的值。
HGETALL key获取在哈希表中指定 key 的所有字段和值
HINCRBY key field increment为哈希表 key 中的指定字段的整数值加上增量 increment 。
HINCRBYFLOAT key field increment为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
HKEYS key获取所有哈希表中的字段
HLEN key获取哈希表中字段的数量
HMGET key field1 [field2]获取所有给定字段的值
HMSET key field1 value1 [field2 value2 ]同时将多个 field-value (域-值)对设置到哈希表 key 中。
HSET key field value将哈希表 key 中的字段 field 的值设为 value 。
HSETNX key field value只有在字段 field 不存在时,设置哈希表字段的值。
HVALS key获取哈希表中所有值。
HSCAN key cursor [MATCH pattern] [COUNT count]迭代哈希表中的键值对。

Hash类型更加贴近于面向对象的概念,通过key确定存储的对象,然后以“field-value”键值对从多个维度来描述对象,每个Hash可以存储2^32-1个键值对,大约有40多亿个。

List

该类型是一个插入顺序排序的字符串元素集合, 基于双链表实现。一个列表最多可以包含2^32-1个元素。List的操作命令如下:

指令用途
BLPOP key1 [key2 ] timeout移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
RPUSH key value1 [value2]在列表中添加一个或多个值
RPUSHX key value为已存在的列表添加值
BRPOP key1 [key2 ] timeout移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
LINDEX key index通过索引获取列表中的元素
LLEN key获取列表长度
LPOP key移出并获取列表的第一个元素
LINSERT key BEFOREAFTER pivot value在列表的元素前或者后插入元素
LPUSH key value1 [value2]将一个或多个值插入到列表头部
LPUSHX key value将一个值插入到已存在的列表头部
LRANGE key start stop获取列表指定范围内的元素
LREM key count value移除列表元素
LSET key index value通过索引设置列表元素的值
LTRIM key start stop对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
RPOP key移除列表的最后一个元素,返回值为移除的元素。
RPOPLPUSH source destination移除列表的最后一个元素,并将该元素添加到另一个列表并返回
BRPOPLPUSH source destination timeout从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

List使用双向链表来实现,类似队列结构,队头在左边,队尾在右边。所以,RPUSH相当于在队尾添加元素,LPOP是在对头取出元素。基于此特性,我们可以使用List作为异步队列。如下图所示: image.png

Set

Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。Set类型的底层是通过哈希表实现的,其操作命令为:

指令用途
SADD key member1 [member2]向集合添加一个或多个成员
SCARD key获取集合的成员数
SDIFF key1 [key2]返回第一个集合与其他集合之间的差异。
SDIFFSTORE destination key1 [key2]返回给定所有集合的差集并存储在 destination 中
SINTER key1 [key2]返回给定所有集合的交集
SINTERSTORE destination key1 [key2]返回给定所有集合的交集并存储在 destination 中
SISMEMBER key member判断 member 元素是否是集合 key 的成员
SMEMBERS key返回集合中的所有成员
SMOVE source destination member将 member 元素从 source 集合移动到 destination 集合
SPOP key移除并返回集合中的一个随机元素
SRANDMEMBER key [count]返回集合中一个或多个随机数
SREM key member1 [member2]移除集合中一个或多个成员
SUNION key1 [key2]返回所有给定集合的并集
SUNIONSTORE destination key1 [key2]所有给定集合的并集存储在 destination 集合中
SSCAN key cursor [MATCH pattern] [COUNT count]迭代集合中的元素

Set类型主要应用在某些场景,如社交场景中,通过交集、并集和差集运算,通过Set类型可以非常方便地查找共同好友、共同关注和共同偏好等社交关系。

SortedSet

SortedSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。与Set类型一样,其底层也是通过哈希表实现的。Sorted Set的常用指令及说明见下表:

指令用途
ZADD key score1 member1 [score2 member2]向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key获取有序集合的成员数
ZCOUNT key min max计算在有序集合中指定区间分数的成员数
ZINCRBY key increment member有序集合中对指定成员的分数加上增量 increment
ZRANGE key start stop [WITHSCORES]通过索引区间返回有序集合指定区间内的成员
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]通过分数返回有序集合指定区间内的成员
ZSCORE key member返回有序集中,成员的分数值

SortedSet的特点是内部元素唯一、有序,通过以上指令对数据进行操作可以保持这一特性,我们可以使用它来实现排行榜、延迟队列等功能。

Pub/Sub

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息,可以实现1:N的消息队列。如下图所示: image.png 但是如果订阅者全部下线了,发布者发布的所有消息都会丢失,Pub/Sub并没有提供消息的持久化能力(Redis 5.0提供了Redis Stream弥补了这一缺点)。常用的指令及说明如下:

指令作用
PUBLISH channel message将信息发送到指定的频道。
SUBSCRIBE channel [channel ...]订阅给定的一个或多个频道的信息。
UNSUBSCRIBE [channel [channel ...]]指退订给定的频道。

Redis为什么这么快?

这是一个常见的面试题,回答它之前我们需要先知道Redis到底有多块。Redis官方文章《How fast is Redis?》给出了基准测试数据,由此可知Redis的性能受网络带宽、CPU、物理机/虚拟机、数据大小、客户端连接数等因素的影响。下图来自该文章,数据显示了在高端配置下Redis QPS受连接数影响的情况(横轴为连接数,纵轴为QPS),在连接数小于5000时,QPS可以达到10万+。 image.png 再来看下Redis为什么这么快:

  • Redis是纯内存数据库,一般都是简单的存取操作,线程占用时间短,时间主要花费在IO上。
  • 优秀的数据结构:为了追求高性能,Redis的底层数据结构是专门设计的,结构简单,操作时间复杂度多为O(1)。
  • 单线程模型:请求命令处理使用单线程,保证了每个操作的原子性,避免了不必要的线程间上下文切换与竞争;
  • 使用非阻塞多路复用I/O技术,I/O请求处理高效。(关于多路复用以后单独介绍)

为什么说Redis是单线程的?

先来看下Redis官方是如何回答的:

It's not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, using pipelining Redis running on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.

简单翻译下:CPU成为Redis瓶颈的情况并不常见,Redis通常是受内存或网络限制的。例如,在一般的Linux系统上使用Pipeline,Redis每秒可以处理100万个请求,如果你的应用主要使用O(N)或者O(log(N))的命令,那么它几乎不会占用太多的CPU。

总结一下就是:单线程已经够快了,为啥还要用多线程呢!哈哈哈。。。

其实说Redis是单线程并不严谨,所谓单线程只是Redis在处理网络请求时使用了单线程。由于使用了非阻塞、多路复用的I/O技术,几乎纯内存无延迟的操作,使用单个线程足以满足使用需求。但是,这并不是说Redis内只有一个线程,它还有其他线程来完成辅助工作,比如:数据过期策略、内存淘汰机制、主从复制等都有相关职责的线程。

“单线程机制”为Redis的快提供了强有力的支撑,这是它的一大优势。但是现在我们的服务器动不动就8核、16核、32核,Redis只使用一个线程就没有办法充分利用硬件资源了。为了提供硬件资源利用率,我们可以在一台机器部署多个Redis实例,或者使用虚拟化技术/Docker技术对物理资源再分配。

Redis数据过期策略及内存淘汰机制

Redis具有出色的性能,很大一部分原因是它使用内存作为数据存储,以及Redis开发者对其内部数据结构的精益设计。Redis可用内存的大小除了受物理内存空间限制外,还受系统参数的影响。如下maxmemory代表了Redis最大占用内存字节数:

# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
maxmemory 268435456

Redis可用的内存资源是有限的,为了充分利用内存资源,同时提供高性能的服务,Redis制定了一套完善的数据过期处理策略及内存淘汰机制。数据过期处理策略的目标是那些设置了过期时间且已经过期的数据;内存淘汰机制是指当Redis内存不够用时,为了保证可用性而采取的保障机制。

针对过期数据,Redis并没有简单粗暴的使用定时删除方式,而是采用了“定期清理+惰性删除策略”。来看下原因:

  • 定时删除:用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略。
  • 定期清理+惰性删除:Redis默认每隔100ms检查,是否有过期的key,有过期key则删除。需要说明的是,Redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,Redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是,惰性删除派上用场。当我们获取某个key的时候,Redis会检查一下这个key如果设置了过期时间是否过期了,如果过期了此时就会删除。

仔细想下,“定期清理+惰性删除策略”并不是万无一失的。如果定期删除没删除key,然后也没及时去请求key,也就是说惰性删除也没生效。这样就会导致Redis的内存会越来越高,这时就到内存淘汰机制发挥作用了。Redis提供了以下六种内存淘汰机制:

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据,新写入操作会报错

从上面的描述可以知道:volatile-*的策略针对的是那些设置了过期时间的数据集,如果所有写入的数据都没有设置过期时间,即使设置了这些策略没有什么用;all-*的策略针对的是所有的数据集,如果删除了未过期的数据可能会影响生产安全;no-enviction则会导致Redis无法对外提供新的写入服务,非常不友好。

所以,我们在开发中需要仔细评估是否为数据设置过期时间,并选择合适的内存淘汰策略。在我们公司对Redis使用有着严格的规范:所有数据必须设置过期时间,不能把Redis当成持久化数据库来使用,Redis内所有数据必须可以重建。

我们可以通过配置文件为Redis设置合适的内存淘汰机制:

# 淘汰策略:已设置过期中最近最少使用
maxmemory-policy volatile-lru

总结

本文内容多是理论性的基础知识,也有一些面试中的高频问题,其实Redis的基础远远不止这些,通过后面的文章慢慢补充!

如果觉得对你有用,请分享给需要的朋友!点关注不迷路,微信搜索公众号“码路印记”!