五种基本数据类型
String、Hash、List、Set和Zset。
1、String
等同于java中的,Map<String,String>
string 是redis里面的最基本的数据类型,一个key对应一个value。
- string 是二进制安全的,可以把图片和视频文件保存在String中。
- string的最大内存值 512M,即一个key或者value最大值是512M,官方说可以存2.5亿个key。
应用场景:String是最常用的一种数据类型,普通的key/value存储都可以归为此类,如用户信息,登录信息和配置信息等;
实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作(自增自减等原子操作)时会转成数值型进行计算,此时redisObject的encoding字段为int。
Redis虽然是用C语言写的,但却没有直接用C语言的字符串,而是自己实现了一套字符串。目的就是为了提升速度,提升性能。Redis构建了一个叫做简单动态字符串(Simple Dynamic String),简称SDS。
struct sdshdr{ // 记录已使用长度 int len; // 记录空闲未使用的长度 int free; // 字符数组 char[] buf; };
Redis的字符串也会遵守C语言的字符串的实现规则,即最后一个字符为空字符。然而这个空字符不会被计算在len里头。
- 因为有了对字符串长度定义len, 所以在处理字符串时候不会以零值字节(\0)为字符串结尾标志.
- 二进制安全就是输入任何字节都能正确处理, 即使包含零值字节.
Redis动态扩展步骤:
- 计算出大小是否足够
- 开辟空间至满足所需大小
- 开辟与已使用大小len相长度同的空闲free空间(如果len < 1M),开辟1M长度的空闲free空间(如果len >= 1M)
Redis字符串的性能优势
- 快速获取字符串长度:直接返回len
- 避免缓冲区溢出:每次追加字符串时都会检查空间是否够用
- 降低空间分配次数提升内存使用效率:(1)空间预分配;(2)惰性空间回收
常用命令:set/get/decr/incr/mget等,具体如下;
常用命令 | 命令 |
添加一对kv | set key value |
添加多对kv(可覆盖) | mset key value key value…. |
添加多对kv(不可覆盖,只要有一个已存在,全部取消) | msetnx key value key value…. |
获取 | get value |
获取多对kv | mget key key… |
删除 | del key |
在末尾追加 | append key value |
查询v的长度 | strlen key |
给数值类型的v加/减1 | incr/decr key |
给数值类型增加/减少指定大小的值 | incrby/decrby key value |
获取v的长度 | getrange key |
在指定位置添加指定值(中间默认用空格补全) | setrange key offset value |
添加指定生命周期的kv | setex key seconds value |
如果不存在则添加 | setnx key value |
获取旧值,设置新值 | setget key value |
ps:计数器(字符串的内容为整数的时候可以使用),如 set number 1。
补充:
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过期 (integer) 56
2、Hash
等同于java中的:Map<String,Map<String,String>>
,redis的hash是一个string类型的field和value的映射表,特别适合存储对象。在redis中,hash因为是一个集合,所以有两层。第一层是key:hash集合value,第二层是hashkey:string value。所以判断是否采用hash的时候可以参照有两层key的设计来做参考。并且注意的是,设置过期时间只能在第一层的key上面设置。
- 使用hash,一般是有那种需要两层key的应用场景,也可以是‘删除一个key可以删除所有内容’的场景。例如一个商品有很多规格,规格里面有不同的值。
- 或者查找所有商品的规格,查找商品id即可,具体的规格可以通过两个key查找。
应用场景:我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;
实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时redisObject的encoding字段为int。
常用命令:hget/hset/hgetall等,具体如下:
作用 | 命令 |
添加单个 | hset key field value |
获取单个 | hget key field |
一次性添加多个键值 | hmset key field1 value1 field2 value2 … |
一次性获取多个 | hmget |
获取所有键值 | hgetall key |
删除 | hdel |
获取键值对的个数 | hlen |
检查是否包含某个字段 | hget key field |
查看所有key | hkeys |
给某个数值类型(否则报错)的值增加指定整数值 | hincrby key field increment |
给某个数字类型值,增加指定浮点类型值 | hincrbyfloat key field increment |
如果不存在则添加 | hsetnx |
3、list
等同于java中的Map<String,List<String>>
,list 底层是一个链表,在redis中,插入list中的值,只需要找到list的key即可,而不需要像hash一样插入两层的key。list是一种有序的、可重复的集合。
- redis的list是一个双向链表(易于插入和删除元素)。
- 它会按照我们插入的顺序排序,然后我们可以从他的头部添加/获取元素,也可以从它的尾部添加/获取元素,但带来的额外的空间开销
应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;
实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
常用命令:lpush/rpush/lpop/rpop/lrange等,具体如下:
常用命令 | 命令 |
左压栈 | lpush key v1 v2 v3 v4… |
右压栈 | rpush key v1 v2 … |
查看里面的元素 | lrange key start offset |
左弹栈 | lpop key |
右弹栈 | rpop key |
按照索引查找 | lindex key index |
查看长度 | llen key |
删除几个几 | lrem key 数量 value |
指定开始和结束的位置截取,再赋值给key | ltrim key start offset |
右出栈左压栈,把resoure的左后一个,压倒dest的第一个 | rpoplpush resource destination |
重置指定索引的值 | lset key index value |
在指定元素前/后插入指定元素 | linsert key before/after 值1 值2 |
性能总结:
它是一个字符串链表,left、right都可以插入添加。
- 如果键不存在,创建新的链表。
- 如果键已经存在,新增内容。
- 值全部移除,key消失。
- 由于是链表,所以它对头和尾操作的效率都极高。但是假如是对中间元素的操作,效率就可怜了。
4、Set
等同于java中的Map<String,Set<String>>
,Set 是一种无序的,不能重复的集合。并且在redis中,只有一个key它的底层由hashTable实现的,天生去重。
应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;如保存一些标签的名字。标签的名字不可以重复,顺序是可以无序的。
实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
常用命令:sadd/spop/smembers/sunion等,具体如下:
常用命令 | 命令 |
添加值 | sadd key values |
查看值 | smembers key |
检查集合是否有值 | sismember key value |
查看set集合里面的元素个数 | scard key |
删除集合中的指定元素 | srem key value |
随机弹出某个元素 | srandmember key |
随机出栈 | spop key |
把key1中的某个值赋值给key2 | smove SourceSet destSet member |
数学集合类 | 命令 |
差集 | sdiff |
交集 | sinte |
并集 | sunion |
5、Zset
ZSet(Sorted Set:有序集合) 每个元素都会关联一个double类型的分数score,分数允许重复,集合元素按照score排序(当score相同的时候,会按照被插入的键的字典顺序进行排序),还可以通过 score 的范围来获取元素的列表。
- set的值是 k1 v1 k2 v2
- zset的值 K1 score v1 k2 score v2
应用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
底层实现:zset
是Redis
提供的一个非常特别的数据结构,常用作排行榜等功能,以用户id
为value
,关注时间或者分数作为score
进行排序。实现机制分别是zipList
和skipList
。规则如下:
zipList:满足以下两个条件
[score,value]
键值对数量少于128个;- 每个元素的长度小于64字节;
skipList:不满足以上两个条件时使用跳表、组合了hash和skipList
hash
用来存储value
到score
的映射,这样就可以在O(1)
时间内找到value
对应的分数;skipList
按照从小到大的顺序存储分数skipList
每个元素的值都是[socre,value]
对
为什么用skiplist不用平衡树?
主要从内存占用、对范围查找的支持和实现难易程度这三方面总结的原因。
- 内存占用:skiplist比平衡树更灵活一些。一般来说,平衡树每个节点至少包含2个指针,而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
- 范围查找的支持:在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
- 实现难易程度:skiplist更加简单,平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
拓展:mysql为什么不用跳表?
常用命令:zadd/zrange/zrem/zcard等;
127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重 (integer) 1 127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素 (integer) 2 127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量 (integer) 3 127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重 "3" 127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素 1) "value3" 2) "value2" 3) "value1" 127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value3" 2) "value2" 127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value1" 2) "value2"
三大特殊数据类型
1、geospatial
官网地址:https://redis.io/commands/geoadd
可以用来推算两地之间的距离,方圆半径内的人。
关于经度纬度的限制:https://www.redis.net.cn/order/3685.html
1# 添加三个城市 2127.0.0.1:16379[2]> geoadd china:city 116.40 39.99 beijing 3(integer) 1 4127.0.0.1:16379[2]> geoadd china:city 117.190 39.1255 tianjin 5(integer) 1 6127.0.0.1:16379[2]> geoadd china:city 120.36955 36.094 qingdao 7(integer) 1 8127.0.0.1:16379[2]> 9 10# 获取指定key的经度和纬度 11127.0.0.1:16379[2]> geopos china:city beijing 121) 1) "116.39999896287918091" 13 2) "39.99000043587556519" 14127.0.0.1:16379[2]> 15 16# 获取两个给定位置的距离 17127.0.0.1:16379[2]> geodist china:city beijing qingdao # 默认单位为米 18"555465.2188" 19127.0.0.1:16379[2]> geodist china:city beijing qingdao km 20"555.4652" 21127.0.0.1:16379[2]> geodist china:city beijing qingdao m 22"555465.2188" 23127.0.0.1:16379[2]> geodist china:city beijing qingdao mi # 英里 24"345.1509" 25127.0.0.1:16379[2]> geodist china:city beijing qingdao ft # 英尺 26"1822392.4503" 27 28# 查找附近的人 29# 以给定的经纬度为中心,找出某一半径内的元素 30127.0.0.1:16379[2]> georadius china:city 117.190 39.1255 200 km # 半径为200km 311) "tianjin" 322) "beijing" 33127.0.0.1:16379[2]> georadius china:city 117.190 39.1255 200 km withdist # 指定显示距离 341) 1) "tianjin" 35 2) "0.0001" 362) 1) "beijing" 37 2) "117.6221" 38127.0.0.1:16379[2]> georadius china:city 117.190 39.1255 200 km count 2 # 指定显示2个结果 391) 1) "tianjin" 40 2) "0.0001" 412) 1) "beijing" 42 2) "117.6221" 43 44# 以指定的members为依据,找到它指定范围内的元素 45127.0.0.1:16379[2]> GEORADIUSBYMEMBER china:city beijing 120 km 461) "beijing" 472) "tianjin" 48 49# 返回一个或者多个位置的11位长度的hash串表示 50127.0.0.1:16379[2]> geohash china:city beijing 511) "wx4g2xzyss0" 52127.0.0.1:16379[2]> geohash china:city beijing tianjin 531) "wx4g2xzyss0" 542) "wwgqddx4sc0" 55 56# geo底层使用 zset 实现 57127.0.0.1:16379[2]> ZRANGE china:city 0 -1 581) "qingdao" 592) "tianjin" 603) "beijing" 61 62# 可以通过zrem删除 geo添加的key中的member 63127.0.0.1:16379[2]> ZREM china:city beijing 64(integer) 1 65127.0.0.1:16379[2]> ZRANGE china:city 0 -1 661) "qingdao" 672) "tianjin"
2、Hyperloglog
一般我们使用Hyperloglog做基数统计。
什么是基数?就是一个集合中不重复的数的个数。
集合A:{1,3,5,7,9,7}
集合B:{1,3,5,7,9}
AB集合的基数都是5
应用:统计网站的访问量(一个人访问网站很多次仍然算作一次)。
优点:占用的内存是固定的,找2^64次方个数的基数,只需要12KB内存。
缺点:有0.81%的错误率,可以忽略不计
1# PFCOUNT 计算出来的数量就是Set的基数 2127.0.0.1:16379[2]> PFADD key4 q w e r q 3(integer) 1 4127.0.0.1:16379[2]> PFCOUNT key4 5(integer) 4 6 7# 添加key1和key对应的多个值 8127.0.0.1:16379[2]> PFADD key1 q w e r 9(integer) 1 10 11# 统计key下有多少个值 12127.0.0.1:16379[2]> PFCOUNT key1 13(integer) 4 14 15# 添加key2和key对应的多个值 16127.0.0.1:16379[2]> PFADD key2 a s d f 17(integer) 1 18 19# 合并多个key成为一个key 20127.0.0.1:16379[2]> PFMERGE key3 key1 key2 21OK 22127.0.0.1:16379[2]> PFCOUNT key3 23(integer) 8
3、Bitmap(*)
概述:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间。
应用场景: 适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
针对上面提到的一些场景,这里进行进一步说明。
使用场景一:用户行为分析 很多网站为了分析你的喜好,需要研究你点赞过的内容。
# 记录你喜欢过 001 号小姐姐 127.0.0.1:6379> setbit beauty_girl_001 uid 1 Copy to clipboardErrorCopied
使用场景二:统计活跃用户
使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1
那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个 redis 的命令
# 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 # BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数 BITOP operation destkey key [key ...] Copy to clipboardErrorCopied 初始化数据: 127.0.0.1:6379> setbit 20210308 1 1 (integer) 0 127.0.0.1:6379> setbit 20210308 2 1 (integer) 0 127.0.0.1:6379> setbit 20210309 1 1 (integer) 0 Copy to clipboardErrorCopied 统计 20210308~20210309 总活跃用户数: 1 127.0.0.1:6379> bitop and desk1 20210308 20210309 (integer) 1 127.0.0.1:6379> bitcount desk1 (integer) 1 Copy to clipboardErrorCopied 统计 20210308~20210309 在线活跃用户数: 2 127.0.0.1:6379> bitop or desk2 20210308 20210309 (integer) 1 127.0.0.1:6379> bitcount desk2 (integer) 2 Copy to clipboardErrorCopied
使用场景三:用户在线状态
对于获取或者统计用户在线状态,使用 bitmap 是一个节约空间效率又高的一种方法。
只需要一个 key,然后用户 ID 为 offset,如果在线就设置为 1,不在线就设置为 0。
总结
- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 支持丰富数据类型,支持string,list,set,sorted set(zset),hash
补充:
- 一个字符串类型的值能存储最大容量是512M
- 其余类型元素最大存储量为2^32 - 1,注意hash是键值对的个数