• Redis基础篇


    Redis 基本数据类型

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

    String 字符串

    • 存储类型
      可以用来存储字符串、整数、浮点数。
    • 操作命令
      设置多个值(批量操作,原子性)
    mset qingshan 2673 jack 666

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

    setnx qingshan
      基于此可实现分布式锁。用 del key 释放锁。但如果释放锁的操作失败了,导致其他节点永远获取不到锁,怎么办?
      加过期时间。
    set key value [expiration EX seconds|PX milliseconds][NX|XX]
    set lock1 1 EX 10 NX
      (整数)值递增/(整数)值递减
    incr qingshan 
    incrby qingshan 100
    decr qingshan 
    decrby qingshan 100
      浮点数增量
    set f 2.6 
    incrbyfloat f 7.3
      获取多个值
    mget qingshan jack
      字符串追加内容
    append qingshan good

      获取指定范围的字符

    getrange qingshan 0 8
    • 存储(实现)原理
    数据模型
      set hello word 为例,因为 Redis 是 KV 的数据库,它是通过 hashtable 实现的(我们把这个叫做外层的哈希)。所以每个键值对都会有一个 dictEntry(源码位置:dict.h),里面指向了 key 和 value 的指针。next 指向下一个 dictEntry。
    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、通过从字符串开始到结尾碰到的第一个''来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。 
     
    SDS 的特点:
    1、不用担心内存溢出问题,如果需要会对 SDS 进行扩容。
    2、获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。
    3、通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多
    次重分配内存。
    4、判断是否结束的标志是 len 属性(它同样以''结尾是因为这样就可以使用 C语言中函数库操作字符串的函数了),可以包含''。
    • 应用场景
    缓存
    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 分层的方式来实现,例如:
    mset student:1:sno GP16666 student:1:sname 沐风 student:1:company 腾讯
    获取值的时候一次获取多个值:
    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 值非常大的时候,无法分布到多个节点)
    • 操作命令
    hset h1 f 6 
    hset h1 e 5
    hmset h1 a 1 b 2 c 3 d 4
      hget h1 a
      hmget h1 a b c d
      hkeys h1
      hvals h1
      hgetall h1
     
      hget exists h1
      hdel h1
      hlen h1
    • 存储(实现)原理 

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

    ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)

    hashtable:OBJ_ENCODING_HT(哈希表) 

    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 列表

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

      元素增减:

    lpush queue a 
    lpush queue b c
      rpush queue d e
      lpop queue
      rpop queue
      blpop queue
      brpop queue 
      取值
    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 亿左右)。

    • 操作命令
    添加一个或者多个元素
    sadd myset a b c d e f g
     
      获取所有元素
    smembers myset
     
      统计元素个数
    scard myset 
     
      随机获取一个元素
    srandmember key
     
      随机弹出一个元素 
    spop myset
     
      移除一个或者多个元素
    srem myset d e f
     
      查看元素是否存在
    sismember myset a 
    • 存储(实现)原理
      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 码排序。
    数据结构对比:
    • 操作命令
    
    
    添加元素
    
    
    zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
    
    
    获取全部元素
    
    
    zrange myzset 0 -1 withscores
    
    
    zrevrange myzset 0 -1 withscores
    
    
    根据分值区间获取元素
    zrangebyscore myzset 20 30
    
    
    移除元素
    
    
    也可以根据 score rank 删除
    
    
    zrem myzset php cpp
    
    
    统计元素个数
    
    
    zcard myzset
    
    
    分值递增
    
    
    zincrby myzset 5 python
    
    
    根据分值统计个数
    
    
    zcount myzset 20 60
    
    
    获取元素 rank
    
    
    zrank myzset java
    
    
    获取元素 score
    
    
    zsocre myzset java
    
    
    也有倒序的 rev 操作(reverse)
     
    • 存储(实现)原理
      同时满足以下条件时使用 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
  • 相关阅读:
    浅析 KMP
    【GOJ 3049】玩具
    较详细的gdb入门教程
    【GOJ 2963】记者
    【GOJ 2961】数数
    GF OIer's Talk 维护笔记
    Linux 中 UFW 的使用
    开源是什么?能吃吗?
    个人介绍
    NOIP2020 爆零记
  • 原文地址:https://www.cnblogs.com/talkingcat/p/13803781.html
Copyright © 2020-2023  润新知