redis中存储数据是以key-value
键值对的方式去存储的,其中key为string字符类型,value的数据类型可以是string(字符串)、list(列表)、hash(字典)、set(集合) 、 zset(有序集合)。 这5种数据类型在开发中可以应对大部分场景的存储
这里有一个问题,当我们使用一条redis命令set key value
的时候,redis进行了什么操作?
其实当我们使用命令操作redis的时候,也是会经过redis客户端到redis服务端的过程,这些命令相当于一个请求
同时redis是使用c语言写的,但它底层在存放redis的key时,并没有用c语言原生的字符串数据结构,而是定义了一个属于redis的数据结构SDS(Simple Dynamic String),
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
为什么要用这样的一个数据结构去存储字符串呢?
get aaa\0
:获取 aaa\0
的值。如果是c语言的字符数组就会把\0
吞掉,变为get aaa
,而使用SDS
就会完整的操作aaa\0
,SDS
把所有接受到的数据都转成字符串,即使是一些特殊字符!命令 | 说明 |
---|---|
SET key value | 存入字符串键值对 |
MSET key value [key value …] | 批量存储字符串键值对 |
SETNX key value | 存入一个不存在的字符串键值对 |
GET key | 获取一个字符串键值 |
MGET key [key …] | 批量获取字符串键值 |
EXPIRE key seconds | 设置一个键的过期时间(秒) |
DEL key [key …] | 删除一个键 |
INCR key | 将key中储存的数字值加1 |
DECR key | 将key中储存的数字值减1 |
INCRBY key increment | 将key所储存的值加上increment |
DECRBY key decrement | 将key所储存的值减去decrement |
> SET key value
OK
> GET key
"value"
值得注意的是, 当key存在时,set命令会覆盖掉你上一次设置的值
> SET key newValue
OK
> GET key
"newValue"
EXISTS
和 DEL
关键字来查询是否存在和删除键值对:> EXISTS key
(integer) 1
> DEL key
(integer) 1
> GET key
(nil)
> SET key1 value1
OK
> SET key2 value2
OK
> MGET key1 key2 key3 # 返回一个列表
1) "value1"
2) "value2"
3) (nil)
> MSET key1 value1 key2 value2
> MGET key1 key2
1) "value1"
2) "value2"
> SET key value1
> GET key
"value1"
> EXPIRE name 5 # 5s 后过期
... # 等待 5s
> GET key
(nil)
> SET key value
> GETSET key value1
"value"
这可以对于某一些需要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 INCR
命令操作一个 key,当需要统计时候你就把这个 key 使用 GETSET
命令重新赋值为 0,这样就达到了统计的目的。
SET key value
GET key
使用这两条命令可以做用户id存储、商品库存存储等等!
SETNX product:10001 true //操作product:10001
##执行业务操作...
DEL product:10001 //删除product:10001
其中SETNX key value 命令要求如果key已存在,则其他的setnx命令无法对当前key进行操作。
在使用分布式锁时通常还会通过
SET product:10001 true ex 10 nx 命令设置key的超时时间,防止死锁!
NCR 文章id
可以使用INCR命令实现数量自增,可以用于文章阅读量、热度人数统计等,用户每点进去一次执行一次INCR命令
redis的INCR命令可以实现id、序列号的生成,但如果用户量非常大,每生成一个id、序列号都去redis会给redis添加不小的压力,我们可以一次性从redis中自增1000次,把序列号放入本地内存中,这1000个id用完了,再去redis再取1000个,可有效降低redis的压力!
Redis 中的字典(hash)相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 “数组 + 链表” 的链地址法来解决部分 哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。
命令 | 说明 |
---|---|
HSET key field value | 存储一个哈希表key的键值 |
HGET key field | 获取哈希表key对应的field键值 |
HMSET key field value [field value …] | 在一个哈希表key中存储多个键值对 |
HMGET key field [field …] | 批量获取哈希表key中多个field键值 |
HSETNX key field value | 存储一个不存在的哈希表key的键值 |
HDEL key field [field …] | 删除哈希表key中的field键值 |
HLEN key | 返回哈希表key中field的数量 |
HGETALL key | 返回哈希表key中所有的键值 |
HINCRBY key field increment | 为哈希表key中field键的值加上增量increment |
> HSET books java "think in java" # 命令行的字符串如果包含空格则需要使用引号包裹
(integer) 1
> HSET books python "python cookbook"
(integer) 1
> HGETALL books # key 和 value 间隔出现
1) "java"
2) "think in java"
3) "python"
4) "python cookbook"
> HGET books java
"think in java"
> HSET books java "head first java"
(integer) 0 # 因为是更新操作,所以返回 0
> HMSET books java "effetive java" python "learning python" # 批量操作
OK
hset cart:1001 10088 1
hincrby cart:1001 10088 1
hlen cart:1001
hdel cart:1001 10088
hgetall cart:1001
思考-优点:
1)同类数据归类整合储存,方便数据管理
2)相比string操作消耗内存与cpu更小
因为:string类型通过set key - val 的方式存储数据,通过对key进行hash运算决定当前key是存储在数组哪个位置。如果把hash类型的数据变成string类型来存储,则需要更多的key,同时在存放时也需要更多的hash(key)运算,消耗更多的cpu资源!
3)相比string储存更节省空间
如果把hash类型的数据变成string类型来存储,将需要存储更多key,如果数据量很多的情况下,redis底层那么存储数据的数组将很快会被占满,占满就会进行扩容,加大内存消耗。由此可见,string结构与hash结构只存储一个key相比,需要更多的内存空间!
思考-缺点:
1)过期功能不能使用在field上,只能用在key上
redis的过期时间只能用在key上,而hash的key是一个大的概念,里面的map型结构才是重要数据,但过期时间只能用在外边的大key上,hash结构相比于string不能实现精准过期!
2)hash结构在Redis集群架构下不适合大规模使用
因为如果一个hash的key中的属性很多的话,只能存在一个redis节点上,那么这个节点压力会比其他节点压力大很多,造成redis集群下压力分配不均衡!
Redis 的列表相当于 Java 语言中的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。
命令 | 说明 |
---|---|
LPUSH key value [value …] | 将一个或多个值value插入到key列表的表头(最左边) |
RPUSH key value [value …] | 将一个或多个值value插入到key列表的表尾(最右边) |
LPOP key | 移除并返回key列表的头元素 |
RPOP key | 移除并返回key列表的尾元素 |
LRANGE key start stop | 返回列表key中指定区间内的元素,区间以偏移量start和stop指定 |
BLPOP key [key …] timeout | 从key列表表头弹出一个元素,若列表中没有元素,阻塞等待 |
timeout:秒,如果timeout=0,一直阻塞等待 | |
BRPOP key [key …] timeout | 从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待 |
timeout:秒,如果timeout=0,一直阻塞等待 |
LPUSH
和RPUSH
分别可以向list的左边(头部)和右边(尾部)添加一个新元素LRANGE
命令可以从list中取出一定范围的元素LINDEX
命令可以从list中取出指定下标的元素,相当于java量表操作中的get(int index)
操作> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有
1) "first"
2) "A"
3) "B"
> RPUSH books python java golang
(integer) 3
> LPOP books
"python"
> LPOP books
"java"
> LPOP books
"golang"
> LPOP books
(nil)
> RPUSH books python java golang
> RPOP books
"golang"
> RPOP books
"java"
> RPOP books
"python"
> RPOP books
(nil)
# ①:MacTalk发微博,消息ID为10010
LPUSH msg:{小明_ID} 10010
# ②:备胎说车发微博,消息ID为10086
LPUSH msg:{小明_ID} 10086
# ③:查看最新微博消息(前4条)
LRANGE msg:{小明_ID} 0 4
思考:大V发了消息的是怎么存储在粉丝的redis中呢?一般有两种处理方案!
(1)推送,博主发了消息,通过线程先推送到在线粉丝的队列中,其他不在线的粉丝等后面系统在空闲的时候再慢慢推送过去
(2)拉取,如果粉丝太多,推的方案还是要很长时间去处理,还有一种方案就是拉,每一个粉丝上线后就去关注的博主那里拉取他发送的最新的消息,在使用LRANGE
取出即可。
Redis 的集合相当于 Java 语言中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。
命令 | 说明 |
---|---|
SADD key member [member …] | 往集合key中存入元素,元素存在则忽略,若key不存在则新建 |
SREM key member [member …] | 从集合key中删除元素 |
SMEMBERS key | 获取集合key中所有元素 |
SCARD key | 获取集合key的元素个数 |
SISMEMBER key member | 判断member元素是否存在于集合key中 |
SRANDMEMBER key [count] | 从集合key中选出count个元素,元素不从key中删除 |
SPOP key [count] | 从集合key中选出count个元素,元素从key中删除 |
命令 | 说明 |
---|---|
SINTER key [key …] | 交集运算 |
SINTERSTORE destination key [key …] | 将交集结果存入新集合destination中 |
SUNION key [key …] | 并集运算 |
SUNIONSTORE destination key [key …] | 将并集结果存入新集合destination中 |
SDIFF key [key …] | 差集运算 |
SDIFFSTORE destination key [key …] | SDIFFSTORE destination key [key …] |
> SADD books java
(integer) 1
> SADD books java # 重复
(integer) 0
> SADD books python golang
(integer) 2
> SMEMBERS books # 注意顺序,set 是无序的
1) "java"
2) "python"
3) "golang"
> SISMEMBER books java # 查询某个 value 是否存在,相当于 contains
(integer) 1
> SCARD books # 获取长度
(integer) 3
> SPOP books # 弹出一个
"java"
首先了解一下set的集合操作,假如有三个集合
set1:(a、b、c)
set2:(b、c、d)
set3:(c、d、e)
三个集合的
交集为:SINTER set1 set2 set3 ==> { c }
并集为:SUNION set1 set2 set3 ==> { a,b,c,d,e }
差集为:SDIFF set1 set2 set3 ==> { a }
差集计算方式:set1 - (set2并set3) = {a、b、c} - {b、c、d、e} = {a} 只保留a中单独存在的元素
共同关注A的人:可以用交集来实现
我可能认识的人:可以使用差集来实现,把我的好友求差集,例如a的好友={b,c},b的好友={a,c,d},那么a可能认识的人可以是,b-a={a,b,c,d}-{a,b,c} = {d}。当然这只是一个简单又不大现实的想法,相当于把对方的好友全部推荐给你了,真实情况的还需要考虑到关系网络,好友的权重数等
这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。
zset相比于set多一个score 分值,正是根据这个分值进行排序,所以zset才能展示有序的数据
命令 | 说明 |
---|---|
ZADD key score member [[score member]…] | 往有序集合key中加入带分值元素 |
ZREM key member [member …] | 从有序集合key中删除元素 |
ZSCORE key member | 返回有序集合key中元素member的分值 |
ZINCRBY key increment member | 为有序集合key中元素member的分值加上increment |
ZCARD key | 返回有序集合key中元素个数 |
ZRANGE key start stop [WITHSCORES] | 正序获取有序集合key从start下标到stop下标的元素 |
ZREVRANGE key start stop [WITHSCORES] | 倒序获取有序集合key从start下标到stop下标的元素 |
> ZADD books 9.0 "think in java"
> ZADD books 8.9 "java concurrency"
> ZADD books 8.6 "java cookbook"
> ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围
1) "java cookbook"
2) "java concurrency"
3) "think in java"
> ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围
1) "think in java"
2) "java concurrency"
3) "java cookbook"
> ZCARD books # 相当于 count()
(integer) 3
> ZSCORE books "java concurrency" # 获取指定 value 的 score
"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
> ZRANK books "java concurrency" # 排名
(integer) 1
> ZRANGEBYSCORE books 0 8.91 # 根据分值区间遍历 zset
1) "java cookbook"
2) "java concurrency"
> ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
> ZREM books "java concurrency" # 删除 value
(integer) 1
> ZRANGE books 0 -1
1) "java cookbook"
2) "think in java