文章目录
Redis 命令参考
数据结构和内部编码
type
命令实际返回的就是当前键的数据结构类型。
object encoding
命令查询内部编码。
Redis 支持 5 种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合),Zset(SortedSet:有序集合)
Redis的5种数据结构:
Redis数据结构和内部编码:
Redis内部设计不同编码的好处:可以随时改进内部编码结构,而对外的数据结构和命令没有影响;Redis会根据值在内部切换适合的内部编码结构,不用的内部编码在不同场景下发挥各自的优势;比如字符串就一个数字,就会使用int,如果是一小段字符串,就使用embstr,如果是一段很长的字符串,就使用raw;例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能就会下降,这时Redis会在内部转化为linkedlist。
全局命令
查看所有键:keys *
键总数:dbsize
dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1)。而keys命令会遍历所有键,所以它的时间复杂度是O(n),当Redis保存了大量键时,线上环境禁止使用。
检查键是否存在:exists key
(键存在则返回1,不存在则返回0)
删除键:del key [key ...]
(返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回 0)
设置键过期时间:expire key seconds
查看键剩余过期时间:ttl key
(大于等于0的整数:键剩余的过期时间,单位秒、-1:键没设置过期时间、-2:键不存在)
键的数据结构类型:type key
(分别是:string、hash、list、set、zset;键不存在返回:none)
单线程架构
Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。
客户端调用Redis服务经历了发送命令、执行命令、返回结果三个过程。
一条命令从客户端达到服务端不会被立刻执行,所有命令都会进入一个队列中,然后逐个被执行。
为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?
- 纯内存访问。
- 使用I/O多路复用技术的实现。
- 单线程避免了线程切换和竞态产生的消耗。
- Redis是C语言开发的,C语言是操作系统语言,执行快。
简单理解IO多路复用: 服务端可以同时hold住多个IO入内存队列,然后每次选出一个sock,然后对这个sock上的事件进行处理。
单线程会有一个问题: 对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
字符串(String)
字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能 超过512MB。
设置值:set key value [ex seconds] [px milliseconds] [nx|xx]
- ex seconds:为键设置秒级过期时间。
- px milliseconds:为键设置毫秒级过期时间。
- nx:键必须不存在,才可以设置成功,用于添加。
- xx:与nx相反,键必须存在,才可以设置成功,用于更新。
setnx
、setex
的作用和nx、ex选项一样。
setex
:设置键并同时设置过期时间,减少一次和Redis服务的网络传输。
注: 因为 SET 命令可以通过参数来实现和 SETNX 、 SETEX 和 PSETEX 三个命令的效果,所以将来的 Redis 版本可能会废弃并最终移除 SETNX 、 SETEX 和 PSETEX 这三个命令。所以建议使用 SET 命令并结合参数使用。
批量操作:mset、mget
Redis可以支撑每秒数万的读写操作,但是这指的是Redis服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,因为Redis的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈。
计数:incr key
(默认自增1,键不存在从0开始自增)
自减:decr key
自增指定数字: incrby key increment
自减指定数字: decrby key decrement
追加值:append key value
字符串长度(返回字节数,一个字母或符号占一个字节,一个中文占三个字节):strlen key
设置并返回原值:getset key value
使用场景:
- 缓存功能
- 计数
- 共享Session
- 限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <=5)
{ // 通过 }
else
{// 限速 }
内部编码:
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
字符串类型的内部编码有3种:int(整型)、embstr(小于等于39个字节)、raw(大于39个字节)
。
哈希(Hash)
在Redis中,哈希类型是指键值本身又是一个键值 结构,形如value={{field1,value1},…{fieldN,valueN}}。
设置值:hset key field value
(设置成功会返回1,反之会返回0)
Redis提供了hsetnx
命令,它 们的关系就像set和setnx命令一样,只不过作用域由键变为field。
获取值:hget key field
(如果键或field不存在,会返回nil)
删除field:hdel key field [field ...]
(返回结果为成功删除field的个数)
计算field个数:hlen key
常用命令:hget,hset,hgetall
应用场景:
我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
用户ID,为查找的key,
存储的value用户对象包含姓名name,年龄age,生日birthday 等信息,
如果用普通的key/value结构来存储,主要有以下2种存储方式:
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,
如:set u001 “李三,18,20010101”
这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,
如:mset user:001:name "李三 "user:001:age18 user:001:birthday “20010101”
虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,
并提供了直接存取这个Map成员的接口,
如:hmset user:001 name “李三” age 18 birthday “20010101”
也就是说,Key仍然是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值,
这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过
key(用户ID) + field(属性标签) 操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
实现方式:
上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
内部编码:
哈希类型的内部编码有两种:
- ziplist(压缩列表)使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当有value大于64字节或者field个数超过512,此时ziplist的读写效率会下降,内部编码会变为hashtable。
- hashtable(哈希表)hashtable的读写时间复杂度为O(1)。
列表(List)
使用场景:
- 消息队列(lpush+brpop)
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpsh+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
内部编码:
- ziplist
- linkedlist:某个元素超过64字节或元素个数超过512使用
- quicklist:结合了ziplist和linkedlist两者的优势
集合(Set)
Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
给用户添加标签:
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
...
给标签添加用户:
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...
计算用户共同感兴趣的标签:
sinter user:1:tags user:2:tags
应用场景通常为以下几种:
- sadd=Tagging(标签)
- spop/srandmember=Random item(生成随机数,比如抽奖)
- sadd+sinter=Social Graph(社交需求)
集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面 三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key中。
常用命令: sadd,srem,spop,sdiff ,smembers,sunion。
应用场景:
共同的好友,共同的兴趣爱好,共同关注的人。
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
比如在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。
Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实
内部编码:
- intset:当集合中的元素都是整数且元素个数小于512
- hashtable:当元素个数超过512或某个元素不为整数时
有序集合(Sorted Set)
通过得分进行排序,元素不能重复,但是得分可以重复。
有序集合提供了获取指定分数和元素范围查询、计算成员排名
等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
添加成员:
zadd key score member [score member ...]
zadd user:ranking 251 tom
计算成员个数:
计算某个成员的分数:
计算成员的排名:
删除成员:
增加成员的分数:
返回指定排名范围的成员:
返回指定分数范围的成员:
使用场景:
有序集合比较典型的使用场景就是排行榜系统。
例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。
主要需要实现以下4个功能。
添加用户赞数:zadd user:ranking:2016_03_15 3 mike
之后再获得一个赞,可以使用zincrby:zincrby user:ranking:2016_03_15 1 mike
取消用户赞数:zrem user:ranking:2016_03_15 mike
展示获取赞数最多的十个用户:zrevrangebyrank user:ranking:2016_03_15 0 9
内部编码:
- ziplist
- skiplist(跳跃表)元素个数超过128个或某个元素大于64字节时使用,因为此时ziplist读写效率下降。
列表、集合、有序集合区别
常用命令: zadd,zrange,zrem,zcard等
使用场景:
以某个条件为权重,比如按顶的次数排序.
ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。
比如:twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
比如:全班同学成绩的SortedSets,value可以是同学的学号,而score就可以是其考试得分,这样数据插入集合的,就已经进行了天然的排序。
另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
需要精准设定过期时间的应用
比如你可以把上面说到的sorted set的score值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据了,不仅是清除Redis中的过期数据,你完全可以把Redis里这个过期时间当成是对数据库中数据的索引,用Redis来找出哪些数据需要过期删除,然后再精准地从数据库中删除相应的记录。
BitMap
BitMap 概念
- BitMap 本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。并且单独提供了一套命令。
- 可以把 BitMap 想象成一个以位为单位的数组,数组中的每个单元只能存0或者1,数组的下标在 BitMap 中叫做偏移量。
- 单个 BitMap 的最大长度是512MB,即2^32个比特位。
- BitMap 用一个比特位来映射某个元素的状态,由于一个比特位只能表示 0 和 1 两种状态;所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间。
例如字符串A 一个字节,对应的ASCII码是65,对应的二进制就是01000001,BitMap 就是对A的二进制位进行操作。
BitMap 相关命令
# 设置值,offset 偏移量从0开始,其中value只能是 0 和 1
setbit key offset value
# 获取值
getbit key offset
# 获取指定范围内值为 1 的个数
# [start]和[end]代表起始和结束字节数,可选性,不指定默认全部
bitcount key start end
# BitMap间的运算
# operations 位移操作符,枚举值
AND 与运算 &
OR 或运算 |
XOR 异或 ^
NOT 取反 ~
# result 计算的结果,会存储在该key中
# key1 … keyn 参与运算的key,可以有多个,空格分割,not运算只能一个key
# 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。返回值是保存到 destkey 的字符串的长度(以字节byte为单位),和输入 key 中最长的字符串长度相等。
bitop [operations] [result] [key1] [keyn…]
# 计算Bitmaps中第一个值为targetBit的偏移量
bitpos [key] [targetBit]
BitMap 使用场景
1、用户签到
很多网站都提供了签到功能,并且需要展示最近一个月的签到情况,这种情况可以使用 BitMap 来实现。
根据日期 offset = (今天是一年中的第几天) % (今年的天数),key = 年份:用户id。
# 用户ID为1001在2020年的第1天签到了
setbit 2020:1001 1 1
# 用户ID为1001在2020年的第10天签到了
setbit 2020:1001 10 1
# 用户ID为1002在2020年的第10天签到了
setbit 2020:1002 10 1
2、统计活跃用户(用户登陆情况)
使用日期作为 key,然后用户 id 为 offset,如果当日活跃过就设置为1。
假如:
20201009 活跃用户情况是: [1,0,1,1,0]
20201010 活跃用户情况是 :[ 1,1,0,1,0 ]
统计连续两天活跃的用户总数:
bitop and dest1 20201009 20201010
# dest1 中值为1的offset,就是连续两天活跃用户的ID
bitcount dest1
统计20201009 ~ 20201010 活跃过的用户:
bitop or dest2 20201009 20201010
3、统计用户是否在线
如果需要提供一个查询当前用户是否在线的接口,也可以考虑使用 BitMap 。即节约空间效率又高,只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。
设置2020-01-01用户在线情况:
172.25.201.85:6428> setbit login:users:2020-01-01 0 1
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 5 1
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 11 1
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 15 1
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-01 19 1
(integer) 0
172.25.201.85:6428>
userId=0,5,11,15,19的用户对网站进行了访问,数据存储如下。
172.25.201.85:6428> setbit login:users:2020-01-01 50 1
(integer) 0
此时有一个userId=50的用户访问了网站,此时数据存储如下。
设置2020-01-02用户在线情况:
172.25.201.85:6428> setbit login:users:2020-01-02 0 1
(integer) 0
172.25.201.85:6428> setbit login:users:2020-01-02 5 1
(integer) 0
利用bitop and命令计算两天都访问网站的用户:
# 将两天在线的用户使用and操作放到新的bitmap中,两天都在线的偏移量在新的bitmap中为1,否则为0
172.25.201.85:6428> bitop and dest1 login:users:2020-01-01 login:users:2020-01-02
(integer) 7
# 计算出两天都在线的用户总共为两个
172.25.201.85:6428> bitcount dest1
(integer) 2
4、实现布隆过滤器
解决缓存穿透。
统计每日用户的登录数
每一位标识一个用户ID,当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位设置为1。
这里做了一个 使用 set 和 BitMap 存储的对比。
可以看到,如果活跃用户数量很多,使用 BitMap 明显更有优势,能节省大量的内存。但如果活跃用户数量较少,还是建议使用 set 存储,BitMap 会产生多余的存储开销。
使用经验
- BitMap 是 sting 类型,最大 512 MB
- 注意 setbit 时的偏移量较大时,可能有较大耗时
- 位图不是绝对好,看具体场景
RedisTemplate 操作 Bit
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/set/{key}/{offset}/{state}")
public Boolean set(@PathVariable("offset") Integer offset,
@PathVariable("key") String key,
@PathVariable("state") boolean state) {
return setBit(key, offset, state);
}
@GetMapping("/get/{key}/{offset}")
public Boolean get(@PathVariable("offset") Integer offset,
@PathVariable("key") String key) {
return getBit(key, offset);
}
@GetMapping("/count/{key}")
public Long count(@PathVariable("key") String key) {
return bitCount(key);
}
@GetMapping("/op/{destKey}/{key1}/{key2}")
public Long op(@PathVariable("destKey") String destKey,
@PathVariable("key1") String key1,
@PathVariable("key2") String key2) {
// RedisStringCommands.BitOperation.AND、RedisStringCommands.BitOperation.OR
// RedisStringCommands.BitOperation.XOR、RedisStringCommands.BitOperation.NOT
RedisStringCommands.BitOperation op = RedisStringCommands.BitOperation.AND;
return bitOp(op, destKey, key1, key2);
}
public boolean setBit(String key, int offset, boolean state) {
return (boolean) redisTemplate.execute((RedisCallback<Boolean>) con -> con.setBit(key.getBytes(), offset, state));
}
public boolean getBit(String key, int offset) {
return (boolean) redisTemplate.execute((RedisCallback<Boolean>) con -> con.getBit(key.getBytes(), offset));
}
public Long bitCount(String key) {
return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
}
public Long bitCount(String key, int start, int end) {
return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes(), start, end));
}
public Long bitOp(RedisStringCommands.BitOperation op, String saveKey, String... desKey) {
byte[][] bytes = new byte[desKey.length][];
for (int i = 0; i < desKey.length; i++) {
bytes[i] = desKey[i].getBytes();
}
return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, saveKey.getBytes(), bytes));
}
}
Redis 中 BitMap 的使用场景
Bitmap简介
Redis位图Bitmaps详解
Jedis bitmap
遍历键
全量遍历键: keys *
生产环境应该禁止使用 keys *
命令,因为Redis是单线程架构,如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞。
渐进式遍历:
Redis从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。
和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是 O(1),但是要真正实现keys的功能,需要执行多次scan。
每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将 字典中的所有键遍历完毕。scan的使用方法如下:scan cursor [match pattern] [count number]
cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每 次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
现在要遍历所有的键。第一次执行scan 0,返回结果分为两个部分:第 一个部分的数字XX就是下次scan需要的cursor,第二个部分是10个键,然后使用新的cursor=“XX”,执行scan XX,这次会得到新的cursor,继续执行scan直到得到结果cursor变为0,说明所 有的键已经被遍历过了。
除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan
,它们的用法和scan基本类似。
遍历集合伪代码:
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。