Redis-01基础数据类型和常用数据结构

纵观现在的Java面试,Redis成了必问的点,为什么Redis能这样得到大家的青睐?Redis到底帮我们解决了什么问题提?Redis的优点是有哪些?…

概览

在这里插入图片描述 Redis官网这样描述自己:开源的内存型数据结构化存储,用作数据库、缓存和消息处理。Redis提供了string,hashes,lists,sets,sorted sets(提供范围查询),bitmaps,hyperloglogs,geospatial以及streams数据结构…

Redis(Remote Dictionary Service)最初是为了自己的网站进行数据统计而产生的,由意大利人Antirez发明,经过多年的开源发展,现在已经相当成熟,现被广泛应用于缓存中间件,而且能解决大部分的查询压力,因此在多数企业的应用中均被广泛使用。
Redis作为一个突出的非关系型数据库,具备哪些关系型数据库不具备的优势呢?

关系型数据库:

  • 数据采用行模式进行存储,可以理解成Excel的行和列
  • 存储结构化的数据,如数据表之间有一定的关联关系(schema)
  • 支持使用SQL(结构化查询语言)进行操作,能够支持复杂的语句操作
  • 通过支持事务来提供严格或者实时的数据一致性

在关系型数据库中,当我们的数据逐渐变大,这时候势必要进行扩容,但是扩容只能向上进行垂直扩容,而且不能动态扩容;在开发过程中,如果想要修改某个字段,则该字段下的所有数据均会受到影响;最重要的一点,在高并发的情况下,关系型数据库所能承受的压力不是很乐观。对于磁盘的I/O出现瓶颈。

而对于非关系型数据库,能够存储非结构化的的数据,比如文本、图片、视频、音频等,表和表之间;没有关联,非常易于扩展;保证数据的最终一致性,遵循BASE理论;支持分布式,能够对数据进行分片存储,容量扩展简单;最重要的一点,能够支持海量数据的存储和高并发的高效读写。笔者测试使用Pipeline100万个数字进行set和get,电脑应用水平处于常规工作环境,耗时在1500ms左右,可见效率还是相当高的。

public class PipelineSet {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.0.109", 6379);
        Pipeline pipelined = jedis.pipelined();
        long start = System.currentTimeMillis();
        for (int i=0; i < 1000000; i++) {
            pipelined.set("pipeline_btch_"+i,""+i);
        }
        pipelined.syncAndReturnAll();
        long end= System.currentTimeMillis();
        System.out.println("pipeline耗时:"+(t2-t1)+"ms");
    }
}
public class PipelineGet {
    public static void main(String[] args) {
        new Thread(){
            public void run(){
                Jedis jedis = new Jedis("192.168.0.109", 6379);
                Set<String> keys = jedis.keys("pipeline_btch_*");
                List<Object> result = new ArrayList();
                Pipeline pipelined = jedis.pipelined();
                long start= System.currentTimeMillis();
                for (String key : keys) {
                    pipelined.get(key);
                }
                result = pipelined.syncAndReturnAll();
                for (Object r: result) {
                    //System.out.println(r);
                }
                System.out.println("Pipeline get耗时:"+(System.currentTimeMillis() - start));
            }
        }.start();
    }
}

常见的NoSQL

目前常见的NoSQL主要包含:

  1. KV存储类型:Redis,Memcached
  2. 文档类型:MongoDB
  3. 列存储:HBase
  4. 图存储:Neo4j

Redis脱颖而出是因为其读写速度、跨语言、支持持久化、支持淘汰等,还提供了丰富的功能,比如事务、发布订阅、pipeline、lua脚本、集群和分布式的支持等

Redis常用数据类型

string类型

string的API
命令语法用途示例
setset 键 值保存一个字符串类型的键值对,键存在则覆盖值set name jack
getget 键通过键取出对应的值,如果键不存在则返回nullget name 结果:jack
getsetgetset 键 值先取出键对应的值,然后修改值getset name rose 结果:jack 被修改为rose
incrincr 键将该键对应的值自增1incr age 结果:age自增1
decrdecr 键将该键对应的值自减1decr age 结果:age自减1
incrbyincrby 键 自增值将该键对应的值自增指定数值incrby age 10 结果:age自增10
decrbydecrby 键 自增值将该键对应的值自减指定数值decrby age 10 结果:age自减10
appendappend 键 值如果该键存在,则在该值得基础上追加一段字符串,如果不存在该键则新增一个键值对name原来的值rose,append name you jump 结果:rose you jump
setnxsetnx 键 值当该键存在时,不会进行写操作,返回值为0,如果该键不存在时,会进行写操作,返回值为1
ypedef struct dictEntry {
    void *key;
    // union表示共用体,使用同一段内存,修改了都会修改
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
string的数据结构

key以字符串的形式进行存储到SDS中,由*key直接指向;而value则会采用多种SDS进行存储,使用redisObject进行同一管理,这点类似于Java中使用接口统一规范多个实现的思想。
string通过dictEntry实现,在dictEntry中,封装有*value指针*key指针、以及指向下一个节点的指针*next。dictEntry从命名可以看出这是一个类似于索引的结构,并没有真正存储键和值的内容,而是将键和值得内容放到了另外的存储结构中,指针直接或间接指向这个存储结构。这个存储结构就是Redis自己实现的SDS

在redisObject中,包含的属性含义如下:
type:对象的类型,包括OBJ_STRINGOBJ_LISTOBJ_HASHOBJ_SETOBJ_ZSET
encoding:具体的数据结构,如 intembstr,raw
lru:LRU_BITS:共24bit,对象最后一次被访问的时间,与key淘汰有关
refcount:引用技术。为0的时候,表示已经不被任何对象使用,可以进行回收
*ptr:指向对象实际存储结构的指针
在这里插入图片描述

而在数据结构中:
int:存储8个字节的长整型
embstr:存储小于44个字节的字符串
raw:存储大于44个字节的字符串
Redis内部对于存储结构进行了自动优化:

embstr和raw的区别是:embstr只会分配一次内存空间,即redisObject和SDS是存在一段内存连续的空间里。而raw需要分配两次内存空间,redisObject和SDS分别存储在两段不连续的内存空间内。
在这里插入图片描述
设置的三个key都是字符串,但是三个value的数据结构类型却不一样。如果我们对num进行追加内容,再次查看encoding会发现变化了(读者可以自行尝试,此处不再贴出验证结果)。但是可以知道的是:
当int数据不在是整数,就会转为raw 当int数据的大小超过long的范围(2^63-1),就会转为embstr,embstr长度超过了44字节转为raw,编码转换在Redis写入数据时完成,转换过程不可逆,只能从小内存编码转向大内存编码(set属于重新设置,不在该条规则的讨论范围)

Redis为什么要使用SDS实现字符串的存储呢?

SDS(Simple Dynamic String):简单的动态字符串。使用char数组实现,l

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

其中len表示数组的长度,alloc表示分配的内存大小。而5 8 16 32 64指的是结构能存储的容量。如sdshdr32表示能存储2^32个字节,等于4GB。

SDS的诞生只为解决传统C语言的以下问题:

  1. 内存空间预先会分配好
  2. 获取字符串的长度的时间复杂度是O(n)
  3. 长度变更引起的内存重新分配
  4. 用’\0’来判断字符串的结束(存储链接的时候很可能会碰到\0,但是不应该结束)。
    在这里插入图片描述
string的应用场景

1.用作缓存:标准的key-value用法,可以假想为一个全局(单机)或者多台电脑公用(分布式)的HashMap
2. 分布式session:将sessionId保存在Redis中,解决Session不能共享的问题。
3. 分布式锁:setnx命令的使用。
4. 分布式全局ID:使用incr命令实现。
5. 计数器:使用incr命令实现。
6. 限流:使用incr实现。
7. …

hashes类型

hashes是 key field value结构的存储方式,元素是无序且不能重复的,基于hash表实现,最大可以存储2^32(40亿左右)的数据。

student age 5
student name tom

了解hash就知道,无论key是怎样的数据,经过hash函数后,产出的就是一个数字,因此形成映射关系会比直接存储节省更多空间。
hashes相对于传统的hash来说,增加了field作为第一层key,形成两层key来确定一个value,无疑增加了key的数量,但是能大大减少key的冲突(不同field下的key可以重复),这是一种空间换时间的权衡。

hashes的缺点是不能单独对field设置过期时间,需要考虑数据量分布的问题。不能对field进行拆分存储,且field挂掉,则其下的所有key也直接丢失。

hashes的API
命令语法用途示例
hsethset 键为指定的键所对应的hash数据中的某个属性赋值hset student name jack
hmsethset 键向指定的键所对应的hash数据中一次保存一个或者多个键值对数据
hgethget 键查询指定键对应的hash数据中某个属性的值hget student name,结果:jack
hmgethmget 键查询指定键对应的hash数据中一个或多个键值对的值
hdelhdel 键删除指定键对应的hash数据中的一个或者多个属性
lrangelrange names 0 10结果是names对应的集合中0-10之间的数据,如果数据长度不足,则返回已有的数据,不会出现异常
hgetallhgetall 键返回指定键对应的hash数据的全部属性和值hgetall student
hexistshexists 键 属性判断指定键中是否存在某个属性,返回1表示存在,0表示不存在hexists student name,结果:1
hlenhlen 键返回指定键对应的hash数据的属性个数hlen student
hkeyshkeys 键返回指定键对应的hash数据中的所有属性名称hkeys student
hvalshvals 键返回指定键对应的hash数据中的所有属性值
hincrbyhincrby 键将指定键对应的属性值自增增量的值hincrby student age 10 age的值在原本的基础上+10
hashes的数据结构
  • ziplist–OBJ_ENCODING_ZIPLIST(压缩列表):是一个经过特殊编码的连续内存块组成的双向链表。内部不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度当前节点长度
    在这里插入图片描述
    在这里插入图片描述
    基于ziplist的结构,一般在一个hash对象保存的field数量<512个、一个对象中所有的field和value的字符串长度都<64byte时使用ziplist
typedef struct dictht {
    dictEntry **table;//
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

  • hashtable–OBJ_ENCODING_HT(哈希表):
    将dictEctry封装待dictht中,多出来的dictht是用来在扩容的时候暂存的

比如把a,b的值进行互换,我们会先把a的值放到c里面,再把b的值放到a里面,接着把c的值(实际存的a)放到b里面。此时的c就是起到暂存的作用。

在这里插入图片描述
dictEntry计算hash值的时候出现hash碰撞,则采用HashMap里面的处理方法,用链表来解决----形成一个链。同样如果链的长度过长,会导致效率变低。但是鉴于Redis是基于内存的,因此多数情况下依然能达到较高的速度。

在这里插入图片描述

hashes的使用场景

hashes可以看做是string的增强,因此string能做的,hashes都能做,而且hashes能存储对象类型的数据,能更加细化key的查找路径。

对于用户级别的产品有奇效,如购物车:
key:用户id(每个用户对应一个购物车)
field:商品id(购物车好中的多个商品)
value:商品数量(每种商品数量不同)

商品数量+1:hinc
商品数量-1:hincrby key field -1
删除商品:hdel
全选商品:hgetall
购物车商品种类数:hlen

lists类型

用来存储有序的字符串(从左到右),元素可以重复,最大能够存储2^32-1(40亿左右)的数据

list常用api
命令语法用途示例
lpushlpush 键 值1 值2…在指定的键所对应的list的头部插入所有的values,键不存在则新增lpush names jack rose,集合中最前的两个数据是rose jack
rpushrpush 键 值1 值2…在指定的键所对应的list的尾部插入所有的values,键不存在则新增rpush names jack rose,集合中最后的两个数据是jack rose
lrangelrange 键 下标1 下标2查询从下标1到下标2之间的数据,第一个数据下标为0,下标可以为负数,-1表示倒数第一个数据,-2表示倒数第二个数据,以此类推lrange names 0 10,结果是names对应的集合中0-10之间的数据,如果数据长度不足,则返回已有的数据,不会出现异常
lpushxlpushx键 值1在指定的键存在的情况下,将所有的数据插入到集合的头部lpushx names jack rose,集合中最前的两个数据是rose jack
rpushxrpushx键 值1在指定的键存在的情况下,将所有的数据插入到集合的尾部rpush names jack rose,集合中最后的两个数据是jack rose
lpoplpop 键将该键对应的集合中的第一个数据取出,取出之后第一个数据就从集合中移除lpop names,结果:取出并移除第一个数据
rpoprpop键将该键对应的集合中的最后一个个数据取出,取出之后数据就从集合中移除rpop names,结果:取出并移除最后一个数据
数据结构

在这里插入图片描述
采用quicklist进行存储。quicklist中存储了一个*head指针和一个*tail指针,分别指向有quicklistNode组成的双向链表的头节点和尾部节点。quicklistNode中除了存储指向上一个(图中的pre)和下一个节点(图中的tail)的指针外,还存储了一个ziplist存储结构。

typedef struct quicklist {
    quicklistNode *head; // 指向双向链表的头节点
    quicklistNode *tail; // 指向双向链表的尾结点,这样就等于获取到了一个完整的链表
    unsigned long count; // 所有的ziplist中一个存了多少个元素      /* total count of all entries in all ziplists */
    unsigned long len;  // node的总数量         /* number of quicklistNodes */
    int fill : QL_FILL_BITS; // ziplist最大大小              /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; // 压缩深度 /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS; // 
    quicklistBookmark bookmarks[];
} quicklist;

在这里插入图片描述

lists的使用场景

列表:
消息列表、文章列表、评论列表、公告列表、活动列表等,同时还可以用作朋友圈动态(按照创建时间进行倒叙排序)

sets类型

存储无序且不重复的数据,最大可以存储2^32-1个数据

sets常用API
命令语法用途示例
saddadd 键 值在指定的键对应的set集合中保存一个或多个数据sadd names jack tom
smemberssmembers键查询指定键的Set集合中保存的所有数据smembers names
scardscard 键查询指定键的Set集合中保存的数据个数scard names
sismembersismemb键 值判断值是否是指定键对应的Set集合中的成员,返回1表示是,0表示否sismember names jack
sremsrem 键 值删除指定键对应的Set集合中的一个或多个数据srem names jack
spopspop 键 count随机删除并返回指定键对应的Set集合中的1个或多个数据,数量由count决定,但是在某些版本中count不支持,不提供count时默认count为1spop numbers
srandmembersrandmember 键 count随机返回指定键对应的Set集合中的1个或多个数据,数量由count决定,不提供count时默认count为1srandmember names 2,随机返回names集合中的2个数据
smovesmove 键1 键2 值将键1对应集合中的指定数据移动到键2的集合中smove names1 names2 jack
sdiffsdiff 键1 键2返回键1的集合中在键2的集合中不存在的数据,也就是求键1集合在键2集合中的差集sdiff names1 names2
sdiffstoresdiffstore 键1 键2 键3将键2集合在键3中的差集保存到键1的集合中sdiffstore names names1 names2
sintersinter 键1 键2返回键1集合中在键2集合中也存在的数据,也就是求键1集合和键2集合的交集sinter names1 names2
sinterstoresinterstore 键1 键2 键3将键2集合在键3中的交集保存到键1的集合中sinterstore names names1 names2
sunionsunion 键1 键2返回键1集合和键2集合的并集sunion names1 names2
sunionstoresunion 键1 键2 键3将键2集合和键3集合的并集保存到键1集合中sunionstore names names1 names2
存储结构
  • intset
  • hashtable
    在Redis的配置文件中的set-max-intset-entries配置项可以配置set内部存储结构切换的阈值。
sets使用场景
  • 抽奖:spop pool
  • 点赞、签到、打卡等
  • 商品标签、用户画像(人口属性,信用属性,社交,兴趣爱好),用sets集合的交并补的特性

在这里插入图片描述

  • 用户关注、推荐模型(可能认识的人)

点赞案例:
在这里插入图片描述

sorted sets类型

sorted sets也叫zset,是一种可以存储不重复数据的set,且元素是有序的。但是有序不是值根据key来排序,而是除了key之外,还额外增加一个score字段,用来给可以进行排名。
在这里插入图片描述

常用API
命令语法用途示例
zaddadd 键score value score value… 向指定键对应的集合中添加数据,score为排序值,可以是整数或小数。value为值add names 1 zhangsan 2 lisi
zcardcard键 返回指定键对应的集合中的数据总数card names
zcountcount 键min max 查询指定键的集合中排序值在min和max之间的数据个数count names 1 5
zrangerange键 start stop 返回指定键对应的集合中排序序号(非排序值)在start和stop之间的数据,序号从0开始,数据会升序排列range names jack
zrankrank 键 值 返回指定键对应的集合中某个值得排序序号rank names jack
zremrem键1 值1 值2 移除指定键对应的Set集合中的1个或多个值rem names1 jack
zremrangebyrankremrangebyrank 键start stop 按照排序序号移除多个成员remrangebyrank names 0 2
zremrangebyscoreremrangebyscore键 min max 按照排序值移除多个成员remrangebyscore names 1 2
zadd myzset 10 java 20 php 30 python 40 cpp
zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 whtiscores
zrangebyscore myzset 20 30
zrem myzset php cpp
zcard myzset
zincrby myzset 5 java
zcount myzset 20 40
zrank myzset java
zscore myzset php

sortedsets存储结构
  • ziplist(元素数量<128,所有元素长度小于64bytes)
  • skiplist+dict:对于这样的存储机构
    在这里插入图片描述
    当元素存储的个数越来越大,如果刚好要查一个处于尾节点的元素,必定是要遍历n次。因此这个链表必定要经过该井优化后才能使用。
    在这里插入图片描述
    借鉴二分查找的思想,在某些节点上增加一个指针位,让他跨越一定数量的节点指向下一个,这样通过跳过随机个数的节点,实现横向的查找变成纵向的查找,减少遍历的次数。
sortedsets使用场景

排行榜:
id为7001的新闻被点击一次:
zincr不要hotnew_20210814 1 7001
获取今天热度排名前十五的新闻:
zrevrange hotnew_20210814 0 15 withscores

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值