redis 三. hash应用场景及底层分析

Posted 苹果香蕉西红柿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis 三. hash应用场景及底层分析相关的知识,希望对你有一定的参考价值。

Hash 类型

一. 简单命令示例

  1. Hash 类型,可以简单理解为对应java的Map<String,Map<Object,object>>
  2. 简单命令使用示例
//1.一次设置一个字段值
HSET key field value
//2.一次获取一个字段值
HGET key field
//3.一次设置多个字段值
HMSET key field value[field value...]
//4.一次获取多个字段值
HMGET key field[field...]
//5.获取所有字段值
HGETALL key
//6.获取某个key内的全部数量
HLEN
//7.删除一个key
HDEL
//8.修改
HINCRBY KEY field value

二. java 操作示例

  1. 操作 Hash 散列类型,是一个string类型的field和value的映射表,hash特别适合用于存储对象
	@Test
    public void test02()
        //1.创建操作 Hash类型数据的对象
        HashOperations<String ,String,String> hashOperations = stringRedisTemplate.opsForHash();

        //2.添加数据,第一个为redis的key,第二个为当前数据的field,点三个为值
        hashOperations.put("userInfo","name","gggg");

        //3.添加多条 map类型
        Map<String ,String> map=new HashMap<>();
        map.put("age","18");
        map.put("sex","1");

        //userInfo为redis的key,map中的键为field,值为value
        hashOperations.putAll("userInfo", map);

        //4.根据key,与field查询redis中的Hash类型数据
        String name=hashOperations.get("userInfo","name");

        //5.查询多条,创建查询的field集合
        List<String> keyS=new ArrayList<>();
        keyS.add("age");
        keyS.add("sex");

        //查询多条返回List集合
        List<String> hashValL=hashOperations.multiGet("userInfo",keyS);
        //查询多条返回Map集合
        Map<String,String>hashValue= hashOperations.entries("userInfo");

       //6.删除
        hashOperations.delete("userInfo","name");
    

三. 使用场景

  1. 购物车
//1.对应新增商品命令
hset (购物车+用户id)key <商品id,商品数量>
//2.对应增加商品数量
hincrby (购物车+用户id)Key 商品id,商品数量累加
//3.商品总数
hlen (购物车+用户id)Key
//4.全部选择
hgetall (购物车+用户id)Key

四. 底层分析

  1. 首先执行 “config get hash*” 命令了解两个参数

1)“hash-max-ziplist-entries”: 使用压缩列表保存时哈希集合中的最大元素个数,默认512
2)“hash-max-ziplist-value”: 使用压缩列表保存时哈希集合中单个元素的最大长度,默认64
3)修改"hash-max-ziplist-entries" 命令,修改为3示例: “config set hash-max-ziplist-entries 3”
4)修改"hash-max-ziplist-value" 命令,修改为8示例: “congif set hash-max-ziplist-value 8”

  1. hash类型键的字段个数小于"hash-max-ziplist-entries",并且每个字段名和字段值的长度小于"hash-max-ziplist-value"时, redis 才会使用"OBJ_ENCODING_ZIPLIST"编码格式存储该键,两个条件中任意一个不满足则会使用"OBJ_ENCODING_HT"(HT也就是hashTable)编码格式进行存储(下图中存储了一个hash类型的person数据,查看person的编码格式返回"ziplist")
  2. 示例2
  1. “config set hash-max-ziplist-entries 3”:修改哈希集合中的最大元素个数为3
  2. “congif set hash-max-ziplist-value 8”: 哈希集合中单个元素的最大长度为8
  3. “hset user01 name Z3”: 存储一个hash类型数据,key为user01
  4. “object encoding user01”: 查看 key为user01的编码格式,因为满足上面设置的两个条件3,8,所以返回ziplist
    5)“hset user01 name z3aaaaaa”: 再次执行hset命令覆盖user01这个key值,修改name值为"z3aaaaaa",字段值的长度大于"hash-max-ziplist-value"
    6)再次执行"object encoding user01" 查看编码格式返回 “hashtable”
  1. 由上面案例总结出: hash类型有两种编码格式: ziplist和hashtable, 当hash类型键的字段个数小于"hash-max-ziplist-entries",并且每个字段名和字段值的长度小于"hash-max-ziplist-value"时,采用ziplist,当两个条件任意一个不满足时采用hashtable编码,"hash-max-ziplist-entries"默认512, “hash-max-ziplist-value” 默认64, 注意点:ziplist可以升级为hashtable,但是不会降级

ziplist 压缩列表

  1. 思考,已经有链表了,为什么还需要压缩链表:
  1. 普通的双向链表中有两个指针,在存储数据小的情况下,实际存储的数据可能还没有指针占用空间大,通过ziplist一个特殊的双向链表,并没有维护指向上一个节点与下一个节点的指针,而是保存了上一个节点的长度,跟当前节点的长度,通过长度推算下一个节点的位置,稀释读取性能,获取高效的存储空间,(存储指针比存储长度更耗费内存),可以看为是通过拿长度推算位置的时间,换取直接存储位置指针的空间
  2. 链表在内存中一般不是连续的,遍历较慢,并且ziplist中存储的数据类型是不统一的长度更是不同的,所以ziplist在每个节点中都记录了偏移量(上一个节点的长度),能够更快的连续到上一个或下一个节点
  3. ziplist头节点中通过len属性,记录整个链表的长度,因此获取链表长度时不用去遍历获取,可以通过这个len属性直接获取,时间复杂度是0(1)
  1. ziplist压缩列表是一个经过特殊编码的双向链表,不存储指向上一个节点跟下一个节点的指针地址,存储了上一个节点的长度跟当前节点的长度,总体思想是通过时间换取空间,既以读写性能为代价换取极高的内存空间利用率,因此只会用于字段个数少,且字段值较小的场景,内存利用率高的原因跟连续内存的特性分不开的
  2. 整个ziplist结构:
  1. zlbytes: 记录 ziplist 整个结构体的占用空间大小,通过该字段当需要修改 ziplist 时候不需要遍历即可知道其本身的大小
  2. zltail: 记录整个 ziplist 中最后一个 entry 的偏移量,可以通过该属性直接获取尾部数据
  3. entry: 用于存储数据的节点
  4. zllen: 记录 entry 的数量, Redis 作了特殊的处理:当实体数超过 2^ 16 ,该值被固定为 2^16 - 1,当需要获取所有实体的数量时就必须要遍历整个结构
  5. zlend: ziplist 的结束标识

ziplist内部entry详解

  1. 在ziplist中会将数据存储在内部的entry节点中,了解entry节点内部详情
typedef struct zlentry 
	//上一个节点的长度用的字节
    unsigned int prevrawlensize; 
    //上一个节点的长度
    unsigned int prevrawlen;    
    //编码当前节点长度len锁需要的字节数
    unsigned int lensize; 
    //当前节点长度		
    unsigned int len;           
    //当前节点header大小,headersize=prevrawlensize + lensize
    unsigned int headersize; 
    //当前节点编码格式: ZIP_STR_*或ZIP_INT_*
    unsigned char encoding; 
    //第一个节点的地址指针,prev-entry-len
    unsigned char *p;         
 zlentry;
  1. 解释几个属性:
  1. prevrawlensize 上一个节点的长度用的字节
  2. prevrawlen 上一个节点的长度
    enncoding: 记录了content保存的数据类型和长度
    content: 保存的实际内容
  1. 由上面属性了解ziplist的遍历原理:

在ziplist中有个尾节点记录了整个ziplist的长度,通过尾节点的指针减去前一个节点的长度prevrawlensize就拿到了前一个节点的起始地址值,依次执行,就从表尾遍历到了表头,也就是说我们只要拿到某一个节点起始地址值,通过该节点中的"prevrawlensize 上一个节点的长度"属性,就可以来回遍历

  1. 也可这样取理解节点: 将一个节点看为三个部分: prevLen上一个节点的长度, encoding当前节点的编码, entrydata当前节点存储的实际内容

ziplist内存分配与连锁更新

  1. 在空间不足时,ziplist 节点的 prevlen 属性会根据前一个节点的长度进行分配:
  1. 如果前一个节点的长度小于 254 字节, prevlen 属性需要用 1 字节的空间来保存这个长度值
  2. 如果前一个节点的长度大于等于 254 字节, 那么 prevlen 属性需要用 5 字节的空间来保存这个长度值
  1. 假设有一个 ziplist,每个节点都是等于 253 字节的,此时新增了一个大于等于 254 字节的新节点,由于之前的节点 prevlen 长度是 1 个字节, 为了要记录新增节点的长度所以需要对节点 1 进行扩展,由于节点 1 本身就是 253 字节,再加上扩展为 5 字节的 pervlen 则长度超过了 254 字节,这时候下一个节点又要进行扩展了, 最终导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降,这就是说的连锁更新,也是在 v7.0 被 listpack 所替代的一个重要原因

ziplist 小总结:

  1. 一个ziplist由:总字节的长度zlbytes, 到尾节点的偏移量zltail,整个ziplist中节点的长度zllen, 还有entry集合(多个entry节点), 代表结束的zlend终节点组成
  2. 每个entry节点中也存在几个比较重要的属性: prevrawlensize 上一个节点的长度占用字节, prevrawlen上一个节点的长度 enncoding 当前节点中保存数据的编码格式及当前节点的长度, content 当前节点中存储的实际数据

优点: 提供len属性保存链表长度,不用再去遍历获取长度,去掉普通链表中指向上下节点的前指针与后指针,通过保存上一个节点的长度遍历更快,保证内存连续分配

  1. 通过这些属性我们了解到ziplist遍历原理: 在ziplist中有个尾节点zltail,并且记录了整个ziplist的长度,通过尾节点的指针减去前一个节点的长度prevrawlensize就拿到了前一个节点的起始地址值,依次执行,就从表尾遍历到了表头,也就是说我们只要拿到某一个节点起始地址值,通过该节点中的"prevrawlensize 上一个节点的长度"属性,就可以来回遍历
  2. 总结ziplist特点
  1. ziplist 为了节省内存,采用了紧凑的连续存储,但是在修改操作时需要从新分配新的内存,然后复制到新的空间。
  2. ziplist 是一个双向链表,可以在时间复杂度为 O(1) 从下头部、尾部进行 pop 或 push
  3. 新增或更新元素可能会出现连锁更新现象。
  4. 不能保存过多的元素,否则查询效率就会降低

hashtable 字典

  1. hashtable 是由数组+链表构成的, 编码格式"OBJ_ENCODING_HT", 该编码方式才是真正的哈希表结构,或称为字典,读写复杂度O(1),底层的散列表数据结构是一层一层套下去的,效率很高

redis 六. list应用场景及底层分析

List 类型

一. 简单命令示例

1.首先简单说明: List是一个双端链表的结构,内容是2的32次方减1个元素,大概40多亿,主要功能有push/pop等,一般用在栈,队列,消息队列等场景
2.简单命令

//1.向列表左边添加元素
LPUSH key value[value...]
//2.向列表右边添加元素
RPUSH key value[value...]
//3.查看列表
LRANGE key start stop
//4.获取列表中的元素个数
LLEN key

二. java 操作示例

  1. 操作 List 列表类型,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),通过list进行指定下标的查询,可以做到简单分页
	@Test
    public void test03()
        //1.创建操作List类型数据的对象
        ListOperations<String ,String> listOperations= stringRedisTemplate.opsForList();

        //2.左上添加,第一个添加的会被后一个添加的挤压到下面
        listOperations.leftPush("student","liuliuliu");
        //3.右下追加,第一个添加的会被后一个添加的挤到上面
        listOperations.rightPush("students", "Zhao Liu");

        //4.查询,根据key与下标获取指定位置的数据,左闭右闭,两边包含,返回一个List
        List<String> students = listOperations.range("student", 0,2);

        //5.根据key与指定下标获取单个数据
        String stu = listOperations.index("student", 1);

        //6.获取当前List类型数据的长度
        Long total = listOperations.size("student");

        //7.删除List类型数据,key为student的,value值为"Li",在List中第二次出现的,
        listOperations.remove("student", 2, "Li");

        // 删除多条,有左删除一条,右删除一条等
        stringRedisTemplate.delete("student");
        //8.从左边开始获取并删除
        String srt1 = listOperations.leftPop("student");
        //9.右边开始获取并删除
        String str2= listOperations.rightPop("student");
    

三. 使用场景

  1. 公众号订阅: 假设用户a订阅了某个公众号sss,当sss公众号发布新文章,就会push到a用户的list中
//发布文章命令
LPUSH (like文章模块+a用户id) sss公众号文章id, bbb公众号文章id
//分页
LRANGE (like文章模块+a用户id) 0 9
  1. 商品评论列表: 一个商品会被不同用户评论,保存评论是按照先后顺序排序,查询商品时按照时间逆序排序,list存储时,key是商品id,value是评论信息:商品编号
LPUSH (品id)key 评论者id:评论信息

四. 底层分析

  1. list底层有linkedList、zipList和quickList三种存储方式
  2. 当列表对象保存的所有字符串元素的长度都小于 list-max-ziplist-value 默认 64字节, 并且保存的元素数量小于list-max-ziplist-entries默认512个时采用zipList, 否则采用linkedlist
  3. linkedlist: redis中自建了listNode对象, 通过内部的prev 和 next 指针组成的一个双端链表,
  4. 在redis 在 3.2 版本时,考虑到redis的空间存储效率和时间效率,引入了quicklist快速列表作为 list 的底层实现可以看成ziplist+linkedList实现的一个双端链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当于一个quicklist节点保存的是一片数据,而不再是一个数据
  5. redis中封装了quicklist 结构体变量, 内部通过quicklistNode 保存每一个节点数据,
typedef struct quicklist 
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : QL_FILL_BITS;              /* 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;
typedef struct quicklistNode 
	//上一个node节点
    struct quicklistNode *prev; 
    //下一个node
    struct quicklistNode *next; 
    //保存的数据 压缩前ziplist 压缩后压缩的数据
    unsigned char *zl;   
    //表示zl执行的ziplist的总大小,注意如果ziplist被压缩了,这个sz的值仍然时压缩前的大小         
    unsigned int sz;            
    //表示ziplist包含的数据个数,16bit
    unsigned int count : 16;     
    //表示ziplist是否压缩,1表示没有,2表示压缩了
    unsigned int encoding : 2;   
    //预留字段,当前是一个固定值2,表示使用zplist作为数据容器
    unsigned int container : 2;  
   //解压标记1, 通过该标记可以再次压缩
    unsigned int recompress : 1;
    unsigned int attempted_compress : 1; 
    unsigned int extra : 10; 
 quicklistNode;
  1. 使用quicklist 插入数据时可以头部插入,或者尾部插入,
  1. 如果头节点(或尾节点) 上ziplist大小没有超过限制(即_quicklistModeAlLowEInsert 返回1),那么新数据被直接插入到ziplist中(调明ziplisteush ) 。
  2. 如果头节点(或尾节点)上ziplist太大了,那么新创建一个quicklistlode节点(对应地也会新创建一个ziplist),然后把这个新创建的节点插入到quicklist双向链表中
  1. quicklist 在任意指定位置插入数据时
  1. 当插入位置所在的ziplist大小没有超过限制时,直接插入到ziplist中
  2. 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的quicklist链表节点的ziplist大小没有超过限制,那么就转而插入到相邻的那个quicklist链表节点的ziplist中;
  3. 当插入位置所在的ziplist大小超过了限制,但插入的位置位于ziplist两端,并且相邻的Squicklist链表节点的ziplist大小也超过限制,这时需要新创建一个quicklist链表节点插入。
  4. 对于插入位置所在的ziplist大小超过了限制的其它情况(主要对应于在ziplist中间插入数据的情况),则需要把当前ziplist分裂为两个节点,然后再其中一个节点上插入数据
  1. quicklist 查找: quicklist的节点是由一个一个的ziplist构成的每个ziplist都有大小,所以先根据每个node个数,找到对应的ziplist,调用ziplist的index就能成功找到。
  2. quicklist 删除: 在区间删除时,会先找到start 所在的 quicklistlode,计算删除的元素是否小于要删除的count,如果不满足删除的个数,则会移动至下一个quicklistNode 继续删除,依次循环直到删除完成为止

以上是关于redis 三. hash应用场景及底层分析的主要内容,如果未能解决你的问题,请参考以下文章

Redis简介及应用场景

《闲扯Redis四》List数据类型底层编码转换

深入了解Redis源码下载与参考资料准备

Redis 数据类型及应用场景

redis之管道应用场景及源码分析

玩转Redis-Redis高级数据结构及核心命令-ZSet