Redis【一】基础数据结构&Jedis

目录

一、string(字符串)

二、list(列表)

1.压缩列表(ziplist)

2.双向链表(list)

3.quicklist

三、hash(字典)

四、set(集合)

五、zset(有序列表)

跳跃表

六、容器型数据结构的通用规则

七、过期时间

八、Jedis


Redis有5种基础数据结构,分别为:string(字符串)、list(列表)、hash(字典)、set(集合)和zset(有序集合)。Redis所有的数据结构都以唯一的key字符串作为name,然后通过这个唯一的key值来获取相应的value数据。只不过不同类型的数据结构的value结构不同。

一、string(字符串)

Redis是利用C语言实现,C语言中String类型在编译时会默认被加上"\0"结束标志,而Redis并没有直接采用String类型来表示字符串,而是定义了SDS结构体来表示字符串,其内部使用char[]数组形式实际存储字符串。因为直接采用C语言的String类型是二进制不安全的,即如果字符串中本身就含有"\0",字符串就会被截断,即非二进制安全。虽然在char[]数组末尾也会添加“\0”结束标志,但redis持有独立的len,在处理字符串时候不会以0结束标志作为字符串结尾标志。这样可以保证即使存储的数据中有’\0’这样的字符,它也是可以支持读取的。

在Redis中,字符串string的内部表示就是一个字符数组,如下:

struct sdshdr{
    //记录buf数组中已使用字节的数量,也就是该字符串的长度
    int len;
    //记录buf数组中未使用字节的数量
    int free;
    //字节数组,用于保存字符串
    char buf[];
};

Redis的字符串是动态字符串,是可以修改的字符串,内部结构的实现类似于Java的ArrayList,采用分配冗余空间的方式来减少内存的频繁分配,如上图所示,当前字符串分配的实际空间一般要高于实际字符串长度len,当字符串长度小于1MB时,扩容都是加倍现有的空间。如果字符串长度超过1MB,扩容时只会多扩容1MB的空间。注意,字符串最大长度为512MB

由于SDS不同于普通的C语言String类型,所以它能存储任何形式的字符串,包含二进制数据。你能够用其存储用户的邮箱、JSON化的对象甚至是一张图片。也就是说Redis的SDS可以存储二进制数据,以存储图片为例:

图片转化成String字符串

  1. 我们能够在Redis存储图片的base64编码或者解码。以KV格式,K为普通字符串,V为图片的base64编码。get(K)后再base64解码就能够了;
  2. 我们也能够在Redis中存储图片的网络url或者本地的path路径,详细实现能够使图片本身存储到磁盘中,然后在Redis中以图片的网络url或者本地的path路径为value(V)值存储。

C语言中的堆内存是需要动态申请手动释放的,为了优化性能,Redis不直接释放内存,而是直接将len的值归0,此处已经存在的buf并没有被真正的清除,新的数据可以覆盖写,而不用重新申请内存。 

相关命令:

【键值对】
SET key value          //设置字符串如:SET name yhj   即设置字符串yhj,索引yhj的key值是name
GET key                //获取指定key值对应的value,如GET name 返回的结果为"yhj"
SETNX key value        //如果name不存在就执行SET创建
STRLEN key             //返回 key 所储存的字符串值的长度。

MSET key value [key value ...]    //同时设置多个key-value,如MSET name1 yhj name2 yj
MGET key1 [key2 ...]              //同时获取多个key对应的value,如MGET name1 name2返回结果"yhj" "yj"

【过期】
//如果没有key对应的value则返回null
//还可以对key设置过期时间,到时间会自动删除,这个功能用来控制缓存的失效时间。如
SET age 24    //先存储age
EXPIRE age 5  //字符串“24”会在5s后过

【计数】
//如果value值是一个整数,还可以对他进行自增操作。自增得范围是有限的,它的范围在signed long的最大值和最小值之间,超出这个范围,Redis就会报错。如:
INCR age        //那么刚才的age就会变为25
INCRBY age 5    //相当于age += 5
INCRBY age -5   //相当于age -= 5

注意:如果字符串设置了过期时间,但是期间利用set 修改了该字符串,则过期时间会消失。

二、list(列表)

1.压缩列表(ziplist)

Redis使用字节数组表示一个压缩列表,数组结构如下图:

  1. zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2^32 - 1个字节。
  2. zltail:压缩列表尾元素相对于压缩列表起始地址偏移量,占4个字节。
  3. zllen:压缩列表的元素个数,占2个字节。故一个压缩列表元素最多65535个。
  4. entryX:压缩列表存储的元素,可以是字节数组或者整数,长度不限。
  5. zlend:压缩列表的结尾,占1个字节,恒为0xFF。

假设char * zl指向压缩列表首地址,Redis可通过以下宏定义实现压缩列表各个字段的存取操作。

//zl指向zlbytes字段
#define ZIPLIST_BYTES(zl)         (*((uint32_t*)(zl)))
//zl+4指向zltail字段
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
//zl+zltail指向尾元素首地址;intrev32ifbe使得数据存取统一采用小端法
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
//zl+8指向zllen字段
#define ZIPLIST_LENGTH(zl)  (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
//压缩列表最后一个字节即为zlend字段
#define ZIPLIST_ENTRY_END(zl)    ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

压缩列表元素entry的编码结构如下: 

  1. previous_entry_length: 表示前一个元素的字节长度,占1个字节或者5个字节,如果前一个元素的长度小于254字节,则用1个字节表示;如果前一个元素的长度大于或等于254字节,用5个字节表示,而此时previous_entry_length字段的第一个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度。通过previous_entry_length可以实现压缩列表从头到尾遍历,假设当前元素的首地址为p,那么(p - previous_entry_length)就是前一个元素的首地址。除了遍历压缩列表,由于压缩列表的实际结构是数组,所以往压缩列表中插入和删除元素时需要计算元素的位置,这一点也可以通过previous_entry_length字段完成。
  2. encoding: 表示content字段存储的数据类型(整数或者字节数组)。
  3. content: 存储数据内容。

2.双向链表(list)

双向链表的结构如下图,它的插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢。

3.quicklist

Redis对外提供的列表是压缩列表和双向链表的结合体,即quicklist。quicklist可以看成是用双向链表将若干小型的压缩列表(ziplist)连接到一起组成的一种数据结构。在列表元素较少的情况下,redis会使用一块连续的内存存储,这个结构是ziplist,即压缩列表。他将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成链表。因为普通的链表需要的附加指针空间太大,会浪费空间,还会加重内存的碎片化。所以Redis将链表和ziplist结合起来组成了quicklist(快速链表)。也就是将多个ziplist使用双向指针串起来使用。quicklist既满足了快速的插入删除性能,又不会出现太大的空间冗余。

如下为常用的列表操作:

LLEN key    //获取链表的长度。这个key是索引这个链表的“键”。
【可充当队列:先进先出】
RPUSH key value1 [value2]        //向链表末节点后添加一个节点
LPOP key                         //删除并获取列表的第一个元素

【可充当栈:先进后出】
先RPUSH操作
RPOP key                         //删除并获取列表的最后一个元素
【慢操作】
LINDEX key index                 //通过索引获取列表中的元素,他会从头节点开始索引,索引从0开始。复杂度O(n)
//对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
LTRIM key start stop             //如LTRIM key 1 -1(删除所有元素,O(n))
LRANGE key start stop            //获取列表指定范围内的元素,如LRANGE key 0 -1(这个会返回所有元素,O(n))

三、hash(字典)

Redis的字典相当于Java语言里面的HashMap,它是无序字典,内部存储了许多键值对。实现结构与Java的HashMap一样,也是数组+链表。不同的是Redis的字典值只能是字符串。hash结构也可以用来存储用户信息,与字符串需要一次性全部序列化整个对象不同,hash可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以全部进行读取,这样就会浪费网络流量。但是hash也有缺点,hash结构的存储消耗要高于单个字符串。

常用的操作如下:

HSET key field value         //将哈希表key中的字段field的值设为value。
HMGET key field1 [field2]    //获取所有给定字段的值
HLEN key                     //获取哈希表中字段的数量
HKEYS key                    //获取哈希表中所有字段的值
HGETALL key                  //获取该哈希表中所有的 键和值,entries()
HGET key field               //获取存储在哈希表中指定字段的值。
HEXISTS key field            //查看哈希表 key 中,指定的字段是否存在。

【批量操作】
HMSET key field1 value1 [field2 value2 ]    //同时将多个 field-value对设置到哈希表 key中。
HMGET key field1 [field2]    //获取所有给定字段的值
HDEL key field1 [field2]      //删除一个或多个哈希表字段

【为单个field对应的值计数】
HINCRBY key field increment    //为哈希表key中的指定字段的整数值加上增量increment。
//如: HSET user age 24
//     HINCRBY user age 1       

四、set(集合)

Redis里的集合相当于Java语言里的HashSet,它也存储的是String 类型数据,内部的值对是无序的、唯一的。它也是使用哈希表实现,只不过所有的value都是NULL。常用命令如下:

SCARD key                     //获取集合的成员数
SADD key member1 [member2]    //向集合添加一个或多个成员
SMEMBERS key                  //返回集合中的所有成员(由于集合是无序的,所以和插入顺序不一致)
SISMEMBER key member          //判断 member 元素是否是集合 key 的成员(相当于contains()操作)
SPOP key                      //删除并返回集合中的一个随机元素

五、zset(有序列表)

zset类似于Java中SortedSet和HashMap的结合体,与集合一样,都用于存储String类型数据,一方面它是一个set,保证value的唯一性,另一方面它可以关联一个double类型的sorce(排序权重,sorce是可以重复的)。它的内部实现用的是一种叫做“跳跃表”的数据结构。

跳跃表

有序单链表查找元素时需要遍历链表,时间复杂度是O(N),跳跃表将有序链表中的部分节点分层,每一层都是一个有序的链表。跳跃表查询、插入、删除的平均复杂度都为O(logN)。

跳跃表有如下性质:

  1. 跳跃表由很多层构成。
  2. 跳跃表有一个头(header节点),头节点中有一个64层的结构,每层的结构包含指向本层的下个节点的指针,指向本层下个节点中间所跨越的节点个数为本层的跨度(span)。
  3. 除头节点外,最高层的高度为整个跳跃表的高度。 4.每层都是一个递增的有序链表
  4. 除header节点外,一个元素在上层有序链表中出现,则它一定会在下层有序链表中出现。
  5. 跳跃表拥有一个tail指针,指向跳跃表最后一个节点。
  6. 最底层的有序链表包含所有节点,最底层的节点个数为跳跃表的长度(length)(不包括头节点)
  7. 每个节点包含一个后退的指针,头节点和第一个节点指向NULL;其他节点指向最底层的前一个节点。

 

跳跃表的查找原理: 先从最高层开始向后查找,当到达某个节点时,如果节点的next节点值大于要查找的值或next指针指向NULL,则从当前节点下降一层继续向后查找。采用该原理查找节点,在节点数量较多时,可以跳过一些节点,查询效率大大提升。

Redis的配置文件中关于有序集合底层实现的两个配置如下:

  1. zset-max-ziplist-entries 128:zset采用压缩列表时,元素个数最大值。默认值为128。
  2. zset-max-ziplist-value 64:zset采用压缩列表时,每个元素的字符串长度最大值。默认值为64。

zset插入第一个元素时,会判断下面两种条件:

  • zset-max-ziplist-entries的值是否等于0;
  • zset-max-ziplist-value 小于要插入元素的字符串长度。

满足任一条件Redis就会采用跳跃表作为底层实现,否则采用压缩列表作为底层实现方式。

一般情况下,不会将zset-max-ziplist-entries配置成0,元素的字符串长度也不会太长,所以在创建有序集合时,默认使用压缩列表的底层实现。zset新插入元素时,会判断以下两种条件:

  • zset中元素个数大于zset-max-ziplist-entries
  • 插入元素的字符串长度大于zset-max-ziplist-value

当满足任一条件时,Redis便会将zset的底层实现由压缩列表转为跳跃表。值得注意的是:zset在转为跳跃表之后,即使元素被逐渐删除,也不会重新转为压缩列表。

常用命令如下:

ZADD key score1 member1 [score2 member2]    //向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key             //获取有序集合的成员数
ZCOUNT key min max    //计算在有序集合中指定区间分数的成员数
ZRANGE key start stop //按sorce由小到大获取成员值。通过索引区间返回有序集合指定区间内的成员,若像ZRANGE key 0 -1(会全部输出)
ZREVRANK key member   //按sorce由大到小获取成员值
ZSCORE key member     //返回有序集中,成员的分数值
ZRANK key member      //返回有序集合中指定成员的索引(下标)
ZRANGEBYSCORE key min max //通过分数返回有序集合指定区间内的成员
ZREM key member [member ...]    //移除有序集合中的一个或多个成员

六、容器型数据结构的通用规则

list、set、zset、hash这四种数据结构是容器型数据结构,他们都有如下两条通用规则:

  • 如果容器不存在,则创建一个,再进行操作。
  • 如果里没有任何元素了,则立即删除容器,释放内存。

七、过期时间

Redis所有的数据结构都可以设置过期时间,时间到了,Redis会自动删除相应的对象。需要注意的是,过期是以对象为单位的,比如一个hash结构的过期是整个哈希表过期,而不是其中一个域值对。可以使用EXPIRE key time(单位为s)。

八、Jedis

Jedis是Java用户最常用的Redis开源客户端,它非常稳定,而且使用的方法、参数名称和redis官方文档非常相似。Jedis对象不是线程安全的,在多线程环境下应该使用Jedis的连接池JedisPool,它是线程安全的对象。当我们需要使用Jedis对象时从连接池中拿出一个Jedis对象使用即可,使用完毕再将该对象归还。

public static void method(){
    JedisPool pool = new JedisPool();
    Jedis jedis = null;
    try {
        jedis = pool.getResource();
        //doSomthing
    }finally {
        if(jedis != null){
            jedis.close();
        }
    }
}

我们一般会利用配置文件来初始化连接池,如下:

public class JedisPoolBuilder {
    private int maxTotal;
    private int maxIdle;
    private int minIdle;
    private long maxWaitMillis;
    private String serverHost = "localhost";
    private int serverPort = 6379;

    public JedisPoolBuilder(String configPath) throws IOException {
        //加载配置
        loadConfig(configPath);
        //效验配置
        checkConfig();
    }

    private void loadConfig(String configPath) throws IOException {
        //获取资源文件输入流
        InputStream inputStream = null;
        try {
            inputStream = JedisPoolBuilder.class.getResourceAsStream(configPath);
            Properties pp = new Properties();
            pp.load(inputStream);

            String maxTotal = pp.getProperty("pool.maxTotal");
            this.maxTotal = Integer.parseInt(maxTotal);

            String maxIdle = pp.getProperty("pool.maxIdle");
            this.maxIdle = Integer.parseInt(maxIdle);

            String minIdle = pp.getProperty("pool.get.minIdle");
            this.minIdle = Integer.parseInt(minIdle);

            String maxWaitMillis = pp.getProperty("pool.maxWaitMillis");
            this.maxWaitMillis = Long.parseLong(maxWaitMillis);

            if (pp.containsKey("server.host")) {
                this.serverHost = pp.getProperty("server.host");
            }
            if (pp.containsKey("server.port")) {
                this.serverPort = Integer.parseInt(pp.getProperty("server.port"));
            }
        }finally {
            if(inputStream != null){
                inputStream.close();
            }
        }
    }

    //效验配置
    public void checkConfig(){
        if(this.maxTotal < 0){
            throw new IllegalArgumentException("maxTotal error");
        }
        //……
    }

    public JedisPool builder(){
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxWaitMillis(this.maxWaitMillis);
        poolConfig.setMaxTotal(this.maxTotal);
        poolConfig.setMinIdle(this.minIdle);
        poolConfig.setMaxIdle(this.maxIdle);
        return new JedisPool(poolConfig,serverHost,serverPort);
    }
}

通过JedisPoolBuilder可以初始化不同配置的JedisPool,但是程序中一般只保存一份连接池对象,可以将其封装为单例的。

public class JedisPoolUtil {
    private volatile static JedisPool jedisPool;

    public static JedisPool createJedisPool(String configPath) throws IOException {
        if(jedisPool == null){
            synchronized (jedisPool){
                if(jedisPool == null){
                    JedisPoolBuilder builder = new JedisPoolBuilder(configPath);
                    jedisPool = builder.builder();
                }
            }
        }
        return jedisPool;
    }
}

参考自《Redis深度历险》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值