Redis 基础
- 介绍
- 特性
- 使用场景
- 存储结构
- 重要 value 类型
- 全局命令
- String
- Hash
- List
- Set
- Zset
- BitMap
- 布隆过滤器
- 小结
- 三种方案实现用户信息存储优缺点
- Redis VS Memcached
- 单线程
- 为什么这么快
- 劣势
介绍
Redis 是一种基于键值对(key-value)的,worker 线程为单线程的内存数据库。其中 value 可以为 string、hash、list、set、zset,它们有自己的方法,可以满足很多应用场景。
还提供了键过期,发布订阅,事务,流水线等附加功能。
流水线:Redis的流水线功能允许客户端一次将多个命令请求发送给服务器,并将被执行的多个命令请求的结果在一个命令回复中全部返回给客户端,使用这个功能可以有效地减少客户端在执行多个命令时需要与服务器进行通信的次数。
特性
- 速度快,数据放在内存中,官方给出的读写性能10万/s(与机器性能也有关):
数据放内存中是速度快的主要原因
C语言实现,与操作系统距离近
使用了单线程架构,预防多线程可能产生的竞争问题 - 键值对的数据结构服务器
- 功能丰富
- 简单稳定:单线程
- 持久化:发生断电或机器故障,数据可能会丢失,持久化到硬盘
- 主从复制:实现多个相同数据的redis副本
- 高可用和分布式:哨兵机制实现高可用,保证 redis节点故障发现和自动转移
- 客户端语言多:java php python c c++ nodejs等
使用场景
- 缓存:合理使用缓存加快数据访问速度,降低后端数据源压力
- 排行榜:按照热度排名,按照发布时间排行,主要用到列表和有序集合
- 计数器应用:视频网站播放数,网站浏览数,使用 redis 计数
- 社交网络:赞、踩、粉丝、下拉刷新
- 消息队列:发布和订阅
存储结构
Redis 的内部整体的存储结构就是一个大的 HashMap。每个 dictEntry 里都有 key 与 v,分别对应 set key value 里的 key 和 value。
重要 value 类型
全局命令
- 查看所有键:keys *
- 键总数:dbsize //如果存在大量键,线上禁止使用此指令
- 检查键是否存在:exists key0 //key0存在返回1,不存在返回0
- 删除键:del key0 key1 …//返回删除键个数,删除不存在键返回0
- 键过期:expire key0 seconds0
- 键的数据结构类型:type key0
String
字符串类型:实际上可以是字符串(包括XML、JSON),还有数字(整形浮点数),二进制(图片音频视频),最大不能超过512MB
设值命令
EX:过期时间,秒
PX:过期时间,毫秒
NX:如果不存在 key,可以设置成功。成功返回1。
XX:如果存在 key,可以设置成功。成功返回1。
效果等于上面的 set key value NX获值命令
批量设置
批量获取
计数命令
自增
必须为整数自增1,非整数返回错误,无键从0自增返回1
使用场景的联想:可以用来做文章阅读量的自增。自增一段
浮点自增一段
自减
必须为整数自减1,非整数返回错误,无键从0自减返回-1自减一段
追加命令
截取命令
索引从 0 开始,截取包含 start 和 end
求字节长度
内部编码
int:如果能转化为 long。
embstr:小于等于44字节的字符串(3.2版本)
raw:大于44字节的字符串。最大 512M
embstr和raw都是由SDS(Simple Dynamic String)动态字符串构成的。
区别是:raw是分配内存的时候,redisobject 和 sds 各分配一块内存,而embstr是redisobject和raw在一块儿内存中。
embstr:
分配内存和释放内存只需要1次。对 embstr 的 操作会使 embstr 转化为 raw。
44的由来:整个 redisObject 和 sdshdr 一共占 64 字节,redisObject 占16 字节,SDS 头部占 3 字节,还有结尾 ‘\0’ 占 1 字节。一共 20 字节不能用,所以还剩 44 字节。
为什么要分配 64 字节?因为内存分配器 jemalloc、tcmalloc 分配内存的大小都是按 2/4/8/16/32/64 字节来的。因为 embstr 的“无效”部分就占了 20 字节,如果只申请 32 字节,利用率未免太低。
raw:
分配内存和释放内存需要2次。
空间预分配
若修改之后sds长度小于1MB,则多分配现有len长度的空间
若修改之后sds长度大于等于1MB,则扩充除了满足修改之后的长度外,额外多1MB空间
惰性空间释放
为避免缩短字符串时候的内存重分配操作,sds在数据减少时,并不立刻释放空间。
Hash
是一个 string 类型的 field 和 value 的映射表,hash 特适合用于存储对象。
设值命令
例:购物车场景
购物车编号 001,选择了 1 件 01 号商品,1 件 02 号商品
可以看出,hset 可以批量设值批量设值
取值命令
hget 不能批量取值批量取值
删值命令
字段个数
使用场景的联想:查看购物车里物品数量
判断字段是否存在
获取所有字段
例:
获取所有字段值
例:
获取所有字段与字段值
例:
自增一段
使用场景的联想:购物车商品数 +1
浮点自增一段
例:
内部编码
ziplist:当字段个数小于512且每个字段值小于64字节时
hashtableziplist:
hashtable
字典的结构中包括两个hash表,h[0]和h[1],每次只是用h[0]就够了。若是需要扩容,就一部分一部分的迁移到h[1],在此过程中,若是客户端线程来删改查数据,先从h[0]找,没有的话去h[1],若是添加数据,直接添加到h[1],保证h[0]数据不增加。最后迁移完成释放h[0],将h[1]设置为h[0],再在h[1]新建一个空白 table。
List
用来存储多个有序的字符串,一个列表最多可存 232-1 个元素
左push
使用场景的联想:消息推送
右push
左pop
右pop
左阻塞pop
右阻塞pop
替换
插入
在 pivot 前|后 插入value查询
范围查询
查询索引 [start, stop] 的所有 value。[0, -1] 表示所有 value。长度
删除指定数量的值
从左到右删除 count 个 value保留范围内的值
内部编码
版本 3.2 以后: quicklist
quicklist 可以看作一个双向列表,但是列表的每个节点都是一个ziplist。
可以配置双向列表两边 n 个节点不压缩。
Set
保存无序不重复元素,一个集合最多可存 232-1 个元素,除了支持增删改查,还支持集合交集、并集、差集.
增
只会添加原本没有的 member,返回添加成功的 member 个数
使用场景的联想:黑名单,如抢红包,抢过的人加入黑名单。删
只会删除原本有的 member,返回删除成功的 member 个数随机pop
使用场景的联想:抽奖,从参与抽奖的人群里抽取几个查询成员个数
查询所有成员
查询成员是否存在
随机pop一定数量的成员
count 不能为负数随机查一定数量的成员
如果 count 为负数,允许重复。求交集
使用场景的联想:微关系,共同关注的人求交集并保存结果
求并集
求并集并保存结果
求差集
key1 在前,key2 在后,就是 key1 的数据 - key2 的数据
使用场景的联想:QQ,可能想认识的人求差集并保存结果
返回第一个 key 的 set 减去交集的结果
内部编码
intset:成员都为整数且少于512个
hashtable
Zset
用分值排序的 Set
与 List、Set 对比:
增
NX:key 必须不存在,用于添加
XX:key 必须存在,用于修改。与 INCR 一起使用。
CH:返回结果分值。与 INCR 一起使用。
INCR:接一个整数,增加分值。接浮点数,浮点数会不准。删
查范围
索引在[start, stop]之间的成员。索引顺序与分值顺序一致。
WITHSCORES:显示分值。查成员个数
查名次
名次从 0 开始。顺序从小到大。查排名
查倒序名次
顺序从大到小。查倒序排名
查分数范围内成员
表示从 x 到无穷大:(x +inf
表示从无穷小到 x:(x -inf倒序查分数范围内成员
查分数范围内成员个数
增加分数
不能直接改,只能在原分数上更改。除了用 zadd 以外,还有:
使用场景的联想:热搜排行删名次范围内成员
删分数范围内成员
求交集并保存
destination:要保存的结果的 key 的名字
numkeys:key 个数
WEIGHTS:对应 key 的排列设值,代表各 key 里所有成员的分数权值。在后续的计算之前,将分数与对应权值相乘。
AGGREGATE:聚合方式,有求和、求最小值、求最大值。默认是求和。求并集并保存
内部编码
ziplist:当成员个数小于128且成员值小于64字节
skiplist + dict:skiplist 最大层数64,dict 是为了在O(1)的时间复杂度下通过元素成员得到分数
ziplist:
skiplist:
查找的效率平均是O(logN)
为什么用跳表而不用 BTree?
- 占用内存更小
- 对范围查找的支持更好
- 实现更容易
BitMap
增
如:setbit key1 1 1
0 1 0 0 0 0 0 0
index 大小会决定字节多少
1的数量
字节下标范围内有多少个1
使用场景的联想:时间窗口内天数、每天活跃用户数
逻辑操作
如:bitop and andkey k1 k2
布隆过滤器
略
小结
三种方案实现用户信息存储优缺点
- 原生
set user:1:name Tom
set user:1:age 23
set user:1:sex male
优点:简单直观,每个键对应一个值
缺点:键数过多,占用内存多,用户信息过于分散,不用于生产环境 - 将对象序列化存入redis
set user:1 serialize(userlnfo);
优点:编程简单,若使用序列化合理内存使用率高
缺点:序列化与反序列化有一定开销,更新属性时需要把 userlnfo 全取出来进行反序列化,更新后再序列化到redis - 使用hash类型
hmset user:1 name Tom age 23 sex male
优点:简单直观,使用合理可减少内存空间消耗
缺点:要控制 ziplist 与 hashtable 两种编码转换,且 hashtable 会消耗更多内存
总结:
- 对于更新不多的情况下,可以使用序列化
- 对于VALUE值不大于64字节可以使用 Hash 类型
Redis VS Memcached
Memcached 的 value 只能存储 String,数据向计算移动。
Redis 的 value 能存储多种类型,且每种类型有自己的方法,计算向数据移动。
移动的成本往往大于计算,Redis 的 IO 量更少,这是它的优势。
单线程
为什么这么快
- 开发语言
Redis 就是用 C 语言开发的,C 语言是非常贴近操作系统的语言,所以执行会比较快。 - 纯内存访问
Redis 将所有数据放在内存中,非数据同步正常工作时,是不需要从磁盘读取数据的。 - worker 单线程
第一,单线程简化算法的实现,并发的数据结构实现不但困难而且测试也麻烦。
第二,单线程避免了线程切换以及加锁释放锁带来的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。 - 多路复用
Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型将 epoll 的 read、write、close 等都转换成事件,不在网络 I/O 上浪费过多的时间,从而实现对多个 FD 读写的监控,提高性能。 - 计算向数据移动
Redis 的 value 能存储多种类型,且每种类型有自己的方法。减少 IO 数据包的大小。
劣势
问题:不能发挥多核优势
解决:woker 线程计算,io threads 并行读写。
新问题:不同连接,读写先后顺序,会有问题。这个问题无法解决,回想一下多路复用器的 select。