• 阅读之Redis性能


     

    Redis作为一种KV缓存服务器,有着极高的性能,相对于memcache,Redis支持更多中数据类型,因此在业界广泛应用。

    Redis为什么快:

    1. 数据是存储在内存中的。
    2. Redis是单线程的。

    将数据存储在内存中,读取的时候后不需要进行磁盘的IO,单线程也保证了系统没有线程的上下文切换。

    从数据存储层面上分析Redis性能为何如此高。

    Redis性能如此高的原因,有如下几点

    1. 纯内存操作
    2. 单线程
    3. 高效的数据结构
    4. 合理的数据编码
    5. 其他方面的优化

    在Redis中,常用的5中数据结构和应用场景如下:

    1. String:缓存、计数器、分布式锁等
    2. List:链表、队列、微博关注人世间轴列表等
    3. Hash:用户信息、Hash表等
    4. Set:去重、赞、踩、共同好友等

    Zset:访问量排行榜、点击量排行榜等

    SDS

    Redis是用C语言开发完成的,但在Redis字符串中,并没有使用C语言中的字符串,而是用一种称为SDS(simple dynamic string)的结构体来保存字符串,SDS的结构如下

    struct sdshdr {
          int len;
        int free;
        char buf[];
    }

    len:用于记录buf中已使用空间的长度

    free:buf中空闲空间的长度

    buf[]:存储实际内容

    例如:执行命令set key value,key和value都是一个SDS类型的结构存储在内存中

    二、SDS与C字符串的区别

    1、常数时间内获得字符串长度

    • C字符串本身不记录长度信息,每次获取长度信息都需要遍历整个字符串,复杂度为O(n)
    • C字符串遍历时遇到''时结束

    SDS中len字段保存着字符串的长度,所以总能在常数时间内获取字符串长度,复杂度是O(1)

    2、避免缓冲区溢出

    假设在内存中有两个紧挨着的两个字符串,s1=“xxxxx”和s2=“yyyyy”,由于在内存上紧紧相连,当我们对s1进行扩充的时候,将s1=“xxxxxzzzzz”后,由于没有进行相应的内存重新分配,导致s1把s2覆盖掉,导致s2被莫名其妙的修改。

    但SDS的API对zfc修改时首先会检查空间是否足够,若不充足则会分分配新空间,避免了缓冲区溢出问题。

    3、减少字符串修改时带来的内存重新分配的次数

    在C中,当我们频繁的对一个字符串进行修改(append或trim)操作的时候,需要频繁的进行内存重新分配的操作,十分影响性能。如果不小心忘记,有可能会导致内存溢出或内存泄漏,对于Redis来说,本身就会很频繁的修改字符串,所以使用C字符串并不合适。而SDS实现了空间预分配和惰性空间释放两种优化策略:

    • 空间预分配

    当SDS的API对一个SDS修改后,并且对SDS空间扩充时,程序不仅会为SDS分配所需要的必须空间,还会分配额外的未使用空间,分配规则如下:

    如果对SDS修改后,len的长度小于1M,那么程序将分配和len相同长度的未使用空间。举个例子,如果len=10,重新分配后,buf的实际长度会变为10(已使用空间)+10(额外空间)+1(空字符)=21。

    如果对SDS修改后len长度大于1M,那么程序将分配1M的未使用空间

    • 惰性空间释放

    当对SDS进行缩短操作时,程序并不会回收多余的内存空间,而是使用free字段将这些字节数量记录下来不释放,后面如果需要append操作,则直接使用free中未使用的空间,减少了内存的分配。

    4、二进制安全

    在Redis中不仅可以存储String类型的数据,也可能存储一些二进制数据。二进制数据并不是规则的字符串格式,其中会包含一些特殊的字符如'',在C中遇到''则表示字符串的结束,但在SDS中,标志字符串结束的是len属性。

    一、String对象的编码转化

    String对象的编码可以是int或raw,对于String类型的键值,如果我们存储的是纯数字,Redis底层采用的是int类型的编码,如果其中包括非数字,则会立即转为raw编码

    127.0.0.1:6379> set str 1
    OK
    127.0.0.1:6379> object encoding str
    "int"
    127.0.0.1:6379> set str 1a
    OK
    127.0.0.1:6379> object encoding str
    "raw"
    127.0.0.1:6379>

    二、List对象的编码转化

    List对象的编码可以是ziplist或linkedlist,对于List类型的键值,当列表对象同时满足一下两个条件时采用ziplist编码

    • 列表对象保存的所有字符串元素的长度都小于64字节
    • 列表对象保存的元素个数小于512个

    如果不满足这两个条件的任意一个,就会转化为linkedlist编码

    注意:这两个条件是可以修改的,在redis.conf中

    list-max-ziplist-entries 512
    list-max-ziplist-value 64

    三、Set类型的编码转化

    Set对象的编码可以是intset或hashtable,intset编码的结婚对象使用整数集合作为底层实现,把所有元素都保存在一个整数集合里面。

    127.0.0.1:6379> sadd set 1 2 3
    (integer) 3
    127.0.0.1:6379> object encoding set
    "intset"
    127.0.0.1:6379>

    如果set集合中保存了非整数类型的数据时,Redis会将intset转化为hashtable

    127.0.0.1:6379> sadd set 1 2 3
    (integer) 3
    127.0.0.1:6379> object encoding set
    "intset"
    127.0.0.1:6379> sadd set a
    (integer) 1
    127.0.0.1:6379> object encoding set
    "hashtable"
     127.0.0.1:6379>

    当Set对象同时满足一下两个条件时,对象采用intset编码

    • 保存的所有元素都是整数值(小数不行)
    • Set对象保存的所有元素个数小于512个

    不能满足这两个条件的任意一个,Set都会采用hashtable存储

    注意:第两个条件是可以修改的,在redis.conf中

    set-max-intset-entries 512

    四、Hash对象的编码转化

    Hash对象的编码可以是ziplist或hashtable,当Hash以ziplist编码存储的时候,保存同一键值对的两个节点总是紧挨在一起,键节点在前,值节点在后

    zlbytes

    zltail

    zllen

    name

    tom

    age

    25

    career

    programmer

    zlend

    当Hash对象同时满足一下两个条件时,Hash对象采用ziplist编码

    • Hash对象保存的所有键值对的键和值的字符串长度均小于64字节
    • Hash对象保存的键值对数量小于512个

    如果不满足以上条件的任意一个,ziplist就会转化为hashtable编码

    注意:这两个条件是可以修改的,在redis.conf中

    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64

    五、Zset对象的编码转化

    Zset对象的编码可以是ziplist或zkiplist,当采用ziplist编码存储时,每个集合元素使用两个紧挨在一起的压缩列表来存储。第一个节点存储元素的成员,第二个节点存储元素的分值,并且按分值大小从小到大有序排列。

    zlbytes

    zltail

    zllen

    java

    5.0

    python

    6.0

    html

    7.0

    zlend

    当Zset对象同时满足一下两个条件时采用ziplist编码

    • Zset保存的元素个数小于128
    • Zset元素的成员长度都小于64字节

    如果不满足以上条件的任意一个,ziplist就会转化为zkiplist编码

    注意:这两个条件是可以修改的,在redis.conf中

    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64

    思考:Zset如何做到O(1)复杂度内元素并且快速进行范围操作?

    Zset采用skiplist编码时使用zset结构作为底层实现,该数据结构同时包含了一个跳跃表和一个字典,其结构如下:

    typedef struct zset{
        zskiplist *zsl;
       dict *dict;
    }

    Zset中的dict字典为集合创建了一个从成员到分值之间的映射,字典中的键保存了成员,字典中的值保存了成员的分值,这样定位元素时时间复杂度是O(1)

    Zset中的zsl跳跃表适合范围操作,比如ZRANK、ZRANGE等,程序使用zkiplist。

    另外,虽然zset中使用了dict和skiplist存储数据,但这两种数据结构都会通过指针来共享相同的内存,所以没有必要担心内心的浪费。

    总结

    总而言之,Redis为了高性能,从各方各面都进行了优化,不只是单线程和内存存储了。

  • 相关阅读:
    2020-03-23
    2020-03-22
    2020-03-21
    2020-03-20
    2020-03-19
    2020-03-18
    2020-03-17
    单元测试-java
    2020-03-16
    C语言拯救计划Day3-1之求一批整数中出现最多的个位数字
  • 原文地址:https://www.cnblogs.com/liulala2017/p/11096375.html
Copyright © 2020-2023  润新知