Redis基本类型及Jedis对其的操作

Redis数据结构类型简介

Redis是一种高级的key:value存储系统,其中value支持五种数据类型:String、Hash、List、Set和Sorted Set。
每种数据类型可以用多种方式编码:

字符串(String)可以被编码为 raw (常规字符串) 或者int (用字符串表示64位无符号整数这种编码方式是为了节省空间).
列表类型(List)可以被编码为ziplist 或者 linkedlist. ziplist 是为了节省较小的列表空间而设计一种特殊编码方式.
集合(Set)被编码为 intset 或者 hashtable. intset 是为了存储数字的较小集合而设计的一种特殊编码方式.
哈希表(Hash)可以被编码为 zipmap 或者hashtable. zipmap 是专为了较小的哈希表而设计的一种特殊编码方式
有序集合(Sorted Set)被编码为ziplist 或者 skiplist 格式. ziplist可以表示较小的有序集合, skiplist 表示任意大小多的有序集合.

这里写图片描述

redisObject

为了便于操作,Redis定义了redisObjec结构体来表示string、hash、list、set、zset五种数据类型,如下:
这里写图片描述

redisObject定义在redis.h文件中:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前 提是这个字符串本身可以用数值表示,比如:”123” “456”这样的字符串。

使用如下命令可以查看指定key对应value所使用的内部表示

object encoding key 

例如:

redis> set foo 1000
OK
redis> object encoding foo
"int"
redis> append foo bar
(integer) 7
redis> get foo
"1000bar"
redis> object encoding foo
"raw"

String:

String是简单的key-value类型,value其实不仅是String,也可以是数字。

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

内置数据结构

动态字符串SDS (Simple Dynamic String)

struct sdshdr {   
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 数据空间
    char buf[];
};
jedis 对其的操作
jedis.set(key, value); //增加&修改
jedis.del(key) //删除
jedis.setex(key, expireSecond, value); //设置超期时间

jedis.mset("key01","value01","key02","value02","key03","value03"));
jedis.mget("key01","key02","key03"));

jedis.del(new String[]{"key01","key02"})
jedis.setnx("key1", "value1") //新增键值对防止覆盖原先值
jedis.getSet(key,newValue)// 设置为新value 但返回旧value

//memcached和redis同样有append的操作,但是memcached有prepend的操作,redis中并没有。
jedis.append("key3", "End") 

jedis.incr("key1") //加1
jedis.decr("key2") //减1
jedis.incrBy("key1", n) //加n decrBy 减n

双向链表List

常用命令:lpush,rpush,lpop,rpop,lrange等。

实现方式:
Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

内置数据结构
/*
 * 双端链表结构
 */
typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 节点值复制函数
    void *(*dup)(void *ptr);
    // 节点值释放函数
    void (*free)(void *ptr);
    // 节点值对比函数
    int (*match)(void *ptr, void *key);
    // 链表所包含的节点数量
    unsigned long len;
} list;

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的值
    void *value;
} listNode;

/*
 * 双端链表迭代器
 */
typedef struct listIter {
    // 当前迭代到的节点
    listNode *next;
    // 迭代的方向
    int direction;
} listIter;

这里写图片描述

jedis 对其的操作
 1). 右边入队:jedis.rpush("collections", "ArrayList", "Vector")
              jedis.rpush("userList", "James"); 
 2). 左边出队:右边出栈(rpop),即为对堆栈的操作 压栈 弹栈
              jedis.lpop("userList");  
 3). 返回列表范围:从0开始,到最后一个(-1) [包含] 
              List<String> userList = jedis.lrange("userList", 0, -1);  
              Redis的TopN操作,即使用list完成:lrange
              //获取collections下标为2的元素
              jedis.lindex("collections", 2)
 4). 删除:使用key jedis.del("userList");  
      //删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
      jedis.lrem("collections", 2, "HashMap")
 5). 设置:位置1处为新值----区别于入队
               jedis.lset("userList", 1, "Nick Xu"); 
 6). 返回长度:Long size = jedis.llen("userList");  
 7). 进行裁剪:包含 jedis.ltrim("userList", 1, 2);  

Hash

在Memcached中,我们经常将一些结构化的信息打包成HashMap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。

Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。这样对数据的修改和存取都可以直接通过其内部Map的 Key(Redis里称内部Map的key为field), 也就是通过 key+ field就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。

需要注意的是,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部 Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而令其它客户端的请求完全不响应。

实现方式:

上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。

内置数据结构
/* 保存键值(key(即field) - value)对的结构体*/
typedef struct dictEntry { 
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

/* 哈希表结构 */
typedef struct dictht {   
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

/* 字典的主操作类,对dictht结构再次包装  */
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引 数据动态迁移的下标位置
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;

/*
 * 字典类型特定函数
 */
typedef struct dictType {
    // 计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);

    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

这里写图片描述
这里我们可以看到一个dict 拥有两个 dictht。一般来说只使用 ht[0],当扩容的时候发生了rehash的时候,ht[1]才会被使用。

rehash操作

我们梳理一下插入数据的逻辑。
● 计算Key 的 hash 值。找到 hash 映射到 table 数组的位置。
● 如果数据已经有一个 key 存在了。那就意味着发生了 hash 碰撞。新加入的节点,就会作为链表的一个节点接到之前节点的 next 指针上。
● 如果 key 发生了多次碰撞,造成链表的长度越来越长。会使得字典的查询速度下降。为了维持正常的负载。Redis 会对 字典进行 rehash 操作。来增加 table 数组的长度。所以我们要着重了解一下 Redis 的 rehash。步骤如下:
a. 根据ht[0] 的数据和操作的类型(扩大或缩小),分配 ht[1] 的大小。
b. 将 ht[0] 的数据 rehash 到 ht[1] 上。
c. rehash 完成以后,将ht[1] 设置为 ht[0],生成一个新的ht[1]备用。
● 渐进式的 rehash 。
其实如果字典的 key 数量很大,达到千万级以上,rehash 就会是一个相对较长的时间。所以为了字典能够在 rehash 的时候能够继续提供服务。Redis 提供了一个渐进式的 rehash 实现,rehash的步骤如下:
a. 分配 ht[1] 的空间,让字典同时持有 ht[1] 和 ht[0]。
b. 在字典中维护一个 rehashidx,设置为 0 ,表示字典正在 rehash。
c. 在rehash期间,每次对字典的操作除了进行指定的操作以外,都会根据 ht[0] 在 rehashidx 上对应的键值对 rehash 到 ht[1]上。
d. 随着操作进行, ht[0] 的数据就会全部 rehash 到 ht[1] 。设置ht[0] 的 rehashidx 为 -1,渐进的 rehash 结束。
这样保证数据能够平滑的进行 rehash。防止 rehash 时间过久阻塞线程。
● 在进行 rehash 的过程中,如果进行了 delete 和 update 等操作,会在两个哈希表上进行。如果是 find 的话优先在ht[0] 上进行,如果没有找到,再去 ht[1] 中查找。如果是 insert 的话那就只会在 ht[1]中插入数据。这样就会保证了 ht[1] 的数据只增不减,ht[0]的数据只减不增。

jedis 对其的操作
1. 存放数据:使用HashMap
      Map<String, String>  capital = new HashMap<String, String>();  
      capital.put("shannxi", "xi'an");  
      ...  
      jedis.hmset("capital", capital);  
      jedis.hset("hash", "key5", "value5");
2 ) 获取数据
      List<String> cities = jedis.hmget("capital", "shannxi", "shanghai");  

      Map<String, String> map=jedis.hgetAll(String key)//所有键值对
      jedis.hkeys("hash") 所有键
      jedis.hvals("hash") 所有值
      jedis.hincrBy("hash", "key6", 6) 将key6保存的值加上一个整数,如果key6不存在则添加key6
      hdel("hash", "key2") 删除一个或者多个键值对
      jedis.hlen("hash") 散列hash中键值对的个数
      jedis.hexists("hash","key2") 判断hash中是否存在key2

Set

Set是一堆不重复值,无序的组合。set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

内置数据结构

整数集合 intset用于有序、无重复地保存多个整数值
intset 的数据结构:

typedef struct intset {

    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} ints

encoding 的编码类型如下:

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

根据需要, intset 在插入数据的时候可以自动从 int16_t 升级到 int32_t 或 int64_t ,或者从 int32_t 升级到 int64_t 。但是整数集合不支持降级操作,一旦升级就不能降级了。

当集合只存储整数1、2、4时,底层采用intset来存储;往集合中添加字符串”hello”后,底层存储变为哈希表。

127.0.0.1:6379> sadd s1 1 2 4
(integer) 3
127.0.0.1:6379> object encoding s1
"intset"
127.0.0.1:6379> sadd s1 "hello"
(integer) 1
127.0.0.1:6379> object encoding s1
"hashtable"
jedis 对其的操作
jedis.sadd("fruit", "pear", "watermelon");   添加到set:可一次添加多个
Set<String> fruit = jedis.smembers("fruit");   遍历集合
jedis.srem("fruit", "pear");  移除元素:remove
jedis.scard("fruit");  返回长度
jedis.sismember("fruit", "pear"); 是否包含

jedis.smove("fruit", "food", "cookie")  将cookie从fruit移入food中
jedis.spop("fruit") //随机移除一个元素

jedis.sadd("food", "bread", "milk");   
//集合的交运算(sinter)、差集(sdiff)、并集(sunion)
Set<String> fruitFood = jedis.sunion("fruit", "food");  

Sorted Set

有序集合在集合的基础上,增加了一个用于排序的参数,通过用户额外提供一个优先级(score)的参数来为成员排序。

Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

内置数据结构

跳跃表skiplist是一种基于并联链表的、随机化的数据结构,可以实现平均复杂度为O(logN)的插入、删除和查找操作。
跳表是由一个zskiplist 和 多个 zskiplistNode 组成。

/* ZSETs use a specialized version of Skiplists */
/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {
    // 成员对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

/*
 * 跳跃表
 */
typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

结构图如下:
这里写图片描述

跳表就是一个利用空间换时间的数据结构,利用 level 作为链表的索引

jedis 对其的操作
jedis.zadd("user", 22, "James");  //第二个参数为score
jedis.zadd("user", 24, "James");  //更新score
Set<String> user = jedis.zrange("user", 0, -1);  zset的范围:找到从0到-1的所有元素

SortingParams sortingParameters = new SortingParams(); 
jedis.sort(key, sortingParameters.asc()) 按照指定方式对value排序

其它命令

key
jedis.flushDB(); 清空数据
Set<String> keys = jedis.keys("*")//系统中所有的键
Set<String> keys = jedis.keys("user.userid.*");  对key的模糊查询
jedis.del("password")
jedis.exists("password")  
jedis.expire("username", 5) //设置超期时间5s
jedis.ttl("username") //剩余存在时间
jedis.persist("username") //移除键的生存时间
jedis.type("username") //值的类型
事务
Transaction tx = jedis.multi();  获取事务
批量操作:tx采用和jedis一致的API接口
for(int i = 0;i < 10;i ++) {  
     tx.set("key" + i, "value" + i);   
     System.out.println("--------key" + i);  
     Thread.sleep(1000);    
}  
执行事务:针对每一个操作,返回其执行的结果,成功即为Ok
List<Object> results = tx.exec();  
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值