Redis提供了5种数据结构, 理解每种数据类型的特点对于Redis开发运维非常重要, 同时掌握每种数据类型的常见命令, 会在使用Redis的时候做到游刃有余. 内容如下:
预备知识: 几个全局命令, 数据结构和内部编码, 单线程机制解析.
5种数据类型的特点, 命令使用, 应用场景示例.
键遍历, 数据库管理.
话不多说, 我们来开始认识一下吧.
预备知识
在正式介绍5种数据类型之前, 了解一下Redis的全局命令, 数据类型和内部编码, 单线程命令的处理机制是十分重要的.
主要体现在两个方面:
(1)Redis的命令有上百个, 如果纯靠死记硬背比较困难, 但是如果理解Redis的一些机制, 会发现这些机制有很强的通用性.
(2)Redis不是万金油, 有些数据结构的命令必须在特定场景下使用, 一旦使用不当可能对Redis本身或者应用本身造成致命伤害.
基本全局命令
最最最最基本的一定要记下了, 否则哥们就这么说吧, 你不知道这相当于没学redis.
set key value; (设定键值对, key是String类型的, value可以是多种数据类型).
get key; (获得键对应的值).
KEYS
返回满足样式的(pattern)的key. 支持如下统配样式.
h?llo 匹配hello, hallo, hxllo(?表示的是任意一个字符)
h*llo 匹配hllo, heeeeeello(*匹配的是任意数目的字符,包括0)
h[ae]llo 匹配hallo, hello(匹配的是a或e)
h[^e]llo 匹配hallo, hbllo...... 但不匹配hello(表示不匹配什么)
h[a-e] 匹配hallo, hbllo...... (匹配的是a-e范围内的字符)
语法:
keys pattern
时间复杂度: O(N)
返回值: 匹配pattern的所有key
使用展示:
特别注意, 一定要谨慎使用形如keys *这种查询的数据体量非常大的命令, 它可能会导致奔溃!
特别注意, 一定要谨慎使用形如keys *这种查询的数据体量非常大的命令, 它可能会导致奔溃!
特别注意, 一定要谨慎使用形如keys *这种查询的数据体量非常大的命令, 它可能会导致奔溃!
EXISTS
判断某个key是否存在.
语法:
exists key [key....]
时间复杂度O(1)
返回值: key存在的个数.
使用展示:
注:第二条hcllo, hdllo不存在, 因此只返回了三个.
DEL
删除指定的key.
语法:
DEL key [key...]
时间复杂度:O(1)
返回值: 删除掉key的个数.
使用展示:
EXPIRE
为指定的key添加秒级的过期时间(Time To Live TTL)
语法:
EXPIRE key seconds
时间复杂度: O(1)
返回值: 1表示设置成功. 0表示设置失败.
使用展示:
TTL
获取指定的key过期时间, 秒级.
语法:
TTL key
时间复杂度: O(1)
返回值: 剩余过期时间. -1表示没有关联过期时间, -2表示key不存在
EXPIRE和TTL命令都有对应的支持毫秒为单位的版本: PEXPIRE, PTTL, 详细用法不再展示.
关于键的过期机制, 如图所示:
TYPE
返回key对应的数据类型.
语法:
TYPE key
时间复杂度O(1)
返回值: none, string, list, set, zset, hash and stream.
使用展示:
数据结构和内部编码
type命令实际返回的就是当前键的数据类型, 它们分别是:string(字符串), list(列表), hash(哈希), set(集合), zset(有序集合), 但这些只是Redis对外的数据类型.
实际上Redis针对每种数据结构都有自己的底层内部代码实现, 而且是多种实现, 这种Redis会在合适的场景选择合适的内部编码.(我保证一定有这些数据结构的效果, 但我不一定这样实现).
raw: 长字符串. int: 整数. embstr: 短字符串.
hashtable: 最基本的哈希表. ziplist:当元素相对较少时, 会使用列表
linkedlist: 链表. intset: 只存整数的集合
skiplist:跳表, 类似于链表, 但每个结点上有多个指针,巧妙地搭配这些指针域的指向, 就可以做到从跳表上查询元素的时间复杂度是O(logN).
注: 从redis3.2开始, 引入了新的实现方式 ->quicklist. 同时兼顾了linkedlist和ziplist的优点, quicklist就是一个链表, 每个元素又是一个ziplist, 把空间和效率都折衷地兼顾到.
可以看到每种数据结构都有至少两种以上的内部编码实现, 例如list数据结构包含了linkedlist和ziplist两种内部编码. 同时有些内部编码, 例如ziplist, 可以作为多种数据结构的内部实现, 可以通过object encoding命令查询内部编码:
可以看到hello对应值的内部编码是embstr, 键mylist对应值的内部编码是quicklist.
Redis这样设计有两个好处:
(1)可以改进内部编码, 而对外的数据类型和命令没有任何影响, 这样一旦开发出更优秀的代码, 无需改动外部数据类型和指令.
(2)多种内部编码实现可以在不同场景下发挥各自的优势. 比如在hash中, 数据量庞大可以使用hashtable提高性能, 而数据量比较小的情况下, 可以使用ziplist以节省内存.
单线程架构
Redis使用了单线程架构来实现高性能的内存数据库服务.
引出单线程模型
现在开启了三个redis-cli客户端同时执行命令.
客户端1设置一个字符串键值对:
127.0.0.1:6379> set hello world
客户端2对counter做自增操作:
127.0.0.1:6379> incr counter
客户端3对counter做自增操作:
127.0.0.1:6379> incr counter
我们已经知道了从客户端发送的命令经历了: 发送命令, 执行命令, 返回结果三个阶段, 其中我们重点关注第2步. 我们所谓的Redis是采用单线程模型执行命令是指: 虽然三个客户端看起来是同时要求Redis去执行命令的, 但微观角度, 这些命令还是使用线性方式去执行的, 只是原则上命令的执行顺序是不确定的.
这就好比上高中时, 中午准备放学, 大家迫不及待地去食堂干饭, 下课铃一响, 大家都会同时向食堂"冲锋"(客户端同时要求Redis去执行命令的), 虽然是同时去, 但实际到了餐厅后还是要在 窗口排队(微观角度,用线性方式去执行的).
宏观(同时要求):
围观(有次序地执行):
Redis的单线程模型:
为什么单线程这么快?
通常来讲, 单线程处理能力要比多线程差, 例如有10000公斤货物, 每辆车的运载能力是每次200公斤, 那么要50次才能完成; 但是如果有50辆车, 只要安排合理, 只需要依次就可以完成任务.那么为什么Redis使用单线程模型会达到每秒万级别的处理能力呢? 有以下原因:
1.redis访问内存, 其它数据库访问硬盘
2.redis核心功能, 比其它数据库的核心功能简单 (数据库对于数据的插入删除查询等都有更复杂的功能支持. 这样的功能势必要花费更大的开销. 比如针对插入删除, 数据库的各种约束, 都会使数据库额外工作, redis因为活少, 因此功能提供的比mysql少, 但够用).
3.避免了不必要的竞争开销(它的每个基本操作都是短平快, 不是什么特别消耗CPU的操作).
4.处理网络IO的时候, 使用了epoll这样的IO多路复用的机制.
什么是IO的多路复用?
简单来说, 就是通过一个线程, 管理多个socket.
针对TCP来说, 服务器这边每次要服务一个客户端,都要安排一个socket.
一个服务器服务多个客户端, 同时就有很多个socket, 这些socket上都是无时不刻地在传输数据吗?
不是的, 很多情况下, 每个客户端和服务器之间的通信也没有那么频繁. 此时这么多socket大部分时间都是静默的, 上面是没有数据需要传输的. (同一时刻, 只有少数socket是活跃的).
最开始介绍TCP服务器的时候, 有个版本就是给每个客户端分配一个线程, 客户端多了, 线程多了, 系统开销就大了.
这时, 就可以通过IO多路复用, 用一个线程处理多个socket.
那么怎么通过epoll实现IO多路复用呢?
epoll是目前IO多路复用最高效的机制.
它的工作就类似比如说, 我晚上去小吃摊买饺子, 米线, 煎饼果子三样食物. 由于制作需要时间,因此买完不能直接拿. 而最低效的方法就是买一个等一个, 这样有点慢了. 我换了一个方法, 先把三个都一点, 然后哪个好了我去取(老板叫我取就类似epoll事件通知机制). 这个显然要高效地多. 此时就能让我一个线程同时做到三件事(有一个前提: 这三件事都不频繁, 大部分时间都在等).
虽然单线程给Redis带来很多好处, 但还有一个致命的问题: 对于单个命令的执行时间都是有要求的. 如果某个命令时间过长, 会导致其它命令全部处于等待队列种, 迟迟等不到响应, 造成客户端阻塞, 这对于Redis这种高性能服务来说是非常严重的, 所以Redis是面向快速执行场景的数据库.