1 Redis中最核心的两个命令
get 根据key来取value
set 把key value 存储进去

对于上述的key value 不需要加上引号,就是表示字符串的类型(加上引号也是可以的)

如果当前key不存在会返回 nil ,和null是一个意思
2. Redis 全局命令
Redis 支持很多数据结构,整体上来说,Redis是键值对结构,key固定就是字符串,value实际上会有很多种类型(字符串、哈希表、列表、集合、有序集合......)操作不同的数据结构就会有不同的命令
2.1 KEYS
KEYS pattern
用来查询当前服务器上匹配的key
通过一些特殊符号(通配符)来描述key的摸样,匹配上述模样的key就能被查询出来
pattern 包含特殊符号的字符串
pattern 支持哪些通配符


注意事项:
KEYS 命令的时间复杂度是O(N)
所以在生产环境下,一般都会进制使用 KEYS 命令,尤其是KEYS * (查询Redis中所有的key)
生产环境上的 Key 可能会非常多,而Redis是一个单线程的服务器,执行 KEYS * 的 时间非常长,就使Redis服务器被阻塞,无法给其他客户端提供服务了
Redis经常会用于做缓存,挡在MySQL前面,如果Redis被阻塞了,此时其他查询Reids的操作就会超时,此时这些请求就会直接查询数据库,MySQL容易挂掉
2.2 EXISIT
判断某个key是否存在
EXISTS key key ...
时间复杂度是:O(1)
返回值是 Key 存在的个数
Redis 支持很多数据结构,指的是一个 value 可以是一个复杂的数据结构
Redis 自身的这些键值对,是通过哈希表的方式来组织的
Redis 具体的某个值,又可以是一些数据结构
"EXISTS hello hallo" 和 " EXISTS hello EXISTS hallo " 两者的区别
Reids 是一个客户端 服务器 结构的城西,客户端和服务器之间是通过网络进行通信的
分开的写法会产生更多轮粗的网路通信(效率比较低,成本比较高)封装和分用
2.3 DEL
删除指定的 key
DEL key key....
时间复杂度:O(1)
返回值:删除掉的key个数
之前学MySQL的时候,强调删除类的操作是非常危险的,一旦删除后数据就没了
Redis主要的应用场景就是作为缓存,此时Redis里村的只是一个热点数据,全量数据在MySQL数据库里,此时如果把Redis中的 key 删除几个,一般来书问题不大
但是,如果把所有的数据或者一大半的数据都删除,影响会很大,容易使MySQL挂掉
如果把Redis作为数据库,删除数据的影响大
如果把Redis作为消息队列,这种情况删除数据的影响大不大,就需要具体问题具体分析
2.4 EXPIRE
为指定的key添加秒级的过期时间
EXPIRE key seconds
返回值:1表示设置成功,0表示设置失败
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
2.4 TTL
获取指定key的过期时间,秒级
TTL key
返回值:剩余过期时间, -1表示没有关联过期时间,-2表示key不存在

Redis 的 key 的过期策略怎么实现的?
一个Redis中可能会同时存在很多很多的key,这些key中可能很大一部分都有过期时间,此时Redis 服务器如何知道哪些key已经过期要被删除了,哪些key还没过期
如果直接遍历所有的key,显然是想不通的,效率非常低
Redis 整体的策略是
- 定期删除
每次抽取一部分,进行验证过期时间,保证这个抽取检查的过程足够快(单线程)
- 惰性删除
假设这个key已经到了过去时间了,但是暂时还没删它,key还存在,下一次访问正好用到了这个key,于是这次访问就会让Redis 服务器触达删除 key 操作,同时返回一个nil
虽然有了上述两种策略结合,整体效果一般,仍然可能会有很多过期的key被残留,没有即使删除掉
Redis 为了对上述进行补充,还提供了一系列的内存淘汰策略
注意:Redis 中没有采取定时器的方式来实现过期key删除
拓展:定时器
在走个时间到达之后,执行特定的任务
- 基于优先级队列/堆
正常的队列是先进先出,优先级队列则是按照指定的优先级先出
自定义优先级
在Redis过期key的场景中,就可以通过"过期时间越早就是优先级越高"
队首元素就是最早要过期的key
此时定时器只有分配一个线程,让这个线程去检查队首元素,查看是否过期(避免忙等)
- 基于时间轮实现的定时器
把时间划分成很多小段(划分的力度,看实际需求)
Redis源码中,有一个比较核心的机制是 事件循环
2.5 TYPE
返回key对应value的数据类型
时间复杂度:O(1)
返回值:none、string、list、set、zset、hash、stream(redis作为消息队列时使用的数据类型)
在Redis中上述类型操作方法差别很大
redis> SET key1 "value"
"OK"
redis> LPUSH key2 "value"
(integer) 1
redis> SADD key3 "value"
(integer) 1
redis> TYPE key1
"string"
redis> TYPE key2
"list"
redis> TYPE key3
"set"
3. 数据结构和内部编码

有序集合,相当于除了存储member之外,还需要存储一个score(权重/分数)
Redis底层在实现上述数据结构的时候,会在源码层面进行特定的优化,来达到节省空间、节省时间效果
Redis会自适应,使用时是感知不到的

根据值的类型 和长度自动选择最优方案
raw:最基本的字符串(底层就是持有一个char数组或者byte数组)
int:Reids通常也可以用来实现一些'计数'这样的功能,当value就是一个整数时,Redis可能会直接使用int来保存
embstr:针对短字符串进行的特殊优化

hashtable:最基本的哈希表,Redis内部哈希表的视线(实现方法可能不太一样,思路一样)
ziplist:在哈希表里面元素比较少的时候,可能优化成ziplist,压缩列表节省空间
在 Redis 7.0 之后**,ziplist 已经被淘汰了,取而代之的是 listpack**
Redis上有很多key,可能某些key的value是hash,此时如果key特别多,对于的hash也特别多,但是每个hash又不大的情况下,就尽量去压缩,压缩之后让整体占有的空间更小

lingkedlsit:链表
ziplist:压缩链表
在Redis3.2 开始引入新的视线方法 quiclist,同时兼顾了linkedlist和ziplist的优点
quicklist就是一个双向链表,每个元素又是一个ziplist

intset:集合中存的都是数字
skiplist:跳表
可以通过 obkect encoding 命令查询内部编码
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> lpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist"
Reids这样设计的好处
-
可以改进内部编码,而对外的数据结构和命令没有任何影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令
-
多种内部编码实现可以在不同场景下发挥各自的优势
4. 单线程架构
Redis只使用一个线程处理所有的命令请求,不是说Rdis服务器进程内部真的就只有一个线程,其实也有很多个线程,多线程处理网络IO
多个请求同时到达Redis服务器,也是要在队列中排队,再等待Redis服务器一个一个取出来吗的命令执行,微观上讲,Redis服务器是串行/顺序执行多个命令的
Redis能够使用单线程很好的工作,原因主要在于Redis的核心业务逻辑都是短平快的,不太消耗cpu资源,也就不太吃多核了
为什么单线程还是很快:
-
纯内存访问
-
Redis核心功能比数据可的核心功能更简单
-
多线程避免了线程切换和竞争产生的消耗
-
非阻塞IO:Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型 将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间

一个程线程就可以管理多个socket,针对TCP来说,服务器这边每次要服务的一个客户端,都需要给这个客户端安排一个socket
一个服务器多个客户端,同事就有很多个socket,很多情况下,每个客户端和服务器之间的通信没那么频繁,此时这么多socket大部分时间都是静默的
5. String字符串
5.1 介绍
Redis 所有的key都是字符串
Redis中的字符串直接就是按照二进制数据的方式存储的(不存在任何的编码转换,存的是什么,取出来的就是什么)
不仅仅可以存储文本数据,还可以存储整数、普通的文本字符串、JSON、xml、二进制数据(图片、视频、音频...)
音频、视频....的体积可能会比较大,Redis对于String类型,限制了大小最大是512M
Redis单线程模型,希望进行的操作都能比较快速
5.2 常见命令
SET
SET key value expiration EX seconds \| PX milliseconds NX \| XX
时间复杂度:O(1)
选项:
EX seconds------ 使用秒作为单位设置key的过期时间
PX milliseconds------ 使用毫秒作为单位设置key的过期时间
NX------ 只在key不存在时才进行设置
XX------ 只在key存在时才进行设置
返回值:设置成功返回OK,如果SET指定了NX或XX,但条件不满足,返回nil


GET
如果value的数据类型不是string,会报错
GET key
时间复杂度:O(1)

MGET
一次性获取多个key的值,如果对于的key不存在或者对应的数据不是string,返回nil
MGET key key ...
MSET
一次性设置多个key的值
MSET key value key value ......
使用MGET/MSET 可以有效减少网络时间,性能较高

设置过多可能会导致阻塞
SETNX
SETXX
SETEX
SETPX
5.3 计数命令
INCR
将key对应的string表示的数字加一。如果key不存在,则视为key对应的value是0,如果key对应的string不是一个整型或者范围超过64位有符号整型,则报错
INCR key
返回值:integer类型的加完后的数值

INCRBY
将key对应的string表示的数字加上对应的值。如果key不存在,则视为key对应的value是0,如果key对应的string不是一个整型或者范围超过64位有符号整型,则报错
INCRBY key decrement
返回值:integer类型的加完后的数值
DECR
DECR key
将key对应的string表示的数字减一。如果key不存在,则视为key对应的value是0,如果key对应的string不是一个整型或者范围超过64位有符号整型,则报错
DECRBY
DECR key decrement
将key对应的string表示的数字减去对应的值。如果key不存在,则视为key对应的value是0,如果key对应的string不是一个整型或者范围超过64位有符号整型,则报错
INCRBYFLOAT
INCRBYFLOAT key increment
将key对应的string表⽰的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果 key 不存在,则视为key对应的value是0。如果key对应的不是string,或者不是⼀个浮点数,则报 错。允许采⽤科学计数法表⽰浮点数
5.4 其他命令
APPEND
如果key已经存在并且是一个string,命令会将value追加到原有string的后边,如果key不存在,效果等同于SET命令
APPEND key value
时间复杂度:O(1)
返回值:追加完成后string的长度(单位是字节)

GETRANGE
返回key对于的string淄川,由start和end确定(左闭右闭)可以使用负数表示倒数。-1代表倒数第一个字符,-2表示倒数第二个。超过范围的偏移量会根据string的长度调整成正确的值
GETRANGE key start end
时间复杂度:O(N),区间长度,可以视为O(1)
返回值:string类型的子串
如果字符串保存的是汉字,此时进行子串切分,很可能切出来不是完整的汉字了,切出的结果在utf8码表上不知道会查出来啥
SETRANGE
覆盖字符串的一部分,从指定的偏移开始
SETRANGE key offset value
时间复杂度:O(N),value的长度,可以视为O(1)

STRLEN
获取key对应的string的长度,当kry存放的类不是string,报错
SERLEN key
时间复杂度:O(1)
返回值:string的长度,key不存在,返回0

5.5 命令小结

5.6 内部编码
字符串类型的内部编码有3中:
int:8个字节的长整型
embstr:小于39个字节的字符串
raw:大于39个字节的字符串

Redis存储小数,本质上还是当做字符串来存储
整数直接使用int 来存,比较方便进行算数运算
小数则是使用字符串存储,意味着每次进行算数运算,都需要把字符串转为小数,结果再转回字符串保存
5.7 典型使用场景
缓存功能

整体思路:
应用服务器访问数据的时候,先查询Redis
如果Redis上数据存在了,就直接从Redis取数据交给应用服务器,不再访问数据库
如果Redis上数据不存在,再读取MySQL,把读到的结果返回给应用服务器,同时,把数据写到Redis中
上述策略有一个明显的问题:随着时间的推移,肯定是会有原来越多的key在Redis
此时Redis中的数据就越来越多
解决方法:
-
把数据写给Redis的同时,给key设置过期时间
-
Redis在内存不足的时候,提供了淘汰策略
注意:Redis中没有表、字段这种命名空间。而且也没有对键名的强制要求。存入数据的时候推荐使用" 业务名 : 对象名 : 唯一表示 : 属性" 作为键名
计数功能
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时可以根据异步处理或者落地到其他数据源

Redis并不擅长数据统计
比如,想列出播放量前一百的视频,基于Redis操作是比较困难的
相比之下,如果MySQL来存储上述数据,一个sql就可以搞定
通过异步方法将数据写入统计数据仓库
实际中套开发一个成熟、稳定的真实计数系统,要面临的挑战有很多:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源
共享会话

一个分布式web服务将用户的Session信息保存在各自的服务器中,但这样会造成一个问题:处于负载均衡的考虑。分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,当用户刷新一次访问是可能会被要求重新登录
为了解决这个问题,可以使用Redis将用户的Session信息进行集中管理,在这种模式下,只有保证Redis的高可用性和可扩展性,无论用户被均衡到哪台服务器上,都集中从Redis中查询,更新Session信息
手机验证码
很多应用处于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不回频繁访问,会限制用户没分钟获取验证码的频率
java
String 发送验证码(phoneNumber) {
key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使⽤ NX,只在不存在 key 时才能设置成功
bool r = Redis 执⾏命令:set key 1 ex 60 nx
if (r == false) {
// 说明之前设置过该⼿机的验证码了
long c = Redis 执⾏命令:incr key
if (c > 5) {
// 说明超过了⼀分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过⼿机的验证码;要么次数没有超过 5 次
String validationCode = ⽣成随机的 6 位数的验证码();
validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执⾏命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过⼿机短信发送给⽤⼾
return validationCode ;
}
// 验证⽤⼾输⼊的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
validationKey = "validation:" + phoneNumber;
String value = Redis 执⾏命令:get validationKey;
if (value == null) {
// 说明没有这个⼿机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
6. Hash 哈希

哈希类型中的映射关系通常称为 field-value,用于区分Redis整体的键值对(key-value)
6.1 命令
HSET
设置hash中指定的字段
HEST key field value field value ......
时间复杂度:插入一组filed为O(1),插入N组为O(N);
返回值:添加的字段个数
HGET
获取hsh中指定字段的值
HGET key field
时间复杂度:O(1)
返回值:字段对的值或者nil
HEXISTS
判断hash中是否有指定的字段
HEXISTS key field
时间复杂度:O(1)
返回值:1表示存在,0表示不存在
HDEL
删除hash中指定的字段,注意del删除的是key,hdel删除的是field
HDEL key field field ......
时间复杂度:删除一个元素为O(1),删除N个位O(N)
返回值:本次操作删除的字段个数
HKEYS
获取hash中的所有字段,先根据key找到对于的hash,再遍历hash
HKEYS key
时间复杂度:O(N),N表示field的个数
返回值:字段列表

注意:这个操作也是存在一定风险的,hash中可能会存在大量的field
HVALS
获取hash中的所有值
HVALS key
时间复杂度:O(N),N为field的个数
返回值:所有值
HGETALL
获取hash中的所有字段以及对应的值
HGETALL key
时间复杂度:O(N),N为field的个数
返回值:字段和对应的值
HMGET
一次性获取hash中多个字段的值
HMGET key field field ......
时间复杂度:查询N个,O(N)
有没有HMSET,一次性设置多个field和value?
hset已经支持一次性设置多个field和value了
HSCAN
遍历Redis的hash,但是它属于"渐进式遍历"
敲一次命令,遍历一小部分,在敲一次,再遍历一部分(时间是可控的)
HSCAN key cursor MATCH pattern COUNT count NOVALUS
HSCAN 的第二个参数永远是"游标",不是"限制条数"
HSCAN hash 0 COUNT 3
"42" # 注意!这里的 42 是新的游标,下次要用它
"f1"
"111"
"f2"
"222"
"f3"
"333"
HSCAN hash 42 COUNT 3
"0" # 游标变为 0,表示所有数据已经取完!
"f4"
"444"
...
# 从 0 开始,只匹配 f 开头的,每次大概取 2 个
HSCAN hash 0 MATCH f* COUNT 2
HLEN
获取hash中的所有字段的个数
HLEN key
时间复杂度:O(1)
返回值:字段个数
HSETNX
在字段不存在的情况下,设置hash中的字段和值
HSETNX key field value
时间复杂度:O(1)
返回值:1表示成功,0表示失败
HINCRBY
将hash中字段对于的数值添加指定的值
HINCRBY key field incement
时间复杂度:O(1)
返回值:该字段变化后的值
HINCRBYFLOAT
浮点数版本
HINCRBYFLOAT key field increment
时间复杂度:O(1)
返回值:该字段变化后的值
6.2 命令小结


6.3 内部编码
压缩:
rar、zip、gzip、7z...... 一些具体的压缩算法
压缩的本质是针对数据进行重新编码
不同的数据有不同的特点,结合这些特点,进行精妙的设计
重新编码之后,就能够缩小体积
内部编码有两种
ziplist(压缩列表)
内部的数据结构是精心设计的,目的是节省内存空间
表示一个普通的hash表,可能会浪费一定的空间
(hash首先是一个数组,数组上有些位置有元素,有些没有元素)
ziplist 付出饿代价是,进行读写数据的速度是比较慢的
如果元素个数少,慢的不明显,如果元素个数太多了,慢就会雪上加霜
-
哈希表中的元素个数比较少,使用ziplist表示,元素个数比较多,使用Hashtable来表示
-
每个value的值长度都比较短,使用ziplist来表示,如果某个value的长度太长了,也会转换

hashtable(哈希表)


ziplist 读写速度慢,且存在"连锁更新"问题(插入或删除元素时,可能导致后续所有元素的内存位置重新分配)。Listpack 正是为了解决这个痛点而生的 ,它从根本上杜绝了连锁更新,在内存紧凑性和读写性能之间取得了更好的平衡。所以,你现在看到的 listpack 是 ziplist 的"进化版"。
6.4 使用场景


相比于使用JSON格式的字符串缓存用户信息,哈希类型变得更加直观,并且在更新操作上变得更加灵活。可以将每个用户的 id 定义为后缀,多对 field-value 对应用户的各个属性
java
UserInfo getUserInfo(long uid) {
// 根据 uid 得到 Redis 的键
String key = "user:" + uid;
// 尝试从 Redis 中获取对应的值
userInfoMap = Redis 执⾏命令:hgetall key;
// 如果缓存命中(hit)
if (value != null) {
// 将映射关系还原为对象形式
UserInfo userInfo =利⽤映射关系构建对象(userInfoMap);
return userInfo;
}
// 如果缓存未命中(miss)
// 从数据库中,根据 uid 获取⽤⼾信息
UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>
// 如果表中没有 uid 对应的⽤⼾信息
if (userInfo == null) {
响应 404
return null;
}
// 将缓存以哈希类型保存
Redis 执⾏命令:hmset key name userInfo.name age userInfo.age city userInfo.city
// 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
Redis 执⾏命令:expire key 3600
// 返回⽤⼾信息
return userInfo;
}
注意:哈希类型和关系型数据库有两点不同之处
- 哈希表类型是稀疏的,而关系型数据库是完全结构化的,例如哈希表类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要设置值,即使为null

- 关系数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高
作为缓存
- 原生字符串类型------使用字符串类型,每个属性一个键
java
set user:1:name James
set user:1:age 23
set user:1:city Beijing
优点:实现简单,真毒个别属性变更也很灵活
缺点:占用过多的键,内存占用量较大,同时用户信息在Redis中分散,缺少内聚性,所以这种方法没有实用性
- 序列化字符串类型------例如JSON格式
java
set user:1 经过序列化后的⽤⼾对象字符串
优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案合适,内存的使用效率很高
缺点:本身序列化和反序列化需要一定的开销,同时如果总是操作个别属性则非常不灵活
- 哈希类型
java
hmset user:1 name James age 23 city Beijing
优点:简单、直观、灵活。尤其是针对信息的局部变更和获取操作
缺点:需要控制哈希在ziplist和hashtable两种内部编码的转换,可能会造成内存的较大消耗

7. List列表
7.1 介绍
列表类型是用来存储多个有序的字符串。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发商有很多应用场景
可以两端插入和弹出

注意:list内部的结果(编码方式)并非是一个简单的数组,而是更接近与"双端队列"
约定最左侧元素下表是0,支持负数下标
列表的获取、删除等操作

列表的特点
-
列表中的元素是有序的,意味着可以通过索引下表获取某个元素或者某个范围的元素列表
-
区分获取和删除的区别,列表有顺序,所以"删除"不仅改变长度,还会改变所有后续元素的"身份证号(下标)
-
列表中的元素是允许重复的

7.2 命令
LPUSH
将一个或多个元素从左侧插入(头插)
LPUSH key element element ...
时间复杂度:O(1)
返回值:插入后list的长度
LPUSHX
如果key存在:将一个或多个元素从左侧插入(头插)
LPUSHX key element element ...
时间复杂度:O(1)
返回值:插入后list的长度
RPUSH
将一个或多个元素从右侧插入(尾插)
RPUSH key element element ...
时间复杂度:O(1)
返回值:插入后list的长度
RPUSHX
如果key存在,将一个或多个元素从右侧插入(尾插)
RPUSHX key element element ...
时间复杂度:O(1)
返回值:插入后list的长度
LRANGE
LRANGE key start end
时间复杂度:O(N)
返回值:指定区域的元素
LPOP
从list左侧取出元素(头删)
LPOP key count
时间复杂度:O(1)
返回值:取出的元素或nil
RPOP
从list右侧取出元素(尾删)
RPOP key count
时间复杂度:O(1)
返回值:取出的元素或nil
LINDEX
取出从左数第index的元素
LINDEX key index
时间复杂度:O(N)
返回值:取出的元素或nil
LINSERT
在特定位置(不是下标,是元素)插入元素
如果基准值不止一个,从左往右找到第一个即可
LINSERT key <BEFORE|AFTER> pivot element
时间复杂度:O(N)
返回值:插入后list的长度
LLEN
获取list长度
LLEN key
时间复杂度:O(1)
返回值:list的长度
LREM
LREM key count element
count:要删除的个数
count=0:全部的指定元素
count<0:从右数删除count个指定元素
count>0:从左数删除count个指定元素
TRIM
保留指定范围内的元素
TRIM key start end
LSET
根据下表修改元素
LSET key index element
7.3 阻塞版本命令
BLPOP、BRPOP是LPOP和RPOP的阻塞版本
- 在列表中有元素的情况下,阻塞和非阻塞版本的作用基本一致的。但如果列表中没有元素,非阻塞版本会立即返回nil,但阻塞版本会根据timieout,阻塞一段时间,期间Redis可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态
- 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回
- 如果有多个客户端同时对一个键执行pop,则先执行命令的客户端会得到弹出的元素



BLPOP
BLPOP key key ... timeout
时间复杂度:O(1)
返回值:取出的元素或者nil

BRPOP
BRPOP key key ... timeout
时间复杂度:O(1)
返回值:取出的元素或者nil
7.4 命令小结

Redis中的list是一个双端队列
从两头插入和删除的效率很高
搭配使用RPUSH和LPOP就相当于队列
搭配使用RPUSH和RPOP就相当于栈
7.5 内部编码
旧版:ziplist和linkedlist
新版:quicklist
quicklist相当于是链表和压缩链表的结合
整体还是一个链表,链表的每个节点是一个压缩列表
7.6 使用场景
数组
作为数组这样的结构来存储多个元素
mysql中

Redis中

消息队列
Redis可以使用lpush+brpop命令结合实现经典的阻塞式生产者-消费者模型队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式从队列中"争抢"队首元素。通过多个客户端来保证消费者的负载均衡和可用性

分频道的消息队列
Redis同样使用lpush+brpop命令,但通过不同的键模拟频道的概念,不同的消费者可以通过brpop不同的键值,实现订阅不同频道的概念


微博Timeline
每个用户都有属于自己的Timeline(微博列表),先需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素
1)每篇微博使用哈希结构存储
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
2)向用户Timeline添加微博
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
3)分页获取用户的timeline
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
此方案实际中可能有两个问题:
-
1+n问题。如果每次分页获取的微博个数较少,需要执行多次hgetall操作,此时可以考虑使用piepline(流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用mget获取
-
分裂获取文章时,LRANGE在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表拆分
pipeline(流水线)
将多个Redis命令合并成一个网络请求进行通信,大大降低客户端和服务器之间的交换次数
8. set
集合类型也是保存多个字符串类型的类型,但和列表类型不同的是集合的元素是无序的,元素不允许重复。Redis除了支持集合内的增删改查操作,同时还支持多个集合取交集、并集、差集,合理使用好集合类型,能在实际开发中解决很多问题

8.1 普通命令
SADD
将一个或多个元素添加到set中,注意:重复的元素无法添加到set中
SADD key member member ...
时间复杂度:O(1)
返回值:本次添加成功的元素个数
SMEMBERS
获取一个set中的所有元素,元素间的顺序是无序的
SMEMBERS key
时间复杂度:O(N)
返回值:所有元素的列表
SISMEMBER
判断一个元素在不在set中
SISMEMBER key member
时间复杂度:O(1)
返回值:1表示存在,0表示不存在
SCARD
获取一个set的基数,即set中元素的个数
SCARD key
时间复杂度:O(1)
返回值:set内元素的个数
SPOP
从set中删除并返回一个或多个元素,由于set内的元素是无序的,所以取出哪个元素实际是未定义的行为,是随机的
SPOP key count
时间复杂度:O(N),N是count
返回值:取出的元素
SMOVE
将一个元素从源cset中取出放入目标set中
SMOVE source destination member
时间复杂度:O(1)
返回值: 1表示成功,2表示失败
注意:如果目标集合中已经有了 member ,将源集合中的 member放入目标集合,这种情况Redis不会视为错误
SREM
将指定的元素从set中删除
SREM key member member ...
时间复杂度:O(N),N表示删除元素的个数
返回值:本次操作删除的元素个数
8.2 集合间操作
交集(inter) 并集(union) 差集(diff)

SINTER
获取给的set的交集中的元素
SINTER key key ...
时间复杂度:O(N*M),其中 N 是最小集合的元素个数,M 是参与交集运算的集合个数。这个复杂度表示计算交集需要遍历所有集合,但实际执行时 Redis 会进行优化:首先找到元素最少的集合(N 最小),然后检查该集合中的每个元素是否同时存在于其他 M-1 个集合中。由于集合底层通常使用哈希表实现,检查元素是否存在的时间复杂度接近 O(1),因此总体复杂度可以近似为 O(N*M)。当集合数量 M 较大或最小集合元素数 N 较大时,该命令可能消耗较多 CPU 时间,在生产环境中应谨慎使用大数据集的 SINTER 命令。
返回值:交集的元素
SINTERSTORE
获取给定set的交集的元素并保存到目标set中
SINTERSTORE destination key key ...
时间复杂度:O(N*M),N是最小集合元素的个数,M是集合的个数
返回值:交集的元素个数

SUNION
获取给定set的并集中的元素
SUNION key key ...
时间复杂度:O(N),N是给定所有集合的总元素个数
返回值:并集的元素
SUNIONSTORE
获取给定set的并集中的元素并保存到目标set中
SUNIONSTORE destination key key ...
时间复杂度:O(N),N是给定所有集合的总元素个数
返回值:并集的元素个数
SDIFF
获取给定set的差集中的元素
SDIFF key key ...
时间复杂度:c
返回值:差集的元素
SDIFFSTORE
获取给定set的差集中的元素并保存到目标set中
SDIFFSTORE destination key key ...
时间复杂度:N是给定所有集合的总元素个数
返回值:差集的元素个数
8.3 命令小结

8.4 内部编码
旧版:intest(只能存整数) hashtable
新版:listpack(还可以存短字符串) hashtable
8.5 使用场景
保存用户标签
分析出用户的一些特征,保存这些标签后再投其所好
计算共同好友
基于"集合求交集"
A和B是好友
A和C是好友
B和C和D都是好友
系统机会把D推荐给A
统计UV
核心逻辑是利用set的去重
PV page view 用户每次访问该服务器都会产生一个pv
UV user view 每个用户访问该服务器都会产生一个uv,但是同一个用户多次访问,不会使uv增加,uv按照用户进行去重(上述过程可以使用set的去重实现)
9. zset
有序集合相当于字符串、列表、哈希、集合来说是比较陌生的。它保留了集合不能有重复成员的特点,与集合不同的是,有序集合中的每个元素都有一个唯一的浮点数类型的分数(score)与之关联,这使得有序集合中的元素可以维护有序性。但这个有序不是用下标作为排序的一句而是用这个分数

有序集合提供了获取指定分数和元素的范围查找,合理利用有序集合,可以帮助我们在实际开发中解决很多问题

9.1 普通命令
ZADD
添加或者更新指定的元素以及关联的分数到zset中,分数应该符合double类型
member 和 score 称为是一个"pair",不能直接理解为是一个键值对,对于有序集合来说,可以通过member找到对应的score,也可以通过score找到对应的member
ZADD相关选项:
XX:仅用于更新已存在的元素,不会添加新元素
NX:仅用于添加新元素,不会更新已存在的元素
LT:更新分数时,比之前的分数小则更新成功,否则不更新
GT:更新分数时,比之前的分数大则更新成功,否则不更新
CH:默认情况下,ZADD返回的是本次添加的元素个数,但指定这个选项后,就会还包含本次更新的元素个数
INCR:此时命令类似ZINCRBY的效果,将元素的分数加上指定的分数。此时只能指定一个元素和分数
ZADD key NX \| XX GT \| LT CH INCR score member score member ...
时间复杂度:O(logN)
返回值:本次添加成功的元素个数

ZCARD
获取一个zset的基数,即zset中的元素个数
ZCARD key
时间复杂度:O(1)
返回值:zset内的元素个数
ZCOUNT
返回分数在 min 和 max 之间的元素个数,默认情况下 min 和 max 都是包含的,可以通过 ( 来排除
ZCOUNT key min max
时间复杂度:O(logN)
返回值:满足条件的元素列表个数

查询到元素,就直接知道了元素所在的次序,就可以直接把max对于的元素和min对应的元素次序相减
在浮点数中存在两个特殊的值:
inf 无穷大 -inf 负无穷大 (注意不是无穷小)
zset中支持使用 inf 和 -inf 作为 max 和min

ZRANGE
返回指定区间里的元素,分数按照升序。带上WITHSCORES可以把分数也返回
ZRANGE key start stop WITHSCORE
时间复杂度:O(log(N)+M) ,M是start-stop之间元素的个数
返回值:区间内的元素列表


ZREVRANGE
返回指定区间里的元素,分数按照降序,带上WITHSCORES可以把分数也返回
ZREVRANGE key start stop WITHSCORES
时间复杂度:O(log(N)+M),M是start-stop之间元素的个数
返回值:区间内的元素列表

ZRANGEBYSCORE
返回分数在 min 和 max 之间的元素,可以通过 ( 排除
ZRANGEBYSCORE key min max WITHSCORES
时间复杂度:O(log(N)+M) ,M是start-stop之间元素的个数
返回值:区间内的元素列表
这个命令可能被6.2.0之后放弃,并且功能合并到ZRANGE中
ZRANGE key 10 30 BYSCORE WITHSCORES
ZPOPMAX
删除并返回分数最高的count个元素
ZPOPMAX key count
时间复杂度:O(log(N)*M),M是删除元素的个数,Redis并没有采用类似尾删的方法提高为O(1)
返回值:分数和元素列表
BZPOPMAX
ZPOPMAX的阻塞版本
BZPOPMAX key key ... timeout
时间复杂度:O(log(N))
返回值:元素列表
ZPOPMIN
删除并返回分数最低的count个元素
ZPOPMIN key count
时间复杂度:O(log(N)*M)
返回值:分数和元素列表
BZPOPMIN
ZPOPMIN的阻塞版本
BZPOPMIN key key ... timeout
时间复杂度:O(N)
返回值:元素列表
ZRANK
返回指定元素的排名,升序
ZRANK key member
时间复杂度:O(log(N))
返回值:排名
ZREVRSNK
返回指定元素的排名,降序
ZREVRANK key member
时间复杂度:O(log(N))
返回值:排名
ASCORE
返回指定元素的分数
ASCORE key member
时间复杂度:O(1)
返回值:分数
ZREM
删除指定元素
ZREM key member member ...
时间复杂度:O(log(N)*M)
返回值:本次操作删除的元素个数
ZREMRANGEBYRANK
按照升序删除指定范围里的元素
ZREMRANGEBYRANK key start stop
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数
ZREMRANGEBYSCORE
按照分数删除指定范围的元素
ZREMRANGEBYSCORE key min max
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数
ZINCRBY
为指定的元素关联的分数添加指定的分数值
ZINCRBY key increment member
时间复杂度:O(log(N))
返回值:增加后元素的分数
9.2 集合间操作


ZINTERSTORE
求出给定有序集合元素的交集并保存到目标集合中,在合并过程中以元素为单位进行合并,元素对于的分数按照不同的集合方式和权重得到全新的分数
ZINTERSTORE destination numkeys key key... WEIGHTS weight \[ weight... ] AGGREGET \< SUM \| MIN \| MAX \>
numkeys:涉及集合的个数,前面介绍的命令没有类似的设定,很多命令也支持多个key,此处有这个是因为 key 后面还有别的选项,避免选项和keys混淆
时间复杂度:O(N*K)+O(M*log(M))
-
N :所有输入集合中,元素个数最少的那个集合的大小。
-
K :输入集合的个数(也就是
numkeys的值)。 -
M :结果集合(交集) 中最终包含的元素个数。

返回值:目标集合的元素个数


ZUNIONSTORE
求出给定的有序集合中的并集并保存到目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的分数按照不同的聚合方式和权重得到新放的分数
ZUNIONSTORE destination numkeys key key ... WIGHTS weight \[ weight... ] AGGREGATE \< SUM \| MIN \\ MAX \>
时间复杂度:O(N)+O(M*log(M)) ,N是输入的有序结合总的元素个数,M是最终包含的元素个数
返回值:目标集合的元素个数
9.3 命令小结


9.4 内部编码
旧版:ziplist skiplist(跳表)
新版:listpack skiplist
跳表是一个"复杂链表",查询元素的时间复杂度是logN,相比于树形结构,更适合按照范围获取元素

9.5 使用场景
排行榜系统
-
微博热搜
-
游戏排行榜
-
成绩排名
......
关键要点:用来排行的"分数"是实时变化的

zset是一个选择,不是说必须用Redis中的zset
10. 拓展类型(了解)
Stream
可以用来模拟实现事件传播的机制
Stream就是一个队列(阻塞队列),Redis作为一个消息队列的重要支撑,属于List 中blpop/brpop 升级版本

Geospatial
用来存储坐标(经纬度)
存储一些点之后,就可以让用户给定一个坐标,从这个点进行查找(按照半径、矩形区域...)

HyperLogLog
应用场景只有一个,就是估算集合中元素的个数
Set有一个应用场景,统计服务器的UV,使用Set可以统计,但最大的问题在于UV数据量非常大,Set就会消耗很多的内存空间
用 HyperLogLog 可以省很多的空间
原因在于Set需要存储每个元素,而HyperLogLog不存储元素内容,但是能够记录元素的特征,从而在新增元素的时候,能够知道当前新增的元素是否已经存在,是否技术
核心思路是"位操作",精确值带有一定的误差
Bitmaps
使用bit位表示整数没计算机进行位运算一般是比较高效的
位图本质上还是一个集合,属于Set类型针对整数的特化版本(节省空间)
Bitfielsds
位域,本质上是精确进行位操作的一种方法
相比于之前的String/Hash来说,目的是节省空间
11. 数据库管理
Reids提供了几个面向Redis数据库的操作,分别是dbsize、select、flushall命令
切换数据库
select dbIndex
许多关系型数据库,例如MySQL支持在一个实例下有很多个数据库存在,但是与关系型数据库用字符来区分不同数据库名不同,Redis只是用数字作为多个数据库的视线。Redis默认配置中有16个数据库。select可以切换数据库,默认情况下处于数据库0


清除数据库
flushdb / flushall
清除当前数据库 / 清除所有数据库
