字符串(K-V)
字符串(K-V)是我们在Redis中使用最多的一个类型,其中V的值不能超过512M,甚至很多人用Redis只用这个类型。如果只是单纯的使用K-V其实是并没有把Redis的特性发挥出来,在这种使用情况下,使用Redis和使用Memcache并没有过多的区别。
命令
设置值
set key value
直接设置值。
setex key seconds value
设置值并且带有过期时间。
setnx key value
设置值,并且判断是否存在。如果存在则设置不成功,如果不存在则设置成功。主要用于分布式锁。
getset key value
设置并返回原值。获取值
get key
根据key获取值,如果不存在则返回nil。批量设置值
mset key value [key value ...]
批量设置K-V,例如:mset a 1 b 2 c 3 d 4
。-
批量获取值
mget key [key ...]
下面操作批量获取了键 a、b、c、d 的值:127.0.0.1:6379> mget a b c d 1) "1" 2) "2" 3) "3" 4) "4"
如果有些键不存在,那么它的值为 nil(空),结果是按照传入键的顺序返回:
127.0.0.1:6379> mget a b c f 1) "1" 2) "2" 3) "3" 4) (nil)
批量调用命令与单个命令相比的好处就是节约了网络请求时间。get命令执行5次,相当于5次请求&响应,但是如果执行mget5个key,只有1次请求&响应。
计数
incr key
根据key自增,并且返回执行后的结果。
值不是整数,返回错误。
值是整数,返回自增后的结果。
键不存在,按照值为0自增,返回结果为1。
decr key
自减。
incrby key increment
自增指定数字。
decrby key decrement
自减指定数字。
incrbyfloat key increment
自增浮点数。删除
del key
删除
内部编码
字符串类型的内部编码有3种:
- int:8个字节的长整型。
- embstr:小于等于39个字节的字符串。
- raw:大于39个字节的字符串。
Redis 会根据当前值的类型和长度决定使用哪种内部编码实现。
使用场景
字符串的使用场景比较常见。我们日常用的缓存,session共享等,都是通过字符串的方式去实现。
哈希(hash)
在 Redis 中,哈希类型是指键值本身又是一个键值对结构。相当于 key-{field1-value1},{field2-value2},{field3-value3}......{fieldN-valueN}
命令
设置值
hset key field value
设置key-field-value。获取值
hget key field
如果key或者field不存在,则返回nil。计算 field 个数
hlen key
计算 value 的字符串长度
hstrlen key field
批量设置值&获取值
hmget key field [field ...]
批量获取值,同一个key,不同的fieled
hmset key field value [field value ...]
批量设置值,同一个key,不同的field-value。删除field
hdel key field [field ...]
hdel 会删除一个或多个 field,返回结果为成功删除 field 的个数。判断 field 是否存在
hexists key field
包含时返回结果为1,不包含返回0。获取所有field
hkeys key
根据key,返回所有的field。获取所有value
hvals key
根据key,返回所有的value获取所有的 field-value
hgetall key
根据kye,返回所有的filed及value。如果这个key里面的field太多,而每个field的value也太多,不建议使用,很有可能会引起Redis阻塞。计数(与k-v差不多)
hincrby key field
hincrbyfloat key field
内部编码
哈希类型的内部编码有两种:
- ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于 hash-max-ziplist-value 配置(默认64字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
- hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为O(1)。
使用场景
- 一般情况我们存储一个对象,可以用K-V,key为一个对象的主键,value这个对象(或者对象的json)。这样的话,我们每次get key获取到的就是整个对象,如果这个对象里面我们可能只需要用到一两个字段值,这样的操作其实是非常浪费性能的。而且存储对象的时候,我们需要用到序列化与反序列化,这些也是会消耗一定的性能的。我们可以把整个对象用hash的方式存到Redis中,需要哪个对象的哪些值就获取哪个。
列表(list)
Redis中的列表可以存储多个有序字符串,最多可以存储2^32-1个元素。而且它支持双向操作,可以从列表两端插入(push)和弹出(pop)。因此,它可以用在很多种数据结构上。
命令
列表主要是四种操作类型
操作类型 | 操作 |
---|---|
增 | rpush lpush linsert |
删 | lpop rpop lrem ltrim |
改 | lset |
查 | lrange lindex llen |
阻塞 | blpop brpop |
增(插入)
rpush key value [value ...]
从右边插入value。
lpush key value [value ...]
从左边插入value。
linsert key before|after VAL value
linsert 命令会从列表中找到等于 VAL 的元素,在其前(before)或者后(after)插入一个新的元素 value。删除
lpop key
从列表左侧弹出元素。
rpop key
从列表右侧弹出元素。
ltrim key start end
按照索引范围修剪列表。
lrem key count value
删除指定元素。
rem 命令会从列表中找到等于 value 的元素进行删除,根据 count 的不同分为三种情况:count>0,从左到右,删除最多 count 个元素;count<0,从右到左,删除最多 count 绝对值个元素;
count=0,删除所有。改
lset key index newValue
根据index修改列表中的元素。查
lrange key start end
lrange 操作会获取列表指定索引范围所有的元素。索引下标有两个特点:第一,索引下标从左到右分别是0到 N-1,但是从右到左分别是-1到-N,全部分别是0到-1。第二,lrange 中的 end 选项包含了自身,这个和很多编程语言不包含 end 不太相同。
lindex key index
获取列表指定索引下标的元素。索引下标从左到右分别是0到 N-1,但是从右到左分别是-1到-N。
llen key
获取列表长度。阻塞
blpop key \[key ...\] timeout
brpop key \[key ...\] timeout
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,它们除了弹出方向不同,使用方法基本相同,所以下面以 brpop 命令进行说明,brpop 命令包含两个参数:
key[key...]:多个列表的键。
timeout:阻塞时间(单位:秒)。
列表为空:如果 timeout=3,那么客户端要等到3秒后返回,如果 timeout=0,那么客户端一直阻塞等下去。
列表不为空:客户端会立即返回。
在使用 brpop 时,有两点需要注意。
第一点,如果是多个键,那么 brpop 会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
..阻塞..
此时另一个客户端分别向 list:2和 list:3插入元素:
client-lpush> lpush list:2 element2
(integer) 1
client-lpush> lpush list:3 element3
(integer) 1
客户端会立即返回 list:2中的 element2,因为 list:2最先有可以弹出的元素:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
1) "list:2"
2) "element2_1"
第二点,如果多个客户端对同一个键执行 brpop,那么最先执行 brpop 命令的客户端可以获取到弹出的值。
客户端1:
client-1> brpop list:test 0
...阻塞...
客户端2:
client-2> brpop list:test 0
...阻塞...
客户端3:
client-3> brpop list:test 0
...阻塞...
此时另一个客户端 lpush 一个元素到 list:test 列表中:
client-lpush> lpush list:test element
(integer) 1
那么客户端1最会获取到元素,因为客户端1最先执行 brpop,而客户端2和客户端3继续阻塞:
client> brpop list:test 0
1) "list:test"
2) "element"
内部编码
列表类型的内部编码有两种。
- ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认512个),同时列表中每个元素的值都小于 list-max-ziplist-value 配置时(默认64字节),Redis 会选用 ziplist 来作为列表的内部实现来减少内存的使用。
- linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis 会使用 linkedlist 作为列表的内部实现。
使用场景
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+brpop=Message Queue(消息队列)
集合(set)
集合(set)类型也是用来保存多个的字符串元素,它和Java中的Set是差不多的东西,不允许重复。
命令
添加元素
sadd key element [element ...]
返回结果为添加成功的元素个数(如果已有这个元素,则不计入成功元素)。删除元素
srem key element [element ...]
返回结果为成功删除元素个数。计算元素个数
scard key
判断元素是否在集合中
sismember key element
如果给定元素在集合内返回1,反之返回0。随机从集合返回指定个数元素
srandmember key [count]
[count]是可选参数,如果不写默认为1。从集合随机弹出元素
spop key [count]
获取所有元素
smembers key
求多个集合的交集
sinter key [key ...]
集合1与集合2的共有部分。求多个集合的并集
suinon key [key ...]
集合1+集合2。求多个集合的差集
sdiff key [key ...]
集合1-集合2。将交集、并集、差集的结果保存
sinterstore destination newKey [key1 key2...]
suionstore destination newKey [key1 key2...]
sdiffstore destination newKey [key1 key2...]
将结果保存到newkey中。
内部编码
集合类型的内部编码有两种:
- intset(整数集合):当集合中的元素都是整数且元素个数小于 set-max-intset-entries 配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
- hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。
使用场景
其实set和Java中的set非常像,一般情况如果不需要与其他服务共享数据,Java中的set足够使用了,在某些情况需要共享数据或者存储的情况才需要用到Redis中的set。
比如,使用抽奖的情况下,就可以先将所有人放到Redis中的set,然后利用spop/srandmember可以实现抽奖。
有序集合(zset)
有序集合和集合很相似,set是key中包含多个不重复的字符串,zset也是包含了多个不重复的字符串,但是每个字符串带了一个“分数”,分数允许重复。可以根据分数进行排序操作。
命令
添加成员
zadd key score member [score member ...]
计算元素个数
zcard key
计算某个成员的分数
zscore key member
计算成员的排名
zrank key member
zrevrank key member
zrank是从分数从低到高返回排名,zrevrank反之(排名从0开始计算)。删除成员
zrem key member [member ...]
返回结果为成功删除元素个数。增加成员的分数
zincrby key increment member
返回指定排名范围的成员
zrange key start end [withscores]
zrevrange key start end [withscores]
有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。如果加上withscores选项,同时会返回成员的分数。返回指定分数范围的成员
zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]
其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。[limit offset count]选项可以限制输出的起始位置和个数。同时min和max还支持开区间(小括号)和闭区间(中括号),-inf和+inf分别代表无限小和无限大。例如:
127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores
1) "tim"
2) "220"
3) "martin"
4) "250"
5) "tom"
6) "260"
返回指定分数范围成员个数
zcount key min max
删除指定排名内的升序元素
zremrangebyrank key start end
删除指定分数范围的成员
zremrangebyscore key min max
内部编码
有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
- skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。
位图(Bitmaps)
Bitmaps 本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作,它是一个最大长度为512MB(2^32)的位数组。一般情况Bitmaps在普通的业务上是用不了太多的,这里之所以拿出来讲解,是因为它能够实现一个非常强大的功能:布隆过滤器。
Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。但是它的每个存储单元都是bit,8bit=1Byt,所以它的存在主要就是占用内存非常少。
命令
设置值
setbit key offset value
设置键的第 offset 个位的值(从0算起,value只能是0或者是1)。获取值
getbit key offset
获取键的第 offset 位的值(从0开始算)。获取 Bitmaps 指定范围值为1的个数
bitcount key [start] [end]
Bitmaps 间的运算
bitop 运算符 destkey key [key....]
bitop 是一个复合操作,它可以做多个 Bitmaps 的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在 destkey 中计算 Bitmaps 中第一个值为 targetBit 的偏移量
bitpos key targetBit [start] [end]
使用场景
以下这个业务场景有点牵强,但是还是简单的描述一下吧。如果统计一个网站某一天的用户访问量,我们通常的做法是添加一个用户登录记录流水表,然后根据日期去count(distinct userId)这张表。但是这样的做法会导致这张表里的数据非常多,而且占用大量的空间。
如果用户的ID是自动增长的,那么就可以使用位图。比如ID为125的用户在2019-01-01这一天登录的时候,我们可以使用setbit user:login:2019-01-01 125 1
在Bitmaps的偏移量为125的位置设置值为1;如果还有其他的用户登录,同样根据用户ID将对应偏移量的值设置为1。然后调用bitcount user:login:2019-01-01
计算出2019-01-01这一天的访问量。
使用位图最方便的是占用资源比较小,而且执行速度会比较快。但是如果活跃用户比较少,而且用户ID又非常大的情况,用位图就有点得不偿失了。比如:2019-01-02这一天只有一个用户访问这个网站,而且这个用户的ID为100000,那么就需要占用1bit*100000的空间。
布隆过滤器 (Bloom Filter)
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k,以下图为例,具体的操作流程:首先将位数组进行初始化,将里面每个位都设置为0。这时候我们有一个集合{x, y, z},对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。
综合来说:如果这个元素存在,那么可能会存在误判,其实它有可能是不存在的。如果这个元素不存在,那么它一定不存在!
语法
其实Redis4.0已经帮我们实现了对应的布隆过滤器,我们简单的看下它的语法。
创建布隆过滤器
bf.reserve key error_rate size
key为redis存储键值,error_rate 为错误率(大于0,小于1),size为预计存储的数量(size是比较关键的,需要根据自己的需求情况合理估计,设置太小的话会增大错误率,设置太大会占用过多不必要的空间)添加元素
bf.add key value
添加值到布隆过滤器中(当过滤器不存在的时候会,会以默认值自动创建一个,建议最好提前创建好)
bf.madd key value [value ...]
批量插入元素到布隆过滤器判断是否存在
bf.exists key value
判断值是否存在过滤器中 1(表示很可能存在) 0 (表示绝对不存在)
bf.mexists key value [value ...]
批量判断判断值是否存在过滤器中
应用场景
如果我们结合BitMaps,就可以实现一个布隆过滤器。布隆过滤器的应用场景也非常广泛,举一个比较典型的例子:
通常,我们对缓存的操作是:
Object obj = 查询缓存(key);
if (obj == null) {
obj = 查询数据库(key);
if (obj != null) {
设置缓存数据(key, obj);
}
}
return obj;
但是,这里产生了一个问题:如果这个key本身在我们的数据库就是一个不存在的值,那么这里会直接造成缓存穿透。如果我们的数据库这个表中的数据特别多,而且有人知道这个漏洞,一直调用这个接口,查询这个根本不存在的key,会对数据库造成非常大的压力,甚至压垮数据库。
解决方案:
- 我们把数据库中,这个表全部的key同步到redis中。这个方案应该是一般人第一想法,但是如果这个表中的数据特别多,那么直接会造成Redis内存爆了。
- 利用布隆过滤器,对每个key进行多次hash,将每次hash出来的值当作offset修改Bitmaps的值为1。如果需要查询这个key,先查缓存,再查布隆过滤器对应的Bitmaps,最后确定有这个值就再查数据库。
Object obj = 查询缓存(key);
if (obj == null) {
boolean exists = 查询Bitmaps(key);
if (exists)
{
obj = 查询数据库(key);
if (obj != null) {
设置缓存数据并修改Bitmaps(key, obj);
}
}
}
return obj;