前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis基础篇

Redis基础篇

作者头像
会说话的丶猫
发布2020-10-27 17:24:33
4380
发布2020-10-27 17:24:33
举报

Redis 基本数据类型

  最基本也是最常用的数据类型就是 String。set 和 get 命令就是 String 的操作命令。为什么叫 Binary-safe strings 呢?

String 字符串

  • 存储类型

  可以用来存储字符串、整数、浮点数。

  • 操作命令

  设置多个值(批量操作,原子性)

代码语言:javascript
复制
mset qingshan 2673 jack 666

  设置值,如果 key 存在,则不成功

代码语言:javascript
复制
setnx qingshan

  基于此可实现分布式锁。用 del key 释放锁。但如果释放锁的操作失败了,导致其他节点永远获取不到锁,怎么办?

  加过期时间。

代码语言:javascript
复制
set key value [expiration EX seconds|PX milliseconds][NX|XX]
set lock1 1 EX 10 NX

  (整数)值递增/(整数)值递减

代码语言:javascript
复制
incr qingshan 
incrby qingshan 100
decr qingshan 
decrby qingshan 100

  浮点数增量

代码语言:javascript
复制
set f 2.6 
incrbyfloat f 7.3

  获取多个值

代码语言:javascript
复制
mget qingshan jack

  字符串追加内容

代码语言:javascript
复制
append qingshan good

  获取指定范围的字符

代码语言:javascript
复制
getrange qingshan 0 8
  • 存储(实现)原理

数据模型

  set hello word 为例,因为 Redis 是 KV 的数据库,它是通过 hashtable 实现的(我们把这个叫做外层的哈希)。所以每个键值对都会有一个 dictEntry(源码位置:dict.h),里面指向了 key 和 value 的指针。next 指向下一个 dictEntry。

代码语言:javascript
复制
typedef struct dictEntry { 
void *key; /* key 关键字定义 */ 
union { 
void *val; 
uint64_t u64; /* value 定义 */ 
int64_t s64; double d; } v; 
struct dictEntry *next; /* 指向下一个键值对节点 */ } 
dictEntry;

  key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在自定义的 SDS中。value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储

的。

redisObject

  redisObject 定义在 src/server.h 文件中。

可以使用 type 命令来查看对外的类型。:

127.0.0.1:6379> type qs

string

内部编码

127.0.0.1:6379> object encoding tom "int"

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

1、int,存储 8 个字节的长整型(long,2^63-1)。

2、embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),存储小于 44 个字节的字符串。

3、raw,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)。

问题 1、什么是 SDS?

  Redis 中字符串的实现。

  在 3.2 以后的版本中,SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 2^5=32byte,2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB。

问题 2、为什么 Redis 要用 SDS 实现字符串?

我们知道,C 语言本身没有字符串类型(只能用字符数组 char[]实现)。

1、使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。

2、如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。

3、C 字符串长度的变更会对字符数组做内存重分配。

4、通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。

SDS 的特点:

1、不用担心内存溢出问题,如果需要会对 SDS 进行扩容。

2、获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。

3、通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多

次重分配内存。

4、判断是否结束的标志是 len 属性(它同样以'\0'结尾是因为这样就可以使用 C语言中函数库操作字符串的函数了),可以包含'\0'。

  • 应用场景

缓存

String 类型

例如:热点数据缓存(例如报表,明星出轨),对象缓存,全页缓存。可以提升热点数据的访问速度。

数据共享分布式

STRING 类型,因为 Redis 是分布式的独立服务,可以在多个应用之间共享

例如:分布式 Session

分布式锁

STRING 类型 setnx 方法,只有不存在时才能添加成功,返回 true。http://redisdoc.com/string/set.html 建议用参数的形式

全局 ID

INT 类型,INCRBY,利用原子性

计数器

INT 类型,INCR 方法

例如:文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到数据库。

限流

INT 类型,INCR 方法

以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。

如果一个对象的 value 有多个值的时候,怎么存储?

例如用一个 key 存储一张表的数据。

序列化?例如 JSON/Protobuf/XML,会增加序列化和反序列化的开销,并且不能单独获取、修改一个值。

可以通过 key 分层的方式来实现,例如:

代码语言:javascript
复制
mset student:1:sno GP16666 student:1:sname 沐风 student:1:company 腾讯

获取值的时候一次获取多个值:

代码语言:javascript
复制
mget student:1:sno student:1:sname student:1:company

缺点:key 太长,占用的空间太多。有没有更好的方式?

Hash 哈希

  • 存储类型

  包含键值对的无序散列表。value 只能是字符串,不能嵌套其他类型。

同样是存储字符串,Hash 与 String 的主要区别?

1、把所有相关的值聚集到一个 key 中,节省内存空间

2、只使用一个 key,减少 key 冲突

3、当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU 的消耗

Hash 不适合的场景:

1、Field 不能单独设置过期时间

2、没有 bit 操作

3、需要考虑数据量分布的问题(value 值非常大的时候,无法分布到多个节点)

  • 操作命令
代码语言:javascript
复制
hset h1 f 6 
hset h1 e 5 
hmset h1 a 1 b 2 c 3 d 4
  • 存储(实现)原理

  Redis 的 Hash 本身也是一个 KV 的结构,类似于 Java 中的 HashMap。外层的哈希(Redis KV 的实现)只用到了 hashtable。当存储 hash 数据类型时,我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现:

ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)

hashtable:OBJ_ENCODING_HT(哈希表)

代码语言:javascript
复制
127.0.0.1:6379> hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 
(integer) 1 
127.0.0.1:6379> hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 
(integer) 1 
127.0.0.1:6379> object encoding h2 
"ziplist" 
127.0.0.1:6379> object encoding h3 
"hashtable"

ziplist 压缩列表

  ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值

小的场景里面。

  • 应用场景

  存储对象类型的数据

  比如对象或者一张表的数据,比 String 节省了更多 key 的空间,也更加便于集中管理。

  购物车

key:用户 id;field:商品 id;value:商品数量。

+1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。

List 列表

  • 存储类型

  存储有序的字符串(从左到右),元素可以重复。可以充当队列和栈的角色。

  • 操作命令

  元素增减:

代码语言:javascript
复制
lpush queue a 
lpush queue b c

  取值

代码语言:javascript
复制
lindex queue 0
lrange queue 0 -1
  • 存储(实现)原理

  在早期的版本中,数据量较小时用 ziplist 存储,达到临界值时转换为 linkedlist 进行存储,分别对应 OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST 。

3.2 版本之后,统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点都是一个 ziplist。

  quicklist(快速列表)是 ziplist 和 linkedlist 的结合体。quicklist.h,head 和 tail 指向双向列表的表头和表尾。

  • 应用场景

  用户消息时间线 timeline 因为 List 是有序的,可以用来做用户时间线

消息队列

  List 提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间。BLPOP:BLPOP key1 timeout 移出并获取列表的第一个元素, 如果列表没有元素

会阻塞列表直到等待超时或发现可弹出元素为止。

  BRPOP:BRPOP key1 timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列。栈:先进后出:rpush brpop

Set 集合

  • 存储类型

  String 类型的无序集合,最大存储数量 2^32-1(40 亿左右)。

  • 操作命令
代码语言:javascript
复制
添加一个或者多个元素
  • 存储(实现)原理

  Redis 用 intset 或 hashtable 存储 set。如果元素都是整数类型,就用 inset 存储。如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。

问题:KV 怎么存储 set 的元素?key 就是元素的值,value 为 null。

如果元素个数超过 512 个,也会用 hashtable 存储。

应用场景

抽奖

  随机获取元素

  spop myset

点赞、签到、打卡

这条微博的 ID 是 t1001,用户 ID 是 u3001。

用 like:t1001 来维护 t1001 这条微博的所有点赞用户。

点赞了这条微博:sadd like:t1001 u3001

取消点赞:srem like:t1001 u3001

是否点赞:sismember like:t1001 u3001

点赞的所有用户:smembers like:t1001

点赞数:scard like:t1001

比关系型数据库简单许多。

ZSet 有序集合

  • 存储类型

sorted set,有序的 set,每个元素有个 score。

score 相同时,按照 key 的 ASCII 码排序。

数据结构对比:

  • 操作命令
  • 存储(实现)原理

  同时满足以下条件时使用 ziplist 编码:

   元素数量小于 128 个

   所有 member 的长度都小于 64 字节

  在 ziplist 的内部,按照 score 排序递增来存储。插入的时候要移动之后的数据。

  超过阈值之后,使用 skiplist+dict 存储。

  问题:什么是 skiplist?

  我们先来看一下有序链表:

  在这样一个链表中,如果我们要查找某个数据,那么需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点为止(没找到)。

也就是说,时间复杂度为 O(n)。同样,当我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。

而二分查找法只适用于有序数组,不适用于链表。假如我们每相邻两个节点增加一个指针(或者理解为有三个元素进入了第二层),让指针指向下下个节点。

  这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半(上图中是 7, 19, 26)。在插入一个数据的时候,决定要放到那一层,取决于一个算法(在 redis 中 t_zset.c 有一个 zslRandomLevel 这个方法)。

现在当我们想查找数据的时候,可以先沿着这个新链表进行查找。当碰到比待查数据大的节点时,再回到原来的链表中的下一层进行查找。比如,我们想查找 23,查找的路径是沿着下图中标红的指针所指向的方向进行的:

1. 23 首先和 7 比较,再和 19 比较,比它们都大,继续向后比较。

2. 但 23 和 26 比较的时候,比 26 要小,因此回到下面的链表(原链表),与 22比较。

3. 23 比 22 要大,沿下面的指针继续向后和 26 比较。23 比 26 小,说明待查数据 23 在原链表中不存在

  在这个查找过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了。需要比较的节点数大概只有原来的一半。这就是跳跃表。为什么不用 AVL 树或者红黑树?因为 skiplist 更加简洁。

  • 应用场景

  id 为 6001 的新闻点击数加 1:zincrby hotNews:20190926 1 n6001

  获取今天点击最多的 15 条:zrevrange hotNews:20190926 0 15 withscores

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-10-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis 基本数据类型
    • String 字符串
      • Hash 哈希
        • List 列表
          • Set 集合
            • 应用场景
              • ZSet 有序集合
              相关产品与服务
              云数据库 Redis
              腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档